forked from github/plane
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:
parent
9b96e297b3
commit
15f621ad91
@ -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>
|
||||||
|
@ -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,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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";
|
||||||
|
146
web/components/workspace/member-select.tsx
Normal file
146
web/components/workspace/member-select.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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",
|
||||||
|
];
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user