From 89e982d08979e39e264188b239fcdd4f78ae0ccd Mon Sep 17 00:00:00 2001 From: gurusainath Date: Mon, 2 Oct 2023 12:12:02 +0530 Subject: [PATCH 1/4] chore: issue properties for state, priorit, labels and members --- .../issues/issue-layouts/kanban/block.tsx | 10 +- .../issues/issue-layouts/kanban/default.tsx | 163 ++++++----- .../issue-layouts/kanban/headers/priority.tsx | 1 + .../issue-layouts/kanban/properties.tsx | 246 ++++++++++------ .../issues/issue-layouts/kanban/root.tsx | 11 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 262 +++++++++--------- .../issues/issue-layouts/properties/date.tsx | 4 + .../properties/dropdown-template.tsx | 177 ++++++++++++ .../issue-layouts/properties/estimates.tsx | 215 ++++++++++++++ .../issue-layouts/properties/labels.tsx | 228 +++++++++++++++ .../issue-layouts/properties/member.tsx | 215 ++++++++++++++ .../issue-layouts/properties/priority.tsx | 220 +++++++++++++++ .../issues/issue-layouts/properties/state.tsx | 215 ++++++++++++++ web/components/states/state-select.tsx | 139 +++++----- web/store/issue.ts | 10 +- 15 files changed, 1752 insertions(+), 364 deletions(-) create mode 100644 web/components/issues/issue-layouts/properties/date.tsx create mode 100644 web/components/issues/issue-layouts/properties/dropdown-template.tsx create mode 100644 web/components/issues/issue-layouts/properties/estimates.tsx create mode 100644 web/components/issues/issue-layouts/properties/labels.tsx create mode 100644 web/components/issues/issue-layouts/properties/member.tsx create mode 100644 web/components/issues/issue-layouts/properties/priority.tsx create mode 100644 web/components/issues/issue-layouts/properties/state.tsx diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index ad424df258f..2f23ec9c88a 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -8,9 +8,10 @@ interface IssueBlockProps { columnId: string; issues: any; isDragDisabled: boolean; + handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; } -export const IssueBlock = ({ sub_group_id, columnId, issues, isDragDisabled }: IssueBlockProps) => ( +export const IssueBlock = ({ sub_group_id, columnId, issues, isDragDisabled, handleIssues }: IssueBlockProps) => ( <> {issues && issues.length > 0 ? ( <> @@ -37,7 +38,12 @@ export const IssueBlock = ({ sub_group_id, columnId, issues, isDragDisabled }: I
ONE-{issue.sequence_id}
{issue.name}
- +
diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 842550e4009..ec6a09addee 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -19,12 +19,12 @@ export interface IGroupByKanBan { sub_group_id: string; list: any; listKey: string; - handleIssues?: () => void; isDragDisabled: boolean; + handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; } const GroupByKanBan: React.FC = observer( - ({ issues, sub_group_by, group_by, sub_group_id = "null", list, listKey, isDragDisabled }) => { + ({ issues, sub_group_by, group_by, sub_group_id = "null", list, listKey, isDragDisabled, handleIssues }) => { const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); const verticalAlignPosition = (_list: any) => @@ -42,7 +42,7 @@ const GroupByKanBan: React.FC = observer( column_id={getValueFromObject(_list, listKey) as string} sub_group_by={sub_group_by} group_by={group_by} - issues_count={issues?.[getValueFromObject(_list, listKey) as string].length || 0} + issues_count={issues?.[getValueFromObject(_list, listKey) as string]?.length || 0} /> )} @@ -64,6 +64,7 @@ const GroupByKanBan: React.FC = observer( columnId={getValueFromObject(_list, listKey) as string} issues={issues[getValueFromObject(_list, listKey) as string]} isDragDisabled={isDragDisabled} + handleIssues={handleIssues} /> ) : ( isDragDisabled && ( @@ -90,86 +91,94 @@ export interface IKanBan { sub_group_by: string | null; group_by: string | null; sub_group_id?: string; - handleIssues?: () => void; handleDragDrop?: (result: any) => void | undefined; + handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; } -export const KanBan: React.FC = observer(({ issues, sub_group_by, group_by, sub_group_id = "null" }) => { - const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); +export const KanBan: React.FC = observer( + ({ issues, sub_group_by, group_by, sub_group_id = "null", handleIssues }) => { + const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); - return ( -
- {group_by && group_by === "state" && ( - - )} + return ( +
+ {group_by && group_by === "state" && ( + + )} - {group_by && group_by === "state_detail.group" && ( - - )} + {group_by && group_by === "state_detail.group" && ( + + )} - {group_by && group_by === "priority" && ( - - )} + {group_by && group_by === "priority" && ( + + )} - {group_by && group_by === "labels" && ( - - )} + {group_by && group_by === "labels" && ( + + )} - {group_by && group_by === "assignees" && ( - - )} + {group_by && group_by === "assignees" && ( + + )} - {group_by && group_by === "created_by" && ( - - )} -
- ); -}); + {group_by && group_by === "created_by" && ( + + )} +
+ ); + } +); diff --git a/web/components/issues/issue-layouts/kanban/headers/priority.tsx b/web/components/issues/issue-layouts/kanban/headers/priority.tsx index 38e4afbc4e5..91820530b01 100644 --- a/web/components/issues/issue-layouts/kanban/headers/priority.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/priority.tsx @@ -8,6 +8,7 @@ import { HeaderSubGroupByCard } from "./sub-group-by-card"; // constants import { issuePriorityByKey } from "constants/issue"; + export interface IPriorityHeader { column_id: string; sub_group_by: string | null; diff --git a/web/components/issues/issue-layouts/kanban/properties.tsx b/web/components/issues/issue-layouts/kanban/properties.tsx index b2a8ce8ff89..3ed19bc06da 100644 --- a/web/components/issues/issue-layouts/kanban/properties.tsx +++ b/web/components/issues/issue-layouts/kanban/properties.tsx @@ -1,91 +1,177 @@ +// mobx +import { observer } from "mobx-react-lite"; // lucide icons -import { Circle } from "lucide-react"; - -export const KanBanProperties = () => { - console.log("properties"); - return ( -
- {/* basic properties */} - {/* state */} -
-
- -
-
state
-
+import { Circle, Layers, Link, Paperclip } from "lucide-react"; +// components +import { IssuePropertyState } from "components/issues/issue-layouts/properties/state"; +import { IssuePropertyPriority } from "components/issues/issue-layouts/properties/priority"; +import { IssuePropertyLabels } from "components/issues/issue-layouts/properties/labels"; +import { IssuePropertyMember } from "components/issues/issue-layouts/properties/member"; +import { IssuePropertyEstimates } from "components/issues/issue-layouts/properties/estimates"; +import { Tooltip } from "components/ui"; - {/* priority */} -
-
- -
-
priority
-
+export interface IKanBanProperties { + sub_group_id: string; + columnId: string; + issue: any; + handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; +} - {/* label */} -
-
- -
-
label
-
+export const KanBanProperties: React.FC = observer( + ({ sub_group_id, columnId: group_id, issue, handleIssues }) => { + const handleState = (id: string) => { + if (handleIssues) + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, state: id } + ); + }; - {/* assignee */} -
-
- -
-
assignee
-
+ const handlePriority = (id: string) => { + if (handleIssues) + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, priority: id } + ); + }; - {/* start date */} -
-
- -
-
start date
-
+ const handleLabel = (id: string[]) => { + if (handleIssues) + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, labels: id } + ); + }; - {/* target/due date */} -
-
- -
-
target/due date
-
+ const handleAssignee = (id: string) => { + if (handleIssues) + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, state: id } + ); + }; - {/* extra render properties */} - {/* estimate */} -
-
- -
-
0
-
+ const handleStartDate = (id: string) => { + if (handleIssues) + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, state: id } + ); + }; - {/* sub-issues */} -
-
- -
-
0
-
+ const handleTargetDate = (id: string) => { + if (handleIssues) + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, state: id } + ); + }; - {/* attachments */} -
-
- -
-
0
-
+ const handleEstimates = (id: string) => { + if (handleIssues) + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, state: id } + ); + }; + + return ( +
+ {/* basic properties */} + {/* state */} + handleState(id)} + disabled={false} + /> + + {/* priority */} + handlePriority(id)} + disabled={false} + /> + + {/* label */} + handleLabel(id)} + disabled={false} + /> + + {/* assignee */} + {/* handleAssignee(id)} + disabled={false} + /> */} + + {/* start date */} + {/*
+
+ +
+
start date
+
*/} + + {/* target/due date */} + {/*
+
+ +
+
target/due date
+
*/} + + {/* estimates */} + {/* handleEstimates(id)} + disabled={false} + /> */} + + {/* extra render properties */} + {/* sub-issues */} + {/* +
+
+ +
+
{issue.sub_issues_count}
+
+
*/} + + {/* attachments */} + {/* +
+
+ +
+
{issue.attachment_count}
+
+
*/} - {/* link */} -
-
- -
-
0
+ {/* link */} + {/* +
+
+ +
+
{issue.link_count}
+
+
*/}
-
- ); -}; + ); + } +); diff --git a/web/components/issues/issue-layouts/kanban/root.tsx b/web/components/issues/issue-layouts/kanban/root.tsx index 5701a297d49..23e870989dd 100644 --- a/web/components/issues/issue-layouts/kanban/root.tsx +++ b/web/components/issues/issue-layouts/kanban/root.tsx @@ -45,13 +45,20 @@ export const KanBanLayout: React.FC = observer(() => { : issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); }; + const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => { + console.log("sub_group_by", sub_group_by); + console.log("group_by", group_by); + console.log("issue", issue); + issueStore.updateIssueStructure(group_by, sub_group_by, issue); + }; + return (
{currentKanBanView === "default" ? ( - + ) : ( - + )}
diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 5579b33cd1c..4c72fa3b790 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -54,187 +54,199 @@ const SubGroupSwimlaneHeader: React.FC = ({ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { issues: any; + handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; } -const SubGroupSwimlane: React.FC = observer(({ issues, sub_group_by, group_by, list, listKey }) => { - const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); - - const calculateIssueCount = (column_id: string) => { - let issueCount = 0; - issues?.[column_id] && - Object.keys(issues?.[column_id])?.forEach((_list: any) => { - issueCount += issues?.[column_id]?.[_list]?.length || 0; - }); - return issueCount; - }; - - return ( -
- {list && - list.length > 0 && - list.map((_list: any) => ( -
-
-
- +const SubGroupSwimlane: React.FC = observer( + ({ issues, sub_group_by, group_by, list, listKey, handleIssues }) => { + const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); + + const calculateIssueCount = (column_id: string) => { + let issueCount = 0; + issues?.[column_id] && + Object.keys(issues?.[column_id])?.forEach((_list: any) => { + issueCount += issues?.[column_id]?.[_list]?.length || 0; + }); + return issueCount; + }; + + return ( +
+ {list && + list.length > 0 && + list.map((_list: any) => ( +
+
+
+ +
+
-
+ {!issueKanBanViewStore.kanBanToggle?.subgroupByIssuesVisibility.includes( + getValueFromObject(_list, listKey) as string + ) && ( +
+ +
+ )}
- {!issueKanBanViewStore.kanBanToggle?.subgroupByIssuesVisibility.includes( - getValueFromObject(_list, listKey) as string - ) && ( -
- -
- )} -
- ))} -
- ); -}); + ))} +
+ ); + } +); export interface IKanBanSwimLanes { issues: any; sub_group_by: string | null; group_by: string | null; - handleIssues?: () => void; + handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; } -export const KanBanSwimLanes: React.FC = observer(({ issues, sub_group_by, group_by }) => { - const { project: projectStore }: RootStore = useMobxStore(); +export const KanBanSwimLanes: React.FC = observer( + ({ issues, sub_group_by, group_by, handleIssues }) => { + const { project: projectStore }: RootStore = useMobxStore(); - return ( -
-
- {group_by && group_by === "state" && ( - +
+ {group_by && group_by === "state" && ( + + )} + + {group_by && group_by === "state_detail.group" && ( + + )} + + {group_by && group_by === "priority" && ( + + )} + + {group_by && group_by === "labels" && ( + + )} + + {group_by && group_by === "assignees" && ( + + )} + + {group_by && group_by === "created_by" && ( + + )} +
+ + {sub_group_by && sub_group_by === "state" && ( + )} - {group_by && group_by === "state_detail.group" && ( - )} - {group_by && group_by === "priority" && ( - )} - {group_by && group_by === "labels" && ( - )} - {group_by && group_by === "assignees" && ( - )} - {group_by && group_by === "created_by" && ( - )}
- - {sub_group_by && sub_group_by === "state" && ( - - )} - - {sub_group_by && sub_group_by === "state_detail.group" && ( - - )} - - {sub_group_by && sub_group_by === "priority" && ( - - )} - - {sub_group_by && sub_group_by === "labels" && ( - - )} - - {sub_group_by && sub_group_by === "assignees" && ( - - )} - - {sub_group_by && sub_group_by === "created_by" && ( - - )} -
- ); -}); + ); + } +); diff --git a/web/components/issues/issue-layouts/properties/date.tsx b/web/components/issues/issue-layouts/properties/date.tsx new file mode 100644 index 00000000000..78678b37892 --- /dev/null +++ b/web/components/issues/issue-layouts/properties/date.tsx @@ -0,0 +1,4 @@ +export const IssuePropertyDate = () => { + console.log(""); + return
IssuePropertyDate
; +}; diff --git a/web/components/issues/issue-layouts/properties/dropdown-template.tsx b/web/components/issues/issue-layouts/properties/dropdown-template.tsx new file mode 100644 index 00000000000..4afc04cb01d --- /dev/null +++ b/web/components/issues/issue-layouts/properties/dropdown-template.tsx @@ -0,0 +1,177 @@ +import React from "react"; +// headless ui +import { Combobox } from "@headlessui/react"; +// lucide icons +import { ChevronDown, Search, X, Check } from "lucide-react"; +// hooks +import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; + +interface IFiltersOption { + id: string; + title: string; +} + +export interface IIssuePropertyState { + options: IFiltersOption[]; + value?: any; + onChange?: (id: any, data: IFiltersOption) => void; + disabled?: boolean; + + className?: string; + buttonClassName?: string; + optionsClassName?: string; + dropdownArrow?: boolean; + + children?: any; +} + +export const IssuePropertyState: React.FC = ({ + options, + value, + onChange, + disabled, + + className, + buttonClassName, + optionsClassName, + dropdownArrow = true, + + children, +}) => { + const dropdownBtn = React.useRef(null); + const dropdownOptions = React.useRef(null); + + const [isOpen, setIsOpen] = React.useState(false); + const [search, setSearch] = React.useState(""); + + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + + const selectedOption: IFiltersOption | null | undefined = + (value && options.find((person: IFiltersOption) => person.id === value)) || null; + + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((person: IFiltersOption) => + person.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) + ) + : []; + + const label = () =>
Hello
; + + return ( + { + if (onChange && selectedOption) onChange(data, selectedOption); + }} + disabled={disabled} + > + {({ open }: { open: boolean }) => { + if (open) { + if (!isOpen) setIsOpen(true); + } else if (isOpen) setIsOpen(false); + + return ( + <> + + {children ? ( + children + ) : ( +
{(selectedOption && selectedOption?.title) || "Select option"}
+ )} + + {dropdownArrow && !disabled && ( +
+ +
+ )} +
+ + {options && options.length > 0 ? ( +
+ +
+
+ +
+ +
+ setSearch(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+ + {search && search.length > 0 && ( +
setSearch("")} + className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" + > + +
+ )} +
+ +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `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 }) => ( +
+
{option.title}
+ {selected && ( +
+ +
+ )} +
+ )} +
+ )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+
+
+ ) : ( +

No options available.

+ )} + + ); + }} +
+ ); +}; diff --git a/web/components/issues/issue-layouts/properties/estimates.tsx b/web/components/issues/issue-layouts/properties/estimates.tsx new file mode 100644 index 00000000000..13ce4542118 --- /dev/null +++ b/web/components/issues/issue-layouts/properties/estimates.tsx @@ -0,0 +1,215 @@ +import React from "react"; +// headless ui +import { Combobox } from "@headlessui/react"; +// lucide icons +import { ChevronDown, Search, X, Check } from "lucide-react"; +// mobx +import { observer } from "mobx-react-lite"; +// components +import { StateGroupIcon } from "components/icons"; +// hooks +import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; +// types +import { IState } from "types"; + +interface IFiltersOption { + id: string; + title: string; + group: string; + color: string | null; +} + +export interface IIssuePropertyEstimates { + value?: any; + onChange?: (id: any, data: IFiltersOption) => void; + disabled?: boolean; + + className?: string; + buttonClassName?: string; + optionsClassName?: string; + dropdownArrow?: boolean; +} + +export const IssuePropertyEstimates: React.FC = observer( + ({ + value, + onChange, + disabled, + + className, + buttonClassName, + optionsClassName, + dropdownArrow = true, + }) => { + const { project: projectStore }: RootStore = useMobxStore(); + + const dropdownBtn = React.useRef(null); + const dropdownOptions = React.useRef(null); + + const [isOpen, setIsOpen] = React.useState(false); + const [search, setSearch] = React.useState(""); + + const options: IFiltersOption[] | [] = + (projectStore?.projectStates && + projectStore?.projectStates?.length > 0 && + projectStore?.projectStates.map((_state: IState) => ({ + id: _state?.id, + title: _state?.name, + group: _state?.group, + color: _state?.color || null, + }))) || + []; + + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + + const selectedOption: IFiltersOption | null | undefined = + (value && options.find((person: IFiltersOption) => person.id === value)) || null; + + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((person: IFiltersOption) => + person.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) + ) + : []; + + return ( + { + if (onChange && selectedOption) onChange(data, selectedOption); + }} + disabled={disabled} + > + {({ open }: { open: boolean }) => { + if (open) { + if (!isOpen) setIsOpen(true); + } else if (isOpen) setIsOpen(false); + + return ( + <> + + {selectedOption ? ( +
+
+ +
+
{selectedOption?.title}
+
+ ) : ( +
Select option
+ )} + + {dropdownArrow && !disabled && ( +
+ +
+ )} +
+ +
+ + {options && options.length > 0 ? ( + <> +
+
+ +
+ +
+ setSearch(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+ + {search && search.length > 0 && ( +
setSearch("")} + className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" + > + +
+ )} +
+ +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `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 }) => ( +
+
+ +
+
{option.title}
+ {selected && ( +
+ +
+ )} +
+ )} +
+ )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+ + ) : ( +

No options available.

+ )} +
+
+ + ); + }} +
+ ); + } +); diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx new file mode 100644 index 00000000000..fc8bc706d00 --- /dev/null +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -0,0 +1,228 @@ +import React from "react"; +// headless ui +import { Combobox } from "@headlessui/react"; +// lucide icons +import { ChevronDown, Search, X, Check } from "lucide-react"; +// mobx +import { observer } from "mobx-react-lite"; +// hooks +import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +interface IFiltersOption { + id: string; + title: string; + color: string | null; +} + +export interface IIssuePropertyLabels { + value?: any; + onChange?: (id: any, data: IFiltersOption) => void; + disabled?: boolean; + + className?: string; + buttonClassName?: string; + optionsClassName?: string; + dropdownArrow?: boolean; +} + +export const IssuePropertyLabels: React.FC = observer( + ({ + value, + onChange, + disabled, + + className, + buttonClassName, + optionsClassName, + dropdownArrow = true, + }) => { + const { project: projectStore }: RootStore = useMobxStore(); + + const dropdownBtn = React.useRef(null); + const dropdownOptions = React.useRef(null); + + const [isOpen, setIsOpen] = React.useState(false); + const [search, setSearch] = React.useState(""); + + const options: IFiltersOption[] | [] = + (projectStore?.projectLabels && + projectStore?.projectLabels?.length > 0 && + projectStore?.projectLabels.map((_Label: any) => ({ + id: _Label?.id, + title: _Label?.name, + color: _Label?.color || null, + }))) || + []; + + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + + const selectedOption: IFiltersOption[] | null | undefined = + (value && value?.length > 0 && options.filter((person: IFiltersOption) => value.includes(person.id))) || null; + + console.log("selectedOption", selectedOption); + + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((person: IFiltersOption) => + person.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) + ) + : []; + + return ( + { + console.log("data", data); + if (onChange && selectedOption) onChange(data, selectedOption); + }} + disabled={disabled} + multiple + > + {({ open }: { open: boolean }) => { + if (open) { + if (!isOpen) setIsOpen(true); + } else if (isOpen) setIsOpen(false); + + return ( + <> + + {selectedOption && selectedOption?.length > 0 ? ( + <> + {selectedOption?.length === 1 ? ( + <> +
+
+
{selectedOption[0]?.title}
+
+ + ) : ( + <> +
+
+
{selectedOption?.length} Labels
+
+ + )} + + ) : ( +
Select option
+ )} + + {dropdownArrow && !disabled && ( +
+ +
+ )} + + +
+ + {options && options.length > 0 ? ( + <> +
+
+ +
+ +
+ setSearch(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+ + {search && search.length > 0 && ( +
setSearch("")} + className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" + > + +
+ )} +
+ +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `cursor-pointer select-none truncate rounded px-1 py-1.5 ${ + active || (value && value.length > 0 && value.includes(option?.id)) + ? "bg-custom-background-80" + : "" + } ${ + value && value.length > 0 && value.includes(option?.id) + ? "text-custom-text-100" + : "text-custom-text-200" + }` + } + > +
+
+
{option.title}
+ {value && value.length > 0 && value.includes(option?.id) && ( +
+ +
+ )} +
+ + )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+ + ) : ( +

No options available.

+ )} + +
+ + ); + }} + + ); + } +); diff --git a/web/components/issues/issue-layouts/properties/member.tsx b/web/components/issues/issue-layouts/properties/member.tsx new file mode 100644 index 00000000000..e0c79923126 --- /dev/null +++ b/web/components/issues/issue-layouts/properties/member.tsx @@ -0,0 +1,215 @@ +import React from "react"; +// headless ui +import { Combobox } from "@headlessui/react"; +// lucide icons +import { ChevronDown, Search, X, Check } from "lucide-react"; +// mobx +import { observer } from "mobx-react-lite"; +// components +import { StateGroupIcon } from "components/icons"; +// hooks +import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; +// types +import { IState } from "types"; + +interface IFiltersOption { + id: string; + title: string; + group: string; + color: string | null; +} + +export interface IIssuePropertyMember { + value?: any; + onChange?: (id: any, data: IFiltersOption) => void; + disabled?: boolean; + + className?: string; + buttonClassName?: string; + optionsClassName?: string; + dropdownArrow?: boolean; +} + +export const IssuePropertyMember: React.FC = observer( + ({ + value, + onChange, + disabled, + + className, + buttonClassName, + optionsClassName, + dropdownArrow = true, + }) => { + const { project: projectStore }: RootStore = useMobxStore(); + + const dropdownBtn = React.useRef(null); + const dropdownOptions = React.useRef(null); + + const [isOpen, setIsOpen] = React.useState(false); + const [search, setSearch] = React.useState(""); + + const options: IFiltersOption[] | [] = + (projectStore?.projectStates && + projectStore?.projectStates?.length > 0 && + projectStore?.projectStates.map((_state: IState) => ({ + id: _state?.id, + title: _state?.name, + group: _state?.group, + color: _state?.color || null, + }))) || + []; + + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + + const selectedOption: IFiltersOption | null | undefined = + (value && options.find((person: IFiltersOption) => person.id === value)) || null; + + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((person: IFiltersOption) => + person.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) + ) + : []; + + return ( + { + if (onChange && selectedOption) onChange(data, selectedOption); + }} + disabled={disabled} + > + {({ open }: { open: boolean }) => { + if (open) { + if (!isOpen) setIsOpen(true); + } else if (isOpen) setIsOpen(false); + + return ( + <> + + {selectedOption ? ( +
+
+ +
+
{selectedOption?.title}
+
+ ) : ( +
Select option
+ )} + + {dropdownArrow && !disabled && ( +
+ +
+ )} +
+ +
+ + {options && options.length > 0 ? ( + <> +
+
+ +
+ +
+ setSearch(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+ + {search && search.length > 0 && ( +
setSearch("")} + className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" + > + +
+ )} +
+ +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `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 }) => ( +
+
+ +
+
{option.title}
+ {selected && ( +
+ +
+ )} +
+ )} +
+ )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+ + ) : ( +

No options available.

+ )} +
+
+ + ); + }} +
+ ); + } +); diff --git a/web/components/issues/issue-layouts/properties/priority.tsx b/web/components/issues/issue-layouts/properties/priority.tsx new file mode 100644 index 00000000000..7e100786a93 --- /dev/null +++ b/web/components/issues/issue-layouts/properties/priority.tsx @@ -0,0 +1,220 @@ +import React from "react"; +// headless ui +import { Combobox } from "@headlessui/react"; +// lucide icons +import { ChevronDown, Search, X, Check, AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react"; +// mobx +import { observer } from "mobx-react-lite"; +// hooks +import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; +// constants +import { ISSUE_PRIORITIES } from "constants/issue"; + +interface IFiltersOption { + id: string; + title: string; +} + +export interface IIssuePropertyPriority { + value?: any; + onChange?: (id: any, data: IFiltersOption) => void; + disabled?: boolean; + + className?: string; + buttonClassName?: string; + optionsClassName?: string; + dropdownArrow?: boolean; +} + +const Icon = ({ priority }: any) => ( +
+ {priority === "urgent" ? ( +
+ +
+ ) : priority === "high" ? ( +
+ +
+ ) : priority === "medium" ? ( +
+ +
+ ) : priority === "low" ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+); + +export const IssuePropertyPriority: React.FC = observer( + ({ + value, + onChange, + disabled, + + className, + buttonClassName, + optionsClassName, + dropdownArrow = true, + }) => { + const dropdownBtn = React.useRef(null); + const dropdownOptions = React.useRef(null); + + const [isOpen, setIsOpen] = React.useState(false); + const [search, setSearch] = React.useState(""); + + const options: IFiltersOption[] | [] = + (ISSUE_PRIORITIES && + ISSUE_PRIORITIES?.length > 0 && + ISSUE_PRIORITIES.map((_priority: any) => ({ + id: _priority?.key, + title: _priority?.title, + }))) || + []; + + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + + const selectedOption: IFiltersOption | null | undefined = + (value && options.find((person: IFiltersOption) => person.id === value)) || null; + + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((person: IFiltersOption) => + person.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) + ) + : []; + + return ( + { + if (onChange && selectedOption) onChange(data, selectedOption); + }} + disabled={disabled} + > + {({ open }: { open: boolean }) => { + if (open) { + if (!isOpen) setIsOpen(true); + } else if (isOpen) setIsOpen(false); + + return ( + <> + + {selectedOption ? ( +
+
+ +
+
{selectedOption?.title}
+
+ ) : ( +
Select option
+ )} + + {dropdownArrow && !disabled && ( +
+ +
+ )} +
+ +
+ + {options && options.length > 0 ? ( + <> +
+
+ +
+ +
+ setSearch(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+ + {search && search.length > 0 && ( +
setSearch("")} + className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" + > + +
+ )} +
+ +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `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 }) => ( +
+
+ +
+
{option.title}
+ {selected && ( +
+ +
+ )} +
+ )} +
+ )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+ + ) : ( +

No options available.

+ )} +
+
+ + ); + }} +
+ ); + } +); diff --git a/web/components/issues/issue-layouts/properties/state.tsx b/web/components/issues/issue-layouts/properties/state.tsx new file mode 100644 index 00000000000..6ca4db55fac --- /dev/null +++ b/web/components/issues/issue-layouts/properties/state.tsx @@ -0,0 +1,215 @@ +import React from "react"; +// headless ui +import { Combobox } from "@headlessui/react"; +// lucide icons +import { ChevronDown, Search, X, Check } from "lucide-react"; +// mobx +import { observer } from "mobx-react-lite"; +// components +import { StateGroupIcon } from "components/icons"; +// hooks +import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; +// types +import { IState } from "types"; + +interface IFiltersOption { + id: string; + title: string; + group: string; + color: string | null; +} + +export interface IIssuePropertyState { + value?: any; + onChange?: (id: any, data: IFiltersOption) => void; + disabled?: boolean; + + className?: string; + buttonClassName?: string; + optionsClassName?: string; + dropdownArrow?: boolean; +} + +export const IssuePropertyState: React.FC = observer( + ({ + value, + onChange, + disabled, + + className, + buttonClassName, + optionsClassName, + dropdownArrow = true, + }) => { + const { project: projectStore }: RootStore = useMobxStore(); + + const dropdownBtn = React.useRef(null); + const dropdownOptions = React.useRef(null); + + const [isOpen, setIsOpen] = React.useState(false); + const [search, setSearch] = React.useState(""); + + const options: IFiltersOption[] | [] = + (projectStore?.projectStates && + projectStore?.projectStates?.length > 0 && + projectStore?.projectStates.map((_state: IState) => ({ + id: _state?.id, + title: _state?.name, + group: _state?.group, + color: _state?.color || null, + }))) || + []; + + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + + const selectedOption: IFiltersOption | null | undefined = + (value && options.find((person: IFiltersOption) => person.id === value)) || null; + + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((person: IFiltersOption) => + person.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) + ) + : []; + + return ( + { + if (onChange && selectedOption) onChange(data, selectedOption); + }} + disabled={disabled} + > + {({ open }: { open: boolean }) => { + if (open) { + if (!isOpen) setIsOpen(true); + } else if (isOpen) setIsOpen(false); + + return ( + <> + + {selectedOption ? ( +
+
+ +
+
{selectedOption?.title}
+
+ ) : ( +
Select option
+ )} + + {dropdownArrow && !disabled && ( +
+ +
+ )} +
+ +
+ + {options && options.length > 0 ? ( + <> +
+
+ +
+ +
+ setSearch(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+ + {search && search.length > 0 && ( +
setSearch("")} + className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" + > + +
+ )} +
+ +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `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 }) => ( +
+
+ +
+
{option.title}
+ {selected && ( +
+ +
+ )} +
+ )} +
+ )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+ + ) : ( +

No options available.

+ )} +
+
+ + ); + }} +
+ ); + } +); diff --git a/web/components/states/state-select.tsx b/web/components/states/state-select.tsx index 22e4ef11511..02c2ea36513 100644 --- a/web/components/states/state-select.tsx +++ b/web/components/states/state-select.tsx @@ -87,83 +87,72 @@ export const StateSelect: React.FC = ({ useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); return ( - { - onChange(data, states); - }} - disabled={disabled} - > - {({ open }: { open: boolean }) => { - if (open) { - if (!isOpen) setIsOpen(true); - setFetchStates(true); - } else if (isOpen) setIsOpen(false); - - return ( - <> - - {label} - {!hideDropdownArrow && !disabled &&