From 62607ade6f54d3de7afe02fc4d4450d2a791c1f4 Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Thu, 22 Feb 2024 20:24:15 +0530 Subject: [PATCH 01/67] disable peek overview for temporary issues until it is confirmed (#3749) --- packages/ui/src/control-link/control-link.tsx | 5 ++++- .../issues/issue-layouts/calendar/issue-blocks.tsx | 1 + web/components/issues/issue-layouts/gantt/blocks.tsx | 4 +++- web/components/issues/issue-layouts/kanban/block.tsx | 1 + web/components/issues/issue-layouts/list/block.tsx | 1 + .../issues/issue-layouts/spreadsheet/issue-row.tsx | 1 + 6 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/control-link/control-link.tsx b/packages/ui/src/control-link/control-link.tsx index dbdbaf095..ee4b66d7b 100644 --- a/packages/ui/src/control-link/control-link.tsx +++ b/packages/ui/src/control-link/control-link.tsx @@ -5,10 +5,11 @@ export type TControlLink = React.AnchorHTMLAttributes & { onClick: () => void; children: React.ReactNode; target?: string; + disabled?: boolean; }; export const ControlLink: React.FC = (props) => { - const { href, onClick, children, target = "_self", ...rest } = props; + const { href, onClick, children, target = "_self", disabled = false, ...rest } = props; const LEFT_CLICK_EVENT_CODE = 0; const _onClick = (event: React.MouseEvent) => { @@ -19,6 +20,8 @@ export const ControlLink: React.FC = (props) => { } }; + if (disabled) return <>{children}; + return ( {children} diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index 49ca84eb6..9f3532302 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -79,6 +79,7 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { target="_blank" onClick={() => handleIssuePeekOverview(issue)} className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" + disabled={!!issue?.tempId} > <> {issue?.tempId !== undefined && ( diff --git a/web/components/issues/issue-layouts/gantt/blocks.tsx b/web/components/issues/issue-layouts/gantt/blocks.tsx index 18a767455..d668b8a44 100644 --- a/web/components/issues/issue-layouts/gantt/blocks.tsx +++ b/web/components/issues/issue-layouts/gantt/blocks.tsx @@ -29,6 +29,7 @@ export const IssueGanttBlock: React.FC = observer((props) => { const handleIssuePeekOverview = () => workspaceSlug && issueDetails && + !issueDetails.tempId && setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id }); return ( @@ -89,8 +90,9 @@ export const IssueGanttSidebarBlock: React.FC = observer((props) => { target="_blank" onClick={handleIssuePeekOverview} className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" + disabled={!!issueDetails?.tempId} > -
+
{stateDetails && }
{projectDetails?.identifier} {issueDetails?.sequence_id} diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 15ee24b07..0dc9aa908 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -82,6 +82,7 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop target="_blank" onClick={() => handleIssuePeekOverview(issue)} className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" + disabled={!!issue?.tempId} > {issue.name} diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index 26537c3a5..1bbd574ed 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -76,6 +76,7 @@ export const IssueBlock: React.FC = observer((props: IssueBlock target="_blank" onClick={() => handleIssuePeekOverview(issue)} className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" + disabled={!!issue?.tempId} > {issue.name} diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index 1f6a755a4..b241a5168 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -239,6 +239,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { target="_blank" onClick={() => handleIssuePeekOverview(issueDetail)} className="clickable w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" + disabled={!!issueDetail?.tempId} >
From 3372e2175903fe6878ecd964e36a427f36eeed02 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 23 Feb 2024 18:44:05 +0530 Subject: [PATCH 02/67] [WEB-537] fix: issue in all-issue create view modal, filter needs to disappear when filter is de selected. (#3781) --- web/components/workspace/views/form.tsx | 39 +++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/web/components/workspace/views/form.tsx b/web/components/workspace/views/form.tsx index ce04b2f2d..71627c08a 100644 --- a/web/components/workspace/views/form.tsx +++ b/web/components/workspace/views/form.tsx @@ -8,7 +8,7 @@ import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components // ui import { Button, Input, TextArea } from "@plane/ui"; // types -import { IWorkspaceView } from "@plane/types"; +import { IIssueFilterOptions, IWorkspaceView } from "@plane/types"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; @@ -39,7 +39,7 @@ export const WorkspaceViewForm: React.FC = observer((props) => { reset, setValue, watch, - } = useForm({ + } = useForm({ defaultValues, }); @@ -59,7 +59,35 @@ export const WorkspaceViewForm: React.FC = observer((props) => { }); }, [data, preLoadedData, reset]); - const selectedFilters = watch("filters"); + const selectedFilters: IIssueFilterOptions = watch("filters"); + + // filters whose value not null or empty array + let appliedFilters: IIssueFilterOptions | undefined = undefined; + Object.entries(selectedFilters ?? {}).forEach(([key, value]) => { + if (!value) return; + if (Array.isArray(value) && value.length === 0) return; + if (!appliedFilters) appliedFilters = {}; + appliedFilters[key as keyof IIssueFilterOptions] = value; + }); + + const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { + // To clear all filters of any particular filter key. + if (!value) { + setValue("filters", { + ...selectedFilters, + [key]: [], + }); + return; + } + + let newValues = selectedFilters?.[key] ?? []; + newValues = newValues.filter((val) => val !== value); + + setValue("filters", { + ...selectedFilters, + [key]: newValues, + }); + }; const clearAllFilters = () => { if (!selectedFilters) return; @@ -151,11 +179,12 @@ export const WorkspaceViewForm: React.FC = observer((props) => { {selectedFilters && Object.keys(selectedFilters).length > 0 && (
{}} + handleRemoveFilter={handleRemoveFilter} labels={workspaceLabels ?? undefined} states={undefined} + alwaysAllowEditing />
)} From 18b5115546f398a766d4e4f13d8dde9baa580109 Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:45:48 +0530 Subject: [PATCH 03/67] re enable sub issues toggle in spreadsheet layout (#3779) --- web/constants/issue.ts | 4 ++-- web/store/issue/archived/filter.store.ts | 7 ------- web/store/issue/cycle/filter.store.ts | 8 -------- web/store/issue/draft/filter.store.ts | 8 -------- web/store/issue/module/filter.store.ts | 8 -------- web/store/issue/profile/filter.store.ts | 7 ------- web/store/issue/project-views/filter.store.ts | 7 ------- web/store/issue/project/filter.store.ts | 7 ------- 8 files changed, 2 insertions(+), 54 deletions(-) diff --git a/web/constants/issue.ts b/web/constants/issue.ts index 4bb6bcec0..b2a8cd855 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -395,8 +395,8 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { type: [null, "active", "backlog"], }, extra_options: { - access: false, - values: [], + access: true, + values: ["sub_issue"], }, }, gantt_chart: { diff --git a/web/store/issue/archived/filter.store.ts b/web/store/issue/archived/filter.store.ts index d92453a30..032928cda 100644 --- a/web/store/issue/archived/filter.store.ts +++ b/web/store/issue/archived/filter.store.ts @@ -89,8 +89,6 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc filteredParams ); - if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; - return filteredRouteParams; } @@ -183,11 +181,6 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc _filters.displayFilters.group_by = "state"; updatedDisplayFilters.group_by = "state"; } - // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true - if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { - _filters.displayFilters.sub_issue = false; - updatedDisplayFilters.sub_issue = false; - } runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { diff --git a/web/store/issue/cycle/filter.store.ts b/web/store/issue/cycle/filter.store.ts index dd81cfc0e..b938a36d4 100644 --- a/web/store/issue/cycle/filter.store.ts +++ b/web/store/issue/cycle/filter.store.ts @@ -90,8 +90,6 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI filteredParams ); - if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; - return filteredRouteParams; } @@ -195,12 +193,6 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI updatedDisplayFilters.group_by = "state"; } - // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true - if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { - _filters.displayFilters.sub_issue = false; - updatedDisplayFilters.sub_issue = false; - } - runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { set( diff --git a/web/store/issue/draft/filter.store.ts b/web/store/issue/draft/filter.store.ts index 8295c263d..cc58a7755 100644 --- a/web/store/issue/draft/filter.store.ts +++ b/web/store/issue/draft/filter.store.ts @@ -89,8 +89,6 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI filteredParams ); - if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; - return filteredRouteParams; } @@ -179,12 +177,6 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI updatedDisplayFilters.group_by = "state"; } - // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true - if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { - _filters.displayFilters.sub_issue = false; - updatedDisplayFilters.sub_issue = false; - } - runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { set( diff --git a/web/store/issue/module/filter.store.ts b/web/store/issue/module/filter.store.ts index e92027235..f10a885a3 100644 --- a/web/store/issue/module/filter.store.ts +++ b/web/store/issue/module/filter.store.ts @@ -90,8 +90,6 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul filteredParams ); - if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; - return filteredRouteParams; } @@ -194,12 +192,6 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul updatedDisplayFilters.group_by = "state"; } - // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true - if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { - _filters.displayFilters.sub_issue = false; - updatedDisplayFilters.sub_issue = false; - } - runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { set( diff --git a/web/store/issue/profile/filter.store.ts b/web/store/issue/profile/filter.store.ts index 563af5b01..658980082 100644 --- a/web/store/issue/profile/filter.store.ts +++ b/web/store/issue/profile/filter.store.ts @@ -93,8 +93,6 @@ export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProf filteredParams ); - if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; - return filteredRouteParams; } @@ -188,11 +186,6 @@ export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProf _filters.displayFilters.group_by = "priority"; updatedDisplayFilters.group_by = "priority"; } - // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true - if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { - _filters.displayFilters.sub_issue = false; - updatedDisplayFilters.sub_issue = false; - } runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { diff --git a/web/store/issue/project-views/filter.store.ts b/web/store/issue/project-views/filter.store.ts index b3df3903b..c7c8988b1 100644 --- a/web/store/issue/project-views/filter.store.ts +++ b/web/store/issue/project-views/filter.store.ts @@ -90,8 +90,6 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I filteredParams ); - if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; - return filteredRouteParams; } @@ -192,11 +190,6 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I _filters.displayFilters.group_by = "state"; updatedDisplayFilters.group_by = "state"; } - // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true - if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { - _filters.displayFilters.sub_issue = false; - updatedDisplayFilters.sub_issue = false; - } runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { diff --git a/web/store/issue/project/filter.store.ts b/web/store/issue/project/filter.store.ts index 69393a320..f18654cde 100644 --- a/web/store/issue/project/filter.store.ts +++ b/web/store/issue/project/filter.store.ts @@ -89,8 +89,6 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj filteredParams ); - if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; - return filteredRouteParams; } @@ -191,11 +189,6 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj updatedDisplayFilters.group_by = "state"; } - // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true - if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { - _filters.displayFilters.sub_issue = false; - updatedDisplayFilters.sub_issue = false; - } runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { From 9c50ee39c36c2bfccacbb2f12987c943a0a8b3b7 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:46:33 +0530 Subject: [PATCH 04/67] fix: project and workspace view list flicker on hover fix (#3777) --- web/components/views/view-list-item.tsx | 2 +- web/components/workspace/views/default-view-list-item.tsx | 2 +- web/components/workspace/views/view-list-item.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/components/views/view-list-item.tsx b/web/components/views/view-list-item.tsx index 0bfb57cd6..48cc12ada 100644 --- a/web/components/views/view-list-item.tsx +++ b/web/components/views/view-list-item.tsx @@ -80,7 +80,7 @@ export const ProjectViewListItem: React.FC = observer((props) => { setDeleteViewModal(false)} />
-
+
diff --git a/web/components/workspace/views/default-view-list-item.tsx b/web/components/workspace/views/default-view-list-item.tsx index 6262621f4..ad2f487f7 100644 --- a/web/components/workspace/views/default-view-list-item.tsx +++ b/web/components/workspace/views/default-view-list-item.tsx @@ -15,7 +15,7 @@ export const GlobalDefaultViewListItem: React.FC = observer((props) => { return (
-
+
diff --git a/web/components/workspace/views/view-list-item.tsx b/web/components/workspace/views/view-list-item.tsx index 9dbf69d45..28f25551c 100644 --- a/web/components/workspace/views/view-list-item.tsx +++ b/web/components/workspace/views/view-list-item.tsx @@ -39,7 +39,7 @@ export const GlobalViewListItem: React.FC = observer((props) => { setDeleteViewModal(false)} />
-
+
From 5c64933927f769d237f5eba0088f0454d2aa5f13 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 23 Feb 2024 18:47:15 +0530 Subject: [PATCH 05/67] [WEB-538] style: fix invite member icons in dropdown shrink when name is too large. (#3776) --- .../project/send-project-invitation-modal.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/web/components/project/send-project-invitation-modal.tsx b/web/components/project/send-project-invitation-modal.tsx index 7c02ce8d0..ef7913fb0 100644 --- a/web/components/project/send-project-invitation-modal.tsx +++ b/web/components/project/send-project-invitation-modal.tsx @@ -148,10 +148,14 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { memberDetails?.member.last_name } ${memberDetails?.member.display_name.toLowerCase()}`, content: ( -
- - {memberDetails?.member.display_name} ( - {memberDetails?.member.first_name + " " + memberDetails?.member.last_name}) +
+
+ +
+
+ {memberDetails?.member.display_name} ( + {memberDetails?.member.first_name + " " + memberDetails?.member.last_name}) +
), }; From e0a4d7a12a46483d1840accda9635de174eb3d00 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:51:38 +0530 Subject: [PATCH 06/67] [WEB-459] fix: tables row color retention, images in tables and css fixes (#3748) * fix: tables row color retention, images in tables and css fixes * fix: border colors darker * updated tables to new design * removing comments --- .../editor/core/src/lib/editor-commands.ts | 4 +- packages/editor/core/src/styles/editor.css | 62 ---- packages/editor/core/src/styles/table.css | 58 +++- .../extensions/table/table-cell/table-cell.ts | 9 +- .../table/table-header/table-header.ts | 2 +- .../extensions/table/table-row/table-row.ts | 19 +- .../src/ui/extensions/table/table/icons.ts | 6 +- .../ui/extensions/table/table/table-view.tsx | 276 +++++++++--------- .../src/ui/extensions/table/table/table.ts | 3 +- packages/editor/core/src/ui/props.tsx | 9 - .../src/ui/menu/fixed-menu.tsx | 24 +- .../extensions/src/extensions/drag-drop.tsx | 4 +- .../src/ui/menus/fixed-menu/index.tsx | 23 +- 13 files changed, 211 insertions(+), 288 deletions(-) diff --git a/packages/editor/core/src/lib/editor-commands.ts b/packages/editor/core/src/lib/editor-commands.ts index 4a56f07c2..6524d1ff5 100644 --- a/packages/editor/core/src/lib/editor-commands.ts +++ b/packages/editor/core/src/lib/editor-commands.ts @@ -97,8 +97,8 @@ export const insertTableCommand = (editor: Editor, range?: Range) => { } } } - if (range) editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); - else editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); + if (range) editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3 }).run(); + else editor.chain().focus().insertTable({ rows: 3, cols: 3 }).run(); }; export const unsetLinkEditor = (editor: Editor) => { diff --git a/packages/editor/core/src/styles/editor.css b/packages/editor/core/src/styles/editor.css index b0d2a1021..dbbea671e 100644 --- a/packages/editor/core/src/styles/editor.css +++ b/packages/editor/core/src/styles/editor.css @@ -170,68 +170,6 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { } } -#editor-container { - table { - border-collapse: collapse; - table-layout: fixed; - margin: 0.5em 0 0.5em 0; - - border: 1px solid rgb(var(--color-border-200)); - width: 100%; - - td, - th { - min-width: 1em; - border: 1px solid rgb(var(--color-border-200)); - padding: 10px 15px; - vertical-align: top; - box-sizing: border-box; - position: relative; - transition: background-color 0.3s ease; - - > * { - margin-bottom: 0; - } - } - - th { - font-weight: bold; - text-align: left; - background-color: rgb(var(--color-primary-100)); - } - - td:hover { - background-color: rgba(var(--color-primary-300), 0.1); - } - - .selectedCell:after { - z-index: 2; - position: absolute; - content: ""; - left: 0; - right: 0; - top: 0; - bottom: 0; - background-color: rgba(var(--color-primary-300), 0.1); - pointer-events: none; - } - - .column-resize-handle { - position: absolute; - right: -2px; - top: 0; - bottom: -2px; - width: 2px; - background-color: rgb(var(--color-primary-400)); - pointer-events: none; - } - } -} - -.tableWrapper { - overflow-x: auto; -} - .resize-cursor { cursor: ew-resize; cursor: col-resize; diff --git a/packages/editor/core/src/styles/table.css b/packages/editor/core/src/styles/table.css index 8a47a8c59..ca384d34f 100644 --- a/packages/editor/core/src/styles/table.css +++ b/packages/editor/core/src/styles/table.css @@ -9,15 +9,15 @@ border-collapse: collapse; table-layout: fixed; margin: 0; - margin-bottom: 3rem; - border: 1px solid rgba(var(--color-border-200)); + margin-bottom: 1rem; + border: 2px solid rgba(var(--color-border-300)); width: 100%; } .tableWrapper table td, .tableWrapper table th { min-width: 1em; - border: 1px solid rgba(var(--color-border-200)); + border: 1px solid rgba(var(--color-border-300)); padding: 10px 15px; vertical-align: top; box-sizing: border-box; @@ -43,7 +43,8 @@ .tableWrapper table th { font-weight: bold; text-align: left; - background-color: rgba(var(--color-primary-100)); + background-color: #d9e4ff; + color: #171717; } .tableWrapper table th * { @@ -62,6 +63,35 @@ pointer-events: none; } +.colorPicker { + display: grid; + padding: 8px 8px; + grid-template-columns: repeat(6, 1fr); + gap: 5px; +} + +.colorPickerLabel { + font-size: 0.85rem; + color: #6b7280; + padding: 8px 8px; + padding-bottom: 0px; +} + +.colorPickerItem { + margin: 2px 0px; + width: 24px; + height: 24px; + border-radius: 4px; + border: none; + cursor: pointer; +} + +.divider { + background-color: #e5e7eb; + height: 1px; + margin: 3px 0; +} + .tableWrapper table .column-resize-handle { position: absolute; right: -2px; @@ -69,7 +99,7 @@ bottom: -2px; width: 4px; z-index: 99; - background-color: rgba(var(--color-primary-400)); + background-color: #d9e4ff; pointer-events: none; } @@ -112,7 +142,7 @@ } .tableWrapper .tableControls .rowsControlDiv { - background-color: rgba(var(--color-primary-100)); + background-color: #d9e4ff; border: 1px solid rgba(var(--color-border-200)); border-radius: 2px; background-size: 1.25rem; @@ -127,7 +157,7 @@ } .tableWrapper .tableControls .columnsControlDiv { - background-color: rgba(var(--color-primary-100)); + background-color: #d9e4ff; border: 1px solid rgba(var(--color-border-200)); border-radius: 2px; background-size: 1.25rem; @@ -144,10 +174,12 @@ .tableWrapper .tableControls .tableColorPickerToolbox { border: 1px solid rgba(var(--color-border-300)); background-color: rgba(var(--color-background-100)); + border-radius: 5px; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); padding: 0.25rem; display: flex; flex-direction: column; - width: 200px; + width: max-content; gap: 0.25rem; } @@ -158,7 +190,7 @@ align-items: center; gap: 0.5rem; border: none; - padding: 0.1rem; + padding: 0.3rem 0.5rem 0.1rem 0.1rem; border-radius: 4px; cursor: pointer; transition: all 0.2s; @@ -173,9 +205,7 @@ .tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .iconContainer, .tableWrapper .tableControls .tableToolbox .toolboxItem .colorContainer, .tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .colorContainer { - border: 1px solid rgba(var(--color-border-300)); - border-radius: 3px; - padding: 4px; + padding: 4px 0px; display: flex; align-items: center; justify-content: center; @@ -187,8 +217,8 @@ .tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .iconContainer svg, .tableWrapper .tableControls .tableToolbox .toolboxItem .colorContainer svg, .tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .colorContainer svg { - width: 2rem; - height: 2rem; + width: 1rem; + height: 1rem; } .tableToolbox { diff --git a/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts b/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts index aedb59411..403bd3f02 100644 --- a/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts +++ b/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts @@ -13,7 +13,7 @@ export const TableCell = Node.create({ }; }, - content: "paragraph+", + content: "block+", addAttributes() { return { @@ -33,7 +33,10 @@ export const TableCell = Node.create({ }, }, background: { - default: "none", + default: null, + }, + textColor: { + default: null, }, }; }, @@ -50,7 +53,7 @@ export const TableCell = Node.create({ return [ "td", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { - style: `background-color: ${node.attrs.background}`, + style: `background-color: ${node.attrs.background}; color: ${node.attrs.textColor}`, }), 0, ]; diff --git a/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts b/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts index c0decdbf8..bd994f467 100644 --- a/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts +++ b/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts @@ -33,7 +33,7 @@ export const TableHeader = Node.create({ }, }, background: { - default: "rgb(var(--color-primary-100))", + default: "none", }, }; }, diff --git a/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts b/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts index 28c9a9a48..f961c0582 100644 --- a/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts +++ b/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts @@ -13,6 +13,17 @@ export const TableRow = Node.create({ }; }, + addAttributes() { + return { + background: { + default: null, + }, + textColor: { + default: null, + }, + }; + }, + content: "(tableCell | tableHeader)*", tableRole: "row", @@ -22,6 +33,12 @@ export const TableRow = Node.create({ }, renderHTML({ HTMLAttributes }) { - return ["tr", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; + const style = HTMLAttributes.background + ? `background-color: ${HTMLAttributes.background}; color: ${HTMLAttributes.textColor}` + : ""; + + const attributes = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { style }); + + return ["tr", attributes, 0]; }, }); diff --git a/packages/editor/core/src/ui/extensions/table/table/icons.ts b/packages/editor/core/src/ui/extensions/table/table/icons.ts index c08710ec3..f73c55c09 100644 --- a/packages/editor/core/src/ui/extensions/table/table/icons.ts +++ b/packages/editor/core/src/ui/extensions/table/table/icons.ts @@ -1,7 +1,7 @@ export const icons = { colorPicker: ``, - deleteColumn: ``, - deleteRow: ``, + deleteColumn: ``, + deleteRow: ``, insertLeftTableIcon: ` `, + toggleColumnHeader: ``, + toggleRowHeader: ``, insertBottomTableIcon: ` = { placement: "right", }; -function setCellsBackgroundColor(editor: Editor, backgroundColor: string) { +function setCellsBackgroundColor(editor: Editor, color: { backgroundColor: string; textColor: string }) { return editor .chain() .focus() .updateAttributes("tableCell", { - background: backgroundColor, - }) - .updateAttributes("tableHeader", { - background: backgroundColor, + background: color.backgroundColor, + textColor: color.textColor, }) .run(); } +function setTableRowBackgroundColor(editor: Editor, color: { backgroundColor: string; textColor: string }) { + const { state, dispatch } = editor.view; + const { selection } = state; + if (!(selection instanceof CellSelection)) { + return false; + } + + // Get the position of the hovered cell in the selection to determine the row. + const hoveredCell = selection.$headCell || selection.$anchorCell; + + // Find the depth of the table row node + let rowDepth = hoveredCell.depth; + while (rowDepth > 0 && hoveredCell.node(rowDepth).type.name !== "tableRow") { + rowDepth--; + } + + // If we couldn't find a tableRow node, we can't set the background color + if (hoveredCell.node(rowDepth).type.name !== "tableRow") { + return false; + } + + // Get the position where the table row starts + const rowStartPos = hoveredCell.start(rowDepth); + + // Create a transaction that sets the background color on the tableRow node. + const tr = state.tr.setNodeMarkup(rowStartPos - 1, null, { + ...hoveredCell.node(rowDepth).attrs, + background: color.backgroundColor, + textColor: color.textColor, + }); + + dispatch(tr); + return true; +} + const columnsToolboxItems: ToolboxItem[] = [ { - label: "Add Column Before", + label: "Toggle column header", + icon: icons.toggleColumnHeader, + action: ({ editor }: { editor: Editor }) => editor.chain().focus().toggleHeaderColumn().run(), + }, + { + label: "Add column before", icon: icons.insertLeftTableIcon, action: ({ editor }: { editor: Editor }) => editor.chain().focus().addColumnBefore().run(), }, { - label: "Add Column After", + label: "Add column after", icon: icons.insertRightTableIcon, action: ({ editor }: { editor: Editor }) => editor.chain().focus().addColumnAfter().run(), }, { - label: "Pick Column Color", - icon: icons.colorPicker, - action: ({ - editor, - triggerButton, - controlsContainer, - }: { - editor: Editor; - triggerButton: HTMLElement; - controlsContainer: Element; - }) => { - createColorPickerToolbox({ - triggerButton, - tippyOptions: { - appendTo: controlsContainer, - }, - onSelectColor: (color) => setCellsBackgroundColor(editor, color), - }); - }, + label: "Pick color", + icon: "", // No icon needed for color picker + action: (args: any) => {}, // Placeholder action; actual color picking is handled in `createToolbox` }, { - label: "Delete Column", + label: "Delete column", icon: icons.deleteColumn, action: ({ editor }: { editor: Editor }) => editor.chain().focus().deleteColumn().run(), }, @@ -135,35 +157,24 @@ const columnsToolboxItems: ToolboxItem[] = [ const rowsToolboxItems: ToolboxItem[] = [ { - label: "Add Row Above", + label: "Toggle row header", + icon: icons.toggleRowHeader, + action: ({ editor }: { editor: Editor }) => editor.chain().focus().toggleHeaderRow().run(), + }, + { + label: "Add row above", icon: icons.insertTopTableIcon, action: ({ editor }: { editor: Editor }) => editor.chain().focus().addRowBefore().run(), }, { - label: "Add Row Below", + label: "Add row below", icon: icons.insertBottomTableIcon, action: ({ editor }: { editor: Editor }) => editor.chain().focus().addRowAfter().run(), }, { - label: "Pick Row Color", - icon: icons.colorPicker, - action: ({ - editor, - triggerButton, - controlsContainer, - }: { - editor: Editor; - triggerButton: HTMLButtonElement; - controlsContainer: Element | "parent" | ((ref: Element) => Element) | undefined; - }) => { - createColorPickerToolbox({ - triggerButton, - tippyOptions: { - appendTo: controlsContainer, - }, - onSelectColor: (color) => setCellsBackgroundColor(editor, color), - }); - }, + label: "Pick color", + icon: "", + action: (args: any) => {}, // Placeholder action; actual color picking is handled in `createToolbox` }, { label: "Delete Row", @@ -176,37 +187,57 @@ function createToolbox({ triggerButton, items, tippyOptions, + onSelectColor, onClickItem, + colors, }: { triggerButton: Element | null; items: ToolboxItem[]; tippyOptions: any; onClickItem: (item: ToolboxItem) => void; + onSelectColor: (color: { backgroundColor: string; textColor: string }) => void; + colors: { [key: string]: { backgroundColor: string; textColor: string; icon?: string } }; }): Instance { // @ts-expect-error const toolbox = tippy(triggerButton, { content: h( "div", { className: "tableToolbox" }, - items.map((item) => - h( - "div", - { - className: "toolboxItem", - itemType: "button", - onClick() { - onClickItem(item); + items.map((item, index) => { + if (item.label === "Pick color") { + return h("div", { className: "flex flex-col" }, [ + h("div", { className: "divider" }), + h("div", { className: "colorPickerLabel" }, item.label), + h( + "div", + { className: "colorPicker grid" }, + Object.entries(colors).map(([colorName, colorValue]) => + h("div", { + className: "colorPickerItem", + style: `background-color: ${colorValue.backgroundColor}; + color: ${colorValue.textColor || "inherit"};`, + innerHTML: colorValue?.icon || "", + onClick: () => onSelectColor(colorValue), + }) + ) + ), + h("div", { className: "divider" }), + ]); + } else { + return h( + "div", + { + className: "toolboxItem", + itemType: "div", + onClick: () => onClickItem(item), }, - }, - [ - h("div", { - className: "iconContainer", - innerHTML: item.icon, - }), - h("div", { className: "label" }, item.label), - ] - ) - ) + [ + h("div", { className: "iconContainer", innerHTML: item.icon }), + h("div", { className: "label" }, item.label), + ] + ); + } + }) ), ...tippyOptions, }); @@ -214,71 +245,6 @@ function createToolbox({ return Array.isArray(toolbox) ? toolbox[0] : toolbox; } -function createColorPickerToolbox({ - triggerButton, - tippyOptions, - onSelectColor = () => {}, -}: { - triggerButton: HTMLElement; - tippyOptions: Partial; - onSelectColor?: (color: string) => void; -}) { - const items = { - Default: "rgb(var(--color-primary-100))", - Orange: "#FFE5D1", - Grey: "#F1F1F1", - Yellow: "#FEF3C7", - Green: "#DCFCE7", - Red: "#FFDDDD", - Blue: "#D9E4FF", - Pink: "#FFE8FA", - Purple: "#E8DAFB", - }; - - const colorPicker = tippy(triggerButton, { - ...defaultTippyOptions, - content: h( - "div", - { className: "tableColorPickerToolbox" }, - Object.entries(items).map(([key, value]) => - h( - "div", - { - className: "toolboxItem", - itemType: "button", - onClick: () => { - onSelectColor(value); - colorPicker.hide(); - }, - }, - [ - h("div", { - className: "colorContainer", - style: { - backgroundColor: value, - }, - }), - h( - "div", - { - className: "label", - }, - key - ), - ] - ) - ) - ), - onHidden: (instance) => { - instance.destroy(); - }, - showOnCreate: true, - ...tippyOptions, - }); - - return colorPicker; -} - export class TableView implements NodeView { node: ProseMirrorNode; cellMinWidth: number; @@ -347,10 +313,27 @@ export class TableView implements NodeView { this.rowsControl, this.columnsControl ); + const columnColors = { + Blue: { backgroundColor: "#D9E4FF", textColor: "#171717" }, + Orange: { backgroundColor: "#FFEDD5", textColor: "#171717" }, + Grey: { backgroundColor: "#F1F1F1", textColor: "#171717" }, + Yellow: { backgroundColor: "#FEF3C7", textColor: "#171717" }, + Green: { backgroundColor: "#DCFCE7", textColor: "#171717" }, + Red: { backgroundColor: "#FFDDDD", textColor: "#171717" }, + Pink: { backgroundColor: "#FFE8FA", textColor: "#171717" }, + Purple: { backgroundColor: "#E8DAFB", textColor: "#171717" }, + None: { + backgroundColor: "none", + textColor: "none", + icon: ``, + }, + }; this.columnsToolbox = createToolbox({ triggerButton: this.columnsControl.querySelector(".columnsControlDiv"), items: columnsToolboxItems, + colors: columnColors, + onSelectColor: (color) => setCellsBackgroundColor(this.editor, color), tippyOptions: { ...defaultTippyOptions, appendTo: this.controls, @@ -368,10 +351,12 @@ export class TableView implements NodeView { this.rowsToolbox = createToolbox({ triggerButton: this.rowsControl.firstElementChild, items: rowsToolboxItems, + colors: columnColors, tippyOptions: { ...defaultTippyOptions, appendTo: this.controls, }, + onSelectColor: (color) => setTableRowBackgroundColor(editor, color), onClickItem: (item) => { item.action({ editor: this.editor, @@ -383,8 +368,6 @@ export class TableView implements NodeView { }); } - // Table - this.colgroup = h( "colgroup", null, @@ -437,16 +420,19 @@ export class TableView implements NodeView { } updateControls() { - const { hoveredTable: table, hoveredCell: cell } = Object.values(this.decorations).reduce((acc, curr) => { - if (curr.spec.hoveredCell !== undefined) { - acc["hoveredCell"] = curr.spec.hoveredCell; - } + const { hoveredTable: table, hoveredCell: cell } = Object.values(this.decorations).reduce( + (acc, curr) => { + if (curr.spec.hoveredCell !== undefined) { + acc["hoveredCell"] = curr.spec.hoveredCell; + } - if (curr.spec.hoveredTable !== undefined) { - acc["hoveredTable"] = curr.spec.hoveredTable; - } - return acc; - }, {} as Record) as any; + if (curr.spec.hoveredTable !== undefined) { + acc["hoveredTable"] = curr.spec.hoveredTable; + } + return acc; + }, + {} as Record + ) as any; if (table === undefined || cell === undefined) { return this.root.classList.add("controls--disabled"); @@ -457,12 +443,12 @@ export class TableView implements NodeView { const cellDom = this.editor.view.nodeDOM(cell.pos) as HTMLElement; - if (!this.table) { + if (!this.table || !cellDom) { return; } - const tableRect = this.table.getBoundingClientRect(); - const cellRect = cellDom.getBoundingClientRect(); + const tableRect = this.table?.getBoundingClientRect(); + const cellRect = cellDom?.getBoundingClientRect(); if (this.columnsControl) { this.columnsControl.style.left = `${cellRect.left - tableRect.left - this.table.parentElement!.scrollLeft}px`; diff --git a/packages/editor/core/src/ui/extensions/table/table/table.ts b/packages/editor/core/src/ui/extensions/table/table/table.ts index 5600fd82a..ef595eee2 100644 --- a/packages/editor/core/src/ui/extensions/table/table/table.ts +++ b/packages/editor/core/src/ui/extensions/table/table/table.ts @@ -107,10 +107,9 @@ export const Table = Node.create({ addCommands() { return { insertTable: - ({ rows = 3, cols = 3, withHeaderRow = true } = {}) => + ({ rows = 3, cols = 3, withHeaderRow = false } = {}) => ({ tr, dispatch, editor }) => { const node = createTable(editor.schema, rows, cols, withHeaderRow); - if (dispatch) { const offset = tr.selection.anchor + 1; diff --git a/packages/editor/core/src/ui/props.tsx b/packages/editor/core/src/ui/props.tsx index 2aaeb4264..1846efe47 100644 --- a/packages/editor/core/src/ui/props.tsx +++ b/packages/editor/core/src/ui/props.tsx @@ -42,15 +42,6 @@ export function CoreEditorProps( return false; }, handleDrop: (view, event, _slice, moved) => { - if (typeof window !== "undefined") { - const selection: any = window?.getSelection(); - if (selection.rangeCount !== 0) { - const range = selection.getRangeAt(0); - if (findTableAncestor(range.startContainer)) { - return; - } - } - } if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) { event.preventDefault(); const file = event.dataTransfer.files[0]; diff --git a/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx b/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx index be57a4a91..397e8c576 100644 --- a/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx +++ b/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx @@ -48,34 +48,12 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => { function getComplexItems(): BubbleMenuItem[] { const items: BubbleMenuItem[] = [TableItem(editor)]; - if (shouldShowImageItem()) { - items.push(ImageItem(editor, uploadFile, setIsSubmitting)); - } - + items.push(ImageItem(editor, uploadFile, setIsSubmitting)); return items; } const complexItems: BubbleMenuItem[] = getComplexItems(); - function shouldShowImageItem(): boolean { - if (typeof window !== "undefined") { - const selectionRange: any = window?.getSelection(); - const { selection } = props.editor.state; - - if (selectionRange.rangeCount !== 0) { - const range = selectionRange.getRangeAt(0); - if (findTableAncestor(range.startContainer)) { - return false; - } - if (isCellSelection(selection)) { - return false; - } - } - return true; - } - return false; - } - return (
diff --git a/packages/editor/extensions/src/extensions/drag-drop.tsx b/packages/editor/extensions/src/extensions/drag-drop.tsx index af99fec61..ce4088413 100644 --- a/packages/editor/extensions/src/extensions/drag-drop.tsx +++ b/packages/editor/extensions/src/extensions/drag-drop.tsx @@ -35,7 +35,7 @@ export interface DragHandleOptions { } function absoluteRect(node: Element) { - const data = node.getBoundingClientRect(); + const data = node?.getBoundingClientRect(); return { top: data.top, @@ -65,7 +65,7 @@ function nodeDOMAtCoords(coords: { x: number; y: number }) { } function nodePosAtDOM(node: Element, view: EditorView) { - const boundingRect = node.getBoundingClientRect(); + const boundingRect = node?.getBoundingClientRect(); if (node.nodeName === "IMG") { return view.posAtCoords({ diff --git a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx index 71ad4e0e1..c6786698d 100644 --- a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx @@ -60,34 +60,13 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => { function getComplexItems(): BubbleMenuItem[] { const items: BubbleMenuItem[] = [TableItem(props.editor)]; - if (shouldShowImageItem()) { - items.push(ImageItem(props.editor, props.uploadFile, props.setIsSubmitting)); - } + items.push(ImageItem(props.editor, props.uploadFile, props.setIsSubmitting)); return items; } const complexItems: BubbleMenuItem[] = getComplexItems(); - function shouldShowImageItem(): boolean { - if (typeof window !== "undefined") { - const selectionRange: any = window?.getSelection(); - const { selection } = props.editor.state; - - if (selectionRange.rangeCount !== 0) { - const range = selectionRange.getRangeAt(0); - if (findTableAncestor(range.startContainer)) { - return false; - } - if (isCellSelection(selection)) { - return false; - } - } - return true; - } - return false; - } - const handleAccessChange = (accessKey: string) => { props.commentAccessSpecifier?.onAccessChange(accessKey); }; From 5571d42e1071811e799a4a4e07bce6c9774c1f59 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 23 Feb 2024 18:52:12 +0530 Subject: [PATCH 07/67] [WEB-536] fix: analytics highlight while switching between `scope_and_demand` and `custom` tab. (#3767) --- web/constants/dashboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index 3f251ca78..b1cfa51d7 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -256,7 +256,7 @@ export const SIDEBAR_MENU_ITEMS: { label: "Analytics", href: `/analytics`, access: EUserWorkspaceRoles.MEMBER, - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/analytics`, + highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/analytics`), Icon: BarChart2, }, { From 27fcfcf62045b0e000a8de438395f3444c4d7c6f Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:52:47 +0530 Subject: [PATCH 08/67] [WEB-507] fix: cycle lead details not visible (#3750) * fix: cycle lead details * revert: sidebar padding changes --- packages/types/src/cycles.d.ts | 2 +- .../analytics/custom-analytics/sidebar/sidebar-header.tsx | 2 +- web/components/cycles/active-cycle-details.tsx | 2 +- web/components/cycles/sidebar.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/types/src/cycles.d.ts b/packages/types/src/cycles.d.ts index 0e4890b7f..e7ec66ae2 100644 --- a/packages/types/src/cycles.d.ts +++ b/packages/types/src/cycles.d.ts @@ -30,7 +30,7 @@ export interface ICycle { is_favorite: boolean; issue: string; name: string; - owned_by: string; + owned_by_id: string; progress_snapshot: TProgressSnapshot; project_id: string; status: TCycleGroups; diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx index c2644abe0..6a7b3c7b9 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx @@ -20,7 +20,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => { const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; const projectDetails = projectId ? getProjectById(projectId.toString()) : undefined; - const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by) : undefined; + const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by_id) : undefined; const moduleLeadDetails = moduleDetails && moduleDetails.lead_id ? getUserDetails(moduleDetails.lead_id) : undefined; return ( diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index 7e885635f..1fae0412f 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -69,7 +69,7 @@ export const ActiveCycleDetails: React.FC = observer((props ); const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null; - const cycleOwnerDetails = activeCycle ? getUserDetails(activeCycle.owned_by) : undefined; + const cycleOwnerDetails = activeCycle ? getUserDetails(activeCycle.owned_by_id) : undefined; const { data: activeCycleIssues } = useSWR( workspaceSlug && projectId && currentProjectActiveCycleId diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 20605a90c..646736bd2 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -59,7 +59,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const { getUserDetails } = useMember(); // derived values const cycleDetails = getCycleById(cycleId); - const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by) : undefined; + const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by_id) : undefined; // toast alert const { setToastAlert } = useToast(); // form info From 34f89ba45bae61c2e14334a90e50101c59fe434b Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:57:48 +0530 Subject: [PATCH 09/67] [WEB-512] fix: date inputs keyboard navigation (#3753) * fix: tab indices logic * fix: due date highlight logic * Revert "fix: due date highlight logic" This reverts commit f523078689e1570295a6067ce4f9580a6a031f22. --- web/components/issues/issue-modal/form.tsx | 78 ++++++++++++++++------ 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 564cc9b08..cfb4b912c 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -64,6 +64,31 @@ export interface IssueFormProps { const aiService = new AIService(); const fileService = new FileService(); +const TAB_INDICES = [ + "name", + "description_html", + "feeling_lucky", + "ai_assistant", + "state_id", + "priority", + "assignee_ids", + "label_ids", + "start_date", + "target_date", + "cycle_id", + "module_ids", + "estimate_point", + "parent_id", + "create_more", + "discard_button", + "draft_button", + "submit_button", + "project_id", + "remove_parent", +]; + +const getTabIndex = (key: string) => TAB_INDICES.findIndex((tabIndex) => tabIndex === key) + 1; + export const IssueFormRoot: FC = observer((props) => { const { data, @@ -271,7 +296,7 @@ export const IssueFormRoot: FC = observer((props) => { }} buttonVariant="border-with-text" // TODO: update tabIndex logic - tabIndex={19} + tabIndex={getTabIndex("project_id")} />
)} @@ -294,15 +319,18 @@ export const IssueFormRoot: FC = observer((props) => { {selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id} {selectedParentIssue.name.substring(0, 50)} - { setValue("parent_id", null); handleFormChange(); setSelectedParentIssue(null); }} - tabIndex={20} - /> + tabIndex={getTabIndex("remove_parent")} + > + +
)} @@ -332,7 +360,7 @@ export const IssueFormRoot: FC = observer((props) => { hasError={Boolean(errors.name)} placeholder="Issue Title" className="resize-none text-xl w-full" - tabIndex={1} + tabIndex={getTabIndex("name")} /> )} /> @@ -346,7 +374,7 @@ export const IssueFormRoot: FC = observer((props) => { }`} onClick={handleAutoGenerateDescription} disabled={iAmFeelingLucky} - tabIndex={3} + tabIndex={getTabIndex("feeling_lucky")} > {iAmFeelingLucky ? ( "Generating response" @@ -375,7 +403,7 @@ export const IssueFormRoot: FC = observer((props) => { type="button" className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90" onClick={() => setGptAssistantModal((prevData) => !prevData)} - tabIndex={4} + tabIndex={getTabIndex("ai_assistant")} > AI @@ -426,7 +454,7 @@ export const IssueFormRoot: FC = observer((props) => { }} projectId={projectId} buttonVariant="border-with-text" - tabIndex={6} + tabIndex={getTabIndex("state_id")} />
)} @@ -443,7 +471,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} buttonVariant="border-with-text" - tabIndex={7} + tabIndex={getTabIndex("priority")} />
)} @@ -464,7 +492,7 @@ export const IssueFormRoot: FC = observer((props) => { buttonClassName={value?.length > 0 ? "hover:bg-transparent px-0" : ""} placeholder="Assignees" multiple - tabIndex={8} + tabIndex={getTabIndex("assignee_ids")} />
)} @@ -482,7 +510,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} projectId={projectId} - tabIndex={9} + tabIndex={getTabIndex("label_ids")} />
)} @@ -498,6 +526,7 @@ export const IssueFormRoot: FC = observer((props) => { buttonVariant="border-with-text" maxDate={maxDate ?? undefined} placeholder="Start date" + tabIndex={getTabIndex("start_date")} />
)} @@ -513,6 +542,7 @@ export const IssueFormRoot: FC = observer((props) => { buttonVariant="border-with-text" minDate={minDate ?? undefined} placeholder="Due date" + tabIndex={getTabIndex("target_date")} />
)} @@ -531,7 +561,7 @@ export const IssueFormRoot: FC = observer((props) => { }} value={value} buttonVariant="border-with-text" - tabIndex={11} + tabIndex={getTabIndex("cycle_id")} />
)} @@ -551,7 +581,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} buttonVariant="border-with-text" - tabIndex={12} + tabIndex={getTabIndex("module_ids")} multiple showCount /> @@ -573,7 +603,7 @@ export const IssueFormRoot: FC = observer((props) => { }} projectId={projectId} buttonVariant="border-with-text" - tabIndex={13} + tabIndex={getTabIndex("estimate_point")} />
)} @@ -603,7 +633,7 @@ export const IssueFormRoot: FC = observer((props) => { } placement="bottom-start" - tabIndex={14} + tabIndex={getTabIndex("parent_id")} > {watch("parent_id") ? ( <> @@ -653,7 +683,7 @@ export const IssueFormRoot: FC = observer((props) => { onKeyDown={(e) => { if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled); }} - tabIndex={15} + tabIndex={getTabIndex("create_more")} >
{}} size="sm" /> @@ -661,7 +691,7 @@ export const IssueFormRoot: FC = observer((props) => { Create more
- @@ -673,7 +703,7 @@ export const IssueFormRoot: FC = observer((props) => { size="sm" loading={isSubmitting} onClick={handleSubmit((data) => handleFormSubmit({ ...data, is_draft: false }))} - tabIndex={17} + tabIndex={getTabIndex("draft_button")} > {isSubmitting ? "Moving" : "Move from draft"} @@ -683,7 +713,7 @@ export const IssueFormRoot: FC = observer((props) => { size="sm" loading={isSubmitting} onClick={handleSubmit((data) => handleFormSubmit(data, true))} - tabIndex={17} + tabIndex={getTabIndex("draft_button")} > {isSubmitting ? "Saving" : "Save as draft"} @@ -691,7 +721,13 @@ export const IssueFormRoot: FC = observer((props) => { )} -
From 1f7565ce52a8893fa60f4935af1f2d52719a2b80 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:03:09 +0530 Subject: [PATCH 10/67] fix: email notification assignees (#3762) --- apiserver/plane/app/serializers/issue.py | 6 ++--- apiserver/plane/app/views/inbox.py | 6 +---- apiserver/plane/app/views/issue.py | 23 ++++++++++++++----- .../plane/bgtasks/email_notification_task.py | 3 +++ 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 8d4304f92..411c5b73f 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -601,15 +601,15 @@ class IssueSerializer(DynamicBaseSerializer): # ids cycle_id = serializers.PrimaryKeyRelatedField(read_only=True) module_ids = serializers.ListField( - child=serializers.UUIDField(), required=False, allow_null=True + child=serializers.UUIDField(), required=False, ) # Many to many label_ids = serializers.ListField( - child=serializers.UUIDField(), required=False, allow_null=True + child=serializers.UUIDField(), required=False, ) assignee_ids = serializers.ListField( - child=serializers.UUIDField(), required=False, allow_null=True + child=serializers.UUIDField(), required=False, ) # Count items diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox.py index 85e2f38b2..d70eec4f2 100644 --- a/apiserver/plane/app/views/inbox.py +++ b/apiserver/plane/app/views/inbox.py @@ -296,11 +296,7 @@ class InboxIssueViewSet(BaseViewSet): issue_data = request.data.pop("issue", False) if bool(issue_data): - issue = Issue.objects.get( - pk=inbox_issue.issue_id, - workspace__slug=slug, - project_id=project_id, - ) + issue = self.get_queryset().filter(pk=inbox_issue.issue_id).first() # Only allow guests and viewers to edit name and description if project_member.role <= 10: # viewers and guests since only viewers and guests diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index 9f95c9b43..25c42dc5b 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -572,12 +572,18 @@ class IssueViewSet(WebhookMixin, BaseViewSet): return Response(serializer.data, status=status.HTTP_200_OK) def partial_update(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) + issue = self.get_queryset().filter(pk=pk).first() + + if not issue: + return Response( + {"error": "Issue not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + current_instance = json.dumps( IssueSerializer(issue).data, cls=DjangoJSONEncoder ) + requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) serializer = IssueCreateSerializer( issue, data=request.data, partial=True @@ -2296,9 +2302,14 @@ class IssueDraftViewSet(BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def partial_update(self, request, slug, project_id, pk): - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) + issue = self.get_queryset().filter(pk=pk).first() + + if not issue: + return Response( + {"error": "Issue does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + serializer = IssueSerializer(issue, data=request.data, partial=True) if serializer.is_valid(): diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py index 617bfcfdc..2a98c6b33 100644 --- a/apiserver/plane/bgtasks/email_notification_task.py +++ b/apiserver/plane/bgtasks/email_notification_task.py @@ -10,6 +10,7 @@ from django.utils import timezone from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string from django.utils.html import strip_tags +from django.conf import settings # Module imports from plane.db.models import EmailNotificationLog, User, Issue @@ -301,5 +302,7 @@ def send_email_notification( print("Duplicate task recived. Skipping...") return except (Issue.DoesNotExist, User.DoesNotExist) as e: + if settings.DEBUG: + print(e) release_lock(lock_id=lock_id) return From 849d3a66c143bde26845cb3736a54867da4eb365 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 23 Feb 2024 19:03:45 +0530 Subject: [PATCH 11/67] [WEB-540] fix: hide `sub_issue`, `link`, `attachment` property from list/ kanban view if their count is 0. (#3768) * [WEB-540] fix: hide `sub_issue`, `link`, `attachment` property from list/ kanban view if their count is 0. * chore: use `cn` helper function instead of string interpolation. --- .../issue-layouts/properties/all-properties.tsx | 16 +++++++++++----- .../spreadsheet/columns/sub-issue-column.tsx | 11 +++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index 840967565..7ef9aace8 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -21,6 +21,7 @@ import { } from "components/dropdowns"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { cn } from "helpers/common.helper"; // types import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; // constants @@ -378,12 +379,17 @@ export const IssueProperties: React.FC = observer((props) => { !!properties.sub_issue_count} + shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!issue.sub_issues_count} >
{}} + className={cn( + "flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1", + { + "hover:bg-custom-background-80 cursor-pointer": issue.sub_issues_count, + } + )} >
{issue.sub_issues_count}
@@ -395,7 +401,7 @@ export const IssueProperties: React.FC = observer((props) => { !!properties.attachment_count} + shouldRenderProperty={(properties) => !!properties.attachment_count && !!issue.attachment_count} >
@@ -409,7 +415,7 @@ export const IssueProperties: React.FC = observer((props) => { !!properties.link} + shouldRenderProperty={(properties) => !!properties.link && !!issue.link_count} >
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx index 20864eb96..c635ca85e 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx @@ -5,6 +5,8 @@ import { useRouter } from "next/router"; import { useApplication } from "hooks/store"; // types import { TIssue } from "@plane/types"; +// helpers +import { cn } from "helpers/common.helper"; type Props = { issue: TIssue; @@ -30,8 +32,13 @@ export const SpreadsheetSubIssueColumn: React.FC = observer((props: Props return (
{}} + className={cn( + "flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80", + { + "cursor-pointer": issue?.sub_issues_count, + } + )} > {issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
From 50cbb2f0021e9bde09d745726ef544a1905fcd47 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:04:17 +0530 Subject: [PATCH 12/67] chore: max length validation added in user name inputs (#3774) --- web/components/onboarding/user-details.tsx | 1 + web/pages/profile/index.tsx | 3 +++ 2 files changed, 4 insertions(+) diff --git a/web/components/onboarding/user-details.tsx b/web/components/onboarding/user-details.tsx index ac4a3c198..cd129c74c 100644 --- a/web/components/onboarding/user-details.tsx +++ b/web/components/onboarding/user-details.tsx @@ -190,6 +190,7 @@ export const UserDetails: React.FC = observer((props) => { hasError={Boolean(errors.first_name)} placeholder="Enter your full name..." className="w-full border-onboarding-border-100 focus:border-custom-primary-100" + maxLength={24} /> )} /> diff --git a/web/pages/profile/index.tsx b/web/pages/profile/index.tsx index e967df828..bdde41d08 100644 --- a/web/pages/profile/index.tsx +++ b/web/pages/profile/index.tsx @@ -250,6 +250,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { hasError={Boolean(errors.first_name)} placeholder="Enter your first name" className={`w-full rounded-md ${errors.first_name ? "border-red-500" : ""}`} + maxLength={24} /> )} /> @@ -273,6 +274,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { hasError={Boolean(errors.last_name)} placeholder="Enter your last name" className="w-full rounded-md" + maxLength={24} /> )} /> @@ -369,6 +371,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { hasError={Boolean(errors.display_name)} placeholder="Enter your display name" className={`w-full ${errors.display_name ? "border-red-500" : ""}`} + maxLength={24} /> )} /> From 9f055840ef7aede806b5ea048f0857f4454e54e1 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 23 Feb 2024 19:06:03 +0530 Subject: [PATCH 13/67] [WEB-539] style: add background to user email in dashboard dropdown for better UX on workspace list scroll. (#3769) --- web/components/workspace/sidebar-dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index f625f95b3..aeb0a34c2 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -157,7 +157,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
-
+
{currentUser?.email}
{workspacesList ? ( From 5f6c9a416640140d727da487d9ceac6eb8d058a5 Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:06:47 +0530 Subject: [PATCH 14/67] fix calendar layout distortion because of scrollbar (#3770) --- .../issue-layouts/calendar/calendar.tsx | 74 ++++++++++--------- .../issue-layouts/calendar/day-tile.tsx | 2 +- .../issue-layouts/calendar/week-header.tsx | 2 +- 3 files changed, 40 insertions(+), 38 deletions(-) diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 4dcaab335..badb849fb 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -73,42 +73,44 @@ export const CalendarChart: React.FC = observer((props) => { <>
- -
- {layout === "month" && ( -
- {allWeeksOfActiveMonth && - Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => ( - - ))} -
- )} - {layout === "week" && ( - - )} +
+ +
+ {layout === "month" && ( +
+ {allWeeksOfActiveMonth && + Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => ( + + ))} +
+ )} + {layout === "week" && ( + + )} +
diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index 85ab152a7..f92365a58 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -91,7 +91,7 @@ export const CalendarDayTile: React.FC = observer((props) => { snapshot.isDraggingOver || date.date.getDay() === 0 || date.date.getDay() === 6 ? "bg-custom-background-90" : "bg-custom-background-100" - } ${calendarLayout === "month" ? "min-h-[9rem]" : ""}`} + } ${calendarLayout === "month" ? "min-h-[5rem]" : ""}`} {...provided.droppableProps} ref={provided.innerRef} > diff --git a/web/components/issues/issue-layouts/calendar/week-header.tsx b/web/components/issues/issue-layouts/calendar/week-header.tsx index f5ec41e96..a4b714c94 100644 --- a/web/components/issues/issue-layouts/calendar/week-header.tsx +++ b/web/components/issues/issue-layouts/calendar/week-header.tsx @@ -13,7 +13,7 @@ export const CalendarWeekHeader: React.FC = observer((props) => { return (
From 0aaca709da3c4a1610d05bf8084fcf271fa89167 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:07:32 +0530 Subject: [PATCH 15/67] style: add right padding to sidebar projects list (#3764) --- web/components/project/sidebar-list.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index 249f044f4..289ab6122 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -11,6 +11,7 @@ import useToast from "hooks/use-toast"; import { CreateProjectModal, ProjectSidebarListItem } from "components/project"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; +import { cn } from "helpers/common.helper"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; @@ -109,9 +110,9 @@ export const ProjectSidebarList: FC = observer(() => { )}
From 8c1f169f610db3c10d21291d67043005123a6377 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:08:50 +0530 Subject: [PATCH 16/67] chore: workspace view header scroll to view improvement (#3771) --- web/components/workspace/views/header.tsx | 42 +++++++++++++---------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/web/components/workspace/views/header.tsx b/web/components/workspace/views/header.tsx index faa710131..223fda13c 100644 --- a/web/components/workspace/views/header.tsx +++ b/web/components/workspace/views/header.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useRouter } from "next/router"; import Link from "next/link"; import { observer } from "mobx-react-lite"; @@ -24,9 +24,8 @@ const ViewTab = observer((props: { viewId: string }) => { if (!view) return null; return ( - + { export const GlobalViewsHeader: React.FC = observer(() => { // states const [createViewModal, setCreateViewModal] = useState(false); + const containerRef = useRef(null); // router const router = useRouter(); const { workspaceSlug, globalViewId } = router.query; @@ -54,19 +54,22 @@ export const GlobalViewsHeader: React.FC = observer(() => { // bring the active view to the centre of the header useEffect(() => { - if (!globalViewId) return; - - captureEvent(GLOBAL_VIEW_OPENED, { - view_id: globalViewId, - view_type: ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString()) - ? "Default" - : "Custom", - }); - - const activeTabElement = document.querySelector(`#global-view-${globalViewId.toString()}`); - - if (activeTabElement) activeTabElement.scrollIntoView({ behavior: "smooth", inline: "center" }); - }, [globalViewId]); + if (globalViewId && currentWorkspaceViews) { + captureEvent(GLOBAL_VIEW_OPENED, { + view_id: globalViewId, + view_type: ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString()) + ? "Default" + : "Custom", + }); + const activeTabElement = document.querySelector(`#global-view-${globalViewId.toString()}`); + if (activeTabElement && containerRef.current) { + const containerRect = containerRef.current.getBoundingClientRect(); + const activeTabRect = activeTabElement.getBoundingClientRect(); + const diff = containerRect.right - activeTabRect.right; + activeTabElement.scrollIntoView({ behavior: "smooth", inline: diff > 500 ? "center" : "nearest" }); + } + } + }, [globalViewId, currentWorkspaceViews, containerRef]); const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; @@ -74,9 +77,12 @@ export const GlobalViewsHeader: React.FC = observer(() => { <> setCreateViewModal(false)} />
-
+
{DEFAULT_GLOBAL_VIEWS_LIST.map((tab) => ( - + Date: Fri, 23 Feb 2024 19:09:28 +0530 Subject: [PATCH 17/67] fix: project draft issue header (#3773) --- .../projects/[projectId]/archived-issues/index.tsx | 2 +- .../projects/[projectId]/draft-issues/index.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx index c24c80a92..97583d16c 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx @@ -30,7 +30,7 @@ const ProjectArchivedIssuesPage: NextPageWithLayout = observer(() => { <>
-
+
-
From ba6479674cb971191b8e80c8175e9b7aa28d737a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:10:45 +0530 Subject: [PATCH 18/67] [WEB-306] fix: Gantt chart bugs, refactor Gantt context (#3775) * chore: initialize gantt layout store * fix: modules being refetched on creation * fix: scrollLeft calculation logic * chore: modules list item dropdown position * refactor: active block logic * refactor: main content block component * chore: remove unnecessary conditions for duration --- .../widgets/issue-panels/issues-list.tsx | 9 +- web/components/gantt-chart/blocks/block.tsx | 106 +++++++++++ .../gantt-chart/blocks/blocks-list.tsx | 104 ++--------- web/components/gantt-chart/chart/header.tsx | 13 +- .../gantt-chart/chart/main-content.tsx | 19 +- web/components/gantt-chart/chart/root.tsx | 44 ++--- .../gantt-chart/chart/views/bi-week.tsx | 11 +- .../gantt-chart/chart/views/day.tsx | 11 +- .../gantt-chart/chart/views/hours.tsx | 11 +- .../gantt-chart/chart/views/month.tsx | 9 +- .../gantt-chart/chart/views/quarter.tsx | 11 +- .../gantt-chart/chart/views/week.tsx | 11 +- .../gantt-chart/chart/views/year.tsx | 11 +- web/components/gantt-chart/contexts/index.tsx | 66 ++----- web/components/gantt-chart/data/index.ts | 10 +- .../gantt-chart/helpers/add-block.tsx | 10 +- .../gantt-chart/helpers/block-structure.ts | 12 -- .../gantt-chart/helpers/draggable.tsx | 35 +++- web/components/gantt-chart/helpers/index.ts | 1 - web/components/gantt-chart/hooks/index.ts | 1 + web/components/gantt-chart/hooks/index.tsx | 13 -- .../gantt-chart/hooks/use-gantt-chart.ts | 11 ++ web/components/gantt-chart/index.ts | 2 +- web/components/gantt-chart/root.tsx | 6 +- web/components/gantt-chart/sidebar/cycles.tsx | 158 ---------------- .../gantt-chart/sidebar/cycles/block.tsx | 72 ++++++++ .../gantt-chart/sidebar/cycles/index.ts | 1 + .../gantt-chart/sidebar/cycles/sidebar.tsx | 100 ++++++++++ web/components/gantt-chart/sidebar/issues.tsx | 173 ------------------ .../gantt-chart/sidebar/issues/block.tsx | 77 ++++++++ .../gantt-chart/sidebar/issues/index.ts | 1 + .../gantt-chart/sidebar/issues/sidebar.tsx | 107 +++++++++++ .../gantt-chart/sidebar/modules.tsx | 158 ---------------- .../gantt-chart/sidebar/modules/block.tsx | 72 ++++++++ .../gantt-chart/sidebar/modules/index.ts | 1 + .../gantt-chart/sidebar/modules/sidebar.tsx | 100 ++++++++++ .../gantt-chart/sidebar/project-views.tsx | 91 ++------- web/components/gantt-chart/types/index.ts | 35 ---- .../issue-layouts/gantt/base-gantt-root.tsx | 9 +- web/components/modules/module-list-item.tsx | 2 +- web/helpers/issue.helper.ts | 10 + web/store/issue/issue_gantt_view.store.ts | 95 ++++++++++ web/store/module.store.ts | 1 - 43 files changed, 934 insertions(+), 866 deletions(-) create mode 100644 web/components/gantt-chart/blocks/block.tsx delete mode 100644 web/components/gantt-chart/helpers/block-structure.ts create mode 100644 web/components/gantt-chart/hooks/index.ts delete mode 100644 web/components/gantt-chart/hooks/index.tsx create mode 100644 web/components/gantt-chart/hooks/use-gantt-chart.ts delete mode 100644 web/components/gantt-chart/sidebar/cycles.tsx create mode 100644 web/components/gantt-chart/sidebar/cycles/block.tsx create mode 100644 web/components/gantt-chart/sidebar/cycles/index.ts create mode 100644 web/components/gantt-chart/sidebar/cycles/sidebar.tsx delete mode 100644 web/components/gantt-chart/sidebar/issues.tsx create mode 100644 web/components/gantt-chart/sidebar/issues/block.tsx create mode 100644 web/components/gantt-chart/sidebar/issues/index.ts create mode 100644 web/components/gantt-chart/sidebar/issues/sidebar.tsx delete mode 100644 web/components/gantt-chart/sidebar/modules.tsx create mode 100644 web/components/gantt-chart/sidebar/modules/block.tsx create mode 100644 web/components/gantt-chart/sidebar/modules/index.ts create mode 100644 web/components/gantt-chart/sidebar/modules/sidebar.tsx create mode 100644 web/store/issue/issue_gantt_view.store.ts diff --git a/web/components/dashboard/widgets/issue-panels/issues-list.tsx b/web/components/dashboard/widgets/issue-panels/issues-list.tsx index cf3f32232..3f1250d4d 100644 --- a/web/components/dashboard/widgets/issue-panels/issues-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/issues-list.tsx @@ -14,7 +14,7 @@ import { IssueListItemProps, } from "components/dashboard/widgets"; // ui -import { getButtonStyling } from "@plane/ui"; +import { Loader, getButtonStyling } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; import { getRedirectionFilters } from "helpers/dashboard.helper"; @@ -63,7 +63,12 @@ export const WidgetIssuesList: React.FC = (props) => { <>
{isLoading ? ( - <> + + + + + + ) : issues.length > 0 ? ( <>
diff --git a/web/components/gantt-chart/blocks/block.tsx b/web/components/gantt-chart/blocks/block.tsx new file mode 100644 index 000000000..1e0882aee --- /dev/null +++ b/web/components/gantt-chart/blocks/block.tsx @@ -0,0 +1,106 @@ +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "../hooks"; +import { useIssueDetail } from "hooks/store"; +// components +import { ChartAddBlock, ChartDraggable } from "../helpers"; +// helpers +import { cn } from "helpers/common.helper"; +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +// types +import { IBlockUpdateData, IGanttBlock } from "../types"; +// constants +import { BLOCK_HEIGHT } from "../constants"; + +type Props = { + block: IGanttBlock; + blockToRender: (data: any) => React.ReactNode; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + enableBlockLeftResize: boolean; + enableBlockRightResize: boolean; + enableBlockMove: boolean; + enableAddBlock: boolean; + ganttContainerRef: React.RefObject; +}; + +export const GanttChartBlock: React.FC = observer((props) => { + const { + block, + blockToRender, + blockUpdateHandler, + enableBlockLeftResize, + enableBlockRightResize, + enableBlockMove, + enableAddBlock, + ganttContainerRef, + } = props; + // store hooks + const { updateActiveBlockId, isBlockActive } = useGanttChart(); + const { peekIssue } = useIssueDetail(); + + const isBlockVisibleOnChart = block.start_date && block.target_date; + + const handleChartBlockPosition = ( + block: IGanttBlock, + totalBlockShifts: number, + dragDirection: "left" | "right" | "move" + ) => { + if (!block.start_date || !block.target_date) return; + + const originalStartDate = new Date(block.start_date); + const updatedStartDate = new Date(originalStartDate); + + const originalTargetDate = new Date(block.target_date); + const updatedTargetDate = new Date(originalTargetDate); + + // update the start date on left resize + if (dragDirection === "left") updatedStartDate.setDate(originalStartDate.getDate() - totalBlockShifts); + // update the target date on right resize + else if (dragDirection === "right") updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts); + // update both the dates on x-axis move + else if (dragDirection === "move") { + updatedStartDate.setDate(originalStartDate.getDate() + totalBlockShifts); + updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts); + } + + // call the block update handler with the updated dates + blockUpdateHandler(block.data, { + start_date: renderFormattedPayloadDate(updatedStartDate) ?? undefined, + target_date: renderFormattedPayloadDate(updatedTargetDate) ?? undefined, + }); + }; + + return ( +
+
updateActiveBlockId(block.id)} + onMouseLeave={() => updateActiveBlockId(null)} + > + {isBlockVisibleOnChart ? ( + handleChartBlockPosition(block, ...args)} + enableBlockLeftResize={enableBlockLeftResize} + enableBlockRightResize={enableBlockRightResize} + enableBlockMove={enableBlockMove} + ganttContainerRef={ganttContainerRef} + /> + ) : ( + enableAddBlock && + )} +
+
+ ); +}); diff --git a/web/components/gantt-chart/blocks/blocks-list.tsx b/web/components/gantt-chart/blocks/blocks-list.tsx index 15a3e5295..d98524ecc 100644 --- a/web/components/gantt-chart/blocks/blocks-list.tsx +++ b/web/components/gantt-chart/blocks/blocks-list.tsx @@ -1,16 +1,10 @@ -import { observer } from "mobx-react"; import { FC } from "react"; -// hooks -import { useIssueDetail } from "hooks/store"; -import { useChart } from "../hooks"; -// helpers -import { ChartAddBlock, ChartDraggable } from "components/gantt-chart"; -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; -import { cn } from "helpers/common.helper"; +// components +import { GanttChartBlock } from "./block"; // types import { IBlockUpdateData, IGanttBlock } from "../types"; // constants -import { BLOCK_HEIGHT, HEADER_HEIGHT } from "../constants"; +import { HEADER_HEIGHT } from "../constants"; export type GanttChartBlocksProps = { itemsContainerWidth: number; @@ -21,10 +15,11 @@ export type GanttChartBlocksProps = { enableBlockRightResize: boolean; enableBlockMove: boolean; enableAddBlock: boolean; + ganttContainerRef: React.RefObject; showAllBlocks: boolean; }; -export const GanttChartBlocksList: FC = observer((props) => { +export const GanttChartBlocksList: FC = (props) => { const { itemsContainerWidth, blocks, @@ -34,52 +29,9 @@ export const GanttChartBlocksList: FC = observer((props) enableBlockRightResize, enableBlockMove, enableAddBlock, + ganttContainerRef, showAllBlocks, } = props; - // store hooks - const { peekIssue } = useIssueDetail(); - // chart hook - const { activeBlock, dispatch } = useChart(); - - // update the active block on hover - const updateActiveBlock = (block: IGanttBlock | null) => { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - activeBlock: block, - }, - }); - }; - - const handleChartBlockPosition = ( - block: IGanttBlock, - totalBlockShifts: number, - dragDirection: "left" | "right" | "move" - ) => { - if (!block.start_date || !block.target_date) return; - - const originalStartDate = new Date(block.start_date); - const updatedStartDate = new Date(originalStartDate); - - const originalTargetDate = new Date(block.target_date); - const updatedTargetDate = new Date(originalTargetDate); - - // update the start date on left resize - if (dragDirection === "left") updatedStartDate.setDate(originalStartDate.getDate() - totalBlockShifts); - // update the target date on right resize - else if (dragDirection === "right") updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts); - // update both the dates on x-axis move - else if (dragDirection === "move") { - updatedStartDate.setDate(originalStartDate.getDate() + totalBlockShifts); - updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts); - } - - // call the block update handler with the updated dates - blockUpdateHandler(block.data, { - start_date: renderFormattedPayloadDate(updatedStartDate) ?? undefined, - target_date: renderFormattedPayloadDate(updatedTargetDate) ?? undefined, - }); - }; return (
= observer((props) // hide the block if it doesn't have start and target dates and showAllBlocks is false if (!showAllBlocks && !(block.start_date && block.target_date)) return; - const isBlockVisibleOnChart = block.start_date && block.target_date; - return ( -
-
updateActiveBlock(block)} - onMouseLeave={() => updateActiveBlock(null)} - > - {isBlockVisibleOnChart ? ( - handleChartBlockPosition(block, ...args)} - enableBlockLeftResize={enableBlockLeftResize} - enableBlockRightResize={enableBlockRightResize} - enableBlockMove={enableBlockMove} - /> - ) : ( - enableAddBlock && - )} -
-
+ ); })}
); -}); +}; diff --git a/web/components/gantt-chart/chart/header.tsx b/web/components/gantt-chart/chart/header.tsx index 6dcfdc36f..2ebd0360d 100644 --- a/web/components/gantt-chart/chart/header.tsx +++ b/web/components/gantt-chart/chart/header.tsx @@ -1,10 +1,13 @@ import { Expand, Shrink } from "lucide-react"; // hooks -import { useChart } from "../hooks"; // helpers import { cn } from "helpers/common.helper"; // types import { IGanttBlock, TGanttViews } from "../types"; +// constants +import { VIEWS_LIST } from "components/gantt-chart/data"; +import { useGanttChart } from "../hooks/use-gantt-chart"; +import { observer } from "mobx-react"; type Props = { blocks: IGanttBlock[] | null; @@ -16,10 +19,10 @@ type Props = { toggleFullScreenMode: () => void; }; -export const GanttChartHeader: React.FC = (props) => { +export const GanttChartHeader: React.FC = observer((props) => { const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props; // chart hook - const { currentView, allViews } = useChart(); + const { currentView } = useGanttChart(); return (
@@ -29,7 +32,7 @@ export const GanttChartHeader: React.FC = (props) => {
- {allViews?.map((chartView: any) => ( + {VIEWS_LIST.map((chartView: any) => (
= (props) => {
); -}; +}); diff --git a/web/components/gantt-chart/chart/main-content.tsx b/web/components/gantt-chart/chart/main-content.tsx index 42670b972..0f7320986 100644 --- a/web/components/gantt-chart/chart/main-content.tsx +++ b/web/components/gantt-chart/chart/main-content.tsx @@ -1,3 +1,7 @@ +import { useRef } from "react"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "../hooks/use-gantt-chart"; // components import { BiWeekChartView, @@ -12,7 +16,6 @@ import { TGanttViews, WeekChartView, YearChartView, - useChart, } from "components/gantt-chart"; // helpers import { cn } from "helpers/common.helper"; @@ -36,7 +39,7 @@ type Props = { quickAdd?: React.JSX.Element | undefined; }; -export const GanttChartMainContent: React.FC = (props) => { +export const GanttChartMainContent: React.FC = observer((props) => { const { blocks, blockToRender, @@ -55,13 +58,15 @@ export const GanttChartMainContent: React.FC = (props) => { updateCurrentViewRenderPayload, quickAdd, } = props; + // refs + const ganttContainerRef = useRef(null); // chart hook - const { currentView, currentViewData, updateScrollLeft } = useChart(); + const { currentView, currentViewData } = useGanttChart(); // handling scroll functionality const onScroll = (e: React.UIEvent) => { const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget; - updateScrollLeft(scrollLeft); + // updateScrollLeft(scrollLeft); const approxRangeLeft = scrollLeft >= clientWidth + 1000 ? 1000 : scrollLeft - clientWidth; const approxRangeRight = scrollWidth - (scrollLeft + clientWidth); @@ -95,6 +100,7 @@ export const GanttChartMainContent: React.FC = (props) => { "mb-8": bottomSpacing, } )} + ref={ganttContainerRef} onScroll={onScroll} > = (props) => { title={title} quickAdd={quickAdd} /> -
+
{currentViewData && ( = (props) => { enableBlockRightResize={enableBlockRightResize} enableBlockMove={enableBlockMove} enableAddBlock={enableAddBlock} + ganttContainerRef={ganttContainerRef} showAllBlocks={showAllBlocks} /> )}
); -}; +}); diff --git a/web/components/gantt-chart/chart/root.tsx b/web/components/gantt-chart/chart/root.tsx index 877c15901..be6229ce3 100644 --- a/web/components/gantt-chart/chart/root.tsx +++ b/web/components/gantt-chart/chart/root.tsx @@ -1,6 +1,9 @@ import { FC, useEffect, useState } from "react"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "../hooks/use-gantt-chart"; // components -import { GanttChartHeader, useChart, GanttChartMainContent } from "components/gantt-chart"; +import { GanttChartHeader, GanttChartMainContent } from "components/gantt-chart"; // views import { generateMonthChart, @@ -34,7 +37,7 @@ type ChartViewRootProps = { quickAdd?: React.JSX.Element | undefined; }; -export const ChartViewRoot: FC = (props) => { +export const ChartViewRoot: FC = observer((props) => { const { border, title, @@ -57,7 +60,8 @@ export const ChartViewRoot: FC = (props) => { const [fullScreenMode, setFullScreenMode] = useState(false); const [chartBlocks, setChartBlocks] = useState(null); // hooks - const { currentView, currentViewData, renderView, dispatch } = useChart(); + const { currentView, currentViewData, renderView, updateCurrentView, updateCurrentViewData, updateRenderView } = + useGanttChart(); // rendering the block structure const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) => @@ -87,36 +91,20 @@ export const ChartViewRoot: FC = (props) => { // updating the prevData, currentData and nextData if (currentRender.payload.length > 0) { + updateCurrentViewData(currentRender.state); + if (side === "left") { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - currentView: selectedCurrentView, - currentViewData: currentRender.state, - renderView: [...currentRender.payload, ...renderView], - }, - }); + updateCurrentView(selectedCurrentView); + updateRenderView([...currentRender.payload, ...renderView]); updatingCurrentLeftScrollPosition(currentRender.scrollWidth); setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth); } else if (side === "right") { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - currentView: view, - currentViewData: currentRender.state, - renderView: [...renderView, ...currentRender.payload], - }, - }); + updateCurrentView(view); + updateRenderView([...renderView, ...currentRender.payload]); setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth); } else { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - currentView: view, - currentViewData: currentRender.state, - renderView: [...currentRender.payload], - }, - }); + updateCurrentView(view); + updateRenderView(currentRender.payload); setItemsContainerWidth(currentRender.scrollWidth); setTimeout(() => { handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate); @@ -206,4 +194,4 @@ export const ChartViewRoot: FC = (props) => { />
); -}; +}); diff --git a/web/components/gantt-chart/chart/views/bi-week.tsx b/web/components/gantt-chart/chart/views/bi-week.tsx index 6e53d5390..f0ad084e9 100644 --- a/web/components/gantt-chart/chart/views/bi-week.tsx +++ b/web/components/gantt-chart/chart/views/bi-week.tsx @@ -1,10 +1,11 @@ import { FC } from "react"; -// context -import { useChart } from "components/gantt-chart"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart"; -export const BiWeekChartView: FC = () => { +export const BiWeekChartView: FC = observer(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + const { currentView, currentViewData, renderView } = useGanttChart(); return ( <> @@ -50,4 +51,4 @@ export const BiWeekChartView: FC = () => {
); -}; +}); diff --git a/web/components/gantt-chart/chart/views/day.tsx b/web/components/gantt-chart/chart/views/day.tsx index a50b7748a..84b2edac4 100644 --- a/web/components/gantt-chart/chart/views/day.tsx +++ b/web/components/gantt-chart/chart/views/day.tsx @@ -1,10 +1,11 @@ import { FC } from "react"; -// context -import { useChart } from "../../hooks"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart"; -export const DayChartView: FC = () => { +export const DayChartView: FC = observer(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + const { currentView, currentViewData, renderView } = useGanttChart(); return ( <> @@ -50,4 +51,4 @@ export const DayChartView: FC = () => {
); -}; +}); diff --git a/web/components/gantt-chart/chart/views/hours.tsx b/web/components/gantt-chart/chart/views/hours.tsx index e1fd02e3f..bd1a7b6dd 100644 --- a/web/components/gantt-chart/chart/views/hours.tsx +++ b/web/components/gantt-chart/chart/views/hours.tsx @@ -1,10 +1,11 @@ import { FC } from "react"; -// context -import { useChart } from "components/gantt-chart"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart"; -export const HourChartView: FC = () => { +export const HourChartView: FC = observer(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + const { currentView, currentViewData, renderView } = useGanttChart(); return ( <> @@ -50,4 +51,4 @@ export const HourChartView: FC = () => {
); -}; +}); diff --git a/web/components/gantt-chart/chart/views/month.tsx b/web/components/gantt-chart/chart/views/month.tsx index c559e9688..3bfd077fe 100644 --- a/web/components/gantt-chart/chart/views/month.tsx +++ b/web/components/gantt-chart/chart/views/month.tsx @@ -1,6 +1,7 @@ import { FC } from "react"; +import { observer } from "mobx-react"; // hooks -import { useChart } from "components/gantt-chart"; +import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart"; // helpers import { cn } from "helpers/common.helper"; // types @@ -8,9 +9,9 @@ import { IMonthBlock } from "../../views"; // constants import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "components/gantt-chart/constants"; -export const MonthChartView: FC = () => { +export const MonthChartView: FC = observer(() => { // chart hook - const { currentViewData, renderView } = useChart(); + const { currentViewData, renderView } = useGanttChart(); const monthBlocks: IMonthBlock[] = renderView; return ( @@ -71,4 +72,4 @@ export const MonthChartView: FC = () => { ))}
); -}; +}); diff --git a/web/components/gantt-chart/chart/views/quarter.tsx b/web/components/gantt-chart/chart/views/quarter.tsx index ffbc1cbfe..b8adc4b3a 100644 --- a/web/components/gantt-chart/chart/views/quarter.tsx +++ b/web/components/gantt-chart/chart/views/quarter.tsx @@ -1,10 +1,11 @@ import { FC } from "react"; -// context -import { useChart } from "../../hooks"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart"; -export const QuarterChartView: FC = () => { +export const QuarterChartView: FC = observer(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + const { currentView, currentViewData, renderView } = useGanttChart(); return ( <> @@ -46,4 +47,4 @@ export const QuarterChartView: FC = () => {
); -}; +}); diff --git a/web/components/gantt-chart/chart/views/week.tsx b/web/components/gantt-chart/chart/views/week.tsx index 8170affa4..981fc9236 100644 --- a/web/components/gantt-chart/chart/views/week.tsx +++ b/web/components/gantt-chart/chart/views/week.tsx @@ -1,10 +1,11 @@ import { FC } from "react"; -// context -import { useChart } from "../../hooks"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart"; -export const WeekChartView: FC = () => { +export const WeekChartView: FC = observer(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + const { currentView, currentViewData, renderView } = useGanttChart(); return ( <> @@ -50,4 +51,4 @@ export const WeekChartView: FC = () => {
); -}; +}); diff --git a/web/components/gantt-chart/chart/views/year.tsx b/web/components/gantt-chart/chart/views/year.tsx index 9dbeedece..659126ac3 100644 --- a/web/components/gantt-chart/chart/views/year.tsx +++ b/web/components/gantt-chart/chart/views/year.tsx @@ -1,10 +1,11 @@ import { FC } from "react"; -// context -import { useChart } from "../../hooks"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart"; -export const YearChartView: FC = () => { +export const YearChartView: FC = observer(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + const { currentView, currentViewData, renderView } = useGanttChart(); return ( <> @@ -46,4 +47,4 @@ export const YearChartView: FC = () => {
); -}; +}); diff --git a/web/components/gantt-chart/contexts/index.tsx b/web/components/gantt-chart/contexts/index.tsx index 84e7a19b5..1d8a19f1a 100644 --- a/web/components/gantt-chart/contexts/index.tsx +++ b/web/components/gantt-chart/contexts/index.tsx @@ -1,57 +1,19 @@ -import React, { createContext, useState } from "react"; -// types -import { ChartContextData, ChartContextActionPayload, ChartContextReducer } from "../types"; -// data -import { allViewsWithData, currentViewDataWithView } from "../data"; +import { createContext } from "react"; +// mobx store +import { GanttStore } from "store/issue/issue_gantt_view.store"; -export const ChartContext = createContext(undefined); +let ganttViewStore = new GanttStore(); -const chartReducer = (state: ChartContextData, action: ChartContextActionPayload): ChartContextData => { - switch (action.type) { - case "CURRENT_VIEW": - return { ...state, currentView: action.payload }; - case "CURRENT_VIEW_DATA": - return { ...state, currentViewData: action.payload }; - case "RENDER_VIEW": - return { ...state, currentViewData: action.payload }; - case "PARTIAL_UPDATE": - return { ...state, ...action.payload }; - default: - return state; - } +export const GanttStoreContext = createContext(ganttViewStore); + +const initializeStore = () => { + const _ganttStore = ganttViewStore ?? new GanttStore(); + if (typeof window === "undefined") return _ganttStore; + if (!ganttViewStore) ganttViewStore = _ganttStore; + return _ganttStore; }; -const initialView = "month"; - -export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - // states; - const [state, dispatch] = useState({ - currentView: initialView, - currentViewData: currentViewDataWithView(initialView), - renderView: [], - allViews: allViewsWithData, - activeBlock: null, - }); - const [scrollLeft, setScrollLeft] = useState(0); - - const handleDispatch = (action: ChartContextActionPayload): ChartContextData => { - const newState = chartReducer(state, action); - dispatch(() => newState); - return newState; - }; - - const updateScrollLeft = (scrollLeft: number) => setScrollLeft(scrollLeft); - - return ( - - {children} - - ); +export const GanttStoreProvider = ({ children }: any) => { + const store = initializeStore(); + return {children}; }; diff --git a/web/components/gantt-chart/data/index.ts b/web/components/gantt-chart/data/index.ts index 58ac6e4b2..cc15c5d9e 100644 --- a/web/components/gantt-chart/data/index.ts +++ b/web/components/gantt-chart/data/index.ts @@ -1,5 +1,5 @@ // types -import { WeekMonthDataType, ChartDataType } from "../types"; +import { WeekMonthDataType, ChartDataType, TGanttViews } from "../types"; // constants export const weeks: WeekMonthDataType[] = [ @@ -53,7 +53,7 @@ export const datePreview = (date: Date, includeTime: boolean = false) => { }; // context data -export const allViewsWithData: ChartDataType[] = [ +export const VIEWS_LIST: ChartDataType[] = [ // { // key: "hours", // title: "Hours", @@ -133,7 +133,5 @@ export const allViewsWithData: ChartDataType[] = [ // }, ]; -export const currentViewDataWithView = (view: string = "month") => { - const currentView: ChartDataType | undefined = allViewsWithData.find((_viewData) => _viewData.key === view); - return currentView; -}; +export const currentViewDataWithView = (view: TGanttViews = "month") => + VIEWS_LIST.find((_viewData) => _viewData.key === view); diff --git a/web/components/gantt-chart/helpers/add-block.tsx b/web/components/gantt-chart/helpers/add-block.tsx index bfeddffa2..b7497013f 100644 --- a/web/components/gantt-chart/helpers/add-block.tsx +++ b/web/components/gantt-chart/helpers/add-block.tsx @@ -1,21 +1,21 @@ import { useEffect, useRef, useState } from "react"; import { addDays } from "date-fns"; import { Plus } from "lucide-react"; -// hooks -import { useChart } from "../hooks"; // ui import { Tooltip } from "@plane/ui"; // helpers import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import { IBlockUpdateData, IGanttBlock } from "../types"; +import { useGanttChart } from "../hooks/use-gantt-chart"; +import { observer } from "mobx-react"; type Props = { block: IGanttBlock; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; }; -export const ChartAddBlock: React.FC = (props) => { +export const ChartAddBlock: React.FC = observer((props) => { const { block, blockUpdateHandler } = props; // states const [isButtonVisible, setIsButtonVisible] = useState(false); @@ -24,7 +24,7 @@ export const ChartAddBlock: React.FC = (props) => { // refs const containerRef = useRef(null); // chart hook - const { currentViewData } = useChart(); + const { currentViewData } = useGanttChart(); const handleButtonClick = () => { if (!currentViewData) return; @@ -88,4 +88,4 @@ export const ChartAddBlock: React.FC = (props) => { )}
); -}; +}); diff --git a/web/components/gantt-chart/helpers/block-structure.ts b/web/components/gantt-chart/helpers/block-structure.ts deleted file mode 100644 index 0f18b43cc..000000000 --- a/web/components/gantt-chart/helpers/block-structure.ts +++ /dev/null @@ -1,12 +0,0 @@ -// types -import { TIssue } from "@plane/types"; -import { IGanttBlock } from "components/gantt-chart"; - -export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] => - blocks?.map((block) => ({ - data: block, - id: block.id, - sort_order: block.sort_order, - start_date: block.start_date ? new Date(block.start_date) : null, - target_date: block.target_date ? new Date(block.target_date) : null, - })); diff --git a/web/components/gantt-chart/helpers/draggable.tsx b/web/components/gantt-chart/helpers/draggable.tsx index ac1602346..c2b4dc619 100644 --- a/web/components/gantt-chart/helpers/draggable.tsx +++ b/web/components/gantt-chart/helpers/draggable.tsx @@ -1,11 +1,13 @@ import React, { useEffect, useRef, useState } from "react"; import { ArrowRight } from "lucide-react"; // hooks -import { IGanttBlock, useChart } from "components/gantt-chart"; +import { IGanttBlock } from "components/gantt-chart"; // helpers import { cn } from "helpers/common.helper"; // constants import { SIDEBAR_WIDTH } from "../constants"; +import { useGanttChart } from "../hooks/use-gantt-chart"; +import { observer } from "mobx-react"; type Props = { block: IGanttBlock; @@ -14,19 +16,29 @@ type Props = { enableBlockLeftResize: boolean; enableBlockRightResize: boolean; enableBlockMove: boolean; + ganttContainerRef: React.RefObject; }; -export const ChartDraggable: React.FC = (props) => { - const { block, blockToRender, handleBlock, enableBlockLeftResize, enableBlockRightResize, enableBlockMove } = props; +export const ChartDraggable: React.FC = observer((props) => { + const { + block, + blockToRender, + handleBlock, + enableBlockLeftResize, + enableBlockRightResize, + enableBlockMove, + ganttContainerRef, + } = props; // states const [isLeftResizing, setIsLeftResizing] = useState(false); const [isRightResizing, setIsRightResizing] = useState(false); const [isMoving, setIsMoving] = useState(false); const [isHidden, setIsHidden] = useState(true); + const [scrollLeft, setScrollLeft] = useState(0); // refs const resizableRef = useRef(null); // chart hook - const { currentViewData, scrollLeft } = useChart(); + const { currentViewData } = useGanttChart(); // check if cursor reaches either end while resizing/dragging const checkScrollEnd = (e: MouseEvent): number => { const SCROLL_THRESHOLD = 70; @@ -212,6 +224,17 @@ export const ChartDraggable: React.FC = (props) => { block.position?.width && scrollLeft > block.position.marginLeft + block.position.width; + useEffect(() => { + const ganttContainer = ganttContainerRef.current; + if (!ganttContainer) return; + + const handleScroll = () => setScrollLeft(ganttContainer.scrollLeft); + ganttContainer.addEventListener("scroll", handleScroll); + return () => { + ganttContainer.removeEventListener("scroll", handleScroll); + }; + }, [ganttContainerRef]); + useEffect(() => { const intersectionRoot = document.querySelector("#gantt-container") as HTMLDivElement; const resizableBlock = resizableRef.current; @@ -234,7 +257,7 @@ export const ChartDraggable: React.FC = (props) => { return () => { observer.unobserve(resizableBlock); }; - }, [block.data.name]); + }, []); return ( <> @@ -312,4 +335,4 @@ export const ChartDraggable: React.FC = (props) => {
); -}; +}); diff --git a/web/components/gantt-chart/helpers/index.ts b/web/components/gantt-chart/helpers/index.ts index 1b51dc374..c96d42eec 100644 --- a/web/components/gantt-chart/helpers/index.ts +++ b/web/components/gantt-chart/helpers/index.ts @@ -1,3 +1,2 @@ export * from "./add-block"; -export * from "./block-structure"; export * from "./draggable"; diff --git a/web/components/gantt-chart/hooks/index.ts b/web/components/gantt-chart/hooks/index.ts new file mode 100644 index 000000000..009650675 --- /dev/null +++ b/web/components/gantt-chart/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-gantt-chart"; diff --git a/web/components/gantt-chart/hooks/index.tsx b/web/components/gantt-chart/hooks/index.tsx deleted file mode 100644 index 5fb9bee3f..000000000 --- a/web/components/gantt-chart/hooks/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useContext } from "react"; -// types -import { ChartContextReducer } from "../types"; -// context -import { ChartContext } from "../contexts"; - -export const useChart = (): ChartContextReducer => { - const context = useContext(ChartContext); - - if (!context) throw new Error("useChart must be used within a GanttChart"); - - return context; -}; diff --git a/web/components/gantt-chart/hooks/use-gantt-chart.ts b/web/components/gantt-chart/hooks/use-gantt-chart.ts new file mode 100644 index 000000000..23e025e90 --- /dev/null +++ b/web/components/gantt-chart/hooks/use-gantt-chart.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// mobx store +import { GanttStoreContext } from "components/gantt-chart/contexts"; +// types +import { IGanttStore } from "store/issue/issue_gantt_view.store"; + +export const useGanttChart = (): IGanttStore => { + const context = useContext(GanttStoreContext); + if (context === undefined) throw new Error("useGanttChart must be used within GanttStoreProvider"); + return context; +}; diff --git a/web/components/gantt-chart/index.ts b/web/components/gantt-chart/index.ts index 54a2cc597..78297ffcd 100644 --- a/web/components/gantt-chart/index.ts +++ b/web/components/gantt-chart/index.ts @@ -3,5 +3,5 @@ export * from "./chart"; export * from "./helpers"; export * from "./hooks"; export * from "./root"; -export * from "./types"; export * from "./sidebar"; +export * from "./types"; diff --git a/web/components/gantt-chart/root.tsx b/web/components/gantt-chart/root.tsx index ac132500b..4df5d9931 100644 --- a/web/components/gantt-chart/root.tsx +++ b/web/components/gantt-chart/root.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; // components import { ChartViewRoot, IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; // context -import { ChartContextProvider } from "./contexts"; +import { GanttStoreProvider } from "components/gantt-chart/contexts"; type GanttChartRootProps = { border?: boolean; @@ -42,7 +42,7 @@ export const GanttChartRoot: FC = (props) => { } = props; return ( - + = (props) => { showAllBlocks={showAllBlocks} quickAdd={quickAdd} /> - + ); }; diff --git a/web/components/gantt-chart/sidebar/cycles.tsx b/web/components/gantt-chart/sidebar/cycles.tsx deleted file mode 100644 index 384869a40..000000000 --- a/web/components/gantt-chart/sidebar/cycles.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd"; -import { MoreVertical } from "lucide-react"; -// hooks -import { useChart } from "components/gantt-chart/hooks"; -// ui -import { Loader } from "@plane/ui"; -// components -import { CycleGanttSidebarBlock } from "components/cycles"; -// helpers -import { findTotalDaysInRange } from "helpers/date-time.helper"; -import { cn } from "helpers/common.helper"; -// types -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; -// constants -import { BLOCK_HEIGHT } from "../constants"; - -type Props = { - title: string; - blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - blocks: IGanttBlock[] | null; - enableReorder: boolean; -}; - -export const CycleGanttSidebar: React.FC = (props) => { - const { blockUpdateHandler, blocks, enableReorder } = props; - // chart hook - const { activeBlock, dispatch } = useChart(); - - // update the active block on hover - const updateActiveBlock = (block: IGanttBlock | null) => { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - activeBlock: block, - }, - }); - }; - - const handleOrderChange = (result: DropResult) => { - if (!blocks) return; - - const { source, destination } = result; - - // return if dropped outside the list - if (!destination) return; - - // return if dropped on the same index - if (source.index === destination.index) return; - - let updatedSortOrder = blocks[source.index].sort_order; - - // update the sort order to the lowest if dropped at the top - if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; - // update the sort order to the highest if dropped at the bottom - else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; - // update the sort order to the average of the two adjacent blocks if dropped in between - else { - const destinationSortingOrder = blocks[destination.index].sort_order; - const relativeDestinationSortingOrder = - source.index < destination.index - ? blocks[destination.index + 1].sort_order - : blocks[destination.index - 1].sort_order; - - updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; - } - - // extract the element from the source index and insert it at the destination index without updating the entire array - const removedElement = blocks.splice(source.index, 1)[0]; - blocks.splice(destination.index, 0, removedElement); - - // call the block update handler with the updated sort order, new and old index - blockUpdateHandler(removedElement.data, { - sort_order: { - destinationIndex: destination.index, - newSortOrder: updatedSortOrder, - sourceIndex: source.index, - }, - }); - }; - - return ( - - - {(droppableProvided) => ( -
- <> - {blocks ? ( - blocks.map((block, index) => { - const duration = findTotalDaysInRange(block.start_date, block.target_date); - - return ( - - {(provided, snapshot) => ( -
updateActiveBlock(block)} - onMouseLeave={() => updateActiveBlock(null)} - ref={provided.innerRef} - {...provided.draggableProps} - > -
- {enableReorder && ( - - )} -
-
- -
- {duration && ( -
- {duration} day{duration > 1 ? "s" : ""} -
- )} -
-
-
- )} -
- ); - }) - ) : ( - - - - - - - )} - {droppableProvided.placeholder} - -
- )} -
-
- ); -}; diff --git a/web/components/gantt-chart/sidebar/cycles/block.tsx b/web/components/gantt-chart/sidebar/cycles/block.tsx new file mode 100644 index 000000000..f1374c753 --- /dev/null +++ b/web/components/gantt-chart/sidebar/cycles/block.tsx @@ -0,0 +1,72 @@ +import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { observer } from "mobx-react"; +import { MoreVertical } from "lucide-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks"; +// components +import { CycleGanttSidebarBlock } from "components/cycles"; +// helpers +import { cn } from "helpers/common.helper"; +import { findTotalDaysInRange } from "helpers/date-time.helper"; +// types +import { IGanttBlock } from "components/gantt-chart/types"; +// constants +import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; + +type Props = { + block: IGanttBlock; + enableReorder: boolean; + provided: DraggableProvided; + snapshot: DraggableStateSnapshot; +}; + +export const CyclesSidebarBlock: React.FC = observer((props) => { + const { block, enableReorder, provided, snapshot } = props; + // store hooks + const { updateActiveBlockId, isBlockActive } = useGanttChart(); + + const duration = findTotalDaysInRange(block.start_date, block.target_date); + + return ( +
updateActiveBlockId(block.id)} + onMouseLeave={() => updateActiveBlockId(null)} + ref={provided.innerRef} + {...provided.draggableProps} + > +
+ {enableReorder && ( + + )} +
+
+ +
+ {duration && ( +
+ {duration} day{duration > 1 ? "s" : ""} +
+ )} +
+
+
+ ); +}); diff --git a/web/components/gantt-chart/sidebar/cycles/index.ts b/web/components/gantt-chart/sidebar/cycles/index.ts new file mode 100644 index 000000000..01acaeffb --- /dev/null +++ b/web/components/gantt-chart/sidebar/cycles/index.ts @@ -0,0 +1 @@ +export * from "./sidebar"; diff --git a/web/components/gantt-chart/sidebar/cycles/sidebar.tsx b/web/components/gantt-chart/sidebar/cycles/sidebar.tsx new file mode 100644 index 000000000..11f67a099 --- /dev/null +++ b/web/components/gantt-chart/sidebar/cycles/sidebar.tsx @@ -0,0 +1,100 @@ +import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd"; +// ui +import { Loader } from "@plane/ui"; +// components +import { CyclesSidebarBlock } from "./block"; +// types +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; + +type Props = { + title: string; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + blocks: IGanttBlock[] | null; + enableReorder: boolean; +}; + +export const CycleGanttSidebar: React.FC = (props) => { + const { blockUpdateHandler, blocks, enableReorder } = props; + + const handleOrderChange = (result: DropResult) => { + if (!blocks) return; + + const { source, destination } = result; + + // return if dropped outside the list + if (!destination) return; + + // return if dropped on the same index + if (source.index === destination.index) return; + + let updatedSortOrder = blocks[source.index].sort_order; + + // update the sort order to the lowest if dropped at the top + if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; + // update the sort order to the highest if dropped at the bottom + else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; + // update the sort order to the average of the two adjacent blocks if dropped in between + else { + const destinationSortingOrder = blocks[destination.index].sort_order; + const relativeDestinationSortingOrder = + source.index < destination.index + ? blocks[destination.index + 1].sort_order + : blocks[destination.index - 1].sort_order; + + updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; + } + + // extract the element from the source index and insert it at the destination index without updating the entire array + const removedElement = blocks.splice(source.index, 1)[0]; + blocks.splice(destination.index, 0, removedElement); + + // call the block update handler with the updated sort order, new and old index + blockUpdateHandler(removedElement.data, { + sort_order: { + destinationIndex: destination.index, + newSortOrder: updatedSortOrder, + sourceIndex: source.index, + }, + }); + }; + + return ( + + + {(droppableProvided) => ( +
+ <> + {blocks ? ( + blocks.map((block, index) => ( + + {(provided, snapshot) => ( + + )} + + )) + ) : ( + + + + + + + )} + {droppableProvided.placeholder} + +
+ )} +
+
+ ); +}; diff --git a/web/components/gantt-chart/sidebar/issues.tsx b/web/components/gantt-chart/sidebar/issues.tsx deleted file mode 100644 index 52e30ded5..000000000 --- a/web/components/gantt-chart/sidebar/issues.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { observer } from "mobx-react"; -import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; -import { MoreVertical } from "lucide-react"; -// hooks -import { useChart } from "components/gantt-chart/hooks"; -import { useIssueDetail } from "hooks/store"; -// ui -import { Loader } from "@plane/ui"; -// components -import { IssueGanttSidebarBlock } from "components/issues"; -// helpers -import { findTotalDaysInRange } from "helpers/date-time.helper"; -import { cn } from "helpers/common.helper"; -// types -import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; -import { BLOCK_HEIGHT } from "../constants"; - -type Props = { - blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - blocks: IGanttBlock[] | null; - enableReorder: boolean; - showAllBlocks?: boolean; -}; - -export const IssueGanttSidebar: React.FC = observer((props: Props) => { - const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props; - - const { activeBlock, dispatch } = useChart(); - const { peekIssue } = useIssueDetail(); - - // update the active block on hover - const updateActiveBlock = (block: IGanttBlock | null) => { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - activeBlock: block, - }, - }); - }; - - const handleOrderChange = (result: DropResult) => { - if (!blocks) return; - - const { source, destination } = result; - - // return if dropped outside the list - if (!destination) return; - - // return if dropped on the same index - if (source.index === destination.index) return; - - let updatedSortOrder = blocks[source.index].sort_order; - - // update the sort order to the lowest if dropped at the top - if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; - // update the sort order to the highest if dropped at the bottom - else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; - // update the sort order to the average of the two adjacent blocks if dropped in between - else { - const destinationSortingOrder = blocks[destination.index].sort_order; - const relativeDestinationSortingOrder = - source.index < destination.index - ? blocks[destination.index + 1].sort_order - : blocks[destination.index - 1].sort_order; - - updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; - } - - // extract the element from the source index and insert it at the destination index without updating the entire array - const removedElement = blocks.splice(source.index, 1)[0]; - blocks.splice(destination.index, 0, removedElement); - - // call the block update handler with the updated sort order, new and old index - blockUpdateHandler(removedElement.data, { - sort_order: { - destinationIndex: destination.index, - newSortOrder: updatedSortOrder, - sourceIndex: source.index, - }, - }); - }; - - return ( - <> - - - {(droppableProvided) => ( -
- <> - {blocks ? ( - blocks.map((block, index) => { - const isBlockVisibleOnSidebar = block.start_date && block.target_date; - - // hide the block if it doesn't have start and target dates and showAllBlocks is false - if (!showAllBlocks && !isBlockVisibleOnSidebar) return; - - const duration = - !block.start_date || !block.target_date - ? null - : findTotalDaysInRange(block.start_date, block.target_date); - - return ( - - {(provided, snapshot) => ( -
updateActiveBlock(block)} - onMouseLeave={() => updateActiveBlock(null)} - ref={provided.innerRef} - {...provided.draggableProps} - > -
- {enableReorder && ( - - )} -
-
- -
- {duration && ( -
- - {duration} day{duration > 1 ? "s" : ""} - -
- )} -
-
-
- )} -
- ); - }) - ) : ( - - - - - - - )} - {droppableProvided.placeholder} - -
- )} -
-
- - ); -}); diff --git a/web/components/gantt-chart/sidebar/issues/block.tsx b/web/components/gantt-chart/sidebar/issues/block.tsx new file mode 100644 index 000000000..03a17a65b --- /dev/null +++ b/web/components/gantt-chart/sidebar/issues/block.tsx @@ -0,0 +1,77 @@ +import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { observer } from "mobx-react"; +import { MoreVertical } from "lucide-react"; +// hooks +import { useIssueDetail } from "hooks/store"; +import { useGanttChart } from "components/gantt-chart/hooks"; +// components +import { IssueGanttSidebarBlock } from "components/issues"; +// helpers +import { cn } from "helpers/common.helper"; +import { findTotalDaysInRange } from "helpers/date-time.helper"; +// types +import { IGanttBlock } from "../../types"; +// constants +import { BLOCK_HEIGHT } from "../../constants"; + +type Props = { + block: IGanttBlock; + enableReorder: boolean; + provided: DraggableProvided; + snapshot: DraggableStateSnapshot; +}; + +export const IssuesSidebarBlock: React.FC = observer((props) => { + const { block, enableReorder, provided, snapshot } = props; + // store hooks + const { updateActiveBlockId, isBlockActive } = useGanttChart(); + const { peekIssue } = useIssueDetail(); + + const duration = findTotalDaysInRange(block.start_date, block.target_date); + + return ( +
updateActiveBlockId(block.id)} + onMouseLeave={() => updateActiveBlockId(null)} + ref={provided.innerRef} + {...provided.draggableProps} + > +
+ {enableReorder && ( + + )} +
+
+ +
+ {duration && ( +
+ + {duration} day{duration > 1 ? "s" : ""} + +
+ )} +
+
+
+ ); +}); diff --git a/web/components/gantt-chart/sidebar/issues/index.ts b/web/components/gantt-chart/sidebar/issues/index.ts new file mode 100644 index 000000000..01acaeffb --- /dev/null +++ b/web/components/gantt-chart/sidebar/issues/index.ts @@ -0,0 +1 @@ +export * from "./sidebar"; diff --git a/web/components/gantt-chart/sidebar/issues/sidebar.tsx b/web/components/gantt-chart/sidebar/issues/sidebar.tsx new file mode 100644 index 000000000..323938eec --- /dev/null +++ b/web/components/gantt-chart/sidebar/issues/sidebar.tsx @@ -0,0 +1,107 @@ +import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; +// components +import { IssuesSidebarBlock } from "./block"; +// ui +import { Loader } from "@plane/ui"; +// types +import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; + +type Props = { + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + blocks: IGanttBlock[] | null; + enableReorder: boolean; + showAllBlocks?: boolean; +}; + +export const IssueGanttSidebar: React.FC = (props) => { + const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props; + + const handleOrderChange = (result: DropResult) => { + if (!blocks) return; + + const { source, destination } = result; + + // return if dropped outside the list + if (!destination) return; + + // return if dropped on the same index + if (source.index === destination.index) return; + + let updatedSortOrder = blocks[source.index].sort_order; + + // update the sort order to the lowest if dropped at the top + if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; + // update the sort order to the highest if dropped at the bottom + else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; + // update the sort order to the average of the two adjacent blocks if dropped in between + else { + const destinationSortingOrder = blocks[destination.index].sort_order; + const relativeDestinationSortingOrder = + source.index < destination.index + ? blocks[destination.index + 1].sort_order + : blocks[destination.index - 1].sort_order; + + updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; + } + + // extract the element from the source index and insert it at the destination index without updating the entire array + const removedElement = blocks.splice(source.index, 1)[0]; + blocks.splice(destination.index, 0, removedElement); + + // call the block update handler with the updated sort order, new and old index + blockUpdateHandler(removedElement.data, { + sort_order: { + destinationIndex: destination.index, + newSortOrder: updatedSortOrder, + sourceIndex: source.index, + }, + }); + }; + + return ( + + + {(droppableProvided) => ( +
+ <> + {blocks ? ( + blocks.map((block, index) => { + const isBlockVisibleOnSidebar = block.start_date && block.target_date; + + // hide the block if it doesn't have start and target dates and showAllBlocks is false + if (!showAllBlocks && !isBlockVisibleOnSidebar) return; + + return ( + + {(provided, snapshot) => ( + + )} + + ); + }) + ) : ( + + + + + + + )} + {droppableProvided.placeholder} + +
+ )} +
+
+ ); +}; diff --git a/web/components/gantt-chart/sidebar/modules.tsx b/web/components/gantt-chart/sidebar/modules.tsx deleted file mode 100644 index bdf8ca571..000000000 --- a/web/components/gantt-chart/sidebar/modules.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; -import { MoreVertical } from "lucide-react"; -// hooks -import { useChart } from "components/gantt-chart/hooks"; -// ui -import { Loader } from "@plane/ui"; -// components -import { ModuleGanttSidebarBlock } from "components/modules"; -// helpers -import { findTotalDaysInRange } from "helpers/date-time.helper"; -import { cn } from "helpers/common.helper"; -// types -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; -// constants -import { BLOCK_HEIGHT } from "../constants"; - -type Props = { - title: string; - blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - blocks: IGanttBlock[] | null; - enableReorder: boolean; -}; - -export const ModuleGanttSidebar: React.FC = (props) => { - const { blockUpdateHandler, blocks, enableReorder } = props; - // chart hook - const { activeBlock, dispatch } = useChart(); - - // update the active block on hover - const updateActiveBlock = (block: IGanttBlock | null) => { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - activeBlock: block, - }, - }); - }; - - const handleOrderChange = (result: DropResult) => { - if (!blocks) return; - - const { source, destination } = result; - - // return if dropped outside the list - if (!destination) return; - - // return if dropped on the same index - if (source.index === destination.index) return; - - let updatedSortOrder = blocks[source.index].sort_order; - - // update the sort order to the lowest if dropped at the top - if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; - // update the sort order to the highest if dropped at the bottom - else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; - // update the sort order to the average of the two adjacent blocks if dropped in between - else { - const destinationSortingOrder = blocks[destination.index].sort_order; - const relativeDestinationSortingOrder = - source.index < destination.index - ? blocks[destination.index + 1].sort_order - : blocks[destination.index - 1].sort_order; - - updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; - } - - // extract the element from the source index and insert it at the destination index without updating the entire array - const removedElement = blocks.splice(source.index, 1)[0]; - blocks.splice(destination.index, 0, removedElement); - - // call the block update handler with the updated sort order, new and old index - blockUpdateHandler(removedElement.data, { - sort_order: { - destinationIndex: destination.index, - newSortOrder: updatedSortOrder, - sourceIndex: source.index, - }, - }); - }; - - return ( - - - {(droppableProvided) => ( -
- <> - {blocks ? ( - blocks.map((block, index) => { - const duration = findTotalDaysInRange(block.start_date, block.target_date); - - return ( - - {(provided, snapshot) => ( -
updateActiveBlock(block)} - onMouseLeave={() => updateActiveBlock(null)} - ref={provided.innerRef} - {...provided.draggableProps} - > -
- {enableReorder && ( - - )} -
-
- -
- {duration !== undefined && ( -
- {duration} day{duration > 1 ? "s" : ""} -
- )} -
-
-
- )} -
- ); - }) - ) : ( - - - - - - - )} - {droppableProvided.placeholder} - -
- )} -
-
- ); -}; diff --git a/web/components/gantt-chart/sidebar/modules/block.tsx b/web/components/gantt-chart/sidebar/modules/block.tsx new file mode 100644 index 000000000..4b2e47226 --- /dev/null +++ b/web/components/gantt-chart/sidebar/modules/block.tsx @@ -0,0 +1,72 @@ +import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { observer } from "mobx-react"; +import { MoreVertical } from "lucide-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks"; +// components +import { ModuleGanttSidebarBlock } from "components/modules"; +// helpers +import { cn } from "helpers/common.helper"; +import { findTotalDaysInRange } from "helpers/date-time.helper"; +// types +import { IGanttBlock } from "components/gantt-chart/types"; +// constants +import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; + +type Props = { + block: IGanttBlock; + enableReorder: boolean; + provided: DraggableProvided; + snapshot: DraggableStateSnapshot; +}; + +export const ModulesSidebarBlock: React.FC = observer((props) => { + const { block, enableReorder, provided, snapshot } = props; + // store hooks + const { updateActiveBlockId, isBlockActive } = useGanttChart(); + + const duration = findTotalDaysInRange(block.start_date, block.target_date); + + return ( +
updateActiveBlockId(block.id)} + onMouseLeave={() => updateActiveBlockId(null)} + ref={provided.innerRef} + {...provided.draggableProps} + > +
+ {enableReorder && ( + + )} +
+
+ +
+ {duration !== undefined && ( +
+ {duration} day{duration > 1 ? "s" : ""} +
+ )} +
+
+
+ ); +}); diff --git a/web/components/gantt-chart/sidebar/modules/index.ts b/web/components/gantt-chart/sidebar/modules/index.ts new file mode 100644 index 000000000..01acaeffb --- /dev/null +++ b/web/components/gantt-chart/sidebar/modules/index.ts @@ -0,0 +1 @@ +export * from "./sidebar"; diff --git a/web/components/gantt-chart/sidebar/modules/sidebar.tsx b/web/components/gantt-chart/sidebar/modules/sidebar.tsx new file mode 100644 index 000000000..dee83fa79 --- /dev/null +++ b/web/components/gantt-chart/sidebar/modules/sidebar.tsx @@ -0,0 +1,100 @@ +import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; +// ui +import { Loader } from "@plane/ui"; +// components +import { ModulesSidebarBlock } from "./block"; +// types +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; + +type Props = { + title: string; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + blocks: IGanttBlock[] | null; + enableReorder: boolean; +}; + +export const ModuleGanttSidebar: React.FC = (props) => { + const { blockUpdateHandler, blocks, enableReorder } = props; + + const handleOrderChange = (result: DropResult) => { + if (!blocks) return; + + const { source, destination } = result; + + // return if dropped outside the list + if (!destination) return; + + // return if dropped on the same index + if (source.index === destination.index) return; + + let updatedSortOrder = blocks[source.index].sort_order; + + // update the sort order to the lowest if dropped at the top + if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; + // update the sort order to the highest if dropped at the bottom + else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; + // update the sort order to the average of the two adjacent blocks if dropped in between + else { + const destinationSortingOrder = blocks[destination.index].sort_order; + const relativeDestinationSortingOrder = + source.index < destination.index + ? blocks[destination.index + 1].sort_order + : blocks[destination.index - 1].sort_order; + + updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; + } + + // extract the element from the source index and insert it at the destination index without updating the entire array + const removedElement = blocks.splice(source.index, 1)[0]; + blocks.splice(destination.index, 0, removedElement); + + // call the block update handler with the updated sort order, new and old index + blockUpdateHandler(removedElement.data, { + sort_order: { + destinationIndex: destination.index, + newSortOrder: updatedSortOrder, + sourceIndex: source.index, + }, + }); + }; + + return ( + + + {(droppableProvided) => ( +
+ <> + {blocks ? ( + blocks.map((block, index) => ( + + {(provided, snapshot) => ( + + )} + + )) + ) : ( + + + + + + + )} + {droppableProvided.placeholder} + +
+ )} +
+
+ ); +}; diff --git a/web/components/gantt-chart/sidebar/project-views.tsx b/web/components/gantt-chart/sidebar/project-views.tsx index a27c4dded..a7e7c5e35 100644 --- a/web/components/gantt-chart/sidebar/project-views.tsx +++ b/web/components/gantt-chart/sidebar/project-views.tsx @@ -1,17 +1,10 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; -import { MoreVertical } from "lucide-react"; -// hooks -import { useChart } from "components/gantt-chart/hooks"; // ui import { Loader } from "@plane/ui"; // components -import { IssueGanttSidebarBlock } from "components/issues"; -// helpers -import { findTotalDaysInRange } from "helpers/date-time.helper"; +import { IssuesSidebarBlock } from "./issues/block"; // types import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; -// constants -import { BLOCK_HEIGHT } from "../constants"; type Props = { title: string; @@ -23,18 +16,6 @@ type Props = { export const ProjectViewGanttSidebar: React.FC = (props) => { const { blockUpdateHandler, blocks, enableReorder } = props; - // chart hook - const { activeBlock, dispatch } = useChart(); - - // update the active block on hover - const updateActiveBlock = (block: IGanttBlock | null) => { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - activeBlock: block, - }, - }); - }; const handleOrderChange = (result: DropResult) => { if (!blocks) return; @@ -89,59 +70,23 @@ export const ProjectViewGanttSidebar: React.FC = (props) => { > <> {blocks ? ( - blocks.map((block, index) => { - const duration = findTotalDaysInRange(block.start_date, block.target_date); - - return ( - - {(provided, snapshot) => ( -
updateActiveBlock(block)} - onMouseLeave={() => updateActiveBlock(null)} - ref={provided.innerRef} - {...provided.draggableProps} - > -
- {enableReorder && ( - - )} -
-
- -
- {duration !== undefined && ( -
- {duration} day{duration > 1 ? "s" : ""} -
- )} -
-
-
- )} -
- ); - }) + blocks.map((block, index) => ( + + {(provided, snapshot) => ( + + )} + + )) ) : ( diff --git a/web/components/gantt-chart/types/index.ts b/web/components/gantt-chart/types/index.ts index 1360f9f45..6268e4363 100644 --- a/web/components/gantt-chart/types/index.ts +++ b/web/components/gantt-chart/types/index.ts @@ -1,10 +1,3 @@ -// context types -export type allViewsType = { - key: string; - title: string; - data: Object | null; -}; - export interface IGanttBlock { data: any; id: string; @@ -29,34 +22,6 @@ export interface IBlockUpdateData { export type TGanttViews = "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year"; -export interface ChartContextData { - allViews: allViewsType[]; - currentView: TGanttViews; - currentViewData: ChartDataType | undefined; - renderView: any; - activeBlock: IGanttBlock | null; -} - -export type ChartContextActionPayload = - | { - type: "CURRENT_VIEW"; - payload: TGanttViews; - } - | { - type: "CURRENT_VIEW_DATA" | "RENDER_VIEW"; - payload: ChartDataType | undefined; - } - | { - type: "PARTIAL_UPDATE"; - payload: Partial; - }; - -export interface ChartContextReducer extends ChartContextData { - scrollLeft: number; - updateScrollLeft: (scrollLeft: number) => void; - dispatch: (action: ChartContextActionPayload) => void; -} - // chart render types export interface WeekMonthDataType { key: number; diff --git a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx index b5f092aba..ec33872eb 100644 --- a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -5,12 +5,9 @@ import { observer } from "mobx-react-lite"; import { useIssues, useUser } from "hooks/store"; // components import { GanttQuickAddIssueForm, IssueGanttBlock } from "components/issues"; -import { - GanttChartRoot, - IBlockUpdateData, - renderIssueBlocksStructure, - IssueGanttSidebar, -} from "components/gantt-chart"; +import { GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "components/gantt-chart"; +// helpers +import { renderIssueBlocksStructure } from "helpers/issue.helper"; // types import { TIssue, TUnGroupedIssues } from "@plane/types"; import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx index 1ecc3974d..3d7468f24 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-item.tsx @@ -235,7 +235,7 @@ export const ModuleListItem: React.FC = observer((props) => { ))} - + {isEditingAllowed && ( <> diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index 2dd165f65..789b624e7 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -3,6 +3,7 @@ import { v4 as uuidv4 } from "uuid"; import { orderArrayBy } from "helpers/array.helper"; // types import { TIssue, TIssueGroupByOptions, TIssueLayouts, TIssueOrderByOptions, TIssueParams } from "@plane/types"; +import { IGanttBlock } from "components/gantt-chart"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; @@ -132,3 +133,12 @@ export const createIssuePayload: (projectId: string, formData: Partial) return payload; }; + +export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] => + blocks?.map((block) => ({ + data: block, + id: block.id, + sort_order: block.sort_order, + start_date: block.start_date ? new Date(block.start_date) : null, + target_date: block.target_date ? new Date(block.target_date) : null, + })); diff --git a/web/store/issue/issue_gantt_view.store.ts b/web/store/issue/issue_gantt_view.store.ts new file mode 100644 index 000000000..b087554dd --- /dev/null +++ b/web/store/issue/issue_gantt_view.store.ts @@ -0,0 +1,95 @@ +import { action, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; +// helpers +import { currentViewDataWithView } from "components/gantt-chart/data"; +// types +import { ChartDataType, TGanttViews } from "components/gantt-chart"; + +export interface IGanttStore { + // observables + currentView: TGanttViews; + currentViewData: ChartDataType | undefined; + activeBlockId: string | null; + renderView: any; + // computed functions + isBlockActive: (blockId: string) => boolean; + // actions + updateCurrentView: (view: TGanttViews) => void; + updateCurrentViewData: (data: ChartDataType | undefined) => void; + updateActiveBlockId: (blockId: string | null) => void; + updateRenderView: (data: any[]) => void; +} + +export class GanttStore implements IGanttStore { + // observables + currentView: TGanttViews = "month"; + currentViewData: ChartDataType | undefined = undefined; + activeBlockId: string | null = null; + renderView: any[] = []; + + constructor() { + makeObservable(this, { + // observables + currentView: observable.ref, + currentViewData: observable, + activeBlockId: observable.ref, + renderView: observable, + // actions + updateCurrentView: action.bound, + updateCurrentViewData: action.bound, + updateActiveBlockId: action.bound, + updateRenderView: action.bound, + }); + + this.initGantt(); + } + + /** + * @description check if block is active + * @param {string} blockId + */ + isBlockActive = computedFn((blockId: string): boolean => this.activeBlockId === blockId); + + /** + * @description update current view + * @param {TGanttViews} view + */ + updateCurrentView = (view: TGanttViews) => { + this.currentView = view; + }; + + /** + * @description update current view data + * @param {ChartDataType | undefined} data + */ + updateCurrentViewData = (data: ChartDataType | undefined) => { + this.currentViewData = data; + }; + + /** + * @description update active block + * @param {string | null} block + */ + updateActiveBlockId = (blockId: string | null) => { + this.activeBlockId = blockId; + }; + + /** + * @description update render view + * @param {any[]} data + */ + updateRenderView = (data: any[]) => { + this.renderView = data; + }; + + /** + * @description initialize gantt chart with month view + */ + initGantt = () => { + const newCurrentViewData = currentViewDataWithView(this.currentView); + + runInAction(() => { + this.currentViewData = newCurrentViewData; + }); + }; +} diff --git a/web/store/module.store.ts b/web/store/module.store.ts index c27ace487..cd6b7100a 100644 --- a/web/store/module.store.ts +++ b/web/store/module.store.ts @@ -194,7 +194,6 @@ export class ModulesStore implements IModuleStore { runInAction(() => { set(this.moduleMap, [response?.id], response); }); - this.fetchModules(workspaceSlug, projectId); return response; }); From 33c99ded772550355560f6bd109406debe55f1e0 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:15:59 +0530 Subject: [PATCH 19/67] fix: due date highlight logic (#3763) --- .../issues/issue-detail/sidebar.tsx | 10 +++--- .../properties/all-properties.tsx | 13 ++++---- .../spreadsheet/columns/due-date-column.tsx | 14 ++++++--- .../issues/peek-overview/properties.tsx | 10 +++--- web/helpers/issue.helper.ts | 31 ++++++++++++++++++- 5 files changed, 56 insertions(+), 22 deletions(-) diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx index 854550b0b..eb12250e0 100644 --- a/web/components/issues/issue-detail/sidebar.tsx +++ b/web/components/issues/issue-detail/sidebar.tsx @@ -1,7 +1,6 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { differenceInCalendarDays } from "date-fns"; import { LinkIcon, Signal, @@ -15,7 +14,7 @@ import { CalendarDays, } from "lucide-react"; // hooks -import { useEstimate, useIssueDetail, useProject, useUser } from "hooks/store"; +import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { @@ -41,6 +40,7 @@ import { ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon, UserGroupIcon } import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; import { cn } from "helpers/common.helper"; +import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; // types import type { TIssueOperations } from "./root"; @@ -65,6 +65,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const { issue: { getIssueById }, } = useIssueDetail(); + const { getStateById } = useProjectState(); // states const [deleteIssueModal, setDeleteIssueModal] = useState(false); @@ -83,6 +84,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { }; const projectDetails = issue ? getProjectById(issue.project_id) : null; + const stateDetails = getStateById(issue.state_id); const minDate = issue.start_date ? new Date(issue.start_date) : null; minDate?.setDate(minDate.getDate()); @@ -90,8 +92,6 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const maxDate = issue.target_date ? new Date(issue.target_date) : null; maxDate?.setDate(maxDate.getDate()); - const targetDateDistance = issue.target_date ? differenceInCalendarDays(new Date(issue.target_date), new Date()) : 1; - return ( <> {workspaceSlug && projectId && issue && ( @@ -242,7 +242,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { buttonContainerClassName="w-full text-left" buttonClassName={cn("text-sm", { "text-custom-text-400": !issue.target_date, - "text-red-500": targetDateDistance <= 0, + "text-red-500": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group), })} hideIcon clearIconClassName="h-3 w-3 hidden group-hover:inline !text-custom-text-100" diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index 7ef9aace8..7c8f638ff 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -1,11 +1,10 @@ import { useCallback, useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { differenceInCalendarDays } from "date-fns"; import { Layers, Link, Paperclip } from "lucide-react"; import xor from "lodash/xor"; // hooks -import { useEventTracker, useEstimate, useLabel, useIssues } from "hooks/store"; +import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store"; // components import { IssuePropertyLabels } from "../properties/labels"; import { Tooltip } from "@plane/ui"; @@ -21,6 +20,7 @@ import { } from "components/dropdowns"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; import { cn } from "helpers/common.helper"; // types import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; @@ -48,11 +48,14 @@ export const IssueProperties: React.FC = observer((props) => { const { issues: { addIssueToCycle, removeIssueFromCycle }, } = useIssues(EIssuesStoreType.CYCLE); + const { areEstimatesEnabledForCurrentProject } = useEstimate(); + const { getStateById } = useProjectState(); // router const router = useRouter(); const { workspaceSlug, cycleId, moduleId } = router.query; - const { areEstimatesEnabledForCurrentProject } = useEstimate(); const currentLayout = `${activeLayout} layout`; + // derived values + const stateDetails = getStateById(issue.state_id); const issueOperations = useMemo( () => ({ @@ -232,8 +235,6 @@ export const IssueProperties: React.FC = observer((props) => { const maxDate = issue.target_date ? new Date(issue.target_date) : null; maxDate?.setDate(maxDate.getDate()); - const targetDateDistance = issue.target_date ? differenceInCalendarDays(new Date(issue.target_date), new Date()) : 1; - return (
{/* basic properties */} @@ -301,7 +302,7 @@ export const IssueProperties: React.FC = observer((props) => { minDate={minDate ?? undefined} placeholder="Due date" buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} - buttonClassName={targetDateDistance <= 0 ? "text-red-500" : ""} + buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""} clearIconClassName="!text-custom-text-100" disabled={isReadOnly} showTooltip diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx index e07500c03..ebed73b76 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx @@ -1,13 +1,15 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import differenceInCalendarDays from "date-fns/differenceInCalendarDays"; +// hooks +import { useProjectState } from "hooks/store"; // components import { DateDropdown } from "components/dropdowns"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; +import { cn } from "helpers/common.helper"; // types import { TIssue } from "@plane/types"; -import { cn } from "helpers/common.helper"; type Props = { issue: TIssue; @@ -18,8 +20,10 @@ type Props = { export const SpreadsheetDueDateColumn: React.FC = observer((props: Props) => { const { issue, onChange, disabled, onClose } = props; - - const targetDateDistance = issue.target_date ? differenceInCalendarDays(new Date(issue.target_date), new Date()) : 1; + // store hooks + const { getStateById } = useProjectState(); + // derived values + const stateDetails = getStateById(issue.state_id); return (
@@ -42,7 +46,7 @@ export const SpreadsheetDueDateColumn: React.FC = observer((props: Props) buttonVariant="transparent-with-text" buttonContainerClassName="w-full" buttonClassName={cn("rounded-none text-left", { - "text-red-500": targetDateDistance <= 0, + "text-red-500": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group), })} clearIconClassName="!text-custom-text-100" onClose={onClose} diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index c669c0349..2b428a57b 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -1,9 +1,8 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { differenceInCalendarDays } from "date-fns"; import { Signal, Tag, Triangle, LayoutPanelTop, CircleDot, CopyPlus, XCircle, CalendarDays } from "lucide-react"; // hooks -import { useIssueDetail, useProject } from "hooks/store"; +import { useIssueDetail, useProject, useProjectState } from "hooks/store"; // ui icons import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon, RelatedIcon } from "@plane/ui"; import { @@ -26,6 +25,7 @@ import { import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // helpers import { cn } from "helpers/common.helper"; +import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; interface IPeekOverviewProperties { workspaceSlug: string; @@ -42,11 +42,13 @@ export const PeekOverviewProperties: FC = observer((pro const { issue: { getIssueById }, } = useIssueDetail(); + const { getStateById } = useProjectState(); // derived values const issue = getIssueById(issueId); if (!issue) return <>; const projectDetails = getProjectById(issue.project_id); const isEstimateEnabled = projectDetails?.estimate; + const stateDetails = getStateById(issue.state_id); const minDate = issue.start_date ? new Date(issue.start_date) : null; minDate?.setDate(minDate.getDate()); @@ -54,8 +56,6 @@ export const PeekOverviewProperties: FC = observer((pro const maxDate = issue.target_date ? new Date(issue.target_date) : null; maxDate?.setDate(maxDate.getDate()); - const targetDateDistance = issue.target_date ? differenceInCalendarDays(new Date(issue.target_date), new Date()) : 1; - return (
Properties
@@ -169,7 +169,7 @@ export const PeekOverviewProperties: FC = observer((pro buttonContainerClassName="w-full text-left" buttonClassName={cn("text-sm", { "text-custom-text-400": !issue.target_date, - "text-red-500": targetDateDistance <= 0, + "text-red-500": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group), })} hideIcon clearIconClassName="h-3 w-3 hidden group-hover:inline !text-custom-text-100" diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index 789b624e7..831cb321e 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -1,11 +1,20 @@ import { v4 as uuidv4 } from "uuid"; +import differenceInCalendarDays from "date-fns/differenceInCalendarDays"; // helpers import { orderArrayBy } from "helpers/array.helper"; // types -import { TIssue, TIssueGroupByOptions, TIssueLayouts, TIssueOrderByOptions, TIssueParams } from "@plane/types"; +import { + TIssue, + TIssueGroupByOptions, + TIssueLayouts, + TIssueOrderByOptions, + TIssueParams, + TStateGroups, +} from "@plane/types"; import { IGanttBlock } from "components/gantt-chart"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { STATE_GROUPS } from "constants/state"; type THandleIssuesMutation = ( formData: Partial, @@ -134,6 +143,26 @@ export const createIssuePayload: (projectId: string, formData: Partial) return payload; }; +/** + * @description check if the issue due date should be highlighted + * @param date + * @param stateGroup + * @returns boolean + */ +export const shouldHighlightIssueDueDate = ( + date: string | Date | null, + stateGroup: TStateGroups | undefined +): boolean => { + if (!date || !stateGroup) return false; + // if the issue is completed or cancelled, don't highlight the due date + if ([STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateGroup)) return false; + + const parsedDate = new Date(date); + const targetDateDistance = differenceInCalendarDays(parsedDate, new Date()); + + // if the issue is overdue, highlight the due date + return targetDateDistance <= 0; +}; export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] => blocks?.map((block) => ({ data: block, From d6a32ef75d1236e21dcbc1046f21fddaffd55a4e Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 23 Feb 2024 21:47:25 +0530 Subject: [PATCH 20/67] dev: fix sentry profiling rate (#3785) --- apiserver/plane/settings/common.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index f03209250..5c8947e73 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -1,4 +1,5 @@ """Global Settings""" + # Python imports import os import ssl @@ -307,7 +308,9 @@ if bool(os.environ.get("SENTRY_DSN", False)) and os.environ.get( traces_sample_rate=1, send_default_pii=True, environment=os.environ.get("SENTRY_ENVIRONMENT", "development"), - profiles_sample_rate=1.0, + profiles_sample_rate=float( + os.environ.get("SENTRY_PROFILE_SAMPLE_RATE", 0.5) + ), ) From 9c1d49616567b534a19566d49443912f2e8c8262 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 21:47:57 +0530 Subject: [PATCH 21/67] chore(deps): bump cryptography in /apiserver/requirements (#3784) Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.0 to 42.0.4. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/42.0.0...42.0.4) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apiserver/requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 194bf8d90..eb0f54201 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -30,7 +30,7 @@ openpyxl==3.1.2 beautifulsoup4==4.12.2 dj-database-url==2.1.0 posthog==3.0.2 -cryptography==42.0.0 +cryptography==42.0.4 lxml==4.9.3 boto3==1.28.40 From 812df59d1d83891f58d44d52a9c0519c71ccfb3d Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Sat, 24 Feb 2024 15:45:17 +0530 Subject: [PATCH 22/67] fix: scroll container height (#3783) --- web/components/gantt-chart/chart/main-content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/gantt-chart/chart/main-content.tsx b/web/components/gantt-chart/chart/main-content.tsx index 0f7320986..8a1c5de26 100644 --- a/web/components/gantt-chart/chart/main-content.tsx +++ b/web/components/gantt-chart/chart/main-content.tsx @@ -111,7 +111,7 @@ export const GanttChartMainContent: React.FC = observer((props) => { title={title} quickAdd={quickAdd} /> -
+
{currentViewData && ( Date: Sun, 25 Feb 2024 22:35:42 +0530 Subject: [PATCH 23/67] fix: cycle select dropdown issue layout overflow fix (#3789) --- web/components/dropdowns/cycle.tsx | 2 +- .../issues/issue-layouts/properties/all-properties.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/dropdowns/cycle.tsx b/web/components/dropdowns/cycle.tsx index 0104c3c1f..09b0acde5 100644 --- a/web/components/dropdowns/cycle.tsx +++ b/web/components/dropdowns/cycle.tsx @@ -204,7 +204,7 @@ export const CycleDropdown: React.FC = observer((props) => { > {!hideIcon && } {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( - {selectedCycle?.name ?? placeholder} + {selectedCycle?.name ?? placeholder} )} {dropdownArrow && (