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"
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">
<Command
filter={(value, 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...");
}
}}
>
<div
className={`flex sm:items-center gap-4 p-3 pb-0 ${
issueDetails ? "flex-col sm:flex-row justify-between" : "justify-end"
}`}
<Dialog.Panel className="relative flex items-center justify-center w-full ">
<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">
<Command
filter={(value, 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 && (
<div className="overflow-hidden truncate rounded-md bg-custom-background-80 p-2 text-xs font-medium text-custom-text-200">
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id} {issueDetails.name}
</div>
)}
{projectId && (
<Tooltip tooltipContent="Toggle workspace level search">
<div className="flex-shrink-0 self-end sm:self-center flex items-center gap-1 text-xs cursor-pointer">
<button
type="button"
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
className="flex-shrink-0"
>
Workspace Level
</button>
<ToggleSwitch
value={isWorkspaceLevel}
onChange={() => setIsWorkspaceLevel((prevData) => !prevData)}
/>
<div
className={`flex sm:items-center gap-4 p-3 pb-0 ${
issueDetails ? "flex-col sm:flex-row justify-between" : "justify-end"
}`}
>
{issueDetails && (
<div className="overflow-hidden truncate rounded-md bg-custom-background-80 p-2 text-xs font-medium text-custom-text-200">
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id} {issueDetails.name}
</div>
</Tooltip>
)}
</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>
)}
{projectId && (
<Tooltip tooltipContent="Toggle workspace level search">
<div className="flex-shrink-0 self-end sm:self-center flex items-center gap-1 text-xs cursor-pointer">
<button
type="button"
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
className="flex-shrink-0"
>
Workspace Level
</button>
<ToggleSwitch
value={isWorkspaceLevel}
onChange={() => setIsWorkspaceLevel((prevData) => !prevData)}
/>
</div>
</Tooltip>
)}
</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">
{searchTerm !== "" && (
<h5 className="text-xs text-custom-text-100 mx-[3px] my-4">
Search results for{" "}
<span className="font-medium">
{'"'}
{searchTerm}
{'"'}
</span>{" "}
in {!projectId || isWorkspaceLevel ? "workspace" : "project"}:
</h5>
)}
<Command.List className="max-h-96 overflow-scroll p-2">
{searchTerm !== "" && (
<h5 className="text-xs text-custom-text-100 mx-[3px] my-4">
Search results for{" "}
<span className="font-medium">
{'"'}
{searchTerm}
{'"'}
</span>{" "}
in {!projectId || isWorkspaceLevel ? "workspace" : "project"}:
</h5>
)}
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
<div className="my-4 text-center text-custom-text-200">No results found.</div>
)}
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
<div className="my-4 text-center text-custom-text-200">No results found.</div>
)}
{(isLoading || isSearching) && (
<Command.Loading>
<Loader className="space-y-3">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
</Command.Loading>
)}
{(isLoading || isSearching) && (
<Command.Loading>
<Loader className="space-y-3">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
</Command.Loading>
)}
{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 (
<Command.Group key={key} heading={currentSection.title}>
{section.map((item: any) => (
<Command.Item
key={item.id}
onSelect={() => {
closePalette();
router.push(currentSection.path(item));
}}
value={`${key}-${item?.name}`}
className="focus:outline-none"
>
<div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
{currentSection.icon}
<p className="block flex-1 truncate">{currentSection.itemName(item)}</p>
</div>
</Command.Item>
))}
if (section.length > 0) {
return (
<Command.Group key={key} heading={currentSection.title}>
{section.map((item: any) => (
<Command.Item
key={item.id}
onSelect={() => {
closePalette();
router.push(currentSection.path(item));
}}
value={`${key}-${item?.name}`}
className="focus:outline-none"
>
<div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
{currentSection.icon}
<p className="block flex-1 truncate">{currentSection.itemName(item)}</p>
</div>
</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>
);
}
})}
{!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.Group heading="Issue">
<Command.Item
onSelect={() => {
closePalette();
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);
}}
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
<Rocket className="h-3.5 w-3.5" />
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>
<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">
{page === "settings" && workspaceSlug && (
<>
<Command.Item
onSelect={() => {
setPlaceholder("Search workspace settings...");
setSearchTerm("");
setPages([...pages, "settings"]);
}}
onSelect={() => redirect(`/${workspaceSlug}/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
<SettingIcon className="h-4 w-4 text-custom-text-200" />
General
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setPlaceholder("Change interface theme...");
setSearchTerm("");
setPages([...pages, "change-interface-theme"]);
}}
onSelect={() => redirect(`/${workspaceSlug}/settings/members`)}
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);
}}
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
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Members
</div>
</Command.Item>
<Command.Item
onSelect={() => {
closePalette();
window.open("https://docs.plane.so/", "_blank");
}}
onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)}
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
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Billing and Plans
</div>
</Command.Item>
<Command.Item
onSelect={() => {
closePalette();
window.open("https://discord.com/invite/A92xrEGCge", "_blank");
}}
onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)}
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
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Integrations
</div>
</Command.Item>
<Command.Item
onSelect={() => {
closePalette();
window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank");
}}
onSelect={() => redirect(`/${workspaceSlug}/settings/imports`)}
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
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Import
</div>
</Command.Item>
<Command.Item
onSelect={() => {
closePalette();
(window as any).$crisp.push(["do", "chat:open"]);
}}
onSelect={() => redirect(`/${workspaceSlug}/settings/exports`)}
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
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Export
</div>
</Command.Item>
</Command.Group>
</>
)}
{page === "settings" && workspaceSlug && (
<>
<Command.Item
onSelect={() => redirect(`/${workspaceSlug}/settings`)}
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" />
General
</div>
</Command.Item>
<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>
</>
)}
{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>
</div>
</Dialog.Panel>
</Transition.Child>
</div>

View File

@ -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,
]
);

View File

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

View File

@ -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<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 {
onClose: () => void;
}
@ -59,18 +48,30 @@ const IsGuestCondition: FC<IIsGuestCondition> = ({ onClose }) => {
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;
// 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> = (props) => {
control,
watch,
setValue,
} = useForm<IProject>({
defaultValues,
} = useForm<ICreateProjectForm>({
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 <IsGuestCondition onClose={onClose} />;
const handleClose = () => {
onClose();
setIsChangeInIdentifierRequired(true);
reset(defaultValues);
reset();
};
const handleAddToFavorites = (projectId: string) => {
@ -101,16 +116,16 @@ export const CreateProjectModal: React.FC<Props> = (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> = (props) => {
});
};
const changeIdentifierOnNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!isChangeInIdentifierRequired) return;
const handleNameChange = (onChange: any) => (e: ChangeEvent<HTMLInputElement>) => {
if (!isChangeInIdentifierRequired) {
onChange(e);
return;
}
if (e.target.value === "") setValue("identifier", "");
else
setValue(
@ -146,32 +163,16 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
.toUpperCase()
.substring(0, 5)
);
onChange(e);
};
const handleIdentifierChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleIdentifierChange = (onChange: any) => (e: ChangeEvent<HTMLInputElement>) => {
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: (
<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 (
<Transition.Root show={isOpen} as={Fragment}>
<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",
},
}}
render={({ field: { value, ref } }) => (
render={({ field: { value, onChange } }) => (
<Input
id="name"
name="name"
type="text"
value={value}
onChange={changeIdentifierOnNameChange}
ref={ref}
onChange={handleNameChange(onChange)}
hasError={Boolean(errors.name)}
placeholder="Project Title"
className="w-full"
/>
)}
/>
<span className="text-xs text-red-500">{errors?.name?.message}</span>
</div>
<div>
<Controller
@ -287,20 +288,20 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
message: "Identifier must at most be of 12 characters",
},
}}
render={({ field: { value, ref } }) => (
render={({ field: { value, onChange } }) => (
<Input
id="identifier"
name="identifier"
type="text"
value={value}
onChange={handleIdentifierChange}
ref={ref}
hasError={Boolean(errors.name)}
onChange={handleIdentifierChange(onChange)}
hasError={Boolean(errors.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 className="md:col-span-4">
<Controller
@ -314,7 +315,7 @@ export const CreateProjectModal: React.FC<Props> = (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> = (props) => {
<CustomSelect
value={value}
onChange={onChange}
buttonClassName="border-[0.5px] !px-2 shadow-md"
buttonClassName="border-[0.5px] shadow-md !py-1.5"
label={
<div className="flex items-center gap-2 -mb-0.5 py-1">
<div className="flex items-center gap-2">
{currentNetwork ? (
<>
<currentNetwork.icon className="h-3 w-3" />
<currentNetwork.icon className="h-[18px] w-[18px]" />
{currentNetwork.label}
</>
) : (
@ -351,7 +352,7 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
value={network.key}
className="flex items-center gap-1"
>
<network.icon className="h-3 w-3" />
<network.icon className="h-4 w-4" />
{network.label}
</CustomSelect.Option>
))}
@ -361,39 +362,16 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
</div>
<div className="flex-shrink-0">
<Controller
name="project_lead"
name="project_lead_member"
control={control}
render={({ field: { value, onChange } }) => {
const selectedMember = workspaceMembers?.find((m: any) => m.member.id === value);
return (
<CustomSearchSelect
value={value}
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
/>
);
}}
render={({ field: { value, onChange } }) => (
<WorkspaceMemberSelect
value={value}
onChange={onChange}
options={workspaceMembers}
placeholder="Select Lead"
/>
)}
/>
</div>
</div>
@ -415,4 +393,4 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
</Dialog>
</Transition.Root>
);
};
});

View File

@ -18,12 +18,20 @@ type AvatarProps = {
height?: string;
width?: string;
fontSize?: string;
showName?: boolean;
};
// services
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
className={`relative rounded border-[0.5px] ${
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)}
</div>
)}
{showName && <span>{user?.display_name ? user?.display_name : user?.first_name}</span>}
</div>
);

View File

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

View File

@ -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";

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: "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(() => {
// 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(() => {

View File

@ -68,7 +68,7 @@ export interface IProjectStore {
joinProject: (workspaceSlug: string, projectIds: string[]) => Promise<void>;
leaveProject: (workspaceSlug: string, projectId: string) => Promise<void>;
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>;
}
@ -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<IProject>) => {
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;
}
};

View File

@ -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 {

View File

@ -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;