refactor: MobX store structure (#3228)

* query params from router as computed

* chore: setup workspace store and sub-stores

* chore: update router query store

* chore: update store types

* fix: pages store changes

* change observables and retain object reference

* fix build errors

* chore: changed the structure of workspace, project, cycle, module and pages

* fix: pages fixes

* fix: merge conflicts resolved

* chore: fixed workspace list

* chore: update workspace store accroding to the new response

* fix: adding page details to store

* fix: adding new contexts and providers

* dev: issues store and filters in new store

* dev: optimised the issue fetching in issue base store

* chore: project views id mapped

* update lodash set to directly run inside runInaction since it mutates the object

* fix: context changes

* code refactor kanban for better mainatinability

* optimize Kanban for performance

* chore: implemented hooks for all the created stores

* chore: removed bridge id

* css change and refactor

* chore: update cycle store structure

* chore: implement the new label root store

* chore: removed object structure

* chore: implement project view hook

* Kanban new store implementation for project issues

* fix project root for kanban

* feat: workspace and project members endpoint (#3092)

* fix: merge conflicts resolved

* issue properties optimization

* chore: user stores

* chore: create new store context and update hooks

* chore: setup inbox store and implement router store

* chore: initialize and implement project estimate store

* chore: initialize global view store

* kanban and list view optimization

* chore: use new cycle and module store. (#3172)

* chore: use new cycle and module store.

* chore: minor improvements.

* Revert "chore: merge develop"

This reverts commit 9d2e0e29e7370b55b48fc2fee4fd126093a6cc48, reversing
changes made to 9595493c42be3ea0ddd17b23a0b124555075c062.

* chore: implement useGlobalView hook

* refactor: projects & inbox store instances (#3179)

* refactor: projects & inbox store instances

* fix: formatting

* fix: action usage

* chore: implement useProjectState hook. (#3185)

* dev: issue, cycle store optimiation

* fix build for code

* dev: removed dummy variables

* dev: issue store

* fix: adding todos

* chore: removing legacy store

* dev: issues store types and typos

* chore: cycle module user properties

* fix legacy store deletion issues

* chore: change POST to PATCH

* fix issues rendering for project root

* chore: removed workspace details in workpsaceinvite

* chore: created models for display properties

* chore: setup member store and implement it everywhere

* refactor: module store (#3202)

* refactor: cycle store (#3192)

* refator: cycle store

* some more improvements.

* chore: implement useLabel hook. (#3190)

* refactor: inbox & project related stores. (#3193)

* refactor: inbox -> filter, issues, inoxes & project -> publish, projects store

* refactor: workspace-project-id name

* fix kanban dropdown overlapping issue

* fix kanban layout minor re rendering

* chore: implement useMember store everywhere

* chore: create and implement editor mention store

* chore: removed the issue view user property

* chore: created at id changed

* dev: segway intgegration (#3132)

* feat: implemented rabbitmq

* dev: initialize segway with queue setup

* dev: import refactors

* dev: create communication with the segway server

* dev: create new workers

* dev: create celery node queue for consuming messages from django

* dev: node to celery connection

* dev: setup segway and django connection

* dev: refactor the structure and add database integration to the app

* dev: add external id and source added

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>

* dev: github importer (#3205)

* dev: initiate github import

* dev: github importer all issues import

* dev: github comments and links for the imported issues

* dev: update controller to use logger and spread the resultData in getAllEntities

* dev: removed console log

* dev: update code structure and sync functions

* dev: updated retry logic when exception

* dev: add imported data as well

* dev: update logger and repo fetch

* dev: update jira integration to new structure

* dev: update migrations

* dev: update the reason field

* chore: workspace object id removed

* chore: view's creation fixed

* refactor: mobx store improvements. (#3213)

* fix: state and label errors

* chore: remove legacy code

* fix: branch build fix (#3214)

* branch build fix for release-* in case of space,backend,proxy

* fixes

* chore: update store names and types

* fix - file size limit not work on plane.settings.production (#3160)

* fix - file size limit not work on plane.settings.production

* fix - file size limit not work on plane.settings.production

* fix - file size limit not work on plane.settings.production, move to common.py

---------

Co-authored-by: luanduongtel4vn <hoangluan@tel4vn.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>

* style: instance admin email settings ui & ux update. (#3186)

* refactor: use-user-auth hook (#3215)

* refactor: use-user-auth hook

* fix: user store currentUserLoader

* refactor: project-view & application related stores (#3207)

* refactor: project-view & application related stores

* rename: projectViews -> projectViewIds

* fix: project-view favourite state in store

* chore: remove unnecessary hooks and contexts (#3217)

* chore: update issue assignee property component

* chore: bug fixes & improvement (#3218)

* chore: draft issue validation added to prevent saving empty or whitespace title

* chore: resolve scrolling issue in page empty state

* chore: kanban layout quick add issue improvement

* fix: bugs & improvements (#3189)

* fix: workspace invitation modal form values reset

* fix: profile sidebar avatar letter

* [refactor] Editor code refactoring (#3194)

* removed relative imports from editor core

* Update issue widget file paths and imports to use kebab case instead of camel case, to align with coding conventions and improve consistency.

* Update Tiptap core and extensions versions to 2.1.13 and Tiptap React version to 2.1.13. Update Tiptap table imports to use the new location in package @tiptap/pm/tables. Update AlertLabel component to use the new type definition for LucideIcon.

* updated lock file

* removed default exports from editor/core

* fixed injecting css into the core package itself

* seperated css code to have single source of origin wrt to the package

* removed default imports from document editor

* all instances using index as key while mapping fixed

* Update Lite Text Editor package.json to remove @plane/editor-types as a dependency.

Update Lite Text Editor index.ts to update the import of IMentionSuggestion and IMentionHighlight from @plane/editor-types to @plane/editor-core.

Update Lite Text Editor ui/index.tsx to update the import of UploadImage, DeleteImage, IMentionSuggestion, and RestoreImage from @plane/editor-types to @plane/editor-core.

Update Lite Text Editor ui/menus/fixed-menu/index.tsx to update the import of UploadImage from @plane/editor-types to @plane/editor-core.

Update turbo.json to remove @plane/editor-types#build as a dependency for @plane/lite-text-editor#build, @plane/rich-text-editor#build, and @plane/document-editor#build.

* Remove deprecated import and adjust tippy.js usage in the slash-commands.tsx file of the editor extensions package.

* Update dependencies in `rich-text-editor/package.json`, remove `@plane/editor-types` and add `@plane/editor-core` in `rich-text-editor/src/index.ts`, and update imports in `rich-text-editor/src/ui/extensions/index.tsx` and `rich-text-editor/src/ui/index.tsx` to use `@plane/editor-core` instead of `@plane/editor-types`.

* Update package.json dependencies and add new types for image deletion, upload, restore, mention highlight, mention suggestion, and slash command item.

* Update import statements in various files to use the new package "@plane/editor-core" instead of "@plane/editor-types".

* fixed document editor to follow conventions

* Refactor imports in the Rich Text Editor package to use relative paths instead of absolute paths.

- Updated imports in `index.ts`, `ui/index.tsx`, and `ui/menus/bubble-menu/index.tsx` to use relative paths.
- Updated `tsconfig.json` to include the `baseUrl` compiler option and adjust the `include` and `exclude` paths.

* Refactor Lite Text Editor code to use relative import paths instead of absolute import paths.

* Added LucideIconType to the exports in index.ts for use in other files.
Created a new file lucide-icon.ts which contains the type LucideIconType.
Updated the icon type in HeadingOneItem in menu-items/index.tsx to use LucideIconType.
Updated the Icon type in AlertLabel in alert-label.tsx to use LucideIconType.
Updated the Icon type in VerticalDropdownItemProps in vertical-dropdown-menu.tsx to use LucideIconType.
Updated the Icon type in BubbleMenuItem in fixed-menu/index.tsx to use LucideIconType.
Deleted the file tooltip.tsx since it is no longer used.
Updated the Icon type in BubbleMenuItem in bubble-menu/index.tsx to use LucideIconType.

* ♻️ refactor: simplify rendering logic in slash-commands.tsx

The rendering logic in the file "slash-commands.tsx" has been simplified. Previously, the code used inline positioning for the popup, but it has now been removed. Instead of appending the popup to the document body, it is now appended to the element with the ID "tiptap-container". The "flip" option has also been removed. These changes have improved the readability and maintainability of the code.

* fixed build errors caused due to core's internal imports

* regression: fixed pages not saving issue and not duplicating with proper content issue

* build: Update @tiptap dependencies

Updated the @tiptap dependencies in the package.json files of `document-editor`, `extensions`, and `rich-text-editor` packages to version 2.1.13.

* 🚑 fix: Correct appendTo selector in slash-commands.tsx

Update the `appendTo` function call in `slash-commands.tsx` to use the correct selector `#editor-container` instead of `#tiptap-container`. This ensures that the component is appended to the appropriate container in the editor extension.

Note: The commit message assumes that the change is a fix for an issue or error. If it's not a fix, please provide more context so that an appropriate commit type can be determined.

* style: email placeholder changed across the platform (#3206)

* style: email placeholder changed across the platform

* fix: placeholder text

* dev: updated new filter endpoints and restructured issue and issue filters store

* implement issues and replace useMobxStore

* remove all store legacy references

* dev: updated the orderby and subgroupby filters data

* dev:added projectId in issue filters for consistency

* fix more build errors

* dev: updated profile issues

* dev: removed store legacy

* dev: active cycle issues in the cycle issue store

* fix additional build errors and memoize issueActions in each layout component

* change store enums

* remove all useMobxStore references

* fix more build errors

* dev: reverted workspace invitation

* fix: build errors and warnings

* fix: optimistic update for instant operations (#3221)

* fix: update functions failed case

* fix: typo

* chore: revert back to optimistic update approach for all `update related actions` (#3219)

* fix: merge conflicts resolved

* chore: update memberMap logic in components

* add assignees to kanban groups and properties

* dev: migration fixes

* final bit of optimization on list view

* change all TODOs that are to be done before this release to FIXME

* change base Kanban TODOs that are to be done before this release to FIXME

* dev: add fields and expand for app serializers

* dev: issue detail store

* dev: update issue serializer to return object ids

* fix: Instance key added in settings and converted issues list api to arry instead of dict

* fix: removing segway files

* dev: control expand through query parameters

* revert: github importer

* Revert "dev: segway intgegration (#3132)"

This reverts commit 1cc18a09156d1790d114061dbac8c901e0f2754c.

* dev: remove migrations for segway

* dev: issue structure change and created workspacebasemodel

* dev: issue detail serializer

* fix: changed workspace dict

* dev: updated new issue structure

* chore: build fix

* dev: issue detail store refactor

* dev: created list endpoint for issue-relation

* dev: added issue attachments in issue detail store

* dev: added issue activity computed

* fix: build error

* chore: peek overview modal context added

* chore: build error fix

* dev: added sub_issues in issue details store

* dev: added complete issue serializer for sub issues

* dev: resolved type errors in issue root store

* dev: changed the issue relation structure

* chore: new global dropdowns

* chore: build error fix

* chore: cycle and module selection if disabled

* dev: removed unnecessary code from the workspace root

* chore: build error fix

* chore: issue relation remove endpoint

* fix: build error

* dev: typos and implemented issue relation store

* fix: yarn lock updated

* style: update the UI of all the dropdowns

* fix: state store fixes

* fix: key issue

* fix: state store console logs removed

* refactor: member dropdowns

* fix: moving types to packages

* fix: dropdown arrow positioning

* dev: removed logs

* style: label dropdown

* chore: restrict description notifications

* chore: description changes

* chore: update spreadsheet layout dropdowns

* fix: build errors

* chore: duplicate key change

* fix: ui bugs

* chore: relation activity change

* chore: comment activity changes

* chore: blocking issue removal

* chore: added project_id for relation

* chore: issue relation store and component

* chore: issue redirection issue in the issue realtion in detail page

* chore: created activity changed

* chore: issue links new store implementation on the issue detail

* chore: issue relation deletion acitivity changed

* chore: issue attachments new store implementation on the issue detail

* chore: workspace level issues

* fix: build errors

---------

Co-authored-by: rahulramesha <rahulramesham@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com>
Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
Co-authored-by: Hoang Luan <luandnh98@gmail.com>
Co-authored-by: luanduongtel4vn <hoangluan@tel4vn.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: M. Palanikannan <73993394+Palanikannan1437@users.noreply.github.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
This commit is contained in:
Aaryan Khandelwal 2024-01-02 18:12:55 +05:30 committed by GitHub
parent 1539340113
commit 804b7d8663
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
940 changed files with 26378 additions and 34411 deletions

View file

@ -1,7 +1,6 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// lib
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication, useProject, useUser } from "hooks/store";
// components
import { ProjectCard } from "components/project";
import { Loader } from "@plane/ui";
@ -10,26 +9,22 @@ import emptyProject from "public/empty-state/empty_project.webp";
// icons
import { NewEmptyState } from "components/common/new-empty-state";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
import { EUserProjectRoles } from "constants/project";
export interface IProjectCardList {
workspaceSlug: string;
}
export const ProjectCardList: FC<IProjectCardList> = observer((props) => {
const { workspaceSlug } = props;
// store
export const ProjectCardList = observer(() => {
// store hooks
const {
project: projectStore,
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
user: { currentWorkspaceRole },
} = useMobxStore();
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { workspaceProjectIds, searchedProjects, getProjectById } = useProject();
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
if (!projects) {
if (!workspaceProjectIds)
return (
<Loader className="grid grid-cols-3 gap-4">
<Loader.Item height="100px" />
@ -40,19 +35,22 @@ export const ProjectCardList: FC<IProjectCardList> = observer((props) => {
<Loader.Item height="100px" />
</Loader>
);
}
return (
<>
{projects.length > 0 ? (
{workspaceProjectIds.length > 0 ? (
<div className="h-full w-full overflow-y-auto p-8">
{projectStore.searchedProjects.length == 0 ? (
{searchedProjects.length == 0 ? (
<div className="mt-10 w-full text-center text-custom-text-400">No matching projects</div>
) : (
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
{projectStore.searchedProjects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
{searchedProjects.map((projectId) => {
const projectDetails = getProjectById(projectId);
if (!projectDetails) return;
return <ProjectCard key={projectDetails.id} project={projectDetails} />;
})}
</div>
)}
</div>
@ -66,17 +64,13 @@ export const ProjectCardList: FC<IProjectCardList> = observer((props) => {
direction: "right",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
}}
primaryButton={
isEditingAllowed
? {
text: "Start your first project",
onClick: () => {
setTrackElement("PROJECTS_EMPTY_STATE");
commandPaletteStore.toggleCreateProjectModal(true);
},
}
: null
}
primaryButton={{
text: "Start your first project",
onClick: () => {
setTrackElement("PROJECTS_EMPTY_STATE");
commandPaletteStore.toggleCreateProjectModal(true);
},
}}
disabled={!isEditingAllowed}
/>
)}

View file

@ -1,11 +1,9 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
// icons
import { LinkIcon, Lock, Pencil, Star } from "lucide-react";
// hooks
import { useProject } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { DeleteProjectModal, JoinProjectModal } from "components/project";
@ -15,7 +13,9 @@ import { Avatar, AvatarGroup, Button, Tooltip } from "@plane/ui";
import { copyTextToClipboard } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper";
// types
import type { IProject } from "types";
import type { IProject } from "@plane/types";
// constants
import { EUserProjectRoles } from "constants/project";
export type ProjectCardProps = {
project: IProject;
@ -26,21 +26,22 @@ export const ProjectCard: React.FC<ProjectCardProps> = observer((props) => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// toast
// toast alert
const { setToastAlert } = useToast();
// states
const [deleteProjectModalOpen, setDeleteProjectModal] = useState(false);
const [joinProjectModalOpen, setJoinProjectModal] = useState(false);
// store hooks
const { addProjectToFavorites, removeProjectFromFavorites } = useProject();
const { project: projectStore }: RootStore = useMobxStore();
const isOwner = project.member_role === 20;
const isMember = project.member_role === 15;
project.member_role;
const isOwner = project.member_role === EUserProjectRoles.ADMIN;
const isMember = project.member_role === EUserProjectRoles.MEMBER;
const handleAddToFavorites = () => {
if (!workspaceSlug) return;
projectStore.addProjectToFavorites(workspaceSlug.toString(), project.id).catch(() => {
addProjectToFavorites(workspaceSlug.toString(), project.id).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
@ -52,7 +53,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = observer((props) => {
const handleRemoveFromFavorites = () => {
if (!workspaceSlug || !project) return;
projectStore.removeProjectFromFavorites(workspaceSlug.toString(), project.id).catch(() => {
removeProjectFromFavorites(workspaceSlug.toString(), project.id).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
@ -120,8 +121,8 @@ export const ProjectCard: React.FC<ProjectCardProps> = observer((props) => {
{project.emoji
? renderEmoji(project.emoji)
: project.icon_prop
? renderEmoji(project.icon_prop)
: null}
? renderEmoji(project.icon_prop)
: null}
</span>
</div>

View file

@ -1,14 +1,13 @@
import React, { useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// icons
import { AlertTriangle } from "lucide-react";
// hooks
import { useUser } from "hooks/store";
// ui
import { Button } from "@plane/ui";
// types
import { IUserLite } from "types";
import { IUserLite } from "@plane/types";
type Props = {
data: IUserLite;
@ -19,13 +18,10 @@ type Props = {
export const ConfirmProjectMemberRemove: React.FC<Props> = observer((props) => {
const { data, onSubmit, isOpen, onClose } = props;
// states
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const {
user: { currentUser },
} = useMobxStore();
// store hooks
const { currentUser } = useUser();
const handleClose = () => {
onClose();
@ -98,8 +94,8 @@ export const ConfirmProjectMemberRemove: React.FC<Props> = observer((props) => {
? "Leaving..."
: "Leave"
: isDeleteLoading
? "Removing..."
: "Remove"}
? "Removing..."
: "Remove"}
</Button>
</div>
</Dialog.Panel>

View file

@ -2,24 +2,22 @@ import { useState, useEffect, Fragment, FC, ChangeEvent } from "react";
import { useForm, Controller } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite";
// icons
import { X } from "lucide-react";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useApplication, useProject, useUser, useWorkspace } from "hooks/store";
import useToast from "hooks/use-toast";
import { useWorkspaceMyMembership } from "contexts/workspace-member.context";
// ui
import { Button, CustomSelect, Input, TextArea } from "@plane/ui";
// components
import { WorkspaceMemberSelect } from "components/workspace";
import { ImagePickerPopover } from "components/core";
import EmojiIconPicker from "components/emoji-icon-picker";
import { WorkspaceMemberDropdown } from "components/dropdowns";
// helpers
import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
// types
import { IWorkspaceMember } from "types";
// constants
import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
type Props = {
isOpen: boolean;
@ -64,11 +62,13 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props;
// store
const {
project: projectStore,
workspaceMember: { workspaceMembers },
trackEvent: { postHogEventTracker },
workspace: { currentWorkspace },
} = useMobxStore();
eventTracker: { postHogEventTracker },
} = useApplication();
const {
membership: { currentWorkspaceRole },
} = useUser();
const { currentWorkspace } = useWorkspace();
const { addProjectToFavorites, createProject } = useProject();
// states
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
// toast
@ -95,11 +95,10 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
reValidateMode: "onChange",
});
const { memberDetails } = useWorkspaceMyMembership();
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
if (memberDetails && isOpen) if (memberDetails.role <= 10) return <IsGuestCondition onClose={onClose} />;
if (currentWorkspaceRole && isOpen)
if (currentWorkspaceRole <= EUserWorkspaceRoles.MEMBER) return <IsGuestCondition onClose={onClose} />;
const handleClose = () => {
onClose();
@ -110,7 +109,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
const handleAddToFavorites = (projectId: string) => {
if (!workspaceSlug) return;
projectStore.addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => {
addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
@ -128,8 +127,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
payload.project_lead = formData.project_lead_member;
return projectStore
.createProject(workspaceSlug.toString(), payload)
return createProject(workspaceSlug.toString(), payload)
.then((res) => {
const newPayload = {
...res,
@ -138,7 +136,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
postHogEventTracker("PROJECT_CREATED", newPayload, {
isGrouping: true,
groupType: "Workspace_metrics",
gorupId: res.workspace,
groupId: res.workspace,
});
setToastAlert({
type: "success",
@ -165,7 +163,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
{
isGrouping: true,
groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!,
groupId: currentWorkspace?.id!,
}
);
});
@ -309,8 +307,8 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
message: "Identifier must at least be of 1 character",
},
maxLength: {
value: 6,
message: "Identifier must at most be of 6 characters",
value: 12,
message: "Identifier must at most be of 12 characters",
},
}}
render={({ field: { value, onChange } }) => (
@ -350,20 +348,19 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex-shrink-0" tabIndex={4}>
<Controller
name="network"
control={control}
render={({ field: { onChange, value } }) => (
<Controller
name="network"
control={control}
render={({ field: { onChange, value } }) => (
<div className="flex-shrink-0" tabIndex={4}>
<CustomSelect
value={value}
onChange={onChange}
buttonClassName="border-[0.5px] shadow-md !py-1.5 shadow-none"
label={
<div className="flex items-center gap-2 text-custom-text-300">
<div className="flex items-center gap-1">
{currentNetwork ? (
<>
<currentNetwork.icon className="h-[18px] w-[18px]" />
<currentNetwork.icon className="h-3 w-3" />
{currentNetwork.label}
</>
) : (
@ -371,6 +368,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
)}
</div>
}
placement="bottom-start"
noChevron
>
{NETWORK_CHOICES.map((network) => (
@ -384,25 +382,24 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
<div className="flex-shrink-0" tabIndex={5}>
<Controller
name="project_lead_member"
control={control}
render={({ field: { value, onChange } }) => (
<WorkspaceMemberSelect
value={
workspaceMembers?.filter((member: IWorkspaceMember) => member.member.id === value)[0]
}
</div>
)}
/>
<Controller
name="project_lead_member"
control={control}
render={({ field: { value, onChange } }) => (
<div className="h-7 flex-shrink-0" tabIndex={5}>
<WorkspaceMemberDropdown
value={value}
onChange={onChange}
options={workspaceMembers || []}
placeholder="Select Lead"
placeholder="Lead"
multiple={false}
buttonVariant="border-with-text"
/>
)}
/>
</div>
</div>
)}
/>
</div>
</div>

View file

@ -2,16 +2,14 @@ import React from "react";
import { useRouter } from "next/router";
import { Controller, useForm } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react";
// hooks
import useToast from "hooks/use-toast";
// icons
import { AlertTriangle } from "lucide-react";
// hooks
import { useApplication, useProject, useWorkspace } from "hooks/store";
import useToast from "hooks/use-toast";
// ui
import { Button, Input } from "@plane/ui";
// types
import type { IProject } from "types";
// fetch-keys
import { useMobxStore } from "lib/mobx/store-provider";
import type { IProject } from "@plane/types";
type DeleteProjectModal = {
isOpen: boolean;
@ -26,16 +24,16 @@ const defaultValues = {
export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
const { isOpen, project, onClose } = props;
// store
// store hooks
const {
project: projectStore,
workspace: { currentWorkspace },
trackEvent: { postHogEventTracker },
} = useMobxStore();
eventTracker: { postHogEventTracker },
} = useApplication();
const { currentWorkspace } = useWorkspace();
const { deleteProject } = useProject();
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// toast
// toast alert
const { setToastAlert } = useToast();
// form info
const {
@ -60,8 +58,7 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
const onSubmit = async () => {
if (!workspaceSlug || !canDelete) return;
await projectStore
.deleteProject(workspaceSlug.toString(), project.id)
await deleteProject(workspaceSlug.toString(), project.id)
.then(() => {
if (projectId && projectId.toString() === project.id) router.push(`/${workspaceSlug}/projects`);
@ -74,7 +71,7 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
{
isGrouping: true,
groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!,
groupId: currentWorkspace?.id!,
}
);
setToastAlert({
@ -92,7 +89,7 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
{
isGrouping: true,
groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!,
groupId: currentWorkspace?.id!,
}
);
setToastAlert({

View file

@ -1,11 +1,14 @@
import { FC, useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
// hooks
import { useApplication, useProject, useWorkspace } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import EmojiIconPicker from "components/emoji-icon-picker";
import { ImagePickerPopover } from "components/core";
import { Button, CustomSelect, Input, TextArea } from "@plane/ui";
// types
import { IProject, IWorkspace } from "types";
import { IProject, IWorkspace } from "@plane/types";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
import { renderFormattedDate } from "helpers/date-time.helper";
@ -13,9 +16,6 @@ import { renderFormattedDate } from "helpers/date-time.helper";
import { NETWORK_CHOICES } from "constants/project";
// services
import { ProjectService } from "services/project";
// hooks
import useToast from "hooks/use-toast";
import { useMobxStore } from "lib/mobx/store-provider";
export interface IProjectDetailsForm {
project: IProject;
@ -27,15 +27,15 @@ const projectService = new ProjectService();
export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
const { project, workspaceSlug, isAdmin } = props;
// store
// store hooks
const {
project: projectStore,
trackEvent: { postHogEventTracker },
workspace: { currentWorkspace },
} = useMobxStore();
// toast
eventTracker: { postHogEventTracker },
} = useApplication();
const { currentWorkspace } = useWorkspace();
const { updateProject } = useProject();
// toast alert
const { setToastAlert } = useToast();
// form data
// form info
const {
handleSubmit,
watch,
@ -70,11 +70,10 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
setValue("identifier", formattedValue);
};
const updateProject = async (payload: Partial<IProject>) => {
const handleUpdateChange = async (payload: Partial<IProject>) => {
if (!workspaceSlug || !project) return;
return projectStore
.updateProject(workspaceSlug.toString(), project.id, payload)
return updateProject(workspaceSlug.toString(), project.id, payload)
.then((res) => {
postHogEventTracker(
"PROJECT_UPDATED",
@ -82,7 +81,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
{
isGrouping: true,
groupType: "Workspace_metrics",
gorupId: res.workspace,
groupId: res.workspace,
}
);
setToastAlert({
@ -100,7 +99,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
{
isGrouping: true,
groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!,
groupId: currentWorkspace?.id!,
}
);
setToastAlert({
@ -135,9 +134,9 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
.checkProjectIdentifierAvailability(workspaceSlug as string, payload.identifier ?? "")
.then(async (res) => {
if (res.exists) setError("identifier", { message: "Identifier already exists" });
else await updateProject(payload);
else await handleUpdateChange(payload);
});
else await updateProject(payload);
else await handleUpdateChange(payload);
};
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === project?.network);
@ -242,7 +241,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
/>
</div>
<div className="flex w-full items-baseline justify-between gap-10">
<div className="flex w-full items-center justify-between gap-10">
<div className="flex w-1/2 flex-col gap-1">
<h4 className="text-sm">Identifier</h4>
<Controller
@ -256,8 +255,8 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
message: "Identifier must at least be of 1 character",
},
maxLength: {
value: 6,
message: "Identifier must at most be of 6 characters",
value: 12,
message: "Identifier must at most be of 5 characters",
},
}}
render={({ field: { value, ref } }) => (
@ -275,7 +274,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
/>
)}
/>
<span className="text-xs text-red-500">{errors?.identifier?.message}</span>
</div>
<div className="flex w-1/2 flex-col gap-1">
@ -307,7 +305,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
<div className="flex items-center justify-between py-2">
<>
<Button variant="primary" type="submit" loading={isSubmitting} disabled={!isAdmin}>
{isSubmitting ? "Updating Project..." : "Update Project"}
{isSubmitting ? "Updating" : "Update project"}
</Button>
<span className="text-sm italic text-custom-sidebar-text-400">
Created on {renderFormattedDate(project?.created_at)}

View file

@ -9,8 +9,6 @@ export * from "./form";
export * from "./join-project-modal";
export * from "./leave-project-modal";
export * from "./member-select";
export * from "./members-select";
export * from "./priority-select";
export * from "./sidebar-list-item";
export * from "./sidebar-list";
export * from "./integration-card";

View file

@ -15,7 +15,7 @@ import { SelectRepository, SelectChannel } from "components/integration";
import GithubLogo from "public/logos/github-square.png";
import SlackLogo from "public/services/slack.png";
// types
import { IWorkspaceIntegration } from "types";
import { IWorkspaceIntegration } from "@plane/types";
// fetch-keys
import { PROJECT_GITHUB_REPOSITORY } from "constants/fetch-keys";
@ -78,8 +78,7 @@ export const IntegrationCard: React.FC<Props> = ({ integration }) => {
});
})
.catch((err) => {
console.log(err);
console.error(err);
setToastAlert({
type: "error",
title: "Error!",

View file

@ -1,13 +1,12 @@
import { useState, Fragment } from "react";
import { useRouter } from "next/router";
import { Transition, Dialog } from "@headlessui/react";
// hooks
import { useProject, useUser } from "hooks/store";
// ui
import { Button } from "@plane/ui";
// types
import type { IProject } from "types";
// lib
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
import type { IProject } from "@plane/types";
// type
type TJoinProjectModalProps = {
@ -21,11 +20,11 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = (props) => {
const { handleClose, isOpen, project, workspaceSlug } = props;
// states
const [isJoiningLoading, setIsJoiningLoading] = useState(false);
// store
// store hooks
const {
project: projectStore,
user: { joinProject },
}: RootStore = useMobxStore();
membership: { joinProject },
} = useUser();
const { fetchProjects } = useProject();
// router
const router = useRouter();
@ -35,7 +34,7 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = (props) => {
joinProject(workspaceSlug, [project.id])
.then(() => {
router.push(`/${workspaceSlug}/projects/${project.id}/issues`);
projectStore.fetchProjects(workspaceSlug);
fetchProjects(workspaceSlug);
handleClose();
})
.finally(() => {

View file

@ -4,14 +4,13 @@ import { Controller, useForm } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react";
import { AlertTriangleIcon } from "lucide-react";
import { observer } from "mobx-react-lite";
// hooks
import { useApplication, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// ui
import { Button, Input } from "@plane/ui";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useToast from "hooks/use-toast";
// types
import { IProject } from "types";
import { IProject } from "@plane/types";
type FormData = {
projectName: string;
@ -34,11 +33,13 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store
// store hooks
const {
user: { leaveProject },
trackEvent: { postHogEventTracker },
} = useMobxStore();
eventTracker: { postHogEventTracker },
} = useApplication();
const {
membership: { leaveProject },
} = useUser();
// toast
const { setToastAlert } = useToast();

View file

@ -2,9 +2,8 @@ import { useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useMember, useProject, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { ConfirmProjectMemberRemove } from "components/project";
@ -13,39 +12,40 @@ import { CustomSelect, Tooltip } from "@plane/ui";
// icons
import { ChevronDown, Dot, XCircle } from "lucide-react";
// constants
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
// types
import { IProjectMember, TUserProjectRole } from "types";
import { ROLE } from "constants/workspace";
import { EUserProjectRoles } from "constants/project";
type Props = {
member: IProjectMember;
userId: string;
};
export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
const { member } = props;
const { userId } = props;
// states
const [removeMemberModal, setRemoveMemberModal] = useState(false);
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store
// store hooks
const {
user: { currentUser, currentProjectMemberInfo, currentProjectRole, leaveProject },
projectMember: { removeMemberFromProject, updateMember },
project: { fetchProjects },
} = useMobxStore();
// hooks
currentUser,
membership: { currentProjectRole, leaveProject },
} = useUser();
const { fetchProjects } = useProject();
const {
project: { removeMemberFromProject, getProjectMemberDetails, updateMember },
} = useMember();
// toast alert
const { setToastAlert } = useToast();
// derived values
const isAdmin = currentProjectRole === EUserWorkspaceRoles.ADMIN;
const memberDetails = member.member;
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
const userDetails = getProjectMemberDetails(userId);
const handleRemove = async () => {
if (!workspaceSlug || !projectId) return;
if (!workspaceSlug || !projectId || !userDetails) return;
if (memberDetails.id === currentUser?.id) {
if (userDetails.member.id === currentUser?.id) {
await leaveProject(workspaceSlug.toString(), projectId.toString())
.then(async () => {
await fetchProjects(workspaceSlug.toString());
@ -60,55 +60,58 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
})
);
} else
await removeMemberFromProject(workspaceSlug.toString(), projectId.toString(), member.id).catch((err) =>
setToastAlert({
type: "error",
title: "Error",
message: err?.error || "Something went wrong. Please try again.",
})
await removeMemberFromProject(workspaceSlug.toString(), projectId.toString(), userDetails.member.id).catch(
(err) =>
setToastAlert({
type: "error",
title: "Error",
message: err?.error || "Something went wrong. Please try again.",
})
);
};
if (!userDetails) return null;
return (
<>
<ConfirmProjectMemberRemove
isOpen={removeMemberModal}
onClose={() => setRemoveMemberModal(false)}
data={member.member}
data={userDetails.member}
onSubmit={handleRemove}
/>
<div className="group flex items-center justify-between px-3 py-4 hover:bg-custom-background-90">
<div className="flex items-center gap-x-4 gap-y-2">
{memberDetails.avatar && memberDetails.avatar !== "" ? (
<Link href={`/${workspaceSlug}/profile/${memberDetails.id}`}>
{userDetails.member.avatar && userDetails.member.avatar !== "" ? (
<Link href={`/${workspaceSlug}/profile/${userDetails.member.id}`}>
<span className="relative flex h-10 w-10 items-center justify-center rounded p-4 capitalize text-white">
<img
src={memberDetails.avatar}
alt={memberDetails.display_name || memberDetails.email}
src={userDetails.member.avatar}
alt={userDetails.member.display_name || userDetails.member.email}
className="absolute left-0 top-0 h-full w-full rounded object-cover"
/>
</span>
</Link>
) : (
<Link href={`/${workspaceSlug}/profile/${memberDetails.id}`}>
<Link href={`/${workspaceSlug}/profile/${userDetails.id}`}>
<span className="relative flex h-10 w-10 items-center justify-center rounded bg-gray-700 p-4 capitalize text-white">
{(memberDetails.display_name ?? memberDetails.email ?? "?")[0]}
{(userDetails.member.display_name ?? userDetails.member.email ?? "?")[0]}
</span>
</Link>
)}
<div>
<Link href={`/${workspaceSlug}/profile/${memberDetails.id}`}>
<Link href={`/${workspaceSlug}/profile/${userDetails.member.id}`}>
<span className="text-sm font-medium">
{memberDetails.first_name} {memberDetails.last_name}
{userDetails.member.first_name} {userDetails.member.last_name}
</span>
</Link>
<div className="flex items-center">
<p className="text-xs text-custom-text-300">{memberDetails.display_name}</p>
<p className="text-xs text-custom-text-300">{userDetails.member.display_name}</p>
{isAdmin && (
<>
<Dot height={16} width={16} className="text-custom-text-300" />
<p className="text-xs text-custom-text-300">{memberDetails.email}</p>
<p className="text-xs text-custom-text-300">{userDetails.member.email}</p>
</>
)}
</div>
@ -121,23 +124,23 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
<div className="item-center flex gap-1 rounded px-2 py-0.5">
<span
className={`flex items-center rounded text-xs font-medium ${
memberDetails.id !== currentProjectMemberInfo?.id ? "" : "text-custom-sidebar-text-400"
userDetails.member.id !== currentUser?.id ? "" : "text-custom-text-400"
}`}
>
{ROLE[member.role as keyof typeof ROLE]}
{ROLE[userDetails.role]}
</span>
{memberDetails.id !== currentProjectMemberInfo?.id && (
{userDetails.member.id !== currentUser?.id && (
<span className="grid place-items-center">
<ChevronDown className="h-3 w-3" />
</span>
)}
</div>
}
value={member.role}
onChange={(value: TUserProjectRole | undefined) => {
value={userDetails.role}
onChange={(value: EUserProjectRoles) => {
if (!workspaceSlug || !projectId) return;
updateMember(workspaceSlug.toString(), projectId.toString(), member.id, {
updateMember(workspaceSlug.toString(), projectId.toString(), userDetails.member.id, {
role: value,
}).catch((err) => {
const error = err.error;
@ -151,10 +154,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
});
}}
disabled={
memberDetails.id === currentUser?.id ||
!member.member ||
!currentProjectRole ||
currentProjectRole < member.role
userDetails.member.id === currentUser?.id || !currentProjectRole || currentProjectRole < userDetails.role
}
placement="bottom-end"
>
@ -168,12 +168,8 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
);
})}
</CustomSelect>
{(isAdmin || memberDetails.id === currentProjectMemberInfo?.member.id) && (
<Tooltip
tooltipContent={
memberDetails.id === currentProjectMemberInfo?.member.id ? "Leave project" : "Remove member"
}
>
{(isAdmin || userDetails.member.id === currentUser?.id) && (
<Tooltip tooltipContent={userDetails.member.id === currentUser?.id ? "Leave project" : "Remove member"}>
<button
type="button"
onClick={() => setRemoveMemberModal(true)}

View file

@ -1,49 +1,37 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { Search } from "lucide-react";
// hooks
import { useApplication, useMember } from "hooks/store";
// components
import { ProjectMemberListItem, SendProjectInvitationModal } from "components/project";
// ui
import { Button, Loader } from "@plane/ui";
// icons
import { Search } from "lucide-react";
export const ProjectMemberList: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store
const {
projectMember: { projectMembers, fetchProjectMembers },
trackEvent: { setTrackElement },
} = useMobxStore();
// states
const [inviteModal, setInviteModal] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
// store hooks
const {
eventTracker: { setTrackElement },
} = useApplication();
const {
project: { projectMemberIds, getProjectMemberDetails },
} = useMember();
const searchedMembers = (projectMembers ?? []).filter((member) => {
const fullName = `${member.member.first_name} ${member.member.last_name}`.toLowerCase();
const displayName = member.member.display_name.toLowerCase();
const searchedMembers = (projectMemberIds ?? []).filter((userId) => {
const memberDetails = getProjectMemberDetails(userId);
return displayName.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase());
const fullName = `${memberDetails?.member.first_name} ${memberDetails?.member.last_name}`.toLowerCase();
const displayName = memberDetails?.member.display_name.toLowerCase();
return displayName?.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase());
});
return (
<>
<SendProjectInvitationModal
isOpen={inviteModal}
members={projectMembers ?? []}
onClose={() => setInviteModal(false)}
onSuccess={() => {
mutate(`PROJECT_INVITATIONS_${projectId?.toString()}`);
fetchProjectMembers(workspaceSlug?.toString()!, projectId?.toString()!);
}}
/>
<SendProjectInvitationModal isOpen={inviteModal} onClose={() => setInviteModal(false)} />
<div className="flex items-center justify-between gap-4 border-b border-custom-border-100 py-3.5">
<h4 className="text-xl font-medium">Members</h4>
@ -64,10 +52,10 @@ export const ProjectMemberList: React.FC = observer(() => {
setInviteModal(true);
}}
>
Add Member
Add member
</Button>
</div>
{!projectMembers ? (
{!projectMemberIds ? (
<Loader className="space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
@ -76,11 +64,11 @@ export const ProjectMemberList: React.FC = observer(() => {
</Loader>
) : (
<div className="divide-y divide-custom-border-100">
{projectMembers.length > 0
? searchedMembers.map((member) => <ProjectMemberListItem key={member.id} member={member} />)
{projectMemberIds.length > 0
? searchedMembers.map((userId) => <ProjectMemberListItem key={userId} userId={userId} />)
: null}
{searchedMembers.length === 0 && (
<h4 className="text-md mt-20 text-center text-custom-text-400">No matching member</h4>
<h4 className="text-sm mt-16 text-center text-custom-text-400">No matching members</h4>
)}
</div>
)}

View file

@ -1,10 +1,8 @@
import React from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { Ban } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useMember } from "hooks/store";
// ui
import { Avatar, CustomSearchSelect } from "@plane/ui";
@ -16,40 +14,35 @@ type Props = {
export const MemberSelect: React.FC<Props> = observer((props) => {
const { value, onChange, isDisabled = false } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store
// store hooks
const {
projectMember: { fetchProjectMembers, projectMembers },
} = useMobxStore();
project: { projectMemberIds, getProjectMemberDetails },
} = useMember();
useSWR(
workspaceSlug && projectId ? `PROJECT_MEMBERS_${projectId.toString().toUpperCase()}` : null,
workspaceSlug && projectId ? () => fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) : null
);
const options = projectMemberIds?.map((userId) => {
const memberDetails = getProjectMemberDetails(userId);
const options = projectMembers?.map((member) => ({
value: member.member.id,
query: member.member.display_name,
content: (
<div className="flex items-center gap-2">
<Avatar name={member?.member.display_name} src={member?.member.avatar} />
{member.member.display_name}
</div>
),
}));
const selectedOption = projectMembers?.find((m) => m.member.id === value)?.member;
return {
value: `${memberDetails?.member.id}`,
query: `${memberDetails?.member.display_name}`,
content: (
<div className="flex items-center gap-2">
<Avatar name={memberDetails?.member.display_name} src={memberDetails?.member.avatar} />
{memberDetails?.member.display_name}
</div>
),
};
});
const selectedOption = getProjectMemberDetails(value);
return (
<CustomSearchSelect
value={value}
label={
<div className="flex items-center gap-2">
{selectedOption && <Avatar name={selectedOption.display_name} src={selectedOption.avatar} />}
{selectedOption && <Avatar name={selectedOption.member.display_name} src={selectedOption.member.avatar} />}
{selectedOption ? (
selectedOption?.display_name
selectedOption.member.display_name
) : (
<div className="flex items-center gap-2">
<Ban className="h-3.5 w-3.5 rotate-90 text-custom-sidebar-text-400" />

View file

@ -1,179 +0,0 @@
import React, { useState } from "react";
import { usePopper } from "react-popper";
import { Placement } from "@popperjs/core";
import { Combobox } from "@headlessui/react";
import { Check, ChevronDown, Search, User2 } from "lucide-react";
// ui
import { Avatar, AvatarGroup, Tooltip } from "@plane/ui";
// types
import { IUserLite } from "types";
type Props = {
members: IUserLite[] | undefined;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
placement?: Placement;
hideDropdownArrow?: boolean;
disabled?: boolean;
} & (
| {
value: string[];
onChange: (data: string[]) => void;
multiple: true;
}
| {
value: string;
onChange: (data: string) => void;
multiple: false;
}
);
export const MembersSelect: React.FC<Props> = ({
value,
onChange,
members,
className = "",
buttonClassName = "",
optionsClassName = "",
placement,
hideDropdownArrow = false,
disabled = false,
multiple = true,
}) => {
const [query, setQuery] = useState("");
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
const options = members?.map((member) => ({
value: member.id,
query: member.display_name,
content: (
<div className="flex items-center gap-2">
<Avatar name={member.display_name} src={member.avatar} />
{member.display_name}
</div>
),
}));
const filteredOptions =
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
const label = (
<Tooltip
tooltipHeading="Assignee"
tooltipContent={
value && value.length > 0
? members
?.filter((m) => value.includes(m.display_name))
.map((m) => m.display_name)
.join(", ")
: "No Assignee"
}
position="top"
>
<div className="flex h-full w-full cursor-pointer items-center gap-2 text-custom-text-200">
{value && value.length > 0 && Array.isArray(value) ? (
<AvatarGroup showTooltip={false}>
{value.map((assigneeId) => {
const member = members?.find((m) => m.id === assigneeId);
if (!member) return null;
return <Avatar key={member.id} name={member.display_name} src={member.avatar} />;
})}
</AvatarGroup>
) : (
<span
className="flex h-full w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs duration-300 focus:outline-none
"
>
<User2 className="h-3 w-3" />
</span>
)}
</div>
</Tooltip>
);
const comboboxProps: any = { value, onChange, disabled };
if (multiple) comboboxProps.multiple = true;
return (
<Combobox as="div" className={`flex-shrink-0 text-left ${className}`} {...comboboxProps}>
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={`flex w-full items-center justify-between gap-1 text-xs ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
{label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
</button>
</Combobox.Button>
<Combobox.Options className="fixed z-10">
<div
className={`my-1 w-48 whitespace-nowrap rounded border border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none ${optionsClassName}`}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-300" />
<Combobox.Input
className="w-full bg-transparent px-2 py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className={`mt-2 max-h-48 space-y-1 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
{option.content}
{selected && <Check className={`h-3.5 w-3.5`} />}
</>
)}
</Combobox.Option>
))
) : (
<span className="flex items-center gap-2 p-1">
<p className="text-left text-custom-text-200 ">No matching results</p>
</span>
)
) : (
<p className="text-center text-custom-text-200">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
</Combobox>
);
};

View file

@ -1,154 +0,0 @@
import React, { useState } from "react";
import { usePopper } from "react-popper";
import { Placement } from "@popperjs/core";
import { Combobox } from "@headlessui/react";
import { Check, ChevronDown, Search } from "lucide-react";
import { PriorityIcon, Tooltip } from "@plane/ui";
// helpers
import { capitalizeFirstLetter } from "helpers/string.helper";
// types
import { TIssuePriorities } from "types";
// constants
import { ISSUE_PRIORITIES } from "constants/issue";
type Props = {
value: TIssuePriorities;
onChange: (data: TIssuePriorities) => void;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
placement?: Placement;
showTitle?: boolean;
highlightUrgentPriority?: boolean;
hideDropdownArrow?: boolean;
disabled?: boolean;
};
export const PrioritySelect: React.FC<Props> = ({
value,
onChange,
className = "",
buttonClassName = "",
optionsClassName = "",
placement,
showTitle = false,
//highlightUrgentPriority = true,
hideDropdownArrow = false,
disabled = false,
}) => {
const [query, setQuery] = useState("");
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
const options = ISSUE_PRIORITIES?.map((priority) => ({
value: priority.key,
query: priority.key,
content: (
<div className="flex items-center gap-2">
<PriorityIcon priority={priority.key} className="h-3.5 w-3.5" />
{priority.title}
</div>
),
}));
const filteredOptions =
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
const selectedOption = value ? capitalizeFirstLetter(value) : "None";
const label = (
<Tooltip tooltipHeading="Priority" tooltipContent={selectedOption} position="top">
<div className="flex w-full items-center gap-2">
<PriorityIcon priority={value} className={`h-3.5 w-3.5`} />
{showTitle && <span className="text-xs capitalize">{value}</span>}
</div>
</Tooltip>
);
return (
<Combobox
as="div"
className={`flex-shrink-0 text-left ${className}`}
value={value}
onChange={onChange}
disabled={disabled}
>
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={`flex h-full w-full items-center justify-between gap-1 ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer"
} ${buttonClassName}`}
onClick={(e) => e.stopPropagation()}
>
{label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-2.5 w-2.5" aria-hidden="true" />}
</button>
</Combobox.Button>
<Combobox.Options className="fixed z-10">
<div
className={`my-1 w-48 whitespace-nowrap rounded border border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none ${optionsClassName}`}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-300" />
<Combobox.Input
className="w-full bg-transparent px-2 py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className={`mt-2 max-h-48 space-y-1 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={(e) => e.stopPropagation()}
>
{({ selected }) => (
<>
{option.content}
{selected && <Check className="h-3.5 w-3.5" />}
</>
)}
</Combobox.Option>
))
) : (
<span className="flex items-center gap-2 p-1">
<p className="text-left text-custom-text-200 ">No matching results</p>
</span>
)
) : (
<p className="text-center text-custom-text-200">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
</Combobox>
);
};

View file

@ -1,10 +1,9 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// store
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useProject, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
import { Controller, useForm } from "react-hook-form";
@ -12,10 +11,11 @@ import { MemberSelect } from "components/project";
// ui
import { Loader } from "@plane/ui";
// types
import { IProject, IUserLite, IWorkspace } from "types";
import { IProject, IUserLite, IWorkspace } from "@plane/types";
// fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
import { EUserWorkspaceRoles } from "constants/workspace";
// constants
import { EUserProjectRoles } from "constants/project";
const defaultValues: Partial<IProject> = {
project_lead: null,
@ -26,11 +26,13 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store
const { user: userStore, project: projectStore } = useMobxStore();
const { currentProjectDetails } = projectStore;
const { currentProjectRole } = userStore;
const isAdmin = currentProjectRole === EUserWorkspaceRoles.ADMIN;
// store hooks
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails, fetchProjectDetails, updateProject } = useProject();
// derived values
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
// hooks
const { setToastAlert } = useToast();
// form info
@ -38,9 +40,7 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
// fetching user members
useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId.toString()) : null,
workspaceSlug && projectId
? () => projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString())
: null
workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) : null
);
useEffect(() => {
@ -48,7 +48,8 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
reset({
...currentProjectDetails,
default_assignee: currentProjectDetails.default_assignee?.id ?? currentProjectDetails.default_assignee,
default_assignee:
(currentProjectDetails.default_assignee as IUserLite)?.id ?? currentProjectDetails.default_assignee,
project_lead: (currentProjectDetails.project_lead as IUserLite)?.id ?? currentProjectDetails.project_lead,
workspace: (currentProjectDetails.workspace as IWorkspace).id,
});
@ -59,18 +60,18 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
reset({
...currentProjectDetails,
default_assignee: currentProjectDetails?.default_assignee?.id ?? currentProjectDetails?.default_assignee,
default_assignee:
(currentProjectDetails?.default_assignee as IUserLite)?.id ?? currentProjectDetails?.default_assignee,
project_lead: (currentProjectDetails?.project_lead as IUserLite)?.id ?? currentProjectDetails?.project_lead,
...formData,
});
await projectStore
.updateProject(workspaceSlug.toString(), projectId.toString(), {
default_assignee: formData.default_assignee === "none" ? null : formData.default_assignee,
project_lead: formData.project_lead === "none" ? null : formData.project_lead,
})
await updateProject(workspaceSlug.toString(), projectId.toString(), {
default_assignee: formData.default_assignee === "none" ? null : formData.default_assignee,
project_lead: formData.project_lead === "none" ? null : formData.project_lead,
})
.then(() => {
projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString());
fetchProjectDetails(workspaceSlug.toString(), projectId.toString());
setToastAlert({
title: "Success",
type: "success",
@ -78,7 +79,7 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
});
})
.catch((err) => {
console.log(err);
console.error(err);
});
};

View file

@ -1,19 +1,18 @@
import React, { useEffect, useState } from "react";
import { Fragment, useEffect, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// ui components
import { Button, Loader, ToggleSwitch } from "@plane/ui";
import { Check, CircleDot, Globe2 } from "lucide-react";
import { CustomPopover } from "./popover";
import { IProjectPublishSettings, TProjectPublishViews } from "store/project";
// hooks
import { useProjectPublish } from "hooks/store";
import useToast from "hooks/use-toast";
// ui
import { Button, Loader, ToggleSwitch } from "@plane/ui";
import { CustomPopover } from "./popover";
// types
import { IProject } from "types";
import { IProject } from "@plane/types";
import { IProjectPublishSettings, TProjectPublishViews } from "store/project/project-publish.store";
type Props = {
isOpen: boolean;
@ -52,22 +51,29 @@ const viewOptions: {
export const PublishProjectModal: React.FC<Props> = observer((props) => {
const { isOpen, project, onClose } = props;
const [isUnpublishing, setIsUnpublishing] = useState(false);
// states
const [isUnPublishing, setIsUnPublishing] = useState(false);
const [isUpdateRequired, setIsUpdateRequired] = useState(false);
let plane_deploy_url = process.env.NEXT_PUBLIC_DEPLOY_URL;
if (typeof window !== "undefined" && !plane_deploy_url)
plane_deploy_url = window.location.protocol + "//" + window.location.host + "/spaces";
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const { projectPublish: projectPublishStore } = useMobxStore();
// store hooks
const {
projectPublishSettings,
getProjectSettingsAsync,
publishProject,
updateProjectSettingsAsync,
unPublishProject,
fetchSettingsLoader,
} = useProjectPublish();
// toast alert
const { setToastAlert } = useToast();
// form info
const {
control,
formState: { isSubmitting },
@ -88,14 +94,11 @@ export const PublishProjectModal: React.FC<Props> = observer((props) => {
// prefill form with the saved settings if the project is already published
useEffect(() => {
if (
projectPublishStore.projectPublishSettings &&
projectPublishStore.projectPublishSettings !== "not-initialized"
) {
if (projectPublishSettings && projectPublishSettings !== "not-initialized") {
let userBoards: TProjectPublishViews[] = [];
if (projectPublishStore.projectPublishSettings?.views) {
const savedViews = projectPublishStore.projectPublishSettings?.views;
if (projectPublishSettings?.views) {
const savedViews = projectPublishSettings?.views;
if (!savedViews) return;
@ -109,32 +112,31 @@ export const PublishProjectModal: React.FC<Props> = observer((props) => {
}
const updatedData = {
id: projectPublishStore.projectPublishSettings?.id || null,
comments: projectPublishStore.projectPublishSettings?.comments || false,
reactions: projectPublishStore.projectPublishSettings?.reactions || false,
votes: projectPublishStore.projectPublishSettings?.votes || false,
inbox: projectPublishStore.projectPublishSettings?.inbox || null,
id: projectPublishSettings?.id || null,
comments: projectPublishSettings?.comments || false,
reactions: projectPublishSettings?.reactions || false,
votes: projectPublishSettings?.votes || false,
inbox: projectPublishSettings?.inbox || null,
views: userBoards,
};
reset({ ...updatedData });
}
}, [reset, projectPublishStore.projectPublishSettings, isOpen]);
}, [reset, projectPublishSettings, isOpen]);
// fetch publish settings
useEffect(() => {
if (!workspaceSlug || !isOpen) return;
if (projectPublishStore.projectPublishSettings === "not-initialized") {
projectPublishStore.getProjectSettingsAsync(workspaceSlug.toString(), project.id);
if (projectPublishSettings === "not-initialized") {
getProjectSettingsAsync(workspaceSlug.toString(), project.id);
}
}, [isOpen, workspaceSlug, project, projectPublishStore]);
}, [isOpen, workspaceSlug, project, projectPublishSettings, getProjectSettingsAsync]);
const handlePublishProject = async (payload: IProjectPublishSettings) => {
if (!workspaceSlug) return;
return projectPublishStore
.publishProject(workspaceSlug.toString(), project.id, payload)
return publishProject(workspaceSlug.toString(), project.id, payload)
.then((res) => {
handleClose();
// window.open(`${plane_deploy_url}/${workspaceSlug}/${project.id}`, "_blank");
@ -146,8 +148,7 @@ export const PublishProjectModal: React.FC<Props> = observer((props) => {
const handleUpdatePublishSettings = async (payload: IProjectPublishSettings) => {
if (!workspaceSlug) return;
await projectPublishStore
.updateProjectSettingsAsync(workspaceSlug.toString(), project.id, payload.id ?? "", payload)
await updateProjectSettingsAsync(workspaceSlug.toString(), project.id, payload.id ?? "", payload)
.then((res) => {
setToastAlert({
type: "success",
@ -159,18 +160,17 @@ export const PublishProjectModal: React.FC<Props> = observer((props) => {
return res;
})
.catch((error) => {
console.log("error", error);
console.error("error", error);
return error;
});
};
const handleUnpublishProject = async (publishId: string) => {
const handleUnPublishProject = async (publishId: string) => {
if (!workspaceSlug || !publishId) return;
setIsUnpublishing(true);
setIsUnPublishing(true);
await projectPublishStore
.unPublishProject(workspaceSlug.toString(), project.id, publishId)
await unPublishProject(workspaceSlug.toString(), project.id, publishId)
.then((res) => {
handleClose();
return res;
@ -179,10 +179,10 @@ export const PublishProjectModal: React.FC<Props> = observer((props) => {
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong while unpublishing the project.",
message: "Something went wrong while un-publishing the project.",
})
)
.finally(() => setIsUnpublishing(false));
.finally(() => setIsUnPublishing(false));
};
const CopyLinkToClipboard = ({ copy_link }: { copy_link: string }) => {
@ -236,10 +236,9 @@ export const PublishProjectModal: React.FC<Props> = observer((props) => {
// check if an update is required or not
const checkIfUpdateIsRequired = () => {
if (!projectPublishStore.projectPublishSettings || projectPublishStore.projectPublishSettings === "not-initialized")
return;
if (!projectPublishSettings || projectPublishSettings === "not-initialized") return;
const currentSettings = projectPublishStore.projectPublishSettings as IProjectPublishSettings;
const currentSettings = projectPublishSettings;
const newSettings = getValues();
if (
@ -265,10 +264,10 @@ export const PublishProjectModal: React.FC<Props> = observer((props) => {
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
as={Fragment}
enter="ease-out duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
@ -282,7 +281,7 @@ export const PublishProjectModal: React.FC<Props> = observer((props) => {
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={React.Fragment}
as={Fragment}
enter="ease-out duration-200"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
@ -298,16 +297,16 @@ export const PublishProjectModal: React.FC<Props> = observer((props) => {
{project.is_deployed && (
<Button
variant="danger"
onClick={() => handleUnpublishProject(watch("id") ?? "")}
loading={isUnpublishing}
onClick={() => handleUnPublishProject(watch("id") ?? "")}
loading={isUnPublishing}
>
{isUnpublishing ? "Unpublishing..." : "Unpublish"}
{isUnPublishing ? "Un-publishing..." : "Un-publish"}
</Button>
)}
</div>
{/* content */}
{projectPublishStore.fetchSettingsLoader ? (
{fetchSettingsLoader ? (
<Loader className="space-y-4 px-6">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
@ -462,7 +461,7 @@ export const PublishProjectModal: React.FC<Props> = observer((props) => {
<Globe2 className="h-4 w-4" />
<div className="text-sm">Anyone with the link can access</div>
</div>
{!projectPublishStore.fetchSettingsLoader && (
{!fetchSettingsLoader && (
<div className="relative flex items-center gap-2">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel

View file

@ -4,28 +4,23 @@ import { observer } from "mobx-react-lite";
import { useForm, Controller, useFieldArray } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react";
import { ChevronDown, Plus, X } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication, useMember, useUser, useWorkspace } from "hooks/store";
import useToast from "hooks/use-toast";
// ui
import { Avatar, Button, CustomSelect, CustomSearchSelect } from "@plane/ui";
// services
import { ProjectMemberService } from "services/project";
// hooks
import useToast from "hooks/use-toast";
// types
import { IProjectMember, TUserProjectRole } from "types";
// constants
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
import { ROLE } from "constants/workspace";
import { EUserProjectRoles } from "constants/project";
type Props = {
isOpen: boolean;
members: IProjectMember[];
onClose: () => void;
onSuccess: () => void;
onSuccess?: () => void;
};
type member = {
role: TUserProjectRole;
role: EUserProjectRoles;
member_id: string;
};
@ -42,24 +37,26 @@ const defaultValues: FormValues = {
],
};
// services
const projectMemberService = new ProjectMemberService();
export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
const { isOpen, members, onClose, onSuccess } = props;
const { isOpen, onClose, onSuccess } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// toast alert
const { setToastAlert } = useToast();
// store hooks
const {
user: { currentProjectRole },
workspaceMember: { workspaceMembers },
trackEvent: { postHogEventTracker },
workspace: { currentWorkspace },
} = useMobxStore();
eventTracker: { postHogEventTracker },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { currentWorkspace } = useWorkspace();
const {
project: { projectMemberIds, bulkAddMembersToProject },
workspace: { workspaceMemberIds, getWorkspaceMemberDetails },
} = useMember();
// form info
const {
formState: { errors, isSubmitting },
reset,
@ -72,8 +69,8 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
name: "members",
});
const uninvitedPeople = workspaceMembers?.filter((person) => {
const isInvited = members?.find((member) => member.member.id === person.member.id);
const uninvitedPeople = workspaceMemberIds?.filter((userId) => {
const isInvited = projectMemberIds?.find((u) => u === userId);
return !isInvited;
});
@ -83,15 +80,14 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
const payload = { ...formData };
await projectMemberService
.bulkAddMembersToProject(workspaceSlug.toString(), projectId.toString(), payload)
await bulkAddMembersToProject(workspaceSlug.toString(), projectId.toString(), payload)
.then((res) => {
onSuccess();
if (onSuccess) onSuccess();
onClose();
setToastAlert({
title: "Success",
type: "success",
message: "Member added successfully",
message: "Members added successfully.",
});
postHogEventTracker(
"MEMBER_ADDED",
@ -102,12 +98,12 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
{
isGrouping: true,
groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!,
groupId: currentWorkspace?.id!,
}
);
})
.catch((error) => {
console.log(error);
console.error(error);
postHogEventTracker(
"MEMBER_ADDED",
{
@ -116,7 +112,7 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
{
isGrouping: true,
groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!,
groupId: currentWorkspace?.id!,
}
);
})
@ -152,16 +148,23 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
}
}, [fields, append]);
const options = uninvitedPeople?.map((person) => ({
value: person.member.id,
query: person.member.display_name,
content: (
<div className="flex items-center gap-2">
<Avatar name={person.member?.display_name} src={person.member?.avatar} />
{person.member.display_name} ({person.member.first_name + " " + person.member.last_name})
</div>
),
}));
const options = uninvitedPeople?.map((userId) => {
const memberDetails = getWorkspaceMemberDetails(userId);
return {
value: `${memberDetails?.member.id}`,
query: `${memberDetails?.member.first_name} ${
memberDetails?.member.last_name
} ${memberDetails?.member.display_name.toLowerCase()}`,
content: (
<div className="flex items-center gap-2">
<Avatar name={memberDetails?.member.display_name} src={memberDetails?.member.avatar} />
{memberDetails?.member.display_name} (
{memberDetails?.member.first_name + " " + memberDetails?.member.last_name})
</div>
),
};
});
return (
<Transition.Root show={isOpen} as={React.Fragment}>
@ -208,7 +211,7 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
name={`members.${index}.member_id`}
rules={{ required: "Please select a member" }}
render={({ field: { value, onChange } }) => {
const selectedMember = workspaceMembers?.find((p) => p.member.id === value)?.member;
const selectedMember = getWorkspaceMemberDetails(value);
return (
<CustomSearchSelect
@ -217,8 +220,11 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
<button className="flex w-full items-center justify-between gap-1 rounded-md border border-custom-border-200 px-3 py-2 text-left text-sm text-custom-text-200 shadow-sm duration-300 hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none">
{value && value !== "" ? (
<div className="flex items-center gap-2">
<Avatar name={selectedMember?.display_name} src={selectedMember?.avatar} />
{selectedMember?.display_name}
<Avatar
name={selectedMember?.member.display_name}
src={selectedMember?.member.avatar}
/>
{selectedMember?.member.display_name}
</div>
) : (
<div className="flex items-center gap-2 py-0.5">Select co-worker</div>
@ -263,8 +269,7 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
width="w-full"
>
{Object.entries(ROLE).map(([key, label]) => {
if (parseInt(key) > (currentProjectRole ?? EUserWorkspaceRoles.GUEST))
return null;
if (parseInt(key) > (currentProjectRole ?? EUserProjectRoles.GUEST)) return null;
return (
<CustomSelect.Option key={key} value={key}>

View file

@ -6,7 +6,7 @@ import { Button, Loader } from "@plane/ui";
// icons
import { ChevronDown, ChevronUp } from "lucide-react";
// types
import { IProject } from "types";
import { IProject } from "@plane/types";
export interface IDeleteProjectSection {
projectDetails: IProject;

View file

@ -3,13 +3,13 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { ContrastIcon, FileText, Inbox, Layers } from "lucide-react";
import { DiceIcon, ToggleSwitch } from "@plane/ui";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication, useProject, useUser, useWorkspace } from "hooks/store";
import useToast from "hooks/use-toast";
// types
import { IProject } from "types";
import { EUserWorkspaceRoles } from "constants/workspace";
import { IProject } from "@plane/types";
// constants
import { EUserProjectRoles } from "constants/project";
type Props = {};
@ -50,15 +50,18 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store
// store hooks
const {
workspace: { currentWorkspace },
project: { currentProjectDetails, updateProject },
user: { currentUser, currentProjectRole },
trackEvent: { setTrackElement, postHogEventTracker },
} = useMobxStore();
const isAdmin = currentProjectRole === EUserWorkspaceRoles.ADMIN;
// hooks
eventTracker: { setTrackElement, postHogEventTracker },
} = useApplication();
const {
currentUser,
membership: { currentProjectRole },
} = useUser();
const { currentWorkspace } = useWorkspace();
const { currentProjectDetails, updateProject } = useProject();
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
// toast alert
const { setToastAlert } = useToast();
const handleSubmit = async (formData: Partial<IProject>) => {
@ -88,9 +91,8 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
</div>
</div>
<ToggleSwitch
value={currentProjectDetails?.[feature.property as keyof IProject]}
value={Boolean(currentProjectDetails?.[feature.property as keyof IProject])}
onChange={() => {
console.log(currentProjectDetails?.[feature.property as keyof IProject]);
setTrackElement("PROJECT_SETTINGS_FEATURES_PAGE");
postHogEventTracker(`TOGGLE_${feature.title.toUpperCase()}`, {
workspace_id: currentWorkspace?.id,

View file

@ -18,20 +18,18 @@ import {
MoreHorizontal,
} from "lucide-react";
// hooks
import { useApplication, useProject } from "hooks/store";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import useToast from "hooks/use-toast";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// types
import { IProject } from "types";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { CustomMenu, Tooltip, ArchiveIcon, PhotoFilterIcon, DiceIcon, ContrastIcon, LayersIcon } from "@plane/ui";
import { LeaveProjectModal, PublishProjectModal } from "components/project";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { EUserProjectRoles } from "constants/project";
type Props = {
project: IProject;
projectId: string;
provided?: DraggableProvided;
snapshot?: DraggableStateSnapshot;
handleCopyText: () => void;
@ -73,34 +71,37 @@ const navigation = (workspaceSlug: string, projectId: string) => [
export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { project, provided, snapshot, handleCopyText, shortContextMenu = false } = props;
// store
const { projectId, provided, snapshot, handleCopyText, shortContextMenu = false } = props;
// store hooks
const {
project: projectStore,
theme: themeStore,
trackEvent: { setTrackElement },
} = useMobxStore();
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// toast
const { setToastAlert } = useToast();
eventTracker: { setTrackElement },
} = useApplication();
const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject();
// states
const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false);
const [publishModalOpen, setPublishModal] = useState(false);
const [isMenuActive, setIsMenuActive] = useState(false);
// router
const router = useRouter();
const { workspaceSlug, projectId: URLProjectId } = router.query;
// toast alert
const { setToastAlert } = useToast();
// derived values
const project = getProjectById(projectId);
const isAdmin = project.member_role === 20;
const isViewerOrGuest = project.member_role === 10 || project.member_role === 5;
const isAdmin = project?.member_role === EUserProjectRoles.ADMIN;
const isViewerOrGuest =
project?.member_role && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(project.member_role);
const isCollapsed = themeStore.sidebarCollapsed;
const actionSectionRef = useRef<HTMLDivElement | null>(null);
const handleAddToFavorites = () => {
if (!workspaceSlug) return;
if (!workspaceSlug || !project) return;
projectStore.addProjectToFavorites(workspaceSlug.toString(), project.id).catch(() => {
addProjectToFavorites(workspaceSlug.toString(), project.id).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
@ -110,9 +111,9 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
};
const handleRemoveFromFavorites = () => {
if (!workspaceSlug) return;
if (!workspaceSlug || !project) return;
projectStore.removeProjectFromFavorites(workspaceSlug.toString(), project.id).catch(() => {
removeProjectFromFavorites(workspaceSlug.toString(), project.id).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
@ -132,11 +133,13 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
if (!project) return null;
return (
<>
<PublishProjectModal isOpen={publishModalOpen} project={project} onClose={() => setPublishModal(false)} />
<LeaveProjectModal project={project} isOpen={leaveProjectModalOpen} onClose={handleLeaveProjectModalClose} />
<Disclosure key={`${project.id} ${projectId}`} defaultOpen={projectId === project.id}>
<Disclosure key={`${project.id} ${URLProjectId}`} defaultOpen={URLProjectId === project.id}>
{({ open }) => (
<>
<div
@ -213,7 +216,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
</div>
}
className={`hidden flex-shrink-0 group-hover:block ${isMenuActive ? "!block" : ""}`}
buttonClassName="!text-custom-sidebar-text-400 hover:text-custom-sidebar-text-400"
buttonClassName="!text-custom-sidebar-text-400"
ellipsis
placement="bottom-start"
>

View file

@ -1,22 +1,16 @@
import React, { useState, FC, useRef, useEffect } from "react";
import { useState, FC, useRef, useEffect } from "react";
import { useRouter } from "next/router";
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
import { Disclosure, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite";
import { ChevronDown, ChevronRight, Plus } from "lucide-react";
// hooks
import { useApplication, useProject, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { CreateProjectModal, ProjectSidebarListItem } from "components/project";
// icons
import { ChevronDown, ChevronRight, Plus } from "lucide-react";
// helpers
import { copyUrlToClipboard } from "helpers/string.helper";
import { orderArrayBy } from "helpers/array.helper";
// types
import { IProject } from "types";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
@ -30,28 +24,26 @@ export const ProjectSidebarList: FC = observer(() => {
const {
theme: { sidebarCollapsed },
project: { joinedProjects, favoriteProjects, orderProjectsWithSortOrder, updateProjectView },
commandPalette: { toggleCreateProjectModal },
trackEvent: { setTrackElement },
user: { currentWorkspaceRole },
} = useMobxStore();
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentWorkspaceRole },
} = useUser();
const {
joinedProjectIds: joinedProjects,
favoriteProjectIds: favoriteProjects,
orderProjectsWithSortOrder,
updateProjectView,
} = useProject();
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// toast
const { setToastAlert } = useToast();
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const orderedJoinedProjects: IProject[] | undefined = joinedProjects
? orderArrayBy(joinedProjects, "sort_order", "ascending")
: undefined;
const orderedFavProjects: IProject[] | undefined = favoriteProjects
? orderArrayBy(favoriteProjects, "sort_order", "ascending")
: undefined;
const handleCopyText = (projectId: string) => {
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => {
setToastAlert({
@ -125,7 +117,7 @@ export const ProjectSidebarList: FC = observer(() => {
<Droppable droppableId="favorite-projects">
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{orderedFavProjects && orderedFavProjects.length > 0 && (
{favoriteProjects && favoriteProjects.length > 0 && (
<Disclosure as="div" className="flex flex-col" defaultOpen>
{({ open }) => (
<>
@ -166,21 +158,22 @@ export const ProjectSidebarList: FC = observer(() => {
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel as="div" className="space-y-2">
{orderedFavProjects.map((project, index) => (
{favoriteProjects.map((projectId, index) => (
<Draggable
key={project.id}
draggableId={project.id}
key={projectId}
draggableId={projectId}
index={index}
isDragDisabled={!project.is_member}
// FIXME refactor the Draggable to a different component
//isDragDisabled={!project.is_member}
>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps}>
<ProjectSidebarListItem
key={project.id}
project={project}
key={projectId}
projectId={projectId}
provided={provided}
snapshot={snapshot}
handleCopyText={() => handleCopyText(project.id)}
handleCopyText={() => handleCopyText(projectId)}
shortContextMenu
/>
</div>
@ -202,7 +195,7 @@ export const ProjectSidebarList: FC = observer(() => {
<Droppable droppableId="joined-projects">
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{orderedJoinedProjects && orderedJoinedProjects.length > 0 && (
{joinedProjects && joinedProjects.length > 0 && (
<Disclosure as="div" className="flex flex-col" defaultOpen>
{({ open }) => (
<>
@ -242,16 +235,16 @@ export const ProjectSidebarList: FC = observer(() => {
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel as="div" className="space-y-2">
{orderedJoinedProjects.map((project, index) => (
<Draggable key={project.id} draggableId={project.id} index={index}>
{joinedProjects.map((projectId, index) => (
<Draggable key={projectId} draggableId={projectId} index={index}>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps}>
<ProjectSidebarListItem
key={project.id}
project={project}
key={projectId}
projectId={projectId}
provided={provided}
snapshot={snapshot}
handleCopyText={() => handleCopyText(project.id)}
handleCopyText={() => handleCopyText(projectId)}
/>
</div>
)}