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>
This commit is contained in:
sriram veeraghanta 2023-10-18 20:29:56 +05:30 committed by GitHub
parent 9b96e297b3
commit 15f621ad91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 761 additions and 583 deletions

View File

@ -255,505 +255,507 @@ export const CommandModal: React.FC<Props> = (props) => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-custom-border-200 divide-opacity-10 rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all"> <Dialog.Panel className="relative flex items-center justify-center w-full ">
<Command <div className="w-full max-w-2xl transform divide-y divide-custom-border-200 divide-opacity-10 rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all">
filter={(value, search) => { <Command
if (value.toLowerCase().includes(search.toLowerCase())) return 1; filter={(value, search) => {
return 0; if (value.toLowerCase().includes(search.toLowerCase())) return 1;
}} return 0;
onKeyDown={(e) => { }}
// when search is empty and page is undefined onKeyDown={(e) => {
// when user tries to close the modal with esc // when search is empty and page is undefined
if (e.key === "Escape" && !page && !searchTerm) { // when user tries to close the modal with esc
closePalette(); if (e.key === "Escape" && !page && !searchTerm) {
} closePalette();
// Escape goes to previous page }
// Backspace goes to previous page when search is empty // Escape goes to previous page
if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) { // Backspace goes to previous page when search is empty
e.preventDefault(); if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) {
setPages((pages) => pages.slice(0, -1)); e.preventDefault();
setPlaceholder("Type a command or search..."); setPages((pages) => pages.slice(0, -1));
} setPlaceholder("Type a command or search...");
}} }
> }}
<div
className={`flex sm:items-center gap-4 p-3 pb-0 ${
issueDetails ? "flex-col sm:flex-row justify-between" : "justify-end"
}`}
> >
{issueDetails && ( <div
<div className="overflow-hidden truncate rounded-md bg-custom-background-80 p-2 text-xs font-medium text-custom-text-200"> className={`flex sm:items-center gap-4 p-3 pb-0 ${
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id} {issueDetails.name} issueDetails ? "flex-col sm:flex-row justify-between" : "justify-end"
</div> }`}
)} >
{projectId && ( {issueDetails && (
<Tooltip tooltipContent="Toggle workspace level search"> <div className="overflow-hidden truncate rounded-md bg-custom-background-80 p-2 text-xs font-medium text-custom-text-200">
<div className="flex-shrink-0 self-end sm:self-center flex items-center gap-1 text-xs cursor-pointer"> {issueDetails.project_detail.identifier}-{issueDetails.sequence_id} {issueDetails.name}
<button
type="button"
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
className="flex-shrink-0"
>
Workspace Level
</button>
<ToggleSwitch
value={isWorkspaceLevel}
onChange={() => setIsWorkspaceLevel((prevData) => !prevData)}
/>
</div> </div>
</Tooltip> )}
)} {projectId && (
</div> <Tooltip tooltipContent="Toggle workspace level search">
<div className="relative"> <div className="flex-shrink-0 self-end sm:self-center flex items-center gap-1 text-xs cursor-pointer">
<MagnifyingGlassIcon <button
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-custom-text-200" type="button"
aria-hidden="true" onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
/> className="flex-shrink-0"
<Command.Input >
className="w-full border-0 border-b border-custom-border-200 bg-transparent p-4 pl-11 text-custom-text-100 placeholder:text-custom-text-400 outline-none focus:ring-0 text-sm" Workspace Level
placeholder={placeholder} </button>
value={searchTerm} <ToggleSwitch
onValueChange={(e) => { value={isWorkspaceLevel}
setSearchTerm(e); onChange={() => setIsWorkspaceLevel((prevData) => !prevData)}
}} />
autoFocus </div>
tabIndex={1} </Tooltip>
/> )}
</div> </div>
<div className="relative">
<MagnifyingGlassIcon
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-custom-text-200"
aria-hidden="true"
/>
<Command.Input
className="w-full border-0 border-b border-custom-border-200 bg-transparent p-4 pl-11 text-custom-text-100 placeholder:text-custom-text-400 outline-none focus:ring-0 text-sm"
placeholder={placeholder}
value={searchTerm}
onValueChange={(e) => {
setSearchTerm(e);
}}
autoFocus
tabIndex={1}
/>
</div>
<Command.List className="max-h-96 overflow-scroll p-2"> <Command.List className="max-h-96 overflow-scroll p-2">
{searchTerm !== "" && ( {searchTerm !== "" && (
<h5 className="text-xs text-custom-text-100 mx-[3px] my-4"> <h5 className="text-xs text-custom-text-100 mx-[3px] my-4">
Search results for{" "} Search results for{" "}
<span className="font-medium"> <span className="font-medium">
{'"'} {'"'}
{searchTerm} {searchTerm}
{'"'} {'"'}
</span>{" "} </span>{" "}
in {!projectId || isWorkspaceLevel ? "workspace" : "project"}: in {!projectId || isWorkspaceLevel ? "workspace" : "project"}:
</h5> </h5>
)} )}
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && ( {!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
<div className="my-4 text-center text-custom-text-200">No results found.</div> <div className="my-4 text-center text-custom-text-200">No results found.</div>
)} )}
{(isLoading || isSearching) && ( {(isLoading || isSearching) && (
<Command.Loading> <Command.Loading>
<Loader className="space-y-3"> <Loader className="space-y-3">
<Loader.Item height="40px" /> <Loader.Item height="40px" />
<Loader.Item height="40px" /> <Loader.Item height="40px" />
<Loader.Item height="40px" /> <Loader.Item height="40px" />
<Loader.Item height="40px" /> <Loader.Item height="40px" />
</Loader> </Loader>
</Command.Loading> </Command.Loading>
)} )}
{debouncedSearchTerm !== "" && {debouncedSearchTerm !== "" &&
Object.keys(results.results).map((key) => { Object.keys(results.results).map((key) => {
const section = (results.results as any)[key]; const section = (results.results as any)[key];
const currentSection = commandGroups[key]; const currentSection = commandGroups[key];
if (section.length > 0) { if (section.length > 0) {
return ( return (
<Command.Group key={key} heading={currentSection.title}> <Command.Group key={key} heading={currentSection.title}>
{section.map((item: any) => ( {section.map((item: any) => (
<Command.Item <Command.Item
key={item.id} key={item.id}
onSelect={() => { onSelect={() => {
closePalette(); closePalette();
router.push(currentSection.path(item)); router.push(currentSection.path(item));
}} }}
value={`${key}-${item?.name}`} value={`${key}-${item?.name}`}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 overflow-hidden text-custom-text-200"> <div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
{currentSection.icon} {currentSection.icon}
<p className="block flex-1 truncate">{currentSection.itemName(item)}</p> <p className="block flex-1 truncate">{currentSection.itemName(item)}</p>
</div> </div>
</Command.Item> </Command.Item>
))} ))}
</Command.Group>
);
}
})}
{!page && (
<>
{issueId && (
<Command.Group heading="Issue actions">
<Command.Item
onSelect={() => {
closePalette();
setPlaceholder("Change state...");
setSearchTerm("");
setPages([...pages, "change-issue-state"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<DoubleCircleIcon className="h-3.5 w-3.5" />
Change state...
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setPlaceholder("Change priority...");
setSearchTerm("");
setPages([...pages, "change-issue-priority"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<Signal className="h-3.5 w-3.5" />
Change priority...
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setPlaceholder("Assign to...");
setSearchTerm("");
setPages([...pages, "change-issue-assignee"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<UserGroupIcon className="h-3.5 w-3.5" />
Assign to...
</div>
</Command.Item>
<Command.Item
onSelect={() => {
handleIssueAssignees(user.id);
setSearchTerm("");
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
{issueDetails?.assignees.includes(user.id) ? (
<>
<UserMinus2 className="h-3.5 w-3.5" />
Un-assign from me
</>
) : (
<>
<UserPlus2 className="h-3.5 w-3.5" />
Assign to me
</>
)}
</div>
</Command.Item>
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
<div className="flex items-center gap-2 text-custom-text-200">
<Trash2 className="h-3.5 w-3.5" />
Delete issue
</div>
</Command.Item>
<Command.Item
onSelect={() => {
closePalette();
copyIssueUrlToClipboard();
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<LinkIcon className="h-3.5 w-3.5" />
Copy issue URL
</div>
</Command.Item>
</Command.Group> </Command.Group>
); )}
} <Command.Group heading="Issue">
})}
{!page && (
<>
{issueId && (
<Command.Group heading="Issue actions">
<Command.Item
onSelect={() => {
closePalette();
setPlaceholder("Change state...");
setSearchTerm("");
setPages([...pages, "change-issue-state"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<DoubleCircleIcon className="h-3.5 w-3.5" />
Change state...
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setPlaceholder("Change priority...");
setSearchTerm("");
setPages([...pages, "change-issue-priority"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<Signal className="h-3.5 w-3.5" />
Change priority...
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setPlaceholder("Assign to...");
setSearchTerm("");
setPages([...pages, "change-issue-assignee"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<UserGroupIcon className="h-3.5 w-3.5" />
Assign to...
</div>
</Command.Item>
<Command.Item
onSelect={() => {
handleIssueAssignees(user.id);
setSearchTerm("");
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
{issueDetails?.assignees.includes(user.id) ? (
<>
<UserMinus2 className="h-3.5 w-3.5" />
Un-assign from me
</>
) : (
<>
<UserPlus2 className="h-3.5 w-3.5" />
Assign to me
</>
)}
</div>
</Command.Item>
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
<div className="flex items-center gap-2 text-custom-text-200">
<Trash2 className="h-3.5 w-3.5" />
Delete issue
</div>
</Command.Item>
<Command.Item
onSelect={() => {
closePalette();
copyIssueUrlToClipboard();
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<LinkIcon className="h-3.5 w-3.5" />
Copy issue URL
</div>
</Command.Item>
</Command.Group>
)}
<Command.Group heading="Issue">
<Command.Item
onSelect={() => {
closePalette();
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
}}
className="focus:bg-custom-background-80"
>
<div className="flex items-center gap-2 text-custom-text-200">
<LayersIcon className="h-3.5 w-3.5" />
Create new issue
</div>
<kbd>C</kbd>
</Command.Item>
</Command.Group>
{workspaceSlug && (
<Command.Group heading="Project">
<Command.Item <Command.Item
onSelect={() => { onSelect={() => {
closePalette(); closePalette();
const e = new KeyboardEvent("keydown", { const e = new KeyboardEvent("keydown", {
key: "p", key: "c",
});
document.dispatchEvent(e);
}}
className="focus:bg-custom-background-80"
>
<div className="flex items-center gap-2 text-custom-text-200">
<LayersIcon className="h-3.5 w-3.5" />
Create new issue
</div>
<kbd>C</kbd>
</Command.Item>
</Command.Group>
{workspaceSlug && (
<Command.Group heading="Project">
<Command.Item
onSelect={() => {
closePalette();
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<FolderPlus className="h-3.5 w-3.5" />
Create new project
</div>
<kbd>P</kbd>
</Command.Item>
</Command.Group>
)}
{projectId && (
<>
<Command.Group heading="Cycle">
<Command.Item
onSelect={() => {
closePalette();
const e = new KeyboardEvent("keydown", {
key: "q",
});
document.dispatchEvent(e);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<ContrastIcon className="h-3.5 w-3.5" />
Create new cycle
</div>
<kbd>Q</kbd>
</Command.Item>
</Command.Group>
<Command.Group heading="Module">
<Command.Item
onSelect={() => {
closePalette();
const e = new KeyboardEvent("keydown", {
key: "m",
});
document.dispatchEvent(e);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<DiceIcon className="h-3.5 w-3.5" />
Create new module
</div>
<kbd>M</kbd>
</Command.Item>
</Command.Group>
<Command.Group heading="View">
<Command.Item
onSelect={() => {
closePalette();
const e = new KeyboardEvent("keydown", {
key: "v",
});
document.dispatchEvent(e);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<PhotoFilterIcon className="h-3.5 w-3.5" />
Create new view
</div>
<kbd>V</kbd>
</Command.Item>
</Command.Group>
<Command.Group heading="Page">
<Command.Item
onSelect={() => {
closePalette();
const e = new KeyboardEvent("keydown", {
key: "d",
});
document.dispatchEvent(e);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<FileText className="h-3.5 w-3.5" />
Create new page
</div>
<kbd>D</kbd>
</Command.Item>
</Command.Group>
</>
)}
<Command.Group heading="Workspace Settings">
<Command.Item
onSelect={() => {
setPlaceholder("Search workspace settings...");
setSearchTerm("");
setPages([...pages, "settings"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<Settings className="h-3.5 w-3.5" />
Search settings...
</div>
</Command.Item>
</Command.Group>
<Command.Group heading="Account">
<Command.Item onSelect={createNewWorkspace} className="focus:outline-none">
<div className="flex items-center gap-2 text-custom-text-200">
<FolderPlus className="h-3.5 w-3.5" />
Create new workspace
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setPlaceholder("Change interface theme...");
setSearchTerm("");
setPages([...pages, "change-interface-theme"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<Settings className="h-3.5 w-3.5" />
Change interface theme...
</div>
</Command.Item>
</Command.Group>
<Command.Group heading="Help">
<Command.Item
onSelect={() => {
closePalette();
const e = new KeyboardEvent("keydown", {
key: "h",
}); });
document.dispatchEvent(e); document.dispatchEvent(e);
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-custom-text-200"> <div className="flex items-center gap-2 text-custom-text-200">
<FolderPlus className="h-3.5 w-3.5" /> <Rocket className="h-3.5 w-3.5" />
Create new project Open keyboard shortcuts
</div>
</Command.Item>
<Command.Item
onSelect={() => {
closePalette();
window.open("https://docs.plane.so/", "_blank");
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<FileText className="h-3.5 w-3.5" />
Open Plane documentation
</div>
</Command.Item>
<Command.Item
onSelect={() => {
closePalette();
window.open("https://discord.com/invite/A92xrEGCge", "_blank");
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<DiscordIcon className="h-4 w-4" color="rgb(var(--color-text-200))" />
Join our Discord
</div>
</Command.Item>
<Command.Item
onSelect={() => {
closePalette();
window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank");
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<GithubIcon className="h-4 w-4" color="rgb(var(--color-text-200))" />
Report a bug
</div>
</Command.Item>
<Command.Item
onSelect={() => {
closePalette();
(window as any).$crisp.push(["do", "chat:open"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<MessageSquare className="h-3.5 w-3.5" />
Chat with us
</div> </div>
<kbd>P</kbd>
</Command.Item> </Command.Item>
</Command.Group> </Command.Group>
)} </>
)}
{projectId && ( {page === "settings" && workspaceSlug && (
<> <>
<Command.Group heading="Cycle">
<Command.Item
onSelect={() => {
closePalette();
const e = new KeyboardEvent("keydown", {
key: "q",
});
document.dispatchEvent(e);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<ContrastIcon className="h-3.5 w-3.5" />
Create new cycle
</div>
<kbd>Q</kbd>
</Command.Item>
</Command.Group>
<Command.Group heading="Module">
<Command.Item
onSelect={() => {
closePalette();
const e = new KeyboardEvent("keydown", {
key: "m",
});
document.dispatchEvent(e);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<DiceIcon className="h-3.5 w-3.5" />
Create new module
</div>
<kbd>M</kbd>
</Command.Item>
</Command.Group>
<Command.Group heading="View">
<Command.Item
onSelect={() => {
closePalette();
const e = new KeyboardEvent("keydown", {
key: "v",
});
document.dispatchEvent(e);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<PhotoFilterIcon className="h-3.5 w-3.5" />
Create new view
</div>
<kbd>V</kbd>
</Command.Item>
</Command.Group>
<Command.Group heading="Page">
<Command.Item
onSelect={() => {
closePalette();
const e = new KeyboardEvent("keydown", {
key: "d",
});
document.dispatchEvent(e);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<FileText className="h-3.5 w-3.5" />
Create new page
</div>
<kbd>D</kbd>
</Command.Item>
</Command.Group>
</>
)}
<Command.Group heading="Workspace Settings">
<Command.Item <Command.Item
onSelect={() => { onSelect={() => redirect(`/${workspaceSlug}/settings`)}
setPlaceholder("Search workspace settings...");
setSearchTerm("");
setPages([...pages, "settings"]);
}}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-custom-text-200"> <div className="flex items-center gap-2 text-custom-text-200">
<Settings className="h-3.5 w-3.5" /> <SettingIcon className="h-4 w-4 text-custom-text-200" />
Search settings... General
</div>
</Command.Item>
</Command.Group>
<Command.Group heading="Account">
<Command.Item onSelect={createNewWorkspace} className="focus:outline-none">
<div className="flex items-center gap-2 text-custom-text-200">
<FolderPlus className="h-3.5 w-3.5" />
Create new workspace
</div> </div>
</Command.Item> </Command.Item>
<Command.Item <Command.Item
onSelect={() => { onSelect={() => redirect(`/${workspaceSlug}/settings/members`)}
setPlaceholder("Change interface theme...");
setSearchTerm("");
setPages([...pages, "change-interface-theme"]);
}}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-custom-text-200"> <div className="flex items-center gap-2 text-custom-text-200">
<Settings className="h-3.5 w-3.5" /> <SettingIcon className="h-4 w-4 text-custom-text-200" />
Change interface theme... Members
</div>
</Command.Item>
</Command.Group>
<Command.Group heading="Help">
<Command.Item
onSelect={() => {
closePalette();
const e = new KeyboardEvent("keydown", {
key: "h",
});
document.dispatchEvent(e);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<Rocket className="h-3.5 w-3.5" />
Open keyboard shortcuts
</div> </div>
</Command.Item> </Command.Item>
<Command.Item <Command.Item
onSelect={() => { onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)}
closePalette();
window.open("https://docs.plane.so/", "_blank");
}}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-custom-text-200"> <div className="flex items-center gap-2 text-custom-text-200">
<FileText className="h-3.5 w-3.5" /> <SettingIcon className="h-4 w-4 text-custom-text-200" />
Open Plane documentation Billing and Plans
</div> </div>
</Command.Item> </Command.Item>
<Command.Item <Command.Item
onSelect={() => { onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)}
closePalette();
window.open("https://discord.com/invite/A92xrEGCge", "_blank");
}}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-custom-text-200"> <div className="flex items-center gap-2 text-custom-text-200">
<DiscordIcon className="h-4 w-4" color="rgb(var(--color-text-200))" /> <SettingIcon className="h-4 w-4 text-custom-text-200" />
Join our Discord Integrations
</div> </div>
</Command.Item> </Command.Item>
<Command.Item <Command.Item
onSelect={() => { onSelect={() => redirect(`/${workspaceSlug}/settings/imports`)}
closePalette();
window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank");
}}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-custom-text-200"> <div className="flex items-center gap-2 text-custom-text-200">
<GithubIcon className="h-4 w-4" color="rgb(var(--color-text-200))" /> <SettingIcon className="h-4 w-4 text-custom-text-200" />
Report a bug Import
</div> </div>
</Command.Item> </Command.Item>
<Command.Item <Command.Item
onSelect={() => { onSelect={() => redirect(`/${workspaceSlug}/settings/exports`)}
closePalette();
(window as any).$crisp.push(["do", "chat:open"]);
}}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-custom-text-200"> <div className="flex items-center gap-2 text-custom-text-200">
<MessageSquare className="h-3.5 w-3.5" /> <SettingIcon className="h-4 w-4 text-custom-text-200" />
Chat with us Export
</div> </div>
</Command.Item> </Command.Item>
</Command.Group> </>
</> )}
)} {page === "change-issue-state" && issueDetails && (
<ChangeIssueState issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
{page === "settings" && workspaceSlug && ( )}
<> {page === "change-issue-priority" && issueDetails && (
<Command.Item <ChangeIssuePriority issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
onSelect={() => redirect(`/${workspaceSlug}/settings`)} )}
className="focus:outline-none" {page === "change-issue-assignee" && issueDetails && (
> <ChangeIssueAssignee issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
<div className="flex items-center gap-2 text-custom-text-200"> )}
<SettingIcon className="h-4 w-4 text-custom-text-200" /> {page === "change-interface-theme" && <ChangeInterfaceTheme setIsPaletteOpen={closePalette} />}
General </Command.List>
</div> </Command>
</Command.Item> </div>
<Command.Item
onSelect={() => redirect(`/${workspaceSlug}/settings/members`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Members
</div>
</Command.Item>
<Command.Item
onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Billing and Plans
</div>
</Command.Item>
<Command.Item
onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Integrations
</div>
</Command.Item>
<Command.Item
onSelect={() => redirect(`/${workspaceSlug}/settings/imports`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Import
</div>
</Command.Item>
<Command.Item
onSelect={() => redirect(`/${workspaceSlug}/settings/exports`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Export
</div>
</Command.Item>
</>
)}
{page === "change-issue-state" && issueDetails && (
<ChangeIssueState issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
)}
{page === "change-issue-priority" && issueDetails && (
<ChangeIssuePriority issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
)}
{page === "change-issue-assignee" && issueDetails && (
<ChangeIssueAssignee issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
)}
{page === "change-interface-theme" && <ChangeInterfaceTheme setIsPaletteOpen={closePalette} />}
</Command.List>
</Command>
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>
</div> </div>

View File

@ -53,7 +53,7 @@ export const CommandPalette: FC = observer(() => {
isDeleteIssueModalOpen, isDeleteIssueModalOpen,
toggleDeleteIssueModal, toggleDeleteIssueModal,
} = commandPalette; } = commandPalette;
const { setSidebarCollapsed } = themeStore; const { toggleSidebar } = themeStore;
const { user } = useUser(); const { user } = useUser();
@ -109,22 +109,22 @@ export const CommandPalette: FC = observer(() => {
copyIssueUrlToClipboard(); copyIssueUrlToClipboard();
} else if (keyPressed === "b") { } else if (keyPressed === "b") {
e.preventDefault(); e.preventDefault();
setSidebarCollapsed(); toggleSidebar();
} }
} else { } else {
if (keyPressed === "c") { if (keyPressed === "c") {
toggleCreateIssueModal(true); toggleCreateIssueModal(true);
} else if (keyPressed === "p") { } else if (keyPressed === "p") {
toggleCreateProjectModal(true); toggleCreateProjectModal(true);
} else if (keyPressed === "v") {
toggleCreateViewModal(true);
} else if (keyPressed === "d") {
toggleCreatePageModal(true);
} else if (keyPressed === "h") { } else if (keyPressed === "h") {
toggleShortcutModal(true); 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); toggleCreateCycleModal(true);
} else if (keyPressed === "m") { } else if (keyPressed === "m" && workspaceSlug && projectId) {
toggleCreateModuleModal(true); toggleCreateModuleModal(true);
} else if (keyPressed === "backspace" || keyPressed === "delete") { } else if (keyPressed === "backspace" || keyPressed === "delete") {
e.preventDefault(); e.preventDefault();
@ -142,8 +142,10 @@ export const CommandPalette: FC = observer(() => {
toggleCreateModuleModal, toggleCreateModuleModal,
toggleBulkDeleteIssueModal, toggleBulkDeleteIssueModal,
toggleCommandPaletteModal, toggleCommandPaletteModal,
setSidebarCollapsed, toggleSidebar,
toggleCreateIssueModal, toggleCreateIssueModal,
projectId,
workspaceSlug,
] ]
); );

View File

@ -22,8 +22,6 @@ export const ProjectCardList: FC<IProjectCardList> = observer((props) => {
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
console.log("projects", projects);
if (!projects) { if (!projects) {
return ( return (
<Loader className="grid grid-cols-3 gap-4"> <Loader className="grid grid-cols-3 gap-4">

View File

@ -1,16 +1,14 @@
import { useState, useEffect, Fragment, FC } from "react"; import { useState, useEffect, Fragment, FC, ChangeEvent } from "react";
import { useRouter } from "next/router";
import { useForm, Controller } from "react-hook-form"; import { useForm, Controller } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// icons // icons
import { Users2, X } from "lucide-react"; import { X } from "lucide-react";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import { useWorkspaceMyMembership } from "contexts/workspace-member.context"; import { useWorkspaceMyMembership } from "contexts/workspace-member.context";
import useWorkspaceMembers from "hooks/use-workspace-members";
// ui // ui
import { CustomSelect, Avatar, CustomSearchSelect } from "components/ui"; import { CustomSelect } from "components/ui";
import { Button, Input, TextArea } from "@plane/ui"; import { Button, Input, TextArea } from "@plane/ui";
// components // components
import { ImagePickerPopover } from "components/core"; import { ImagePickerPopover } from "components/core";
@ -18,9 +16,11 @@ import EmojiIconPicker from "components/emoji-icon-picker";
// helpers // helpers
import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
// types // types
import { IProject } from "types"; import { IWorkspaceMember } from "types";
// constants // 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 = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -29,17 +29,6 @@ type Props = {
workspaceSlug: string; workspaceSlug: string;
}; };
const defaultValues: Partial<IProject> = {
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 { interface IIsGuestCondition {
onClose: () => void; onClose: () => void;
} }
@ -59,18 +48,30 @@ const IsGuestCondition: FC<IIsGuestCondition> = ({ onClose }) => {
return null; return null;
}; };
export const CreateProjectModal: React.FC<Props> = (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<Props> = observer((props) => {
const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props; const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props;
// store // store
const { project: projectStore } = useMobxStore(); const { project: projectStore, workspace: workspaceStore } = useMobxStore();
const workspaceMembers = workspaceStore.members[workspaceSlug] || [];
// states // states
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
// toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// form info
const { memberDetails } = useWorkspaceMyMembership(); const cover_image = PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)];
const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
const { const {
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
handleSubmit, handleSubmit,
@ -78,15 +79,29 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
control, control,
watch, watch,
setValue, setValue,
} = useForm<IProject>({ } = useForm<ICreateProjectForm>({
defaultValues, defaultValues: {
cover_image,
description: "",
emoji_and_icon: getRandomEmoji(),
identifier: "",
name: "",
network: 2,
project_lead: undefined,
},
reValidateMode: "onChange", 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} />;
const handleClose = () => { const handleClose = () => {
onClose(); onClose();
setIsChangeInIdentifierRequired(true); setIsChangeInIdentifierRequired(true);
reset(defaultValues); reset();
}; };
const handleAddToFavorites = (projectId: string) => { const handleAddToFavorites = (projectId: string) => {
@ -101,16 +116,16 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
}); });
}; };
const onSubmit = async (formData: IProject) => { const onSubmit = async (formData: ICreateProjectForm) => {
if (!workspaceSlug) return;
// eslint-disable-next-line @typescript-eslint/no-unused-vars // 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; if (typeof formData.emoji_and_icon === "object") payload.icon_prop = formData.emoji_and_icon;
else payload.emoji = 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) .createProject(workspaceSlug.toString(), payload)
.then((res) => { .then((res) => {
setToastAlert({ setToastAlert({
@ -134,9 +149,11 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
}); });
}; };
const changeIdentifierOnNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNameChange = (onChange: any) => (e: ChangeEvent<HTMLInputElement>) => {
if (!isChangeInIdentifierRequired) return; if (!isChangeInIdentifierRequired) {
onChange(e);
return;
}
if (e.target.value === "") setValue("identifier", ""); if (e.target.value === "") setValue("identifier", "");
else else
setValue( setValue(
@ -146,32 +163,16 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
.toUpperCase() .toUpperCase()
.substring(0, 5) .substring(0, 5)
); );
onChange(e);
}; };
const handleIdentifierChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleIdentifierChange = (onChange: any) => (e: ChangeEvent<HTMLInputElement>) => {
const { value } = e.target; const { value } = e.target;
const alphanumericValue = value.replace(/[^a-zA-Z0-9]/g, ""); const alphanumericValue = value.replace(/[^a-zA-Z0-9]/g, "");
setValue("identifier", alphanumericValue.toUpperCase());
setIsChangeInIdentifierRequired(false); setIsChangeInIdentifierRequired(false);
onChange(alphanumericValue.toUpperCase());
}; };
const options = workspaceMembers?.map((member: any) => ({
value: member.member.id,
query: member.member.display_name,
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.display_name}
</div>
),
}));
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
if (memberDetails && isOpen) if (memberDetails.role <= 10) return <IsGuestCondition onClose={onClose} />;
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <Dialog as="div" className="relative z-20" onClose={handleClose}>
@ -255,20 +256,20 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
message: "Title should be less than 255 characters", message: "Title should be less than 255 characters",
}, },
}} }}
render={({ field: { value, ref } }) => ( render={({ field: { value, onChange } }) => (
<Input <Input
id="name" id="name"
name="name" name="name"
type="text" type="text"
value={value} value={value}
onChange={changeIdentifierOnNameChange} onChange={handleNameChange(onChange)}
ref={ref}
hasError={Boolean(errors.name)} hasError={Boolean(errors.name)}
placeholder="Project Title" placeholder="Project Title"
className="w-full" className="w-full"
/> />
)} )}
/> />
<span className="text-xs text-red-500">{errors?.name?.message}</span>
</div> </div>
<div> <div>
<Controller <Controller
@ -287,20 +288,20 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
message: "Identifier must at most be of 12 characters", message: "Identifier must at most be of 12 characters",
}, },
}} }}
render={({ field: { value, ref } }) => ( render={({ field: { value, onChange } }) => (
<Input <Input
id="identifier" id="identifier"
name="identifier" name="identifier"
type="text" type="text"
value={value} value={value}
onChange={handleIdentifierChange} onChange={handleIdentifierChange(onChange)}
ref={ref} hasError={Boolean(errors.identifier)}
hasError={Boolean(errors.name)}
placeholder="Identifier" placeholder="Identifier"
className="text-sm w-full" className="text-xs w-full"
/> />
)} )}
/> />
<span className="text-xs text-red-500">{errors?.identifier?.message}</span>
</div> </div>
<div className="md:col-span-4"> <div className="md:col-span-4">
<Controller <Controller
@ -314,7 +315,7 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
placeholder="Description..." placeholder="Description..."
onChange={onChange} onChange={onChange}
className="text-sm !h-24" className="text-sm !h-24"
hasError={Boolean(errors?.name)} hasError={Boolean(errors?.description)}
/> />
)} )}
/> />
@ -330,12 +331,12 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
<CustomSelect <CustomSelect
value={value} value={value}
onChange={onChange} onChange={onChange}
buttonClassName="border-[0.5px] !px-2 shadow-md" buttonClassName="border-[0.5px] shadow-md !py-1.5"
label={ label={
<div className="flex items-center gap-2 -mb-0.5 py-1"> <div className="flex items-center gap-2">
{currentNetwork ? ( {currentNetwork ? (
<> <>
<currentNetwork.icon className="h-3 w-3" /> <currentNetwork.icon className="h-[18px] w-[18px]" />
{currentNetwork.label} {currentNetwork.label}
</> </>
) : ( ) : (
@ -351,7 +352,7 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
value={network.key} value={network.key}
className="flex items-center gap-1" className="flex items-center gap-1"
> >
<network.icon className="h-3 w-3" /> <network.icon className="h-4 w-4" />
{network.label} {network.label}
</CustomSelect.Option> </CustomSelect.Option>
))} ))}
@ -361,39 +362,16 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
</div> </div>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Controller <Controller
name="project_lead" name="project_lead_member"
control={control} control={control}
render={({ field: { value, onChange } }) => { render={({ field: { value, onChange } }) => (
const selectedMember = workspaceMembers?.find((m: any) => m.member.id === value); <WorkspaceMemberSelect
value={value}
return ( onChange={onChange}
<CustomSearchSelect options={workspaceMembers}
value={value} placeholder="Select Lead"
onChange={onChange} />
options={options} )}
buttonClassName="border-[0.5px] !px-2 shadow-md"
label={
<div className="flex items-center justify-center gap-2 py-[1px]">
{value ? (
<>
<Avatar user={selectedMember?.member} />
<span>{selectedMember?.member.display_name} </span>
<span onClick={() => onChange(null)}>
<X className="h-3 w-3 -mb-0.5 text-custom-text-200 hover:text-custom-text-100" />
</span>
</>
) : (
<>
<Users2 className="h-3.5 w-3.5 text-custom-text-400" />
<span className="text-custom-text-400">Lead</span>
</>
)}
</div>
}
noChevron
/>
);
}}
/> />
</div> </div>
</div> </div>
@ -415,4 +393,4 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
); );
}; });

View File

@ -18,12 +18,20 @@ type AvatarProps = {
height?: string; height?: string;
width?: string; width?: string;
fontSize?: string; fontSize?: string;
showName?: boolean;
}; };
// services // services
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
export const Avatar: React.FC<AvatarProps> = ({ user, index, height = "24px", width = "24px", fontSize = "12px" }) => ( export const Avatar: React.FC<AvatarProps> = ({
user,
index,
height = "24px",
width = "24px",
fontSize = "12px",
showName,
}) => (
<div <div
className={`relative rounded border-[0.5px] ${ className={`relative rounded border-[0.5px] ${
index && index !== 0 ? "-ml-3.5 border-custom-border-200" : "border-transparent" index && index !== 0 ? "-ml-3.5 border-custom-border-200" : "border-transparent"
@ -61,6 +69,7 @@ export const Avatar: React.FC<AvatarProps> = ({ user, index, height = "24px", wi
{user?.display_name?.charAt(0)} {user?.display_name?.charAt(0)}
</div> </div>
)} )}
{showName && <span>{user?.display_name ? user?.display_name : user?.first_name}</span>}
</div> </div>
); );

View File

@ -92,7 +92,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
<button <button
type="button" type="button"
className="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 md:hidden" className="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 md:hidden"
onClick={() => themeStore.setSidebarCollapsed(!isCollapsed)} onClick={() => themeStore.toggleSidebar()}
> >
<MoveLeft className="h-3.5 w-3.5" /> <MoveLeft className="h-3.5 w-3.5" />
</button> </button>
@ -101,7 +101,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = 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 ${ 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" : "" isCollapsed ? "w-full" : ""
}`} }`}
onClick={() => themeStore.setSidebarCollapsed(!isCollapsed)} onClick={() => themeStore.toggleSidebar()}
> >
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isCollapsed ? "rotate-180" : ""}`} /> <MoveLeft className={`h-3.5 w-3.5 duration-300 ${isCollapsed ? "rotate-180" : ""}`} />
</button> </button>

View File

@ -10,3 +10,4 @@ export * from "./issues-stats";
export * from "./sidebar-dropdown"; export * from "./sidebar-dropdown";
export * from "./sidebar-menu"; export * from "./sidebar-menu";
export * from "./sidebar-quick-action"; export * from "./sidebar-quick-action";
export * from "./member-select";

View File

@ -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<IWorkspaceMemberSelect> = (props) => {
const { value, onChange, options, placeholder = "Select Member", disabled = false } = props;
// states
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: "auto",
});
// const options = workspaceMembers?.map((member: any) => ({
// value: member.member.id,
// query: member.member.display_name,
// content: (
// <div className="flex items-center gap-2">
// <Avatar user={member.member} />
// {member.member.display_name}
// </div>
// ),
// }));
// 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 = (
<Tooltip
tooltipHeading="Assignee"
tooltipContent={
options && options.length > 0
? options.map((assignee) => assignee?.member.display_name).join(", ")
: "No Assignee"
}
position="top"
>
<div
className="flex items-center justify-between gap-2 w-full text-xs px-2.5 py-1.5 rounded-md border border-custom-border-300 duration-300 focus:outline-none
"
>
{value ? (
<>
<Avatar height="18px" width="18px" user={value?.member} />
<span className="text-xs leading-4"> {value?.member.display_name}</span>
</>
) : (
<>
<User2 className="h-[18px] w-[18px]" />
<span className="text-xs leading-4">{placeholder}</span>
</>
)}
</div>
</Tooltip>
);
return (
<Listbox as="div" className={`flex-shrink-0 text-left`} value={value} onChange={onChange} disabled={disabled}>
<Listbox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={`flex items-center justify-between gap-1 w-full text-xs ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} `}
>
{label}
</button>
</Listbox.Button>
<Listbox.Options>
<div
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1`}
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" />
<Input
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 border-none focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
/>
</div>
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((workspaceMember: IWorkspaceMember) => (
<Listbox.Option
key={workspaceMember.id}
value={workspaceMember}
className={({ active, selected }) =>
`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 }) => (
<>
<div className="flex items-center gap-2">
<Avatar user={workspaceMember.member} />
{workspaceMember.member.display_name}
</div>
{selected && <Check className="h-3.5 w-3.5" />}
</>
)}
</Listbox.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>
</Listbox.Options>
</Listbox>
);
};

View File

@ -50,3 +50,22 @@ export const PROJECT_AUTOMATION_MONTHS = [
{ label: "9 Months", value: 9 }, { label: "9 Months", value: 9 },
{ label: "12 Months", value: 12 }, { 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",
];

View File

@ -30,14 +30,11 @@ const MobxStoreInit = observer(() => {
useEffect(() => { useEffect(() => {
// sidebar collapsed toggle // sidebar collapsed toggle
if (localStorage && localStorage.getItem("app_sidebar_collapsed") && themeStore?.sidebarCollapsed === null) const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed");
themeStore.setSidebarCollapsed( const localBoolValue = localValue ? (localValue === "true" ? true : false) : false;
localStorage.getItem("app_sidebar_collapsed") if (localValue && themeStore?.sidebarCollapsed === undefined) {
? localStorage.getItem("app_sidebar_collapsed") === "true" themeStore.toggleSidebar(localBoolValue);
? true }
: false
: false
);
}, [themeStore, userStore, setTheme]); }, [themeStore, userStore, setTheme]);
useEffect(() => { useEffect(() => {

View File

@ -68,7 +68,7 @@ export interface IProjectStore {
joinProject: (workspaceSlug: string, projectIds: string[]) => Promise<void>; joinProject: (workspaceSlug: string, projectIds: string[]) => Promise<void>;
leaveProject: (workspaceSlug: string, projectId: string) => Promise<void>; leaveProject: (workspaceSlug: string, projectId: string) => Promise<void>;
createProject: (workspaceSlug: string, data: any) => Promise<any>; createProject: (workspaceSlug: string, data: any) => Promise<any>;
updateProject: (workspaceSlug: string, projectId: string, data: any) => Promise<any>; updateProject: (workspaceSlug: string, projectId: string, data: Partial<IProject>) => Promise<any>;
deleteProject: (workspaceSlug: string, projectId: string) => Promise<void>; deleteProject: (workspaceSlug: string, projectId: string) => Promise<void>;
} }
@ -413,17 +413,39 @@ export class ProjectStore implements IProjectStore {
addProjectToFavorites = async (workspaceSlug: string, projectId: string) => { addProjectToFavorites = async (workspaceSlug: string, projectId: string) => {
try { 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); const response = await this.projectService.addProjectToFavorites(workspaceSlug, projectId);
await this.fetchProjects(workspaceSlug);
return response; return response;
} catch (error) { } catch (error) {
console.log("Failed to add project to favorite"); console.log("Failed to add project to favorite");
await this.fetchProjects(workspaceSlug);
throw error; throw error;
} }
}; };
removeProjectFromFavorites = async (workspaceSlug: string, projectId: string) => { removeProjectFromFavorites = async (workspaceSlug: string, projectId: string) => {
try { 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); const response = await this.projectService.removeProjectFromFavorites(workspaceSlug, projectId);
await this.fetchProjects(workspaceSlug); await this.fetchProjects(workspaceSlug);
return response; 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<IProject>) => {
try { try {
runInAction(() => {
this.projects = {
...this.projects,
[workspaceSlug]: this.projects[workspaceSlug].map((p) => (p.id === projectId ? { ...p, ...data } : p)),
};
});
const response = await this.projectService.updateProject( const response = await this.projectService.updateProject(
workspaceSlug, workspaceSlug,
projectId, projectId,
data, data,
this.rootStore.user.currentUser this.rootStore.user.currentUser
); );
await this.fetchProjectDetails(workspaceSlug, projectId);
await this.fetchProjects(workspaceSlug);
return response; return response;
} catch (error) { } catch (error) {
console.log("Failed to create project from project store"); console.log("Failed to create project from project store");
this.fetchProjects(workspaceSlug);
throw error; throw error;
} }
}; };

View File

@ -5,14 +5,14 @@ import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper";
export interface IThemeStore { export interface IThemeStore {
theme: string | null; theme: string | null;
sidebarCollapsed: boolean | null; sidebarCollapsed: boolean | undefined;
setSidebarCollapsed: (collapsed?: boolean) => void; toggleSidebar: (collapsed?: boolean) => void;
setTheme: (theme: any) => void; setTheme: (theme: any) => void;
} }
class ThemeStore implements IThemeStore { class ThemeStore implements IThemeStore {
sidebarCollapsed: boolean | null = null; sidebarCollapsed: boolean | undefined = undefined;
theme: string | null = null; theme: string | null = null;
// root store // root store
rootStore; rootStore;
@ -23,7 +23,7 @@ class ThemeStore implements IThemeStore {
sidebarCollapsed: observable.ref, sidebarCollapsed: observable.ref,
theme: observable.ref, theme: observable.ref,
// action // action
setSidebarCollapsed: action, toggleSidebar: action,
setTheme: action, setTheme: action,
// computed // computed
}); });
@ -31,17 +31,14 @@ class ThemeStore implements IThemeStore {
this.rootStore = _rootStore; this.rootStore = _rootStore;
this.initialLoad(); this.initialLoad();
} }
toggleSidebar = (collapsed?: boolean) => {
setSidebarCollapsed(collapsed?: boolean) { if (collapsed === undefined) {
if (!collapsed) { this.sidebarCollapsed = !this.sidebarCollapsed;
let _sidebarCollapsed: string | boolean | null = localStorage.getItem("app_sidebar_collapsed");
_sidebarCollapsed = _sidebarCollapsed ? (_sidebarCollapsed === "true" ? true : false) : false;
this.sidebarCollapsed = _sidebarCollapsed;
} else { } else {
this.sidebarCollapsed = collapsed; this.sidebarCollapsed = collapsed;
localStorage.setItem("app_sidebar_collapsed", collapsed.toString());
} }
} localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString());
};
setTheme = async (_theme: { theme: any }) => { setTheme = async (_theme: { theme: any }) => {
try { try {

View File

@ -15,8 +15,8 @@ export interface IWorkspaceStore {
// observables // observables
workspaceSlug: string | null; workspaceSlug: string | null;
workspaces: IWorkspace[]; workspaces: IWorkspace[];
labels: { [workspaceSlug: string]: IIssueLabels[] } | {}; // workspaceSlug: labels[] labels: { [workspaceSlug: string]: IIssueLabels[] }; // workspaceSlug: labels[]
members: { [workspaceSlug: string]: IWorkspaceMember[] } | {}; // workspaceSlug: members[] members: { [workspaceSlug: string]: IWorkspaceMember[] }; // workspaceSlug: members[]
// actions // actions
setWorkspaceSlug: (workspaceSlug: string) => void; setWorkspaceSlug: (workspaceSlug: string) => void;