mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
f639e467f8
commit
7eeac188d7
@ -2,7 +2,7 @@ import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const FullScreenPeekIcon: React.FC<ISvgIcons> = ({
|
||||
export const CenterPanelIcon: React.FC<ISvgIcons> = ({
|
||||
className = "text-current",
|
||||
...rest
|
||||
}) => (
|
||||
@ -16,14 +16,18 @@ export const FullScreenPeekIcon: React.FC<ISvgIcons> = ({
|
||||
>
|
||||
<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"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<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"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
@ -2,7 +2,7 @@ import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const ModalPeekIcon: React.FC<ISvgIcons> = ({
|
||||
export const FullScreenPanelIcon: React.FC<ISvgIcons> = ({
|
||||
className = "text-current",
|
||||
...rest
|
||||
}) => (
|
@ -9,9 +9,9 @@ export * from "./subscribe-icon";
|
||||
export * from "./external-link-icon";
|
||||
export * from "./copy-icon";
|
||||
export * from "./layer-stack";
|
||||
export * from "./side-peek-icon";
|
||||
export * from "./modal-peek-icon";
|
||||
export * from "./panel-center-icon";
|
||||
export * from "./side-panel-icon";
|
||||
export * from "./center-panel-icon";
|
||||
export * from "./full-screen-panel-icon";
|
||||
export * from "./priority-icon";
|
||||
export * from "./state";
|
||||
export * from "./blocked-icon";
|
||||
|
@ -2,7 +2,7 @@ import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const SidePeekIcon: React.FC<ISvgIcons> = ({
|
||||
export const SidePanelIcon: React.FC<ISvgIcons> = ({
|
||||
className = "text-current",
|
||||
...rest
|
||||
}) => (
|
@ -24,7 +24,7 @@ import useToast from "hooks/use-toast";
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
import { LinkModal, LinksList } from "components/core";
|
||||
// types
|
||||
import { ICycle, IIssue, IIssueLink, IModule, TIssuePriorities, linkDetails } from "types";
|
||||
import { IIssue, IIssueLink, TIssuePriorities, linkDetails } from "types";
|
||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
// services
|
||||
import { IssueService } from "services/issue";
|
||||
@ -42,7 +42,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
const [linkModal, setLinkModal] = useState(false);
|
||||
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
|
||||
|
||||
const { user: userStore } = useMobxStore();
|
||||
const { user: userStore, cycleIssue: cycleIssueStore, moduleIssue: moduleIssueStore } = useMobxStore();
|
||||
const userRole = userStore.currentProjectRole;
|
||||
|
||||
const router = useRouter();
|
||||
@ -71,11 +71,15 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
const handleParent = (_parent: string) => {
|
||||
issueUpdate({ ...issue, parent: _parent });
|
||||
};
|
||||
const handleCycle = (_cycle: ICycle) => {
|
||||
issueUpdate({ ...issue, cycle: _cycle.id });
|
||||
const addIssueToCycle = async (cycleId: string) => {
|
||||
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>) => {
|
||||
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">
|
||||
{/* state */}
|
||||
<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" />
|
||||
<p>State</p>
|
||||
</div>
|
||||
@ -198,7 +202,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
|
||||
{/* assignee */}
|
||||
<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" />
|
||||
<p>Assignees</p>
|
||||
</div>
|
||||
@ -209,7 +213,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
|
||||
{/* priority */}
|
||||
<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" />
|
||||
<p>Priority</p>
|
||||
</div>
|
||||
@ -220,7 +224,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
|
||||
{/* estimate */}
|
||||
<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 " />
|
||||
<p>Estimate</p>
|
||||
</div>
|
||||
@ -231,7 +235,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
|
||||
{/* start date */}
|
||||
<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" />
|
||||
<p>Start date</p>
|
||||
</div>
|
||||
@ -249,7 +253,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
|
||||
{/* due date */}
|
||||
<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" />
|
||||
<p>Due date</p>
|
||||
</div>
|
||||
@ -267,7 +271,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
|
||||
{/* parent */}
|
||||
<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" />
|
||||
<p>Parent</p>
|
||||
</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 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" />
|
||||
<p>Cycle</p>
|
||||
</div>
|
||||
<div>
|
||||
<SidebarCycleSelect issueDetail={issue} handleCycleChange={handleCycle} disabled={isNotAllowed} />
|
||||
<SidebarCycleSelect issueDetail={issue} handleCycleChange={addIssueToCycle} disabled={isNotAllowed} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
<p>Module</p>
|
||||
</div>
|
||||
<div>
|
||||
<SidebarModuleSelect issueDetail={issue} handleModuleChange={handleModule} disabled={isNotAllowed} />
|
||||
<SidebarModuleSelect issueDetail={issue} handleModuleChange={addIssueToModule} disabled={isNotAllowed} />
|
||||
</div>
|
||||
</div>
|
||||
<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" />
|
||||
<p>Label</p>
|
||||
</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-2 w-full">
|
||||
<div className="flex items-center gap-2 w-80">
|
||||
<div className="flex items-center gap-2 w-40">
|
||||
<Link2 className="h-4 w-4 rotate-45 flex-shrink-0" />
|
||||
<div className="flex items-center gap-2 w-40 text-sm">
|
||||
<Link2 className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Links</p>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -7,7 +7,7 @@ import useSWR from "swr";
|
||||
import { PeekOverviewIssueDetails } from "./issue-detail";
|
||||
import { PeekOverviewProperties } from "./properties";
|
||||
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";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
@ -41,17 +41,17 @@ type TPeekModes = "side-peek" | "modal" | "full-screen";
|
||||
const peekOptions: { key: TPeekModes; icon: any; title: string }[] = [
|
||||
{
|
||||
key: "side-peek",
|
||||
icon: SidePeekIcon,
|
||||
icon: SidePanelIcon,
|
||||
title: "Side Peek",
|
||||
},
|
||||
{
|
||||
key: "modal",
|
||||
icon: ModalPeekIcon,
|
||||
icon: CenterPanelIcon,
|
||||
title: "Modal",
|
||||
},
|
||||
{
|
||||
key: "full-screen",
|
||||
icon: FullScreenPeekIcon,
|
||||
icon: FullScreenPanelIcon,
|
||||
title: "Full Screen",
|
||||
},
|
||||
];
|
||||
@ -206,7 +206,13 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
>
|
||||
{peekOptions.map((mode) => (
|
||||
<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.title}
|
||||
</div>
|
||||
|
@ -3,9 +3,9 @@ import Link from "next/link";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { CustomSelect, FullScreenPeekIcon, ModalPeekIcon, SidePeekIcon } from "@plane/ui";
|
||||
import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon } from "@plane/ui";
|
||||
// icons
|
||||
import { LinkIcon, MoveRight, Trash2 } from "lucide-react";
|
||||
import { LinkIcon, MoveDiagonal, MoveRight, Trash2 } from "lucide-react";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
@ -26,15 +26,15 @@ const peekModes: {
|
||||
icon: any;
|
||||
label: string;
|
||||
}[] = [
|
||||
{ key: "side", icon: SidePeekIcon, label: "Side Peek" },
|
||||
{ key: "side", icon: SidePanelIcon, label: "Side Peek" },
|
||||
{
|
||||
key: "modal",
|
||||
icon: ModalPeekIcon,
|
||||
icon: CenterPanelIcon,
|
||||
label: "Modal Peek",
|
||||
},
|
||||
{
|
||||
key: "full",
|
||||
icon: FullScreenPeekIcon,
|
||||
icon: FullScreenPanelIcon,
|
||||
label: "Full Screen Peek",
|
||||
},
|
||||
];
|
||||
@ -73,7 +73,7 @@ export const PeekOverviewHeader: React.FC<Props> = ({
|
||||
)}
|
||||
<Link href={`/${workspaceSlug}/projects/${issue?.project}/issues/${issue?.id}`}>
|
||||
<a>
|
||||
<FullScreenPeekIcon className="h-3.5 w-3.5" />
|
||||
<MoveDiagonal className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</Link>
|
||||
<CustomSelect
|
||||
|
@ -19,11 +19,12 @@ type Props = {
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
projectId: string;
|
||||
label?: JSX.Element;
|
||||
};
|
||||
|
||||
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
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
@ -56,7 +57,9 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
|
||||
ref={setReferenceElement}
|
||||
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">
|
||||
<IssueLabelsList
|
||||
labels={value.map((v) => issueLabels?.find((l) => l.id === v)) ?? []}
|
||||
|
@ -8,17 +8,15 @@ import useSWR, { mutate } from "swr";
|
||||
import { IssueService } from "services/issue";
|
||||
import { CycleService } from "services/cycle.service";
|
||||
// ui
|
||||
import { CustomSelect, Spinner, Tooltip } from "@plane/ui";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
import { ContrastIcon, CustomSearchSelect, Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { ICycle, IIssue } from "types";
|
||||
import { IIssue } from "types";
|
||||
// fetch-keys
|
||||
import { CYCLE_ISSUES, INCOMPLETE_CYCLES_LIST, ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
issueDetail: IIssue | undefined;
|
||||
handleCycleChange: (cycle: ICycle) => void;
|
||||
handleCycleChange: (cycleId: string) => void;
|
||||
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;
|
||||
|
||||
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={
|
||||
<div>
|
||||
<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" : ""
|
||||
}`}
|
||||
>
|
||||
<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"}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
value={issueCycle ? issueCycle.cycle_detail.id : null}
|
||||
onChange={(value: any) => {
|
||||
!value
|
||||
? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "")
|
||||
: handleCycleChange(incompleteCycles?.find((c) => c.id === value) as ICycle);
|
||||
}}
|
||||
width="w-full"
|
||||
maxHeight="rg"
|
||||
width="max-w-[10rem]"
|
||||
noChevron
|
||||
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>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -4,15 +4,16 @@ import useSWR from "swr";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { TwitterPicker } from "react-color";
|
||||
// headless ui
|
||||
import { Listbox, Popover, Transition } from "@headlessui/react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import { IssueLabelService } from "services/issue";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
// ui
|
||||
import { Input, Spinner } from "@plane/ui";
|
||||
import { Input } from "@plane/ui";
|
||||
import { IssueLabelSelect } from "../select";
|
||||
// icons
|
||||
import { Component, Plus, X } from "lucide-react";
|
||||
import { Plus, X } from "lucide-react";
|
||||
// types
|
||||
import { IIssue, IIssueLabels } from "types";
|
||||
// fetch-keys
|
||||
@ -117,102 +118,21 @@ export const SidebarLabelSelect: React.FC<Props> = ({
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
<Listbox
|
||||
as="div"
|
||||
<IssueLabelSelect
|
||||
setIsOpen={setCreateLabelForm}
|
||||
value={issueDetails?.labels ?? []}
|
||||
onChange={(val: any) => submitChanges({ labels: val })}
|
||||
className="flex-shrink-0"
|
||||
multiple
|
||||
disabled={isNotAllowed || uneditable}
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<Listbox.Button
|
||||
className={`flex ${
|
||||
isNotAllowed || uneditable ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-90"
|
||||
} 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>
|
||||
projectId={issueDetails?.project_detail.id ?? ""}
|
||||
label={
|
||||
<span
|
||||
className={`flex ${
|
||||
isNotAllowed || uneditable ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-90"
|
||||
} 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
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
{!isNotAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
|
@ -4,17 +4,15 @@ import useSWR, { mutate } from "swr";
|
||||
// services
|
||||
import { ModuleService } from "services/module.service";
|
||||
// ui
|
||||
import { CustomSelect, Spinner, Tooltip } from "@plane/ui";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
import { CustomSearchSelect, DiceIcon, Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue, IModule } from "types";
|
||||
import { IIssue } from "types";
|
||||
// fetch-keys
|
||||
import { ISSUE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
issueDetail: IIssue | undefined;
|
||||
handleModuleChange: (module: IModule) => void;
|
||||
handleModuleChange: (moduleId: string) => void;
|
||||
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;
|
||||
|
||||
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={
|
||||
<div>
|
||||
<Tooltip
|
||||
@ -60,41 +78,21 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
|
||||
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"}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
value={issueModule ? issueModule.module_detail?.id : null}
|
||||
onChange={(value: any) => {
|
||||
!value
|
||||
? removeIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "")
|
||||
: handleModuleChange(modules?.find((m) => m.id === value) as IModule);
|
||||
}}
|
||||
width="w-full"
|
||||
maxHeight="rg"
|
||||
width="max-w-[10rem]"
|
||||
noChevron
|
||||
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>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ import useSWR from "swr";
|
||||
// services
|
||||
import { ProjectStateService } from "services/project";
|
||||
// ui
|
||||
import { CustomSelect, Spinner, StateGroupIcon } from "@plane/ui";
|
||||
import { CustomSearchSelect, StateGroupIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.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 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 (
|
||||
<CustomSelect
|
||||
<CustomSearchSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
customButton={
|
||||
<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">
|
||||
<StateGroupIcon stateGroup={selectedState.group} color={selectedState.color} />
|
||||
{addSpaceIfCamelCase(selectedState?.name ?? "")}
|
||||
<StateGroupIcon stateGroup={selectedOption.group} color={selectedOption.color} />
|
||||
{addSpaceIfCamelCase(selectedOption?.name ?? "")}
|
||||
</div>
|
||||
) : inboxIssueId ? (
|
||||
<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>
|
||||
}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
optionsClassName="w-min"
|
||||
width="min-w-[10rem]"
|
||||
noChevron
|
||||
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>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -37,7 +37,7 @@ import { ContrastIcon, DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import type { ICycle, IIssue, IIssueLink, linkDetails, IModule } from "types";
|
||||
import type { IIssue, IIssueLink, linkDetails } from "types";
|
||||
// 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 handleCycleChange = useCallback(
|
||||
(cycleDetails: ICycle) => {
|
||||
(cycleId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueDetail || !user) return;
|
||||
|
||||
issueService
|
||||
.addIssueToCycle(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
cycleDetails.id,
|
||||
cycleId,
|
||||
{
|
||||
issues: [issueDetail.id],
|
||||
},
|
||||
@ -118,14 +118,14 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
);
|
||||
|
||||
const handleModuleChange = useCallback(
|
||||
(moduleDetail: IModule) => {
|
||||
(moduleId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueDetail || !user) return;
|
||||
|
||||
moduleService
|
||||
.addIssuesToModule(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
moduleDetail.id,
|
||||
moduleId,
|
||||
{
|
||||
issues: [issueDetail.id],
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user