From beb3b84be530ce89136469c6a40dadbf73b1d25a Mon Sep 17 00:00:00 2001 From: Loc Nguyen Date: Sat, 18 May 2024 15:09:03 +0700 Subject: [PATCH 1/2] Split out renderQuickActions for BaseCalendarRoot --- .../calendar/base-calendar-root.tsx | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 1b8235af8be..6934080ed3e 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -13,7 +13,7 @@ import { useIssues, useUser } from "@/hooks/store"; import { useIssuesActions } from "@/hooks/use-issues-actions"; // ui // types -import { IQuickActionProps } from "../list/list-view-types"; +import { IQuickActionProps, TRenderQuickActions } from "../list/list-view-types"; import { handleDragDrop } from "./utils"; type CalendarStoreType = @@ -79,6 +79,21 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { } }; + const renderQuickActions: TRenderQuickActions = ({ issue, parentRef, customActionButton, placement }) => ( + removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} + readOnly={!isEditingAllowed || isCompletedCycle} + placements={placement} + /> + ); + return ( <>
@@ -89,22 +104,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { groupedIssueIds={groupedIssueIds} layout={displayFilters?.calendar?.layout} showWeekends={displayFilters?.calendar?.show_weekends ?? false} - quickActions={({ issue, parentRef, customActionButton, placement }) => ( - removeIssue(issue.project_id, issue.id)} - handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} - handleRemoveFromView={async () => - removeIssueFromView && removeIssueFromView(issue.project_id, issue.id) - } - handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} - handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} - readOnly={!isEditingAllowed || isCompletedCycle} - placements={placement} - /> - )} + quickActions={renderQuickActions} addIssuesToView={addIssuesToView} quickAddCallback={issues.quickAddIssue} viewId={viewId} From 8bfdd948b2877746e192d152fd1370d161a56818 Mon Sep 17 00:00:00 2001 From: Loc Nguyen Date: Sat, 18 May 2024 15:12:27 +0700 Subject: [PATCH 2/2] Add Calendar layout to Global Issues page --- web/components/headers/global-issues.tsx | 27 +++- .../issue-layouts/calendar/calendar-view.tsx | 151 ++++++++++++++++++ .../issues/issue-layouts/calendar/index.ts | 1 + .../roots/all-issue-layout-root.tsx | 139 ++++++++++------ web/constants/issue.ts | 21 +++ web/hooks/use-issues-actions.tsx | 1 + web/store/issue/workspace/filter.store.ts | 11 +- web/store/issue/workspace/issue.store.ts | 12 +- 8 files changed, 298 insertions(+), 65 deletions(-) create mode 100644 web/components/issues/issue-layouts/calendar/calendar-view.tsx diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx index b936c079356..bd882747ccd 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/components/headers/global-issues.tsx @@ -4,12 +4,12 @@ import { useRouter } from "next/router"; // icons import { PlusIcon } from "lucide-react"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // ui import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; // components import { BreadcrumbLink } from "@/components/common"; -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; import { CreateUpdateWorkspaceViewModal } from "@/components/workspace"; // constants import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; @@ -22,7 +22,7 @@ export const GlobalIssuesHeader: React.FC = observer(() => { const [createViewModal, setCreateViewModal] = useState(false); // router const router = useRouter(); - const { workspaceSlug, globalViewId } = router.query; + const { workspaceSlug, globalViewId, projectId } = router.query; // store hooks const { issuesFilter: { filters, updateFilters }, @@ -36,6 +36,22 @@ export const GlobalIssuesHeader: React.FC = observer(() => { } = useMember(); const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined; + const activeLayout = issueFilters?.displayFilters?.layout; + + const handleLayoutChange = useCallback( + (layout: TIssueLayouts) => { + // (updatedDisplayFilter: Partial) => { + if (!workspaceSlug) return; + updateFilters( + workspaceSlug.toString(), + undefined, + EIssueFilterType.DISPLAY_FILTERS, + { layout: layout }, + globalViewId.toString(), + ); + }, + [workspaceSlug, updateFilters, globalViewId] + ); const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { @@ -110,6 +126,11 @@ export const GlobalIssuesHeader: React.FC = observer(() => {
<> + handleLayoutChange(layout)} + selectedLayout={activeLayout} + /> ) => void; + // issueIds: string[] | undefined; + // quickActions: TRenderQuickActions; + // updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + // openIssuesListModal?: (() => void) | null; + // quickAddCallback?: ( + // workspaceSlug: string, + // projectId: string, + // data: TIssue, + // viewId?: string + // ) => Promise; + // viewId?: string; + // canEditProperties: (projectId: string | undefined) => boolean; + // enableQuickCreateIssue?: boolean; + // disableIssueCreation?: boolean; + // isWorkspaceLevel?: boolean; + + // issues + issuesFilterStore: IWorkspaceIssuesFilter, + issues: TIssueMap, + // issueIds: string[], + groupedIssueIds: TGroupedIssues, + // Layout + // layout: "month" | "week" | undefined; + // showWeekends: boolean; + // handlers + quickActions: TRenderQuickActions; + quickAddIssues?: ( + // quickAddCallback?: ( + workspaceSlug: string, + projectId: string, + data: TIssue, + viewId?: string + ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; + // handleDisplayFilterUpdate?: ( + // projectId: string, + // filterType: EIssueFilterType, + // filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + // ) => Promise; + // updateIssue?: (projectId: string, issueId: string, data: Partial) => Promise; + viewId?: string; + readOnly?: boolean; + updateFilters?: ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => Promise + isWorkspaceLevel?: boolean; + // handleDragDrop?: ( + // source: DraggableLocation, + // destination: DraggableLocation, + // workspaceSlug: string | undefined, + // projectId: string | undefined, + // issueMap: IIssueMap, + // issueWithIds: TGroupedIssues, + // updateIssue?: ((projectId: string, issueId: string, data: TIssue) => Promise) | undefined + // ) => Promise +}; + +export const CalendarView: React.FC = observer((props: Props) => { + const { + issuesFilterStore, + issues, + groupedIssueIds, + // layout, + // showWeekends, + quickActions, + quickAddIssues, + // updateIssue, + addIssuesToView, + viewId, + readOnly, + updateFilters, + isWorkspaceLevel = true + } = props; + // refs + const containerRef = useRef(null); + const portalRef = useRef(null); + + const { currentProjectDetails } = useProject(); + + const isEstimateEnabled: boolean = currentProjectDetails?.estimate !== null; + + const displayFilters = issuesFilterStore.issueFilters.displayFilters; + console.log("CalendarView.displayFilters", displayFilters) + + const onDragEnd = async (result: DropResult) => { + if (!result) return; + + // return if not dropped on the correct place + if (!result.destination) return; + + // return if dropped on the same date + if (result.destination.droppableId === result.source.droppableId) return; + + if (handleDragDrop) { + await handleDragDrop + } + }; + + return ( +
+
+ + + +
+
+ ); +}); diff --git a/web/components/issues/issue-layouts/calendar/index.ts b/web/components/issues/issue-layouts/calendar/index.ts index 527a9eff481..87f3f76a41a 100644 --- a/web/components/issues/issue-layouts/calendar/index.ts +++ b/web/components/issues/issue-layouts/calendar/index.ts @@ -1,5 +1,6 @@ export * from "./dropdowns"; export * from "./roots"; +export * from "./calendar-view"; export * from "./calendar"; export * from "./types.d"; export * from "./day-tile"; diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 8c185811b7c..2988109846c 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -3,20 +3,21 @@ import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { IIssueDisplayFilterOptions } from "@plane/types"; // hooks // components import { EmptyState } from "@/components/empty-state"; import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "@/components/issues"; -import { SpreadsheetView } from "@/components/issues/issue-layouts"; +import { GanttLayout, KanBanLayout, CalendarLayout, SpreadsheetView, CalendarView, AllIssueCalendarLayout } from "@/components/issues/issue-layouts"; import { AllIssueQuickActions } from "@/components/issues/issue-layouts/quick-action-dropdowns"; -import { SpreadsheetLayoutLoader } from "@/components/ui"; +import { ActiveLoader, SpreadsheetLayoutLoader } from "@/components/ui"; // types +import { IIssueDisplayFilterOptions, TGroupedIssues } from "@plane/types"; // constants import { EMPTY_STATE_DETAILS, EmptyStateType } from "@/constants/empty-state"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; import { EUserProjectRoles } from "@/constants/project"; -import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "@/hooks/store"; +import { useApplication, useEventTracker, useGlobalView, useProject, useUser } from "@/hooks/store"; +import { useIssues } from "@/hooks/store/use-issues"; import { useIssuesActions } from "@/hooks/use-issues-actions"; import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; import { TRenderQuickActions } from "../list/list-view-types"; @@ -30,10 +31,11 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { // store const { commandPalette: commandPaletteStore } = useApplication(); const { - issuesFilter: { filters, fetchFilters, updateFilters }, - issues: { loader, groupedIssueIds, fetchIssues }, + issuesFilter, //: { filters, fetchFilters, updateFilters }, + issues: { loader, groupedIssueIds, quickAddIssue, fetchIssues }, + issueMap } = useIssues(EIssuesStoreType.GLOBAL); - const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL); + const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } = useIssuesActions(EIssuesStoreType.GLOBAL); const { dataViewId, issueIds } = groupedIssueIds; const { @@ -46,6 +48,9 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(groupedIssueIds.dataViewId); const currentView = isDefaultView ? groupedIssueIds.dataViewId : "custom-view"; + const issueFilters = globalViewId ? issuesFilter.filters?.[globalViewId.toString()] : undefined; + const activeLayout = issueFilters?.displayFilters?.layout; + // filter init from the query params const routerFilterParams = () => { @@ -58,16 +63,23 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { Object.keys(routeFilters).forEach((key) => { const filterKey: any = key; const filterValue = routeFilters[key]?.toString() || undefined; - if ( - ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet.filters.includes(filterKey) && - filterKey && - filterValue - ) - issueFilters = { ...issueFilters, [filterKey]: filterValue.split(",") }; + if (filterKey && filterValue) { + if ( + ((activeLayout === "spreadsheet") && ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet.filters.includes(filterKey)) + ) { + issueFilters = { ...issueFilters, [filterKey]: filterValue.split(",") }; + } + else if ((activeLayout === "calendar") && ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.calendar.filters.includes(filterKey)) { + issueFilters = { ...issueFilters, [filterKey]: filterValue.split(",") }; + } + else { + console.warn("Could not find filterKey in ISSUE_DISPLAY_FILTERS_BY_LAYOUT"); + } + } }); if (!isEmpty(routeFilters)) - updateFilters( + issuesFilter.updateFilters( workspaceSlug.toString(), undefined, EIssueFilterType.FILTERS, @@ -92,8 +104,12 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { async () => { if (workspaceSlug && globalViewId) { await fetchAllGlobalViews(workspaceSlug.toString()); - await fetchFilters(workspaceSlug.toString(), globalViewId.toString()); - await fetchIssues(workspaceSlug.toString(), globalViewId.toString(), issueIds ? "mutation" : "init-loader"); + await issuesFilter.fetchFilters(workspaceSlug.toString(), globalViewId.toString()); + await fetchIssues( + workspaceSlug.toString(), + globalViewId.toString(), + groupedIssueIds ? "mutation" : "init-loader" + ); routerFilterParams(); } }, @@ -111,13 +127,11 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { [currentWorkspaceAllProjectsRole] ); - const issueFilters = globalViewId ? filters?.[globalViewId.toString()] : undefined; - const handleDisplayFiltersUpdate = useCallback( (updatedDisplayFilter: Partial) => { if (!workspaceSlug || !globalViewId) return; - updateFilters( + issuesFilter.updateFilters( workspaceSlug.toString(), undefined, EIssueFilterType.DISPLAY_FILTERS, @@ -125,28 +139,28 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { globalViewId.toString() ); }, - [updateFilters, workspaceSlug, globalViewId] + [issuesFilter.updateFilters, workspaceSlug, globalViewId] ); - const renderQuickActions: TRenderQuickActions = useCallback( - ({ issue, parentRef, customActionButton, placement, portalElement }) => ( - removeIssue(issue.project_id, issue.id)} - handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} - handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} - portalElement={portalElement} - readOnly={!canEditProperties(issue.project_id)} - placements={placement} - /> - ), - [canEditProperties, removeIssue, updateIssue, archiveIssue] + const renderQuickActions: TRenderQuickActions = ({ issue, parentRef, customActionButton, placement }) => ( + // removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} + readOnly={!canEditProperties} + placements={placement} + /> ); if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) { - return ; + // return ; + return <>{activeLayout && }; } const emptyStateType = @@ -164,29 +178,48 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { (workspaceProjectIds ?? []).length > 0 ? currentView !== "custom-view" && currentView !== "subscribed" ? () => { - setTrackElement("All issues empty state"); - commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - } - : undefined - : () => { setTrackElement("All issues empty state"); - commandPaletteStore.toggleCreateProjectModal(true); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); } + : undefined + : () => { + setTrackElement("All issues empty state"); + commandPaletteStore.toggleCreateProjectModal(true); + } } /> ) : ( - +
+ {activeLayout === "calendar" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
{/* peek overview */}
diff --git a/web/constants/issue.ts b/web/constants/issue.ts index 9ec163935ef..d33588fb512 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -262,6 +262,27 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { values: ["sub_issue"], }, }, + calendar: { + filters: [ + "priority", + "state_group", + "labels", + "assignees", + "created_by", + "subscriber", + "project", + "start_date", + "target_date", + ], + display_properties: true, + display_filters: { + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["sub_issue"], + }, + }, list: { filters: [ "priority", diff --git a/web/hooks/use-issues-actions.tsx b/web/hooks/use-issues-actions.tsx index f77513f2134..b7fe4e4cf44 100644 --- a/web/hooks/use-issues-actions.tsx +++ b/web/hooks/use-issues-actions.tsx @@ -558,6 +558,7 @@ const useGlobalIssueActions = () => { filterType: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters ) => { + // TODO: Cannot change layout yet, maybe because of this? if (!globalViewId || !workspaceSlug) return; return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, globalViewId); }, diff --git a/web/store/issue/workspace/filter.store.ts b/web/store/issue/workspace/filter.store.ts index 101c3f0c74e..edbf4d60411 100644 --- a/web/store/issue/workspace/filter.store.ts +++ b/web/store/issue/workspace/filter.store.ts @@ -37,7 +37,7 @@ export interface IWorkspaceIssuesFilter { projectId: string | undefined, filterType: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters, - viewId: string + viewId: string, ) => Promise; //helper action getIssueFilters: (viewId: string | undefined) => IIssueFilters | undefined; @@ -123,7 +123,7 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo }; const _filters = this.handleIssuesLocalFilters.get(EIssuesStoreType.GLOBAL, workspaceSlug, undefined, viewId); - displayFilters = this.computedDisplayFilters(_filters?.display_filters, { layout: "spreadsheet" }); + displayFilters = this.computedDisplayFilters(_filters?.display_filters); // TODO: Is this wrong? displayProperties = this.computedDisplayProperties(_filters?.display_properties); kanbanFilters = { group_by: _filters?.kanban_filters?.group_by || [], @@ -224,15 +224,16 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo if (this.requiresServerUpdate(updatedDisplayFilters)) this.rootIssueStore.workspaceIssues.fetchIssues(workspaceSlug, viewId, "mutation"); - if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) + if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) { this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, { display_filters: _filters.displayFilters, }); - else + } + else { await this.issueFilterService.updateView(workspaceSlug, viewId, { display_filters: _filters.displayFilters, }); - + } break; case EIssueFilterType.DISPLAY_PROPERTIES: const updatedDisplayProperties = filters as IIssueDisplayProperties; diff --git a/web/store/issue/workspace/issue.store.ts b/web/store/issue/workspace/issue.store.ts index ad77ec0617b..b23e86e399b 100644 --- a/web/store/issue/workspace/issue.store.ts +++ b/web/store/issue/workspace/issue.store.ts @@ -4,7 +4,7 @@ import { action, observable, makeObservable, computed, runInAction } from "mobx" // base class import { IssueService, IssueArchiveService } from "@/services/issue"; import { WorkspaceService } from "@/services/workspace.service"; -import { TIssue, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; +import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services // types @@ -16,7 +16,7 @@ export interface IWorkspaceIssues { issues: { [viewId: string]: string[] }; viewFlags: ViewFlags; // computed - groupedIssueIds: { dataViewId: string; issueIds: TUnGroupedIssues | undefined }; + groupedIssueIds: { dataViewId: string; issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined }; // actions fetchIssues: (workspaceSlug: string, viewId: string, loadType: TLoader) => Promise; createIssue: ( @@ -93,7 +93,10 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue const displayFilters = this.rootIssueStore?.workspaceIssuesFilter?.filters?.[viewId]?.displayFilters; if (!displayFilters) return { dataViewId: viewId, issueIds: undefined }; + const subGroupBy = displayFilters?.sub_group_by; + const groupBy = displayFilters?.group_by; const orderBy = displayFilters?.order_by; + const layout = displayFilters?.layout; const viewIssueIds = this.issues[uniqueViewId]; @@ -104,7 +107,8 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue let issueIds: TIssue | TUnGroupedIssues | undefined = undefined; - issueIds = this.unGroupedIssues(orderBy ?? "-created_at", _issues); + if (layout === "calendar") issueIds = this.groupedIssues("target_date", "target_date", _issues, true); + else if (layout === "spreadsheet") issueIds = this.unGroupedIssues(orderBy ?? "-created_at", _issues); return { dataViewId: viewId, issueIds }; } @@ -131,7 +135,7 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue return response; } catch (error) { - console.error(error); + console.error(`workspaceIssues.fetchIssues: ${error}`); this.loader = undefined; throw error; }