From 15f621ad910f6dcd9a44d7f37e71419a0eaecaee Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 18 Oct 2023 20:29:56 +0530 Subject: [PATCH] fix: command palette fixes and sidebar fixes (#2482) * fix: project fav changes * fix: project create workspace member * style: member select dropdown ui and command k modal alignment fix (#2473) * style: member select dropdown fix * style: command k modal alignment fix * fix: project create modal changes * fix: sidebar shortcut fixes * fix: minor console issues --------- Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> --- .../command-palette/command-modal.tsx | 884 +++++++++--------- .../command-palette/command-pallette.tsx | 20 +- web/components/project/card-list.tsx | 2 - .../project/create-project-modal.tsx | 180 ++-- web/components/ui/avatar.tsx | 11 +- web/components/workspace/help-section.tsx | 4 +- web/components/workspace/index.ts | 1 + web/components/workspace/member-select.tsx | 146 +++ web/constants/project.ts | 19 + web/lib/mobx/store-init.tsx | 13 +- web/store/project/project.store.ts | 39 +- web/store/theme.store.ts | 21 +- web/store/workspace/workspace.store.ts | 4 +- 13 files changed, 761 insertions(+), 583 deletions(-) create mode 100644 web/components/workspace/member-select.tsx diff --git a/web/components/command-palette/command-modal.tsx b/web/components/command-palette/command-modal.tsx index bf4da0f30..33f8047da 100644 --- a/web/components/command-palette/command-modal.tsx +++ b/web/components/command-palette/command-modal.tsx @@ -255,505 +255,507 @@ export const CommandModal: React.FC = (props) => { leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - - { - if (value.toLowerCase().includes(search.toLowerCase())) return 1; - return 0; - }} - onKeyDown={(e) => { - // when search is empty and page is undefined - // when user tries to close the modal with esc - if (e.key === "Escape" && !page && !searchTerm) { - closePalette(); - } - // Escape goes to previous page - // Backspace goes to previous page when search is empty - if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) { - e.preventDefault(); - setPages((pages) => pages.slice(0, -1)); - setPlaceholder("Type a command or search..."); - } - }} - > -
+
+ { + if (value.toLowerCase().includes(search.toLowerCase())) return 1; + return 0; + }} + onKeyDown={(e) => { + // when search is empty and page is undefined + // when user tries to close the modal with esc + if (e.key === "Escape" && !page && !searchTerm) { + closePalette(); + } + // Escape goes to previous page + // Backspace goes to previous page when search is empty + if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) { + e.preventDefault(); + setPages((pages) => pages.slice(0, -1)); + setPlaceholder("Type a command or search..."); + } + }} > - {issueDetails && ( -
- {issueDetails.project_detail.identifier}-{issueDetails.sequence_id} {issueDetails.name} -
- )} - {projectId && ( - -
- - setIsWorkspaceLevel((prevData) => !prevData)} - /> +
+ {issueDetails && ( +
+ {issueDetails.project_detail.identifier}-{issueDetails.sequence_id} {issueDetails.name}
- - )} -
-
-
+ )} + {projectId && ( + +
+ + setIsWorkspaceLevel((prevData) => !prevData)} + /> +
+
+ )} +
+
+
- - {searchTerm !== "" && ( -
- Search results for{" "} - - {'"'} - {searchTerm} - {'"'} - {" "} - in {!projectId || isWorkspaceLevel ? "workspace" : "project"}: -
- )} + + {searchTerm !== "" && ( +
+ Search results for{" "} + + {'"'} + {searchTerm} + {'"'} + {" "} + in {!projectId || isWorkspaceLevel ? "workspace" : "project"}: +
+ )} - {!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && ( -
No results found.
- )} + {!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && ( +
No results found.
+ )} - {(isLoading || isSearching) && ( - - - - - - - - - )} + {(isLoading || isSearching) && ( + + + + + + + + + )} - {debouncedSearchTerm !== "" && - Object.keys(results.results).map((key) => { - const section = (results.results as any)[key]; - const currentSection = commandGroups[key]; + {debouncedSearchTerm !== "" && + Object.keys(results.results).map((key) => { + const section = (results.results as any)[key]; + const currentSection = commandGroups[key]; - if (section.length > 0) { - return ( - - {section.map((item: any) => ( - { - closePalette(); - router.push(currentSection.path(item)); - }} - value={`${key}-${item?.name}`} - className="focus:outline-none" - > -
- {currentSection.icon} -

{currentSection.itemName(item)}

-
-
- ))} + if (section.length > 0) { + return ( + + {section.map((item: any) => ( + { + closePalette(); + router.push(currentSection.path(item)); + }} + value={`${key}-${item?.name}`} + className="focus:outline-none" + > +
+ {currentSection.icon} +

{currentSection.itemName(item)}

+
+
+ ))} +
+ ); + } + })} + + {!page && ( + <> + {issueId && ( + + { + closePalette(); + setPlaceholder("Change state..."); + setSearchTerm(""); + setPages([...pages, "change-issue-state"]); + }} + className="focus:outline-none" + > +
+ + Change state... +
+
+ { + setPlaceholder("Change priority..."); + setSearchTerm(""); + setPages([...pages, "change-issue-priority"]); + }} + className="focus:outline-none" + > +
+ + Change priority... +
+
+ { + setPlaceholder("Assign to..."); + setSearchTerm(""); + setPages([...pages, "change-issue-assignee"]); + }} + className="focus:outline-none" + > +
+ + Assign to... +
+
+ { + handleIssueAssignees(user.id); + setSearchTerm(""); + }} + className="focus:outline-none" + > +
+ {issueDetails?.assignees.includes(user.id) ? ( + <> + + Un-assign from me + + ) : ( + <> + + Assign to me + + )} +
+
+ +
+ + Delete issue +
+
+ { + closePalette(); + copyIssueUrlToClipboard(); + }} + className="focus:outline-none" + > +
+ + Copy issue URL +
+
- ); - } - })} - - {!page && ( - <> - {issueId && ( - - { - closePalette(); - setPlaceholder("Change state..."); - setSearchTerm(""); - setPages([...pages, "change-issue-state"]); - }} - className="focus:outline-none" - > -
- - Change state... -
-
- { - setPlaceholder("Change priority..."); - setSearchTerm(""); - setPages([...pages, "change-issue-priority"]); - }} - className="focus:outline-none" - > -
- - Change priority... -
-
- { - setPlaceholder("Assign to..."); - setSearchTerm(""); - setPages([...pages, "change-issue-assignee"]); - }} - className="focus:outline-none" - > -
- - Assign to... -
-
- { - handleIssueAssignees(user.id); - setSearchTerm(""); - }} - className="focus:outline-none" - > -
- {issueDetails?.assignees.includes(user.id) ? ( - <> - - Un-assign from me - - ) : ( - <> - - Assign to me - - )} -
-
- -
- - Delete issue -
-
- { - closePalette(); - copyIssueUrlToClipboard(); - }} - className="focus:outline-none" - > -
- - Copy issue URL -
-
-
- )} - - { - closePalette(); - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); - }} - className="focus:bg-custom-background-80" - > -
- - Create new issue -
- C -
-
- - {workspaceSlug && ( - + )} + { closePalette(); const e = new KeyboardEvent("keydown", { - key: "p", + key: "c", + }); + document.dispatchEvent(e); + }} + className="focus:bg-custom-background-80" + > +
+ + Create new issue +
+ C +
+
+ + {workspaceSlug && ( + + { + closePalette(); + const e = new KeyboardEvent("keydown", { + key: "p", + }); + document.dispatchEvent(e); + }} + className="focus:outline-none" + > +
+ + Create new project +
+ P +
+
+ )} + + {projectId && ( + <> + + { + closePalette(); + const e = new KeyboardEvent("keydown", { + key: "q", + }); + document.dispatchEvent(e); + }} + className="focus:outline-none" + > +
+ + Create new cycle +
+ Q +
+
+ + { + closePalette(); + const e = new KeyboardEvent("keydown", { + key: "m", + }); + document.dispatchEvent(e); + }} + className="focus:outline-none" + > +
+ + Create new module +
+ M +
+
+ + { + closePalette(); + const e = new KeyboardEvent("keydown", { + key: "v", + }); + document.dispatchEvent(e); + }} + className="focus:outline-none" + > +
+ + Create new view +
+ V +
+
+ + { + closePalette(); + const e = new KeyboardEvent("keydown", { + key: "d", + }); + document.dispatchEvent(e); + }} + className="focus:outline-none" + > +
+ + Create new page +
+ D +
+
+ + )} + + + { + setPlaceholder("Search workspace settings..."); + setSearchTerm(""); + setPages([...pages, "settings"]); + }} + className="focus:outline-none" + > +
+ + Search settings... +
+
+
+ + +
+ + Create new workspace +
+
+ { + setPlaceholder("Change interface theme..."); + setSearchTerm(""); + setPages([...pages, "change-interface-theme"]); + }} + className="focus:outline-none" + > +
+ + Change interface theme... +
+
+
+ + { + closePalette(); + const e = new KeyboardEvent("keydown", { + key: "h", }); document.dispatchEvent(e); }} className="focus:outline-none" >
- - Create new project + + Open keyboard shortcuts +
+
+ { + closePalette(); + window.open("https://docs.plane.so/", "_blank"); + }} + className="focus:outline-none" + > +
+ + Open Plane documentation +
+
+ { + closePalette(); + window.open("https://discord.com/invite/A92xrEGCge", "_blank"); + }} + className="focus:outline-none" + > +
+ + Join our Discord +
+
+ { + closePalette(); + window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank"); + }} + className="focus:outline-none" + > +
+ + Report a bug +
+
+ { + closePalette(); + (window as any).$crisp.push(["do", "chat:open"]); + }} + className="focus:outline-none" + > +
+ + Chat with us
- P
- )} + + )} - {projectId && ( - <> - - { - closePalette(); - const e = new KeyboardEvent("keydown", { - key: "q", - }); - document.dispatchEvent(e); - }} - className="focus:outline-none" - > -
- - Create new cycle -
- Q -
-
- - { - closePalette(); - const e = new KeyboardEvent("keydown", { - key: "m", - }); - document.dispatchEvent(e); - }} - className="focus:outline-none" - > -
- - Create new module -
- M -
-
- - { - closePalette(); - const e = new KeyboardEvent("keydown", { - key: "v", - }); - document.dispatchEvent(e); - }} - className="focus:outline-none" - > -
- - Create new view -
- V -
-
- - { - closePalette(); - const e = new KeyboardEvent("keydown", { - key: "d", - }); - document.dispatchEvent(e); - }} - className="focus:outline-none" - > -
- - Create new page -
- D -
-
- - )} - - + {page === "settings" && workspaceSlug && ( + <> { - setPlaceholder("Search workspace settings..."); - setSearchTerm(""); - setPages([...pages, "settings"]); - }} + onSelect={() => redirect(`/${workspaceSlug}/settings`)} className="focus:outline-none" >
- - Search settings... -
-
-
- - -
- - Create new workspace + + General
{ - setPlaceholder("Change interface theme..."); - setSearchTerm(""); - setPages([...pages, "change-interface-theme"]); - }} + onSelect={() => redirect(`/${workspaceSlug}/settings/members`)} className="focus:outline-none" >
- - Change interface theme... -
-
-
- - { - closePalette(); - const e = new KeyboardEvent("keydown", { - key: "h", - }); - document.dispatchEvent(e); - }} - className="focus:outline-none" - > -
- - Open keyboard shortcuts + + Members
{ - closePalette(); - window.open("https://docs.plane.so/", "_blank"); - }} + onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)} className="focus:outline-none" >
- - Open Plane documentation + + Billing and Plans
{ - closePalette(); - window.open("https://discord.com/invite/A92xrEGCge", "_blank"); - }} + onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)} className="focus:outline-none" >
- - Join our Discord + + Integrations
{ - closePalette(); - window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank"); - }} + onSelect={() => redirect(`/${workspaceSlug}/settings/imports`)} className="focus:outline-none" >
- - Report a bug + + Import
{ - closePalette(); - (window as any).$crisp.push(["do", "chat:open"]); - }} + onSelect={() => redirect(`/${workspaceSlug}/settings/exports`)} className="focus:outline-none" >
- - Chat with us + + Export
-
- - )} - - {page === "settings" && workspaceSlug && ( - <> - redirect(`/${workspaceSlug}/settings`)} - className="focus:outline-none" - > -
- - General -
-
- redirect(`/${workspaceSlug}/settings/members`)} - className="focus:outline-none" - > -
- - Members -
-
- redirect(`/${workspaceSlug}/settings/billing`)} - className="focus:outline-none" - > -
- - Billing and Plans -
-
- redirect(`/${workspaceSlug}/settings/integrations`)} - className="focus:outline-none" - > -
- - Integrations -
-
- redirect(`/${workspaceSlug}/settings/imports`)} - className="focus:outline-none" - > -
- - Import -
-
- redirect(`/${workspaceSlug}/settings/exports`)} - className="focus:outline-none" - > -
- - Export -
-
- - )} - {page === "change-issue-state" && issueDetails && ( - - )} - {page === "change-issue-priority" && issueDetails && ( - - )} - {page === "change-issue-assignee" && issueDetails && ( - - )} - {page === "change-interface-theme" && } -
-
+ + )} + {page === "change-issue-state" && issueDetails && ( + + )} + {page === "change-issue-priority" && issueDetails && ( + + )} + {page === "change-issue-assignee" && issueDetails && ( + + )} + {page === "change-interface-theme" && } + + +
diff --git a/web/components/command-palette/command-pallette.tsx b/web/components/command-palette/command-pallette.tsx index e42317a6b..985443158 100644 --- a/web/components/command-palette/command-pallette.tsx +++ b/web/components/command-palette/command-pallette.tsx @@ -53,7 +53,7 @@ export const CommandPalette: FC = observer(() => { isDeleteIssueModalOpen, toggleDeleteIssueModal, } = commandPalette; - const { setSidebarCollapsed } = themeStore; + const { toggleSidebar } = themeStore; const { user } = useUser(); @@ -109,22 +109,22 @@ export const CommandPalette: FC = observer(() => { copyIssueUrlToClipboard(); } else if (keyPressed === "b") { e.preventDefault(); - setSidebarCollapsed(); + toggleSidebar(); } } else { if (keyPressed === "c") { toggleCreateIssueModal(true); } else if (keyPressed === "p") { toggleCreateProjectModal(true); - } else if (keyPressed === "v") { - toggleCreateViewModal(true); - } else if (keyPressed === "d") { - toggleCreatePageModal(true); } else if (keyPressed === "h") { toggleShortcutModal(true); - } else if (keyPressed === "q") { + } else if (keyPressed === "v" && workspaceSlug && projectId) { + toggleCreateViewModal(true); + } else if (keyPressed === "d" && workspaceSlug && projectId) { + toggleCreatePageModal(true); + } else if (keyPressed === "q" && workspaceSlug && projectId) { toggleCreateCycleModal(true); - } else if (keyPressed === "m") { + } else if (keyPressed === "m" && workspaceSlug && projectId) { toggleCreateModuleModal(true); } else if (keyPressed === "backspace" || keyPressed === "delete") { e.preventDefault(); @@ -142,8 +142,10 @@ export const CommandPalette: FC = observer(() => { toggleCreateModuleModal, toggleBulkDeleteIssueModal, toggleCommandPaletteModal, - setSidebarCollapsed, + toggleSidebar, toggleCreateIssueModal, + projectId, + workspaceSlug, ] ); diff --git a/web/components/project/card-list.tsx b/web/components/project/card-list.tsx index 681c96afe..37432e367 100644 --- a/web/components/project/card-list.tsx +++ b/web/components/project/card-list.tsx @@ -22,8 +22,6 @@ export const ProjectCardList: FC = observer((props) => { const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; - console.log("projects", projects); - if (!projects) { return ( diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 88548f544..56c3e6f05 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -1,16 +1,14 @@ -import { useState, useEffect, Fragment, FC } from "react"; -import { useRouter } from "next/router"; +import { useState, useEffect, Fragment, FC, ChangeEvent } from "react"; import { useForm, Controller } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; // icons -import { Users2, X } from "lucide-react"; +import { X } from "lucide-react"; // hooks import { useMobxStore } from "lib/mobx/store-provider"; import useToast from "hooks/use-toast"; import { useWorkspaceMyMembership } from "contexts/workspace-member.context"; -import useWorkspaceMembers from "hooks/use-workspace-members"; // ui -import { CustomSelect, Avatar, CustomSearchSelect } from "components/ui"; +import { CustomSelect } from "components/ui"; import { Button, Input, TextArea } from "@plane/ui"; // components import { ImagePickerPopover } from "components/core"; @@ -18,9 +16,11 @@ import EmojiIconPicker from "components/emoji-icon-picker"; // helpers import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; // types -import { IProject } from "types"; +import { IWorkspaceMember } from "types"; // constants -import { NETWORK_CHOICES } from "constants/project"; +import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project"; +import { WorkspaceMemberSelect } from "components/workspace"; +import { observer } from "mobx-react-lite"; type Props = { isOpen: boolean; @@ -29,17 +29,6 @@ type Props = { workspaceSlug: string; }; -const defaultValues: Partial = { - cover_image: - "https://images.unsplash.com/photo-1531045535792-b515d59c3d1f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80", - description: "", - emoji_and_icon: getRandomEmoji(), - identifier: "", - name: "", - network: 2, - project_lead: null, -}; - interface IIsGuestCondition { onClose: () => void; } @@ -59,18 +48,30 @@ const IsGuestCondition: FC = ({ onClose }) => { return null; }; -export const CreateProjectModal: React.FC = (props) => { +export interface ICreateProjectForm { + name: string; + identifier: string; + description: string; + emoji_and_icon: string; + network: number; + project_lead_member: IWorkspaceMember; + project_lead: string; + cover_image: string; + icon_prop: any; + emoji: string; +} + +export const CreateProjectModal: FC = observer((props) => { const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props; // store - const { project: projectStore } = useMobxStore(); + const { project: projectStore, workspace: workspaceStore } = useMobxStore(); + const workspaceMembers = workspaceStore.members[workspaceSlug] || []; // states const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); - + // toast const { setToastAlert } = useToast(); - - const { memberDetails } = useWorkspaceMyMembership(); - const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); - + // form info + const cover_image = PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)]; const { formState: { errors, isSubmitting }, handleSubmit, @@ -78,15 +79,29 @@ export const CreateProjectModal: React.FC = (props) => { control, watch, setValue, - } = useForm({ - defaultValues, + } = useForm({ + defaultValues: { + cover_image, + description: "", + emoji_and_icon: getRandomEmoji(), + identifier: "", + name: "", + network: 2, + project_lead: undefined, + }, reValidateMode: "onChange", }); + const { memberDetails } = useWorkspaceMyMembership(); + + const currentNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network")); + + if (memberDetails && isOpen) if (memberDetails.role <= 10) return ; + const handleClose = () => { onClose(); setIsChangeInIdentifierRequired(true); - reset(defaultValues); + reset(); }; const handleAddToFavorites = (projectId: string) => { @@ -101,16 +116,16 @@ export const CreateProjectModal: React.FC = (props) => { }); }; - const onSubmit = async (formData: IProject) => { - if (!workspaceSlug) return; - + const onSubmit = async (formData: ICreateProjectForm) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { emoji_and_icon, ...payload } = formData; + const { emoji_and_icon, project_lead_member, ...payload } = formData; if (typeof formData.emoji_and_icon === "object") payload.icon_prop = formData.emoji_and_icon; else payload.emoji = formData.emoji_and_icon; - await projectStore + payload.project_lead = formData.project_lead_member?.member.id; + + return projectStore .createProject(workspaceSlug.toString(), payload) .then((res) => { setToastAlert({ @@ -134,9 +149,11 @@ export const CreateProjectModal: React.FC = (props) => { }); }; - const changeIdentifierOnNameChange = (e: React.ChangeEvent) => { - if (!isChangeInIdentifierRequired) return; - + const handleNameChange = (onChange: any) => (e: ChangeEvent) => { + if (!isChangeInIdentifierRequired) { + onChange(e); + return; + } if (e.target.value === "") setValue("identifier", ""); else setValue( @@ -146,32 +163,16 @@ export const CreateProjectModal: React.FC = (props) => { .toUpperCase() .substring(0, 5) ); + onChange(e); }; - const handleIdentifierChange = (e: React.ChangeEvent) => { + const handleIdentifierChange = (onChange: any) => (e: ChangeEvent) => { const { value } = e.target; - const alphanumericValue = value.replace(/[^a-zA-Z0-9]/g, ""); - - setValue("identifier", alphanumericValue.toUpperCase()); setIsChangeInIdentifierRequired(false); + onChange(alphanumericValue.toUpperCase()); }; - const options = workspaceMembers?.map((member: any) => ({ - value: member.member.id, - query: member.member.display_name, - content: ( -
- - {member.member.display_name} -
- ), - })); - - const currentNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network")); - - if (memberDetails && isOpen) if (memberDetails.role <= 10) return ; - return ( @@ -255,20 +256,20 @@ export const CreateProjectModal: React.FC = (props) => { message: "Title should be less than 255 characters", }, }} - render={({ field: { value, ref } }) => ( + render={({ field: { value, onChange } }) => ( )} /> + {errors?.name?.message}
= (props) => { message: "Identifier must at most be of 12 characters", }, }} - render={({ field: { value, ref } }) => ( + render={({ field: { value, onChange } }) => ( )} /> + {errors?.identifier?.message}
= (props) => { placeholder="Description..." onChange={onChange} className="text-sm !h-24" - hasError={Boolean(errors?.name)} + hasError={Boolean(errors?.description)} /> )} /> @@ -330,12 +331,12 @@ export const CreateProjectModal: React.FC = (props) => { +
{currentNetwork ? ( <> - + {currentNetwork.label} ) : ( @@ -351,7 +352,7 @@ export const CreateProjectModal: React.FC = (props) => { value={network.key} className="flex items-center gap-1" > - + {network.label} ))} @@ -361,39 +362,16 @@ export const CreateProjectModal: React.FC = (props) => {
{ - const selectedMember = workspaceMembers?.find((m: any) => m.member.id === value); - - return ( - - {value ? ( - <> - - {selectedMember?.member.display_name} - onChange(null)}> - - - - ) : ( - <> - - Lead - - )} -
- } - noChevron - /> - ); - }} + render={({ field: { value, onChange } }) => ( + + )} />
@@ -415,4 +393,4 @@ export const CreateProjectModal: React.FC = (props) => {
); -}; +}); diff --git a/web/components/ui/avatar.tsx b/web/components/ui/avatar.tsx index cef8e2c62..0eb76fe93 100644 --- a/web/components/ui/avatar.tsx +++ b/web/components/ui/avatar.tsx @@ -18,12 +18,20 @@ type AvatarProps = { height?: string; width?: string; fontSize?: string; + showName?: boolean; }; // services const workspaceService = new WorkspaceService(); -export const Avatar: React.FC = ({ user, index, height = "24px", width = "24px", fontSize = "12px" }) => ( +export const Avatar: React.FC = ({ + user, + index, + height = "24px", + width = "24px", + fontSize = "12px", + showName, +}) => (
= ({ user, index, height = "24px", wi {user?.display_name?.charAt(0)}
)} + {showName && {user?.display_name ? user?.display_name : user?.first_name}} ); diff --git a/web/components/workspace/help-section.tsx b/web/components/workspace/help-section.tsx index c10ec5992..87dbbf373 100644 --- a/web/components/workspace/help-section.tsx +++ b/web/components/workspace/help-section.tsx @@ -92,7 +92,7 @@ export const WorkspaceHelpSection: React.FC = observe @@ -101,7 +101,7 @@ export const WorkspaceHelpSection: React.FC = observe className={`hidden md:grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${ isCollapsed ? "w-full" : "" }`} - onClick={() => themeStore.setSidebarCollapsed(!isCollapsed)} + onClick={() => themeStore.toggleSidebar()} > diff --git a/web/components/workspace/index.ts b/web/components/workspace/index.ts index a3fcfc03c..9fe2934b0 100644 --- a/web/components/workspace/index.ts +++ b/web/components/workspace/index.ts @@ -10,3 +10,4 @@ export * from "./issues-stats"; export * from "./sidebar-dropdown"; export * from "./sidebar-menu"; export * from "./sidebar-quick-action"; +export * from "./member-select"; diff --git a/web/components/workspace/member-select.tsx b/web/components/workspace/member-select.tsx new file mode 100644 index 000000000..3fef75cc8 --- /dev/null +++ b/web/components/workspace/member-select.tsx @@ -0,0 +1,146 @@ +import React, { FC, useState, Fragment } from "react"; +// popper js +import { usePopper } from "react-popper"; +// ui +import { Input, Tooltip } from "@plane/ui"; +import { Listbox } from "@headlessui/react"; +import { Avatar } from "components/ui"; +// icons +import { Check, Search, User2 } from "lucide-react"; +// types +import { IWorkspaceMember } from "types"; + +export interface IWorkspaceMemberSelect { + value: IWorkspaceMember | undefined; + onChange: (value: IWorkspaceMember) => void; + options: IWorkspaceMember[]; + placeholder?: string; + disabled?: boolean; +} + +export const WorkspaceMemberSelect: FC = (props) => { + const { value, onChange, options, placeholder = "Select Member", disabled = false } = props; + // states + const [query, setQuery] = useState(""); + + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "auto", + }); + + // const options = workspaceMembers?.map((member: any) => ({ + // value: member.member.id, + // query: member.member.display_name, + // content: ( + //
+ // + // {member.member.display_name} + //
+ // ), + // })); + + // const selectedOption = workspaceMembers?.find((member) => member.member.id === value); + + const filteredOptions = + query === "" + ? options + : options?.filter((option) => option.member.display_name.toLowerCase().includes(query.toLowerCase())); + + const label = ( + 0 + ? options.map((assignee) => assignee?.member.display_name).join(", ") + : "No Assignee" + } + position="top" + > +
+ {value ? ( + <> + + {value?.member.display_name} + + ) : ( + <> + + {placeholder} + + )} +
+
+ ); + + return ( + + + + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((workspaceMember: IWorkspaceMember) => ( + + `flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${ + active && !selected ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> +
+ + {workspaceMember.member.display_name} +
+ {selected && } + + )} +
+ )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}; diff --git a/web/constants/project.ts b/web/constants/project.ts index 3de846415..f8508ad45 100644 --- a/web/constants/project.ts +++ b/web/constants/project.ts @@ -50,3 +50,22 @@ export const PROJECT_AUTOMATION_MONTHS = [ { label: "9 Months", value: 9 }, { label: "12 Months", value: 12 }, ]; + +export const PROJECT_UNSPLASH_COVERS = [ + "https://images.unsplash.com/photo-1531045535792-b515d59c3d1f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80", + "https://images.unsplash.com/photo-1693027407934-e3aa8a54c7ae?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80", + "https://images.unsplash.com/photo-1518837695005-2083093ee35b?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80", + "https://images.unsplash.com/photo-1464925257126-6450e871c667?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80", + "https://images.unsplash.com/photo-1606768666853-403c90a981ad?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80", + "https://images.unsplash.com/photo-1627556592933-ffe99c1cd9eb?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80", + "https://images.unsplash.com/photo-1643330683233-ff2ac89b002c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80", + "https://images.unsplash.com/photo-1542202229-7d93c33f5d07?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80", + "https://images.unsplash.com/photo-1511497584788-876760111969?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80", + "https://images.unsplash.com/photo-1475738972911-5b44ce984c42?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80", + "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80", + "https://images.unsplash.com/photo-1673393058808-50e9baaf4d2c?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80", + "https://images.unsplash.com/photo-1696643830146-44a8755f1905?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80", + "https://images.unsplash.com/photo-1693868769698-6c7440636a09?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80", + "https://images.unsplash.com/photo-1691230995681-480d86cbc135?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80", + "https://images.unsplash.com/photo-1675351066828-6fc770b90dd2?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80", +]; diff --git a/web/lib/mobx/store-init.tsx b/web/lib/mobx/store-init.tsx index 0c8a71e54..b4a4d90ba 100644 --- a/web/lib/mobx/store-init.tsx +++ b/web/lib/mobx/store-init.tsx @@ -30,14 +30,11 @@ const MobxStoreInit = observer(() => { useEffect(() => { // sidebar collapsed toggle - if (localStorage && localStorage.getItem("app_sidebar_collapsed") && themeStore?.sidebarCollapsed === null) - themeStore.setSidebarCollapsed( - localStorage.getItem("app_sidebar_collapsed") - ? localStorage.getItem("app_sidebar_collapsed") === "true" - ? true - : false - : false - ); + const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed"); + const localBoolValue = localValue ? (localValue === "true" ? true : false) : false; + if (localValue && themeStore?.sidebarCollapsed === undefined) { + themeStore.toggleSidebar(localBoolValue); + } }, [themeStore, userStore, setTheme]); useEffect(() => { diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index 7761ec5aa..ea94fc35e 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -68,7 +68,7 @@ export interface IProjectStore { joinProject: (workspaceSlug: string, projectIds: string[]) => Promise; leaveProject: (workspaceSlug: string, projectId: string) => Promise; createProject: (workspaceSlug: string, data: any) => Promise; - updateProject: (workspaceSlug: string, projectId: string, data: any) => Promise; + updateProject: (workspaceSlug: string, projectId: string, data: Partial) => Promise; deleteProject: (workspaceSlug: string, projectId: string) => Promise; } @@ -413,17 +413,39 @@ export class ProjectStore implements IProjectStore { addProjectToFavorites = async (workspaceSlug: string, projectId: string) => { try { + runInAction(() => { + this.projects = { + ...this.projects, + [workspaceSlug]: this.projects[workspaceSlug].map((project) => { + if (project.id === projectId) { + return { ...project, is_favorite: true }; + } + return project; + }), + }; + }); const response = await this.projectService.addProjectToFavorites(workspaceSlug, projectId); - await this.fetchProjects(workspaceSlug); return response; } catch (error) { console.log("Failed to add project to favorite"); + await this.fetchProjects(workspaceSlug); throw error; } }; removeProjectFromFavorites = async (workspaceSlug: string, projectId: string) => { try { + runInAction(() => { + this.projects = { + ...this.projects, + [workspaceSlug]: this.projects[workspaceSlug].map((project) => { + if (project.id === projectId) { + return { ...project, is_favorite: false }; + } + return project; + }), + }; + }); const response = await this.projectService.removeProjectFromFavorites(workspaceSlug, projectId); await this.fetchProjects(workspaceSlug); return response; @@ -546,19 +568,26 @@ export class ProjectStore implements IProjectStore { } }; - updateProject = async (workspaceSlug: string, projectId: string, data: any) => { + updateProject = async (workspaceSlug: string, projectId: string, data: Partial) => { try { + runInAction(() => { + this.projects = { + ...this.projects, + [workspaceSlug]: this.projects[workspaceSlug].map((p) => (p.id === projectId ? { ...p, ...data } : p)), + }; + }); + const response = await this.projectService.updateProject( workspaceSlug, projectId, data, this.rootStore.user.currentUser ); - await this.fetchProjectDetails(workspaceSlug, projectId); - await this.fetchProjects(workspaceSlug); return response; } catch (error) { console.log("Failed to create project from project store"); + + this.fetchProjects(workspaceSlug); throw error; } }; diff --git a/web/store/theme.store.ts b/web/store/theme.store.ts index 585e7f91d..51189a75d 100644 --- a/web/store/theme.store.ts +++ b/web/store/theme.store.ts @@ -5,14 +5,14 @@ import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper"; export interface IThemeStore { theme: string | null; - sidebarCollapsed: boolean | null; + sidebarCollapsed: boolean | undefined; - setSidebarCollapsed: (collapsed?: boolean) => void; + toggleSidebar: (collapsed?: boolean) => void; setTheme: (theme: any) => void; } class ThemeStore implements IThemeStore { - sidebarCollapsed: boolean | null = null; + sidebarCollapsed: boolean | undefined = undefined; theme: string | null = null; // root store rootStore; @@ -23,7 +23,7 @@ class ThemeStore implements IThemeStore { sidebarCollapsed: observable.ref, theme: observable.ref, // action - setSidebarCollapsed: action, + toggleSidebar: action, setTheme: action, // computed }); @@ -31,17 +31,14 @@ class ThemeStore implements IThemeStore { this.rootStore = _rootStore; this.initialLoad(); } - - setSidebarCollapsed(collapsed?: boolean) { - if (!collapsed) { - let _sidebarCollapsed: string | boolean | null = localStorage.getItem("app_sidebar_collapsed"); - _sidebarCollapsed = _sidebarCollapsed ? (_sidebarCollapsed === "true" ? true : false) : false; - this.sidebarCollapsed = _sidebarCollapsed; + toggleSidebar = (collapsed?: boolean) => { + if (collapsed === undefined) { + this.sidebarCollapsed = !this.sidebarCollapsed; } else { this.sidebarCollapsed = collapsed; - localStorage.setItem("app_sidebar_collapsed", collapsed.toString()); } - } + localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString()); + }; setTheme = async (_theme: { theme: any }) => { try { diff --git a/web/store/workspace/workspace.store.ts b/web/store/workspace/workspace.store.ts index a226720ad..9b27d255c 100644 --- a/web/store/workspace/workspace.store.ts +++ b/web/store/workspace/workspace.store.ts @@ -15,8 +15,8 @@ export interface IWorkspaceStore { // observables workspaceSlug: string | null; workspaces: IWorkspace[]; - labels: { [workspaceSlug: string]: IIssueLabels[] } | {}; // workspaceSlug: labels[] - members: { [workspaceSlug: string]: IWorkspaceMember[] } | {}; // workspaceSlug: members[] + labels: { [workspaceSlug: string]: IIssueLabels[] }; // workspaceSlug: labels[] + members: { [workspaceSlug: string]: IWorkspaceMember[] }; // workspaceSlug: members[] // actions setWorkspaceSlug: (workspaceSlug: string) => void;