From fd30ea9a20a679d9b1608b51b325b0f16705f556 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Sat, 24 Jun 2023 18:09:06 +0530 Subject: [PATCH] feat: editable label option added in all view , fix: view page list and kanban view mutation fix, chore: code refactor (#1390) * feat: editable label select component added in spreadsheet view * feat: editable label select option added in all view, chore: code refactor * fix: view page list and kanban view mutation fix and sub issue mutation, chore: refactor partial update issue function * fix: build fix --- .../core/board-view/single-issue.tsx | 139 ++++++++-------- .../core/calendar-view/single-issue.tsx | 94 ++++++----- .../core/list-view/single-issue.tsx | 113 ++++++------- .../core/spreadsheet-view/single-issue.tsx | 100 +++++++----- .../components/issues/my-issues-list-item.tsx | 6 +- .../issues/view-select/assignee.tsx | 4 +- .../issues/view-select/due-date.tsx | 4 +- .../issues/view-select/estimate.tsx | 4 +- .../components/issues/view-select/index.ts | 1 + .../components/issues/view-select/label.tsx | 148 ++++++++++++++++++ .../issues/view-select/priority.tsx | 4 +- .../components/issues/view-select/state.tsx | 4 +- 12 files changed, 390 insertions(+), 231 deletions(-) create mode 100644 apps/app/components/issues/view-select/label.tsx diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx index 5c0cc5102..cde1b2e38 100644 --- a/apps/app/components/core/board-view/single-issue.tsx +++ b/apps/app/components/core/board-view/single-issue.tsx @@ -23,6 +23,7 @@ import { ViewAssigneeSelect, ViewDueDateSelect, ViewEstimateSelect, + ViewLabelSelect, ViewPrioritySelect, ViewStateSelect, } from "components/issues"; @@ -44,7 +45,14 @@ import { LayerDiagonalIcon } from "components/icons"; import { handleIssuesMutation } from "constants/issue"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // types -import { ICurrentUserResponse, IIssue, Properties, TIssueGroupByOptions, UserAuth } from "types"; +import { + ICurrentUserResponse, + IIssue, + ISubIssueResponse, + Properties, + TIssueGroupByOptions, + UserAuth, +} from "types"; // fetch-keys import { CYCLE_DETAILS, @@ -52,6 +60,8 @@ import { MODULE_DETAILS, MODULE_ISSUES_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS, + SUB_ISSUES, + VIEW_ISSUES, } from "constants/fetch-keys"; type Props = { @@ -101,86 +111,68 @@ export const SingleBoardIssue: React.FC = ({ const { orderBy, params } = useIssuesView(); const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { setToastAlert } = useToast(); const partialUpdateIssue = useCallback( - (formData: Partial, issueId: string) => { + (formData: Partial, issue: IIssue) => { if (!workspaceSlug || !projectId) return; - if (cycleId) - mutate< - | { - [key: string]: IIssue[]; - } - | IIssue[] - >( - CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params), - (prevData) => - handleIssuesMutation( - formData, - groupTitle ?? "", - selectedGroup, - index, - orderBy, - prevData - ), - false - ); - else if (moduleId) - mutate< - | { - [key: string]: IIssue[]; - } - | IIssue[] - >( - MODULE_ISSUES_WITH_PARAMS(moduleId as string), - (prevData) => - handleIssuesMutation( - formData, - groupTitle ?? "", - selectedGroup, - index, - orderBy, - prevData - ), - false - ); - else { - mutate< - | { - [key: string]: IIssue[]; - } - | IIssue[] - >( - PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params), + const fetchKey = cycleId + ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) + : moduleId + ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) + : viewId + ? VIEW_ISSUES(viewId.toString(), params) + : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); + + if (issue.parent) { + mutate( + SUB_ISSUES(issue.parent.toString()), (prevData) => { if (!prevData) return prevData; - return handleIssuesMutation( + return { + ...prevData, + sub_issues: (prevData.sub_issues ?? []).map((i) => { + if (i.id === issue.id) { + return { + ...i, + ...formData, + }; + } + return i; + }), + }; + }, + false + ); + } else { + mutate< + | { + [key: string]: IIssue[]; + } + | IIssue[] + >( + fetchKey, + (prevData) => + handleIssuesMutation( formData, groupTitle ?? "", selectedGroup, index, orderBy, prevData - ); - }, + ), false ); } issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) + .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user) .then(() => { - if (cycleId) { - mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); - mutate(CYCLE_DETAILS(cycleId as string)); - } else if (moduleId) { - mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)); - mutate(MODULE_DETAILS(moduleId as string)); - } else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params)); + mutate(fetchKey); }); }, [ @@ -188,6 +180,7 @@ export const SingleBoardIssue: React.FC = ({ projectId, cycleId, moduleId, + viewId, groupTitle, index, selectedGroup, @@ -370,23 +363,15 @@ export const SingleBoardIssue: React.FC = ({ isNotAllowed={isNotAllowed} /> )} - {properties.labels && issue.label_details.length > 0 && ( -
- {issue.label_details.map((label) => ( -
- - {label.name} -
- ))} -
+ {properties.labels && ( + )} {properties.assignee && ( = ({ const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); const partialUpdateIssue = useCallback( - (formData: Partial, issueId: string) => { + (formData: Partial, issue: IIssue) => { if (!workspaceSlug || !projectId) return; const fetchKey = cycleId @@ -79,25 +81,54 @@ export const SingleCalendarIssue: React.FC = ({ ? VIEW_ISSUES(viewId.toString(), params) : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); - mutate( - fetchKey, - (prevData) => - (prevData ?? []).map((p) => { - if (p.id === issueId) { - return { - ...p, - ...formData, - assignees: formData?.assignees_list ?? p.assignees, - }; - } + if (issue.parent) { + mutate( + SUB_ISSUES(issue.parent.toString()), + (prevData) => { + if (!prevData) return prevData; - return p; - }), - false - ); + return { + ...prevData, + sub_issues: (prevData.sub_issues ?? []).map((i) => { + if (i.id === issue.id) { + return { + ...i, + ...formData, + }; + } + return i; + }), + }; + }, + false + ); + } else { + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((p) => { + if (p.id === issue.id) { + return { + ...p, + ...formData, + assignees: formData?.assignees_list ?? p.assignees, + }; + } + + return p; + }), + false + ); + } issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, formData, user) + .patchIssue( + workspaceSlug as string, + projectId as string, + issue.id as string, + formData, + user + ) .then(() => { mutate(fetchKey); }) @@ -207,25 +238,14 @@ export const SingleCalendarIssue: React.FC = ({ isNotAllowed={isNotAllowed} /> )} - {properties.labels && issue.label_details.length > 0 ? ( -
- {issue.label_details.map((label) => ( - - - {label.name} - - ))} -
- ) : ( - "" + {properties.labels && ( + )} {properties.assignee && ( = ({ const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { setToastAlert } = useToast(); const { groupByProperty: selectedGroup, orderBy, params } = useIssueView(); const partialUpdateIssue = useCallback( - (formData: Partial, issueId: string) => { + (formData: Partial, issue: IIssue) => { if (!workspaceSlug || !projectId) return; - if (cycleId) + const fetchKey = cycleId + ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) + : moduleId + ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) + : viewId + ? VIEW_ISSUES(viewId.toString(), params) + : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); + + if (issue.parent) { + mutate( + SUB_ISSUES(issue.parent.toString()), + (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + sub_issues: (prevData.sub_issues ?? []).map((i) => { + if (i.id === issue.id) { + return { + ...i, + ...formData, + }; + } + return i; + }), + }; + }, + false + ); + } else { mutate< | { [key: string]: IIssue[]; } | IIssue[] >( - CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params), + fetchKey, (prevData) => handleIssuesMutation( formData, @@ -109,49 +141,12 @@ export const SingleListIssue: React.FC = ({ ), false ); - - if (moduleId) - mutate< - | { - [key: string]: IIssue[]; - } - | IIssue[] - >( - MODULE_ISSUES_WITH_PARAMS(moduleId as string, params), - (prevData) => - handleIssuesMutation( - formData, - groupTitle ?? "", - selectedGroup, - index, - orderBy, - prevData - ), - false - ); - - mutate< - | { - [key: string]: IIssue[]; - } - | IIssue[] - >( - PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params), - (prevData) => - handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, orderBy, prevData), - false - ); + } issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) + .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user) .then(() => { - if (cycleId) { - mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); - mutate(CYCLE_DETAILS(cycleId as string)); - } else if (moduleId) { - mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)); - mutate(MODULE_DETAILS(moduleId as string)); - } else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params)); + mutate(fetchKey); }); }, [ @@ -159,6 +154,7 @@ export const SingleListIssue: React.FC = ({ projectId, cycleId, moduleId, + viewId, groupTitle, index, selectedGroup, @@ -275,25 +271,14 @@ export const SingleListIssue: React.FC = ({ isNotAllowed={isNotAllowed} /> )} - {properties.labels && issue.label_details.length > 0 ? ( -
- {issue.label_details.map((label) => ( - - - {label.name} - - ))} -
- ) : ( - "" + {properties.labels && ( + )} {properties.assignee && ( = ({ const { setToastAlert } = useToast(); const partialUpdateIssue = useCallback( - (formData: Partial, issueId: string) => { + (formData: Partial, issue: IIssue) => { if (!workspaceSlug || !projectId) return; const fetchKey = cycleId @@ -78,23 +80,52 @@ export const SingleSpreadsheetIssue: React.FC = ({ ? VIEW_ISSUES(viewId.toString(), params) : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); - mutate( - fetchKey, - (prevData) => - (prevData ?? []).map((p) => { - if (p.id === issueId) { - return { - ...p, - ...formData, - }; - } - return p; - }), - false - ); + if (issue.parent) { + mutate( + SUB_ISSUES(issue.parent.toString()), + (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + sub_issues: (prevData.sub_issues ?? []).map((i) => { + if (i.id === issue.id) { + return { + ...i, + ...formData, + }; + } + return i; + }), + }; + }, + false + ); + } else { + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((p) => { + if (p.id === issue.id) { + return { + ...p, + ...formData, + }; + } + return p; + }), + false + ); + } issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, formData, user) + .patchIssue( + workspaceSlug as string, + projectId as string, + issue.id as string, + formData, + user + ) .then(() => { mutate(fetchKey); }) @@ -191,30 +222,19 @@ export const SingleSpreadsheetIssue: React.FC = ({ /> )} - {properties.labels ? ( - issue.label_details.length > 0 ? ( -
- {issue.label_details.slice(0, 4).map((label, index) => ( -
- -
- ))} - {issue.label_details.length > 4 ? +{issue.label_details.length - 4} : null} -
- ) : ( -
- No Labels -
- ) - ) : ( - "" + {properties.labels && ( +
+ +
)} + {properties.due_date && (
= ({ issue, properties, projectId const { setToastAlert } = useToast(); const partialUpdateIssue = useCallback( - (formData: Partial, issueId: string) => { + (formData: Partial, issue: IIssue) => { if (!workspaceSlug) return; mutate( USER_ISSUE(workspaceSlug as string), (prevData) => prevData?.map((p) => { - if (p.id === issueId) return { ...p, ...formData }; + if (p.id === issue.id) return { ...p, ...formData }; return p; }), @@ -59,7 +59,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId ); issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) + .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user) .then((res) => { mutate(USER_ISSUE(workspaceSlug as string)); }) diff --git a/apps/app/components/issues/view-select/assignee.tsx b/apps/app/components/issues/view-select/assignee.tsx index 1dbfbabba..8bfa797fe 100644 --- a/apps/app/components/issues/view-select/assignee.tsx +++ b/apps/app/components/issues/view-select/assignee.tsx @@ -18,7 +18,7 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys"; type Props = { issue: IIssue; - partialUpdateIssue: (formData: Partial, issueId: string) => void; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; position?: "left" | "right"; selfPositioned?: boolean; tooltipPosition?: "left" | "right"; @@ -108,7 +108,7 @@ export const ViewAssigneeSelect: React.FC = ({ if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); else newData.push(data); - partialUpdateIssue({ assignees_list: data }, issue.id); + partialUpdateIssue({ assignees_list: data }, issue); trackEventServices.trackIssuePartialPropertyUpdateEvent( { diff --git a/apps/app/components/issues/view-select/due-date.tsx b/apps/app/components/issues/view-select/due-date.tsx index f74b62689..163816a99 100644 --- a/apps/app/components/issues/view-select/due-date.tsx +++ b/apps/app/components/issues/view-select/due-date.tsx @@ -11,7 +11,7 @@ import { ICurrentUserResponse, IIssue } from "types"; type Props = { issue: IIssue; - partialUpdateIssue: (formData: Partial, issueId: string) => void; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; noBorder?: boolean; user: ICurrentUserResponse | undefined; isNotAllowed: boolean; @@ -48,7 +48,7 @@ export const ViewDueDateSelect: React.FC = ({ priority: issue.priority, state: issue.state, }, - issue.id + issue ); trackEventServices.trackIssuePartialPropertyUpdateEvent( { diff --git a/apps/app/components/issues/view-select/estimate.tsx b/apps/app/components/issues/view-select/estimate.tsx index 02a3e0710..0f932405f 100644 --- a/apps/app/components/issues/view-select/estimate.tsx +++ b/apps/app/components/issues/view-select/estimate.tsx @@ -15,7 +15,7 @@ import { ICurrentUserResponse, IIssue } from "types"; type Props = { issue: IIssue; - partialUpdateIssue: (formData: Partial, issueId: string) => void; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; position?: "left" | "right"; selfPositioned?: boolean; customButton?: boolean; @@ -54,7 +54,7 @@ export const ViewEstimateSelect: React.FC = ({ { - partialUpdateIssue({ estimate_point: val }, issue.id); + partialUpdateIssue({ estimate_point: val }, issue); trackEventServices.trackIssuePartialPropertyUpdateEvent( { workspaceSlug, diff --git a/apps/app/components/issues/view-select/index.ts b/apps/app/components/issues/view-select/index.ts index 55ecfcbdb..a05cf61b6 100644 --- a/apps/app/components/issues/view-select/index.ts +++ b/apps/app/components/issues/view-select/index.ts @@ -3,3 +3,4 @@ export * from "./due-date"; export * from "./estimate"; export * from "./priority"; export * from "./state"; +export * from "./label"; diff --git a/apps/app/components/issues/view-select/label.tsx b/apps/app/components/issues/view-select/label.tsx new file mode 100644 index 000000000..33df5cf9f --- /dev/null +++ b/apps/app/components/issues/view-select/label.tsx @@ -0,0 +1,148 @@ +import React, { useState } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import issuesService from "services/issues.service"; +// component +import { CreateLabelModal } from "components/labels"; +// ui +import { CustomSearchSelect, Tooltip } from "components/ui"; +// icons +import { PlusIcon, TagIcon } from "@heroicons/react/24/outline"; +// types +import { ICurrentUserResponse, IIssue, IIssueLabels } from "types"; +// fetch-keys +import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; + +type Props = { + issue: IIssue; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + position?: "left" | "right"; + selfPositioned?: boolean; + tooltipPosition?: "left" | "right"; + customButton?: boolean; + user: ICurrentUserResponse | undefined; + isNotAllowed: boolean; +}; + +export const ViewLabelSelect: React.FC = ({ + issue, + partialUpdateIssue, + position = "left", + selfPositioned = false, + tooltipPosition = "right", + user, + isNotAllowed, + customButton = false, +}) => { + const [labelModal, setLabelModal] = useState(false); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: issueLabels } = useSWR( + projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null, + workspaceSlug && projectId + ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) + : null + ); + + const options = issueLabels?.map((label) => ({ + value: label.id, + query: label.name, + content: ( +
+ + {label.name} +
+ ), + })); + + const labelsLabel = ( + 0 + ? issue.label_details.map((label) => label.name ?? "").join(", ") + : "No Label" + } + > +
+ {issue.label_details.length > 0 ? ( + <> + {issue.label_details.slice(0, 4).map((label, index) => ( +
+ +
+ ))} + {issue.label_details.length > 4 ? +{issue.label_details.length - 4} : null} + + ) : ( + <> + + + )} +
+
+ ); + + const footerOption = ( + + ); + + return ( + <> + {projectId && ( + setLabelModal(false)} + projectId={projectId.toString()} + user={user} + /> + )} + { + partialUpdateIssue({ labels_list: data }, issue); + }} + options={options} + {...(customButton ? { customButton: labelsLabel } : { label: labelsLabel })} + multiple + noChevron + position={position} + disabled={isNotAllowed} + selfPositioned={selfPositioned} + footerOption={footerOption} + dropdownWidth="w-full min-w-[12rem]" + /> + + ); +}; diff --git a/apps/app/components/issues/view-select/priority.tsx b/apps/app/components/issues/view-select/priority.tsx index 499546931..0afd2dd98 100644 --- a/apps/app/components/issues/view-select/priority.tsx +++ b/apps/app/components/issues/view-select/priority.tsx @@ -17,7 +17,7 @@ import { capitalizeFirstLetter } from "helpers/string.helper"; type Props = { issue: IIssue; - partialUpdateIssue: (formData: Partial, issueId: string) => void; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; position?: "left" | "right"; selfPositioned?: boolean; noBorder?: boolean; @@ -41,7 +41,7 @@ export const ViewPrioritySelect: React.FC = ({ { - partialUpdateIssue({ priority: data }, issue.id); + partialUpdateIssue({ priority: data }, issue); trackEventServices.trackIssuePartialPropertyUpdateEvent( { workspaceSlug, diff --git a/apps/app/components/issues/view-select/state.tsx b/apps/app/components/issues/view-select/state.tsx index c097c7326..5ec0f71c7 100644 --- a/apps/app/components/issues/view-select/state.tsx +++ b/apps/app/components/issues/view-select/state.tsx @@ -19,7 +19,7 @@ import { STATES_LIST } from "constants/fetch-keys"; type Props = { issue: IIssue; - partialUpdateIssue: (formData: Partial, issueId: string) => void; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; position?: "left" | "right"; selfPositioned?: boolean; customButton?: boolean; @@ -83,7 +83,7 @@ export const ViewStateSelect: React.FC = ({ priority: issue.priority, target_date: issue.target_date, }, - issue.id + issue ); trackEventServices.trackIssuePartialPropertyUpdateEvent( {