chore: peek overview improvement and bug fixes (#2627)

* chore: peekoverview issue properties text size fix

* chore: peekoverview icon updated and active view indicator added

* chore: peekoverview and issue sidebar improvement
This commit is contained in:
Anmol Singh Bhatia 2023-11-03 18:01:34 +05:30 committed by GitHub
parent f639e467f8
commit 7eeac188d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 172 additions and 243 deletions

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { ISvgIcons } from "./type"; import { ISvgIcons } from "./type";
export const FullScreenPeekIcon: React.FC<ISvgIcons> = ({ export const CenterPanelIcon: React.FC<ISvgIcons> = ({
className = "text-current", className = "text-current",
...rest ...rest
}) => ( }) => (
@ -16,14 +16,18 @@ export const FullScreenPeekIcon: React.FC<ISvgIcons> = ({
> >
<path <path
d="M19 3H5C3.89543 3 3 3.89543 3 5V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3Z" d="M19 3H5C3.89543 3 3 3.89543 3 5V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3Z"
strokeLinecap="round" stroke="currentColor"
strokeLinejoin="round" stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/> />
<path <path
d="M20 3H12V5V19V21H20C20.5523 21 21 20.1046 21 19V5C21 3.89543 20.5523 3 20 3Z" d="M15.1111 8.00009H8.8001C8.33334 8.00007 8.00003 8.0001 8.00012 8.88897V15.1111C8.00012 16 8.00012 16 8.8001 16H15.1111C16 16 16 16 16 15.1111V8.88897C16 8.00009 16 8.00009 15.1111 8.00009H15.1111Z"
fill="currentColor" fill="currentColor"
strokeLinecap="round" stroke="currentColor"
strokeLinejoin="round" stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/> />
</svg> </svg>
); );

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { ISvgIcons } from "./type"; import { ISvgIcons } from "./type";
export const ModalPeekIcon: React.FC<ISvgIcons> = ({ export const FullScreenPanelIcon: React.FC<ISvgIcons> = ({
className = "text-current", className = "text-current",
...rest ...rest
}) => ( }) => (

View File

@ -9,9 +9,9 @@ export * from "./subscribe-icon";
export * from "./external-link-icon"; export * from "./external-link-icon";
export * from "./copy-icon"; export * from "./copy-icon";
export * from "./layer-stack"; export * from "./layer-stack";
export * from "./side-peek-icon"; export * from "./side-panel-icon";
export * from "./modal-peek-icon"; export * from "./center-panel-icon";
export * from "./panel-center-icon"; export * from "./full-screen-panel-icon";
export * from "./priority-icon"; export * from "./priority-icon";
export * from "./state"; export * from "./state";
export * from "./blocked-icon"; export * from "./blocked-icon";

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { ISvgIcons } from "./type"; import { ISvgIcons } from "./type";
export const SidePeekIcon: React.FC<ISvgIcons> = ({ export const SidePanelIcon: React.FC<ISvgIcons> = ({
className = "text-current", className = "text-current",
...rest ...rest
}) => ( }) => (

View File

@ -24,7 +24,7 @@ import useToast from "hooks/use-toast";
import { CustomDatePicker } from "components/ui"; import { CustomDatePicker } from "components/ui";
import { LinkModal, LinksList } from "components/core"; import { LinkModal, LinksList } from "components/core";
// types // types
import { ICycle, IIssue, IIssueLink, IModule, TIssuePriorities, linkDetails } from "types"; import { IIssue, IIssueLink, TIssuePriorities, linkDetails } from "types";
import { ISSUE_DETAILS } from "constants/fetch-keys"; import { ISSUE_DETAILS } from "constants/fetch-keys";
// services // services
import { IssueService } from "services/issue"; import { IssueService } from "services/issue";
@ -42,7 +42,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
const [linkModal, setLinkModal] = useState(false); const [linkModal, setLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null); const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
const { user: userStore } = useMobxStore(); const { user: userStore, cycleIssue: cycleIssueStore, moduleIssue: moduleIssueStore } = useMobxStore();
const userRole = userStore.currentProjectRole; const userRole = userStore.currentProjectRole;
const router = useRouter(); const router = useRouter();
@ -71,11 +71,15 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
const handleParent = (_parent: string) => { const handleParent = (_parent: string) => {
issueUpdate({ ...issue, parent: _parent }); issueUpdate({ ...issue, parent: _parent });
}; };
const handleCycle = (_cycle: ICycle) => { const addIssueToCycle = async (cycleId: string) => {
issueUpdate({ ...issue, cycle: _cycle.id }); if (!workspaceSlug || !issue || !cycleId) return;
cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), issue.project_detail.id, cycleId, issue.id);
}; };
const handleModule = (_module: IModule) => {
issueUpdate({ ...issue, module: _module.id }); const addIssueToModule = async (moduleId: string) => {
if (!workspaceSlug || !issue || !moduleId) return;
moduleIssueStore.addIssueToModule(workspaceSlug.toString(), issue.project_detail.id, moduleId, issue.id);
}; };
const handleLabels = (formData: Partial<IIssue>) => { const handleLabels = (formData: Partial<IIssue>) => {
issueUpdate({ ...issue, ...formData }); issueUpdate({ ...issue, ...formData });
@ -187,7 +191,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<div className="flex flex-col gap-5 py-5 w-full"> <div className="flex flex-col gap-5 py-5 w-full">
{/* state */} {/* state */}
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
<div className="flex items-center gap-2 w-40"> <div className="flex items-center gap-2 w-40 text-sm">
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" /> <DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
<p>State</p> <p>State</p>
</div> </div>
@ -198,7 +202,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
{/* assignee */} {/* assignee */}
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
<div className="flex items-center gap-2 w-40"> <div className="flex items-center gap-2 w-40 text-sm">
<UserGroupIcon className="h-4 w-4 flex-shrink-0" /> <UserGroupIcon className="h-4 w-4 flex-shrink-0" />
<p>Assignees</p> <p>Assignees</p>
</div> </div>
@ -209,7 +213,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
{/* priority */} {/* priority */}
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
<div className="flex items-center gap-2 w-40"> <div className="flex items-center gap-2 w-40 text-sm">
<Signal className="h-4 w-4 flex-shrink-0" /> <Signal className="h-4 w-4 flex-shrink-0" />
<p>Priority</p> <p>Priority</p>
</div> </div>
@ -220,7 +224,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
{/* estimate */} {/* estimate */}
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
<div className="flex items-center gap-2 w-40"> <div className="flex items-center gap-2 w-40 text-sm">
<Triangle className="h-4 w-4 flex-shrink-0 " /> <Triangle className="h-4 w-4 flex-shrink-0 " />
<p>Estimate</p> <p>Estimate</p>
</div> </div>
@ -231,7 +235,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
{/* start date */} {/* start date */}
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
<div className="flex items-center gap-2 w-40"> <div className="flex items-center gap-2 w-40 text-sm">
<CalendarDays className="h-4 w-4 flex-shrink-0" /> <CalendarDays className="h-4 w-4 flex-shrink-0" />
<p>Start date</p> <p>Start date</p>
</div> </div>
@ -249,7 +253,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
{/* due date */} {/* due date */}
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
<div className="flex items-center gap-2 w-40"> <div className="flex items-center gap-2 w-40 text-sm">
<CalendarDays className="h-4 w-4 flex-shrink-0" /> <CalendarDays className="h-4 w-4 flex-shrink-0" />
<p>Due date</p> <p>Due date</p>
</div> </div>
@ -267,7 +271,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
{/* parent */} {/* parent */}
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
<div className="flex items-center gap-2 w-40"> <div className="flex items-center gap-2 w-40 text-sm">
<User2 className="h-4 w-4 flex-shrink-0" /> <User2 className="h-4 w-4 flex-shrink-0" />
<p>Parent</p> <p>Parent</p>
</div> </div>
@ -281,26 +285,26 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<div className="flex flex-col gap-5 py-5 w-full"> <div className="flex flex-col gap-5 py-5 w-full">
<div className="flex items-center gap-2 w-80"> <div className="flex items-center gap-2 w-80">
<div className="flex items-center gap-2 w-40"> <div className="flex items-center gap-2 w-40 text-sm">
<ContrastIcon className="h-4 w-4 flex-shrink-0" /> <ContrastIcon className="h-4 w-4 flex-shrink-0" />
<p>Cycle</p> <p>Cycle</p>
</div> </div>
<div> <div>
<SidebarCycleSelect issueDetail={issue} handleCycleChange={handleCycle} disabled={isNotAllowed} /> <SidebarCycleSelect issueDetail={issue} handleCycleChange={addIssueToCycle} disabled={isNotAllowed} />
</div> </div>
</div> </div>
<div className="flex items-center gap-2 w-80"> <div className="flex items-center gap-2 w-80">
<div className="flex items-center gap-2 w-40"> <div className="flex items-center gap-2 w-40 text-sm">
<DiceIcon className="h-4 w-4 flex-shrink-0" /> <DiceIcon className="h-4 w-4 flex-shrink-0" />
<p>Module</p> <p>Module</p>
</div> </div>
<div> <div>
<SidebarModuleSelect issueDetail={issue} handleModuleChange={handleModule} disabled={isNotAllowed} /> <SidebarModuleSelect issueDetail={issue} handleModuleChange={addIssueToModule} disabled={isNotAllowed} />
</div> </div>
</div> </div>
<div className="flex items-start gap-2 w-full"> <div className="flex items-start gap-2 w-full">
<div className="flex items-center gap-2 w-40 flex-shrink-0"> <div className="flex items-center gap-2 w-40 text-sm flex-shrink-0">
<Tag className="h-4 w-4 flex-shrink-0" /> <Tag className="h-4 w-4 flex-shrink-0" />
<p>Label</p> <p>Label</p>
</div> </div>
@ -321,8 +325,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<div className="flex flex-col gap-5 pt-5 w-full"> <div className="flex flex-col gap-5 pt-5 w-full">
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<div className="flex items-center gap-2 w-80"> <div className="flex items-center gap-2 w-80">
<div className="flex items-center gap-2 w-40"> <div className="flex items-center gap-2 w-40 text-sm">
<Link2 className="h-4 w-4 rotate-45 flex-shrink-0" /> <Link2 className="h-4 w-4 flex-shrink-0" />
<p>Links</p> <p>Links</p>
</div> </div>
<div> <div>

View File

@ -7,7 +7,7 @@ import useSWR from "swr";
import { PeekOverviewIssueDetails } from "./issue-detail"; import { PeekOverviewIssueDetails } from "./issue-detail";
import { PeekOverviewProperties } from "./properties"; import { PeekOverviewProperties } from "./properties";
import { IssueComment } from "./activity"; import { IssueComment } from "./activity";
import { Button, CustomSelect, FullScreenPeekIcon, ModalPeekIcon, SidePeekIcon } from "@plane/ui"; import { Button, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon } from "@plane/ui";
import { DeleteIssueModal } from "../delete-issue-modal"; import { DeleteIssueModal } from "../delete-issue-modal";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
@ -41,17 +41,17 @@ type TPeekModes = "side-peek" | "modal" | "full-screen";
const peekOptions: { key: TPeekModes; icon: any; title: string }[] = [ const peekOptions: { key: TPeekModes; icon: any; title: string }[] = [
{ {
key: "side-peek", key: "side-peek",
icon: SidePeekIcon, icon: SidePanelIcon,
title: "Side Peek", title: "Side Peek",
}, },
{ {
key: "modal", key: "modal",
icon: ModalPeekIcon, icon: CenterPanelIcon,
title: "Modal", title: "Modal",
}, },
{ {
key: "full-screen", key: "full-screen",
icon: FullScreenPeekIcon, icon: FullScreenPanelIcon,
title: "Full Screen", title: "Full Screen",
}, },
]; ];
@ -206,7 +206,13 @@ export const IssueView: FC<IIssueView> = observer((props) => {
> >
{peekOptions.map((mode) => ( {peekOptions.map((mode) => (
<CustomSelect.Option key={mode.key} value={mode.key}> <CustomSelect.Option key={mode.key} value={mode.key}>
<div className="flex items-center gap-1.5"> <div
className={`flex items-center gap-1.5 ${
currentMode.key === mode.key
? "text-custom-text-200"
: "text-custom-text-400 hover:text-custom-text-200"
}`}
>
<mode.icon className={`h-4 w-4 flex-shrink-0 -my-1 `} /> <mode.icon className={`h-4 w-4 flex-shrink-0 -my-1 `} />
{mode.title} {mode.title}
</div> </div>

View File

@ -3,9 +3,9 @@ import Link from "next/link";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
import { CustomSelect, FullScreenPeekIcon, ModalPeekIcon, SidePeekIcon } from "@plane/ui"; import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon } from "@plane/ui";
// icons // icons
import { LinkIcon, MoveRight, Trash2 } from "lucide-react"; import { LinkIcon, MoveDiagonal, MoveRight, Trash2 } from "lucide-react";
// helpers // helpers
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types // types
@ -26,15 +26,15 @@ const peekModes: {
icon: any; icon: any;
label: string; label: string;
}[] = [ }[] = [
{ key: "side", icon: SidePeekIcon, label: "Side Peek" }, { key: "side", icon: SidePanelIcon, label: "Side Peek" },
{ {
key: "modal", key: "modal",
icon: ModalPeekIcon, icon: CenterPanelIcon,
label: "Modal Peek", label: "Modal Peek",
}, },
{ {
key: "full", key: "full",
icon: FullScreenPeekIcon, icon: FullScreenPanelIcon,
label: "Full Screen Peek", label: "Full Screen Peek",
}, },
]; ];
@ -73,7 +73,7 @@ export const PeekOverviewHeader: React.FC<Props> = ({
)} )}
<Link href={`/${workspaceSlug}/projects/${issue?.project}/issues/${issue?.id}`}> <Link href={`/${workspaceSlug}/projects/${issue?.project}/issues/${issue?.id}`}>
<a> <a>
<FullScreenPeekIcon className="h-3.5 w-3.5" /> <MoveDiagonal className="h-3.5 w-3.5" />
</a> </a>
</Link> </Link>
<CustomSelect <CustomSelect

View File

@ -19,11 +19,12 @@ type Props = {
value: string[]; value: string[];
onChange: (value: string[]) => void; onChange: (value: string[]) => void;
projectId: string; projectId: string;
label?: JSX.Element;
}; };
const issueLabelService = new IssueLabelService(); const issueLabelService = new IssueLabelService();
export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange, projectId }) => { export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange, projectId, label }) => {
// states // states
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -56,7 +57,9 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
ref={setReferenceElement} ref={setReferenceElement}
className="flex items-center gap-2 cursor-pointer text-xs text-custom-text-200" className="flex items-center gap-2 cursor-pointer text-xs text-custom-text-200"
> >
{value && value.length > 0 ? ( {label ? (
label
) : value && value.length > 0 ? (
<span className="flex items-center justify-center gap-2 text-xs"> <span className="flex items-center justify-center gap-2 text-xs">
<IssueLabelsList <IssueLabelsList
labels={value.map((v) => issueLabels?.find((l) => l.id === v)) ?? []} labels={value.map((v) => issueLabels?.find((l) => l.id === v)) ?? []}

View File

@ -8,17 +8,15 @@ import useSWR, { mutate } from "swr";
import { IssueService } from "services/issue"; import { IssueService } from "services/issue";
import { CycleService } from "services/cycle.service"; import { CycleService } from "services/cycle.service";
// ui // ui
import { CustomSelect, Spinner, Tooltip } from "@plane/ui"; import { ContrastIcon, CustomSearchSelect, Tooltip } from "@plane/ui";
// helper
import { truncateText } from "helpers/string.helper";
// types // types
import { ICycle, IIssue } from "types"; import { IIssue } from "types";
// fetch-keys // fetch-keys
import { CYCLE_ISSUES, INCOMPLETE_CYCLES_LIST, ISSUE_DETAILS } from "constants/fetch-keys"; import { CYCLE_ISSUES, INCOMPLETE_CYCLES_LIST, ISSUE_DETAILS } from "constants/fetch-keys";
type Props = { type Props = {
issueDetail: IIssue | undefined; issueDetail: IIssue | undefined;
handleCycleChange: (cycle: ICycle) => void; handleCycleChange: (cycleId: string) => void;
disabled?: boolean; disabled?: boolean;
}; };
@ -52,10 +50,30 @@ export const SidebarCycleSelect: React.FC<Props> = ({ issueDetail, handleCycleCh
}); });
}; };
const options = incompleteCycles?.map((cycle) => ({
value: cycle.id,
query: cycle.name,
content: (
<div className="flex items-center gap-1.5 truncate">
<span className="flex justify-center items-center flex-shrink-0 w-3.5 h-3.5">
<ContrastIcon />
</span>
<span className="truncate flex-grow">{cycle.name}</span>
</div>
),
}));
const issueCycle = issueDetail?.issue_cycle; const issueCycle = issueDetail?.issue_cycle;
return ( return (
<CustomSelect <CustomSearchSelect
value={issueCycle?.cycle_detail.id}
onChange={(value: any) => {
value === issueCycle?.cycle_detail.id
? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "")
: handleCycleChange(value);
}}
options={options}
customButton={ customButton={
<div> <div>
<Tooltip position="left" tooltipContent={`${issueCycle ? issueCycle.cycle_detail.name : "No cycle"}`}> <Tooltip position="left" tooltipContent={`${issueCycle ? issueCycle.cycle_detail.name : "No cycle"}`}>
@ -65,41 +83,21 @@ export const SidebarCycleSelect: React.FC<Props> = ({ issueDetail, handleCycleCh
disabled ? "cursor-not-allowed" : "" disabled ? "cursor-not-allowed" : ""
}`} }`}
> >
<span className={`truncate ${issueCycle ? "text-custom-text-100" : "text-custom-text-200"}`}> <span
className={`flex items-center gap-1.5 truncate ${
issueCycle ? "text-custom-text-100" : "text-custom-text-200"
}`}
>
{issueCycle && <ContrastIcon className="h-3.5 w-3.5" />}
{issueCycle ? issueCycle.cycle_detail.name : "No cycle"} {issueCycle ? issueCycle.cycle_detail.name : "No cycle"}
</span> </span>
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
} }
value={issueCycle ? issueCycle.cycle_detail.id : null} width="max-w-[10rem]"
onChange={(value: any) => { noChevron
!value
? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "")
: handleCycleChange(incompleteCycles?.find((c) => c.id === value) as ICycle);
}}
width="w-full"
maxHeight="rg"
disabled={disabled} disabled={disabled}
> />
{incompleteCycles ? (
incompleteCycles.length > 0 ? (
<>
{incompleteCycles.map((option) => (
<CustomSelect.Option key={option.id} value={option.id}>
<Tooltip position="left-bottom" tooltipContent={option.name}>
<span className="w-full truncate">{truncateText(option.name, 25)}</span>
</Tooltip>
</CustomSelect.Option>
))}
<CustomSelect.Option value={null}>None</CustomSelect.Option>
</>
) : (
<div className="text-center">No cycles found</div>
)
) : (
<Spinner />
)}
</CustomSelect>
); );
}; };

View File

@ -4,15 +4,16 @@ import useSWR from "swr";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { TwitterPicker } from "react-color"; import { TwitterPicker } from "react-color";
// headless ui // headless ui
import { Listbox, Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
// services // services
import { IssueLabelService } from "services/issue"; import { IssueLabelService } from "services/issue";
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
// ui // ui
import { Input, Spinner } from "@plane/ui"; import { Input } from "@plane/ui";
import { IssueLabelSelect } from "../select";
// icons // icons
import { Component, Plus, X } from "lucide-react"; import { Plus, X } from "lucide-react";
// types // types
import { IIssue, IIssueLabels } from "types"; import { IIssue, IIssueLabels } from "types";
// fetch-keys // fetch-keys
@ -117,102 +118,21 @@ export const SidebarLabelSelect: React.FC<Props> = ({
</span> </span>
); );
})} })}
<Listbox <IssueLabelSelect
as="div" setIsOpen={setCreateLabelForm}
value={issueDetails?.labels ?? []} value={issueDetails?.labels ?? []}
onChange={(val: any) => submitChanges({ labels: val })} onChange={(val: any) => submitChanges({ labels: val })}
className="flex-shrink-0" projectId={issueDetails?.project_detail.id ?? ""}
multiple label={
disabled={isNotAllowed || uneditable} <span
> className={`flex ${
{({ open }) => ( isNotAllowed || uneditable ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-90"
<div className="relative"> } items-center gap-2 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs hover:text-custom-text-200 text-custom-text-300`}
<Listbox.Button >
className={`flex ${ Select Label
isNotAllowed || uneditable ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-90" </span>
} items-center gap-2 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs hover:text-custom-text-200 text-custom-text-300`} }
> />
Select Label
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-28 w-40 overflow-auto rounded-md bg-custom-background-80 py-1 text-xs shadow-lg border border-custom-border-100 focus:outline-none">
<div className="py-1">
{issueLabels ? (
issueLabels.length > 0 ? (
issueLabels.map((label: IIssueLabels) => {
const children = issueLabels?.filter((l) => l.parent === label.id);
if (children.length === 0) {
if (!label.parent)
return (
<Listbox.Option
key={label.id}
className={({ active, selected }) =>
`${active || selected ? "bg-custom-background-90" : ""} ${
selected ? "" : "text-custom-text-200"
} flex cursor-pointer select-none items-center gap-2 truncate p-2`
}
value={label.id}
>
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
</Listbox.Option>
);
} else
return (
<div className="border-y border-custom-border-100 bg-custom-background-90">
<div className="flex select-none items-center gap-2 truncate p-2 font-medium text-custom-text-100">
<Component className="h-3 w-3" />
{label.name}
</div>
<div>
{children.map((child) => (
<Listbox.Option
key={child.id}
className={({ active, selected }) =>
`${active || selected ? "bg-custom-background-100" : ""} ${
selected ? "" : "text-custom-text-200"
} flex cursor-pointer select-none items-center gap-2 truncate p-2`
}
value={child.id}
>
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: child?.color ?? "black",
}}
/>
{child.name}
</Listbox.Option>
))}
</div>
</div>
);
})
) : (
<div className="text-center">No labels found</div>
)
) : (
<Spinner />
)}
</div>
</Listbox.Options>
</Transition>
</div>
)}
</Listbox>
{!isNotAllowed && ( {!isNotAllowed && (
<button <button
type="button" type="button"

View File

@ -4,17 +4,15 @@ import useSWR, { mutate } from "swr";
// services // services
import { ModuleService } from "services/module.service"; import { ModuleService } from "services/module.service";
// ui // ui
import { CustomSelect, Spinner, Tooltip } from "@plane/ui"; import { CustomSearchSelect, DiceIcon, Tooltip } from "@plane/ui";
// helper
import { truncateText } from "helpers/string.helper";
// types // types
import { IIssue, IModule } from "types"; import { IIssue } from "types";
// fetch-keys // fetch-keys
import { ISSUE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys"; import { ISSUE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys";
type Props = { type Props = {
issueDetail: IIssue | undefined; issueDetail: IIssue | undefined;
handleModuleChange: (module: IModule) => void; handleModuleChange: (moduleId: string) => void;
disabled?: boolean; disabled?: boolean;
}; };
@ -44,10 +42,30 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
}); });
}; };
const options = modules?.map((module) => ({
value: module.id,
query: module.name,
content: (
<div className="flex items-center gap-1.5 truncate">
<span className="flex justify-center items-center flex-shrink-0 w-3.5 h-3.5">
<DiceIcon />
</span>
<span className="truncate flex-grow">{module.name}</span>
</div>
),
}));
const issueModule = issueDetail?.issue_module; const issueModule = issueDetail?.issue_module;
return ( return (
<CustomSelect <CustomSearchSelect
value={issueModule?.module_detail.id}
onChange={(value: any) => {
value === issueModule?.module_detail.id
? removeIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "")
: handleModuleChange(value);
}}
options={options}
customButton={ customButton={
<div> <div>
<Tooltip <Tooltip
@ -60,41 +78,21 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
disabled ? "cursor-not-allowed" : "" disabled ? "cursor-not-allowed" : ""
}`} }`}
> >
<span className={`truncate ${issueModule ? "text-custom-text-100" : "text-custom-text-200"}`}> <span
className={`flex items-center gap-1.5 truncate ${
issueModule ? "text-custom-text-100" : "text-custom-text-200"
}`}
>
{issueModule && <DiceIcon className="h-3.5 w-3.5" />}
{modules?.find((m) => m.id === issueModule?.module)?.name ?? "No module"} {modules?.find((m) => m.id === issueModule?.module)?.name ?? "No module"}
</span> </span>
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
} }
value={issueModule ? issueModule.module_detail?.id : null} width="max-w-[10rem]"
onChange={(value: any) => { noChevron
!value
? removeIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "")
: handleModuleChange(modules?.find((m) => m.id === value) as IModule);
}}
width="w-full"
maxHeight="rg"
disabled={disabled} disabled={disabled}
> />
{modules ? (
modules.length > 0 ? (
<>
{modules.map((option) => (
<CustomSelect.Option key={option.id} value={option.id}>
<Tooltip position="left-bottom" tooltipContent={option.name}>
<span className="w-full truncate">{truncateText(option.name, 25)}</span>
</Tooltip>
</CustomSelect.Option>
))}
<CustomSelect.Option value={null}>None</CustomSelect.Option>
</>
) : (
<div className="text-center">No modules found</div>
)
) : (
<Spinner />
)}
</CustomSelect>
); );
}; };

View File

@ -7,7 +7,7 @@ import useSWR from "swr";
// services // services
import { ProjectStateService } from "services/project"; import { ProjectStateService } from "services/project";
// ui // ui
import { CustomSelect, Spinner, StateGroupIcon } from "@plane/ui"; import { CustomSearchSelect, StateGroupIcon } from "@plane/ui";
// helpers // helpers
import { getStatesList } from "helpers/state.helper"; import { getStatesList } from "helpers/state.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
@ -33,16 +33,30 @@ export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, disabled
); );
const states = getStatesList(stateGroups); const states = getStatesList(stateGroups);
const selectedState = states?.find((s) => s.id === value); const selectedOption = states?.find((s) => s.id === value);
const options = states?.map((state) => ({
value: state.id,
query: state.name,
content: (
<div className="flex items-center gap-2">
<StateGroupIcon stateGroup={state.group} color={state.color} />
{state.name}
</div>
),
}));
return ( return (
<CustomSelect <CustomSearchSelect
value={value}
onChange={onChange}
options={options}
customButton={ customButton={
<div className="bg-custom-background-80 text-xs rounded px-2.5 py-0.5"> <div className="bg-custom-background-80 text-xs rounded px-2.5 py-0.5">
{selectedState ? ( {selectedOption ? (
<div className="flex items-center gap-1.5 text-left text-custom-text-100"> <div className="flex items-center gap-1.5 text-left text-custom-text-100">
<StateGroupIcon stateGroup={selectedState.group} color={selectedState.color} /> <StateGroupIcon stateGroup={selectedOption.group} color={selectedOption.color} />
{addSpaceIfCamelCase(selectedState?.name ?? "")} {addSpaceIfCamelCase(selectedOption?.name ?? "")}
</div> </div>
) : inboxIssueId ? ( ) : inboxIssueId ? (
<div className="flex items-center gap-1.5 text-left text-custom-text-100"> <div className="flex items-center gap-1.5 text-left text-custom-text-100">
@ -54,27 +68,9 @@ export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, disabled
)} )}
</div> </div>
} }
value={value} width="min-w-[10rem]"
onChange={onChange} noChevron
optionsClassName="w-min"
disabled={disabled} disabled={disabled}
> />
{states ? (
states.length > 0 ? (
states.map((state) => (
<CustomSelect.Option key={state.id} value={state.id}>
<>
<StateGroupIcon stateGroup={state.group} color={state.color} />
{state.name}
</>
</CustomSelect.Option>
))
) : (
<div className="text-center">No states found</div>
)
) : (
<Spinner />
)}
</CustomSelect>
); );
}; };

View File

@ -37,7 +37,7 @@ import { ContrastIcon, DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/
// helpers // helpers
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types // types
import type { ICycle, IIssue, IIssueLink, linkDetails, IModule } from "types"; import type { IIssue, IIssueLink, linkDetails } from "types";
// fetch-keys // fetch-keys
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
@ -97,14 +97,14 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const handleCycleChange = useCallback( const handleCycleChange = useCallback(
(cycleDetails: ICycle) => { (cycleId: string) => {
if (!workspaceSlug || !projectId || !issueDetail || !user) return; if (!workspaceSlug || !projectId || !issueDetail || !user) return;
issueService issueService
.addIssueToCycle( .addIssueToCycle(
workspaceSlug as string, workspaceSlug as string,
projectId as string, projectId as string,
cycleDetails.id, cycleId,
{ {
issues: [issueDetail.id], issues: [issueDetail.id],
}, },
@ -118,14 +118,14 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
); );
const handleModuleChange = useCallback( const handleModuleChange = useCallback(
(moduleDetail: IModule) => { (moduleId: string) => {
if (!workspaceSlug || !projectId || !issueDetail || !user) return; if (!workspaceSlug || !projectId || !issueDetail || !user) return;
moduleService moduleService
.addIssuesToModule( .addIssuesToModule(
workspaceSlug as string, workspaceSlug as string,
projectId as string, projectId as string,
moduleDetail.id, moduleId,
{ {
issues: [issueDetail.id], issues: [issueDetail.id],
}, },