From b5b809500dc671e8aba29d64a6ee6fcc073db3db Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Wed, 4 Oct 2023 14:38:49 +0530 Subject: [PATCH] chore: issue properties for list and kanban layouts and implemented estimates in project store (#2363) * chore: issue properties for state, priorit, labels and members * feat: implemented assignee, labels properties * fix: implemented estimates in project store and issue properties * chore: staer_date and due_date and validation properties in kanban --- web/components/core/views/all-views.tsx | 45 +-- .../issues/issue-layouts/kanban/block.tsx | 27 +- .../issues/issue-layouts/kanban/default.tsx | 182 +++++++----- .../issue-layouts/kanban/headers/priority.tsx | 1 + .../issue-layouts/kanban/properties.tsx | 272 +++++++++++++----- .../issues/issue-layouts/kanban/root.tsx | 24 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 267 +++++++++-------- .../issue-layouts/properties/assignee.tsx | 266 +++++++++++++++++ .../issues/issue-layouts/properties/date.tsx | 96 +++++++ .../properties/dropdown-template.tsx | 177 ++++++++++++ .../issue-layouts/properties/estimates.tsx | 215 ++++++++++++++ .../issue-layouts/properties/labels.tsx | 234 +++++++++++++++ .../issue-layouts/properties/priority.tsx | 224 +++++++++++++++ .../issues/issue-layouts/properties/state.tsx | 218 ++++++++++++++ web/components/states/state-select.tsx | 139 +++++---- web/components/ui/tooltip.tsx | 10 +- web/store/issue.ts | 8 +- web/store/project.ts | 62 +++- 18 files changed, 2074 insertions(+), 393 deletions(-) create mode 100644 web/components/issues/issue-layouts/properties/assignee.tsx 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/priority.tsx create mode 100644 web/components/issues/issue-layouts/properties/state.tsx diff --git a/web/components/core/views/all-views.tsx b/web/components/core/views/all-views.tsx index 85830ee27..a8d6f929a 100644 --- a/web/components/core/views/all-views.tsx +++ b/web/components/core/views/all-views.tsx @@ -19,32 +19,39 @@ export const AllViews: React.FC = observer(() => { const { issue: issueStore, project: projectStore, issueFilter: issueFilterStore } = useMobxStore(); - useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES` : null, async () => { - if (workspaceSlug && projectId) { - await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId); + useSWR( + workspaceSlug && projectId ? `PROJECT_ISSUES` : null, + async () => { + if (workspaceSlug && projectId) { + await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId); - await projectStore.fetchProjectStates(workspaceSlug, projectId); - await projectStore.fetchProjectLabels(workspaceSlug, projectId); - await projectStore.fetchProjectMembers(workspaceSlug, projectId); + await projectStore.fetchProjectStates(workspaceSlug, projectId); + await projectStore.fetchProjectLabels(workspaceSlug, projectId); + await projectStore.fetchProjectMembers(workspaceSlug, projectId); + await projectStore.fetchProjectEstimates(workspaceSlug, projectId); - await issueStore.fetchIssues(workspaceSlug, projectId); - } - }); + await issueStore.fetchIssues(workspaceSlug, projectId); + } + }, + { revalidateOnFocus: false } + ); const activeLayout = issueFilterStore.userDisplayFilters.layout; return (
- - {activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} + +
+ {activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
); }); diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index ad424df25..c65680dc1 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -8,9 +8,18 @@ interface IssueBlockProps { columnId: string; issues: any; isDragDisabled: boolean; + handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; + display_properties: any; } -export const IssueBlock = ({ sub_group_id, columnId, issues, isDragDisabled }: IssueBlockProps) => ( +export const IssueBlock = ({ + sub_group_id, + columnId, + issues, + isDragDisabled, + handleIssues, + display_properties, +}: IssueBlockProps) => ( <> {issues && issues.length > 0 ? ( <> @@ -30,14 +39,22 @@ export const IssueBlock = ({ sub_group_id, columnId, issues, isDragDisabled }: I ref={provided.innerRef} >
-
ONE-{issue.sequence_id}
+ {display_properties && display_properties?.key && ( +
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 842550e40..395e93ea8 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -19,12 +19,23 @@ 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; + display_properties: any; } 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, + display_properties, + }) => { const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); const verticalAlignPosition = (_list: any) => @@ -42,7 +53,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 +75,8 @@ const GroupByKanBan: React.FC = observer( columnId={getValueFromObject(_list, listKey) as string} issues={issues[getValueFromObject(_list, listKey) as string]} isDragDisabled={isDragDisabled} + handleIssues={handleIssues} + display_properties={display_properties} /> ) : ( isDragDisabled && ( @@ -90,86 +103,101 @@ 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; + display_properties: any; } -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, display_properties }) => { + 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 38e4afbc4..91820530b 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 b2a8ce8ff..03df73fca 100644 --- a/web/components/issues/issue-layouts/kanban/properties.tsx +++ b/web/components/issues/issue-layouts/kanban/properties.tsx @@ -1,91 +1,205 @@ +// mobx +import { observer } from "mobx-react-lite"; // lucide icons -import { Circle } from "lucide-react"; +import { Layers, Link, Paperclip } from "lucide-react"; +// components +import { IssuePropertyState } from "../properties/state"; +import { IssuePropertyPriority } from "../properties/priority"; +import { IssuePropertyLabels } from "../properties/labels"; +import { IssuePropertyAssignee } from "../properties/assignee"; +import { IssuePropertyEstimates } from "../properties/estimates"; +import { IssuePropertyStartDate } from "../properties/date"; +import { Tooltip } from "components/ui"; -export const KanBanProperties = () => { - console.log("properties"); - return ( -
- {/* basic properties */} - {/* state */} -
-
- -
-
state
-
+export interface IKanBanProperties { + sub_group_id: string; + columnId: string; + issue: any; + handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; + display_properties: any; +} - {/* priority */} -
-
- -
-
priority
-
+export const KanBanProperties: React.FC = observer( + ({ sub_group_id, columnId: group_id, issue, handleIssues, display_properties }) => { + 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 } + ); + }; - {/* label */} -
-
- -
-
label
-
+ 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 } + ); + }; - {/* assignee */} -
-
- -
-
assignee
-
+ const handleLabel = (ids: 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: ids } + ); + }; - {/* start date */} -
-
- -
-
start date
-
+ const handleAssignee = (ids: string[]) => { + if (handleIssues) + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, assignees: ids } + ); + }; - {/* target/due date */} -
-
- -
-
target/due date
-
+ const handleStartDate = (date: string) => { + if (handleIssues) + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, start_date: date } + ); + }; - {/* extra render properties */} - {/* estimate */} -
-
- -
-
0
-
+ const handleTargetDate = (date: string) => { + if (handleIssues) + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, target_date: date } + ); + }; - {/* sub-issues */} -
-
- -
-
0
-
+ const handleEstimate = (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, estimate_point: id } + ); + }; - {/* attachments */} -
-
- -
-
0
-
+ return ( +
+ {/* basic properties */} + {/* state */} + {display_properties && display_properties?.state && ( + handleState(id)} + disabled={false} + /> + )} - {/* link */} -
-
- -
-
0
+ {/* priority */} + {display_properties && display_properties?.priority && ( + handlePriority(id)} + disabled={false} + /> + )} + + {/* label */} + {display_properties && display_properties?.labels && ( + handleLabel(ids)} + disabled={false} + /> + )} + + {/* assignee */} + {display_properties && display_properties?.assignee && ( + handleAssignee(ids)} + disabled={false} + /> + )} + + {/* start date */} + {display_properties && display_properties?.start_date && ( + handleStartDate(date)} + disabled={false} + /> + )} + + {/* target/due date */} + {display_properties && display_properties?.due_date && ( + handleTargetDate(date)} + disabled={false} + /> + )} + + {/* estimates */} + {display_properties && display_properties?.estimate && ( + handleEstimate(id)} + disabled={false} + workspaceSlug={issue?.workspace_detail?.slug || null} + projectId={issue?.project_detail?.id || null} + /> + )} + + {/* extra render properties */} + {/* sub-issues */} + {display_properties && display_properties?.sub_issue_count && ( + +
+
+ +
+
{issue.sub_issues_count}
+
+
+ )} + + {/* attachments */} + {display_properties && display_properties?.attachment_count && ( + +
+
+ +
+
{issue.attachment_count}
+
+
+ )} + + {/* link */} + {display_properties && display_properties?.link && ( + +
+
+ +
+
{issue.link_count}
+
+
+ )}
-
- ); -}; + ); + } +); + +created_on: true; +updated_on: true; +due_date: true; + +key: true; diff --git a/web/components/issues/issue-layouts/kanban/root.tsx b/web/components/issues/issue-layouts/kanban/root.tsx index 5701a297d..4d72936c0 100644 --- a/web/components/issues/issue-layouts/kanban/root.tsx +++ b/web/components/issues/issue-layouts/kanban/root.tsx @@ -25,6 +25,8 @@ export const KanBanLayout: React.FC = observer(() => { const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; + const display_properties = issueFilterStore?.userDisplayProperties || null; + const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by ? "swimlanes" : "default"; @@ -45,13 +47,29 @@ 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) => { + 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 5579b33cd..6f0dee695 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -54,187 +54,208 @@ const SubGroupSwimlaneHeader: React.FC = ({ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { issues: any; + handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; + display_properties: any; } -const SubGroupSwimlane: React.FC = observer(({ issues, sub_group_by, group_by, list, listKey }) => { - const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); +const SubGroupSwimlane: React.FC = observer( + ({ issues, sub_group_by, group_by, list, listKey, handleIssues, display_properties }) => { + 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; - }; + 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) => ( -
-
-
- + 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; + display_properties: any; } -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, display_properties }) => { + 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/assignee.tsx b/web/components/issues/issue-layouts/properties/assignee.tsx new file mode 100644 index 000000000..26ec6f204 --- /dev/null +++ b/web/components/issues/issue-layouts/properties/assignee.tsx @@ -0,0 +1,266 @@ +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 { Tooltip } from "components/ui"; +// 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; + avatar: string; +} + +export interface IIssuePropertyAssignee { + value?: any; + onChange?: (id: any, data: any) => void; + disabled?: boolean; + + className?: string; + buttonClassName?: string; + optionsClassName?: string; + dropdownArrow?: boolean; +} + +export const IssuePropertyAssignee: 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?.projectMembers && + projectStore?.projectMembers?.length > 0 && + projectStore?.projectMembers.map((_member: any) => ({ + id: _member?.member?.id, + title: _member?.member?.display_name, + avatar: _member?.member?.avatar && _member?.member?.avatar !== "" ? _member?.member?.avatar : null, + }))) || + []; + + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + + const selectedOption: IFiltersOption[] = + (value && value?.length > 0 && options.filter((_member: IFiltersOption) => value.includes(_member.id))) || []; + + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((_member: IFiltersOption) => + _member.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) + ) + : []; + + const assigneeRenderLength = 5; + + return ( + _member.id) as string[]} + onChange={(data: string[]) => { + 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?.length > 0 ? ( + <> + {selectedOption?.length > 1 ? ( + _label.title) || []).join(", ")} + > +
+ {selectedOption.slice(0, assigneeRenderLength).map((_assignee) => ( +
+ {_assignee && _assignee.avatar ? ( + {_assignee.title} + ) : ( + _assignee.title[0] + )} +
+ ))} + {selectedOption.length > assigneeRenderLength && ( +
+ +{selectedOption?.length - assigneeRenderLength} +
+ )} +
+
+ ) : ( + _label.title) || []).join(", ")} + > +
+
+ {selectedOption[0] && selectedOption[0].avatar ? ( + {selectedOption[0].title} + ) : ( +
+ {selectedOption[0].title[0]} +
+ )} +
+
{selectedOption[0].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 || (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 && option.avatar ? ( + {option.title} + ) : ( +
+ {option.title[0]} +
+ )} +
+
{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/date.tsx b/web/components/issues/issue-layouts/properties/date.tsx new file mode 100644 index 000000000..37853ced1 --- /dev/null +++ b/web/components/issues/issue-layouts/properties/date.tsx @@ -0,0 +1,96 @@ +import React from "react"; +// headless ui +import { Popover } from "@headlessui/react"; +// lucide icons +import { Calendar, X } from "lucide-react"; +// react date picker +import DatePicker from "react-datepicker"; +// mobx +import { observer } from "mobx-react-lite"; +// components +import { Tooltip } from "components/ui"; +// hooks +import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; +// helpers +import { renderDateFormat } from "helpers/date-time.helper"; + +export interface IIssuePropertyStartDate { + value?: any; + onChange?: (date: any) => void; + disabled?: boolean; +} + +export const IssuePropertyStartDate: React.FC = observer(({ value, onChange, disabled }) => { + const dropdownBtn = React.useRef(null); + const dropdownOptions = React.useRef(null); + + const [isOpen, setIsOpen] = React.useState(false); + + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + + return ( + + {({ open }) => { + if (open) { + if (!isOpen) setIsOpen(true); + } else if (isOpen) setIsOpen(false); + + return ( + <> + + +
+
+ +
+ {value ? ( + <> +
{value}
+
{ + if (onChange) onChange(null); + }} + > + +
+ + ) : ( +
Select date
+ )} +
+
+
+ +
+ + {({ close }) => ( + { + if (onChange && val) { + onChange(renderDateFormat(val)); + close(); + } + }} + dateFormat="dd-MM-yyyy" + calendarClassName="h-full" + inline + /> + )} + +
+ + ); + }} +
+ ); +}); 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 000000000..4afc04cb0 --- /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 000000000..1647d1a16 --- /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, Triangle } from "lucide-react"; +// mobx +import { observer } from "mobx-react-lite"; +// components +import { Tooltip } from "components/ui"; +// 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; + key: string; +} + +export interface IIssuePropertyEstimates { + value?: any; + onChange?: (id: any) => void; + disabled?: boolean; + + workspaceSlug?: string; + projectId?: string; + + className?: string; + buttonClassName?: string; + optionsClassName?: string; + dropdownArrow?: boolean; +} + +export const IssuePropertyEstimates: React.FC = observer( + ({ + value, + onChange, + disabled, + + workspaceSlug, + projectId, + + 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 projectDetail = + (workspaceSlug && projectId && projectStore?.getProjectById(workspaceSlug, projectId)) || null; + const projectEstimateId = (projectDetail && projectDetail?.estimate) || null; + const estimates = (projectEstimateId && projectStore?.getProjectEstimateById(projectEstimateId)) || null; + + const options: IFiltersOption[] | [] = + (estimates && + estimates.points && + estimates.points.length > 0 && + estimates.points.map((_estimate) => ({ + id: _estimate?.id, + title: _estimate?.value, + key: _estimate?.key.toString(), + }))) || + []; + + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + + const selectedOption: IFiltersOption | null | undefined = + (value && options.find((_estimate: IFiltersOption) => _estimate.key === value)) || null; + + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((_estimate: IFiltersOption) => + _estimate.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) + ) + : []; + + return ( + { + if (onChange) onChange(data); + }} + 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 000000000..c1f60f909 --- /dev/null +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -0,0 +1,234 @@ +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 { Tooltip } from "components/ui"; +// 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: any) => 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[] = + (value && value?.length > 0 && options.filter((_label: IFiltersOption) => value.includes(_label.id))) || []; + + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((_label: IFiltersOption) => + _label.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) + ) + : []; + + return ( + _label.id) as string[]} + onChange={(data: string[]) => { + 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?.length > 0 ? ( + <> + {selectedOption?.length === 1 ? ( + _label.title) || []).join(", ")} + > +
+
+
{selectedOption[0]?.title}
+
+ + ) : ( + _label.title) || []).join(", ")} + > +
+
+
{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/priority.tsx b/web/components/issues/issue-layouts/properties/priority.tsx new file mode 100644 index 000000000..a6776633a --- /dev/null +++ b/web/components/issues/issue-layouts/properties/priority.tsx @@ -0,0 +1,224 @@ +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"; +// components +import { Tooltip } from "components/ui"; +// 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((_priority: IFiltersOption) => _priority.id === value)) || null; + + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((_priority: IFiltersOption) => + _priority.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 000000000..28ed1fe0e --- /dev/null +++ b/web/components/issues/issue-layouts/properties/state.tsx @@ -0,0 +1,218 @@ +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 { Tooltip } from "components/ui"; +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((_state: IFiltersOption) => _state.id === value)) || null; + + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((_state: IFiltersOption) => + _state.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 22e4ef115..02c2ea365 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 &&