diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index 2a7e9d02179..edce172f9c0 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -944,9 +944,33 @@ class IssueDetailEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id): filters = issue_filters(request.query_params, "GET") + + # check for the project member role, if the role is 5 then check for the guest_view_all_features + # if it is true then show all the issues else show only the issues created by the user + project_member_subquery = ProjectMember.objects.filter( + project_id=OuterRef("project_id"), + member=self.request.user, + is_active=True, + ).filter( + Q(role__gt=ROLE.GUEST.value) + | Q( + role=ROLE.GUEST.value, project__guest_view_all_features=True + ) + ) + + # Main issue query issue = ( Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id) - .select_related("workspace", "project", "state", "parent") + .filter( + Q(Exists(project_member_subquery)) + | Q( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + project__project_projectmember__role=ROLE.GUEST.value, + project__guest_view_all_features=False, + created_by=self.request.user, + ) + ) .prefetch_related("assignees", "labels", "issue_module__module") .annotate( cycle_id=Subquery( @@ -1014,6 +1038,7 @@ def get(self, request, slug, project_id): .values("count") ) ) + issue = issue.filter(**filters) order_by_param = request.GET.get("order_by", "-created_at") # Issue queryset diff --git a/packages/types/src/layout/gantt.d.ts b/packages/types/src/layout/gantt.d.ts index ad5b2afde9e..990ae3fc3f7 100644 --- a/packages/types/src/layout/gantt.d.ts +++ b/packages/types/src/layout/gantt.d.ts @@ -9,6 +9,7 @@ export interface IGanttBlock { sort_order: number | undefined; start_date: string | undefined; target_date: string | undefined; + project_id: string | undefined; } export interface IBlockUpdateData { @@ -25,6 +26,7 @@ export interface IBlockUpdateDependencyData { id: string; start_date?: string; target_date?: string; + project_id?: string; } export type TGanttViews = "week" | "month" | "quarter"; diff --git a/packages/utils/src/work-item/base.ts b/packages/utils/src/work-item/base.ts index 9c37605e02a..6a706892877 100644 --- a/packages/utils/src/work-item/base.ts +++ b/packages/utils/src/work-item/base.ts @@ -185,6 +185,7 @@ export const getIssueBlocksStructure = (block: TIssue): IGanttBlock => ({ sort_order: block?.sort_order, start_date: block?.start_date ?? undefined, target_date: block?.target_date ?? undefined, + project_id: block?.project_id ?? undefined, }); export const formatTextList = (TextArray: string[]): string => { @@ -260,7 +261,7 @@ export const getComputedDisplayFilters = ( displayFilters: IIssueDisplayFilterOptions = {}, defaultValues?: IIssueDisplayFilterOptions ): IIssueDisplayFilterOptions => { - const filters = displayFilters || defaultValues; + const filters = !isEmpty(displayFilters) ? displayFilters : defaultValues; return { calendar: { diff --git a/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx index 1819ac0ee08..f5ba4ecb990 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx @@ -1,11 +1,11 @@ "use client"; -import { useCallback, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { Layers } from "lucide-react"; // plane constants -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; +import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; @@ -19,6 +19,7 @@ import { CreateUpdateWorkspaceViewModal } from "@/components/workspace"; // helpers // hooks import { useLabel, useMember, useIssues, useGlobalView } from "@/hooks/store"; +import { GlobalViewLayoutSelection } from "@/plane-web/components/views/helper"; export const GlobalIssuesHeader = observer(() => { // states @@ -38,6 +39,7 @@ export const GlobalIssuesHeader = observer(() => { const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined; + const activeLayout = issueFilters?.displayFilters?.layout; const viewDetails = getViewDetailsById(globalViewId.toString()); const handleFiltersUpdate = useCallback( @@ -95,8 +97,27 @@ export const GlobalIssuesHeader = observer(() => { [workspaceSlug, updateFilters, globalViewId] ); + const handleLayoutChange = useCallback( + (layout: EIssueLayoutTypes) => { + if (!workspaceSlug || !globalViewId) return; + updateFilters( + workspaceSlug.toString(), + undefined, + EIssueFilterType.DISPLAY_FILTERS, + { layout: layout }, + globalViewId.toString() + ); + }, + [workspaceSlug, updateFilters, globalViewId] + ); + const isLocked = viewDetails?.is_locked; + const currentLayoutFilters = useMemo(() => { + const layout = activeLayout ?? EIssueLayoutTypes.SPREADSHEET; + return ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues[layout]; + }, [activeLayout]); + return ( <> setCreateViewModal(false)} /> @@ -113,13 +134,18 @@ export const GlobalIssuesHeader = observer(() => { {!isLocked ? ( <> + { void; + selectedLayout: EIssueLayoutTypes; + workspaceSlug: string; +}; + +export const GlobalViewLayoutSelection = (props: TLayoutSelectionProps) => <>; + +export const WorkspaceAdditionalLayouts = (props: TWorkspaceLayoutProps) => <>; diff --git a/web/ce/store/issue/workspace/issue.store.ts b/web/ce/store/issue/workspace/issue.store.ts new file mode 100644 index 00000000000..7317da96d3b --- /dev/null +++ b/web/ce/store/issue/workspace/issue.store.ts @@ -0,0 +1 @@ +export * from "@/store/issue/workspace/issue.store"; diff --git a/web/ce/store/timeline/base-timeline.store.ts b/web/ce/store/timeline/base-timeline.store.ts index 57a67cc5ef0..c021fa93ea8 100644 --- a/web/ce/store/timeline/base-timeline.store.ts +++ b/web/ce/store/timeline/base-timeline.store.ts @@ -22,6 +22,7 @@ type BlockData = { sort_order: number | null; start_date?: string | undefined | null; target_date?: string | undefined | null; + project_id?: string | undefined | null; }; export interface IBaseTimelineStore { @@ -194,6 +195,7 @@ export class BaseTimeLineStore implements IBaseTimelineStore { sort_order: blockData?.sort_order ?? undefined, start_date: blockData?.start_date ?? undefined, target_date: blockData?.target_date ?? undefined, + project_id: blockData?.project_id ?? undefined, }; if (this.currentViewData && (this.currentViewData?.data?.startDate || this.currentViewData?.data?.dayWidth)) { block.position = getItemPositionWidth(this.currentViewData, block); diff --git a/web/core/components/gantt-chart/blocks/block.tsx b/web/core/components/gantt-chart/blocks/block.tsx index f459a02af9f..0087a389647 100644 --- a/web/core/components/gantt-chart/blocks/block.tsx +++ b/web/core/components/gantt-chart/blocks/block.tsx @@ -20,6 +20,7 @@ type Props = { enableBlockLeftResize: boolean; enableBlockRightResize: boolean; enableBlockMove: boolean; + enableDependency: boolean; ganttContainerRef: RefObject; updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise; }; @@ -33,6 +34,7 @@ export const GanttChartBlock: React.FC = observer((props) => { enableBlockRightResize, enableBlockMove, ganttContainerRef, + enableDependency, updateBlockDates, } = props; // store hooks @@ -90,6 +92,7 @@ export const GanttChartBlock: React.FC = observer((props) => { enableBlockLeftResize={enableBlockLeftResize} enableBlockRightResize={enableBlockRightResize} enableBlockMove={enableBlockMove && !!isBlockComplete} + enableDependency={enableDependency} isMoving={isMoving} ganttContainerRef={ganttContainerRef} /> diff --git a/web/core/components/gantt-chart/blocks/blocks-list.tsx b/web/core/components/gantt-chart/blocks/blocks-list.tsx index c8644b465ed..154a72d9f59 100644 --- a/web/core/components/gantt-chart/blocks/blocks-list.tsx +++ b/web/core/components/gantt-chart/blocks/blocks-list.tsx @@ -12,6 +12,7 @@ export type GanttChartBlocksProps = { ganttContainerRef: React.RefObject; showAllBlocks: boolean; updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise; + enableDependency: boolean | ((blockId: string) => boolean); }; export const GanttChartBlocksList: FC = (props) => { @@ -24,6 +25,7 @@ export const GanttChartBlocksList: FC = (props) => { ganttContainerRef, showAllBlocks, updateBlockDates, + enableDependency, } = props; return ( @@ -41,6 +43,7 @@ export const GanttChartBlocksList: FC = (props) => { typeof enableBlockRightResize === "function" ? enableBlockRightResize(blockId) : enableBlockRightResize } enableBlockMove={typeof enableBlockMove === "function" ? enableBlockMove(blockId) : enableBlockMove} + enableDependency={typeof enableDependency === "function" ? enableDependency(blockId) : enableDependency} ganttContainerRef={ganttContainerRef} updateBlockDates={updateBlockDates} /> diff --git a/web/core/components/gantt-chart/chart/main-content.tsx b/web/core/components/gantt-chart/chart/main-content.tsx index 8297bfae5c0..5f9fb00435c 100644 --- a/web/core/components/gantt-chart/chart/main-content.tsx +++ b/web/core/components/gantt-chart/chart/main-content.tsx @@ -41,6 +41,7 @@ type Props = { enableReorder: boolean | ((blockId: string) => boolean); enableSelection: boolean | ((blockId: string) => boolean); enableAddBlock: boolean | ((blockId: string) => boolean); + enableDependency: boolean | ((blockId: string) => boolean); itemsContainerWidth: number; showAllBlocks: boolean; sidebarToRender: (props: any) => React.ReactNode; @@ -67,6 +68,7 @@ export const GanttChartMainContent: React.FC = observer((props) => { enableReorder, enableAddBlock, enableSelection, + enableDependency, itemsContainerWidth, showAllBlocks, sidebarToRender, @@ -215,6 +217,7 @@ export const GanttChartMainContent: React.FC = observer((props) => { enableBlockRightResize={enableBlockRightResize} enableBlockMove={enableBlockMove} ganttContainerRef={ganttContainerRef} + enableDependency={enableDependency} showAllBlocks={showAllBlocks} updateBlockDates={updateBlockDates} /> diff --git a/web/core/components/gantt-chart/chart/root.tsx b/web/core/components/gantt-chart/chart/root.tsx index 2509e8d55db..b69254cae6c 100644 --- a/web/core/components/gantt-chart/chart/root.tsx +++ b/web/core/components/gantt-chart/chart/root.tsx @@ -37,6 +37,7 @@ type ChartViewRootProps = { enableReorder: boolean | ((blockId: string) => boolean); enableAddBlock: boolean | ((blockId: string) => boolean); enableSelection: boolean | ((blockId: string) => boolean); + enableDependency: boolean | ((blockId: string) => boolean); bottomSpacing: boolean; showAllBlocks: boolean; loadMoreBlocks?: () => void; @@ -70,6 +71,7 @@ export const ChartViewRoot: FC = observer((props) => { enableReorder, enableAddBlock, enableSelection, + enableDependency, bottomSpacing, showAllBlocks, quickAdd, @@ -204,6 +206,7 @@ export const ChartViewRoot: FC = observer((props) => { enableReorder={enableReorder} enableSelection={enableSelection} enableAddBlock={enableAddBlock} + enableDependency={enableDependency} itemsContainerWidth={itemsContainerWidth} showAllBlocks={showAllBlocks} sidebarToRender={sidebarToRender} diff --git a/web/core/components/gantt-chart/helpers/draggable.tsx b/web/core/components/gantt-chart/helpers/draggable.tsx index 35babf9c3ec..6031f3efd94 100644 --- a/web/core/components/gantt-chart/helpers/draggable.tsx +++ b/web/core/components/gantt-chart/helpers/draggable.tsx @@ -18,6 +18,7 @@ type Props = { enableBlockLeftResize: boolean; enableBlockRightResize: boolean; enableBlockMove: boolean; + enableDependency: boolean | ((blockId: string) => boolean); ganttContainerRef: RefObject; }; @@ -29,6 +30,7 @@ export const ChartDraggable: React.FC = observer((props) => { enableBlockLeftResize, enableBlockRightResize, enableBlockMove, + enableDependency, isMoving, ganttContainerRef, } = props; @@ -36,7 +38,9 @@ export const ChartDraggable: React.FC = observer((props) => { return (
{/* left resize drag handle */} - + {(typeof enableDependency === "function" ? enableDependency(block.id) : enableDependency) && ( + + )} = observer((props) => { isMoving={isMoving} position={block.position} /> - + {(typeof enableDependency === "function" ? enableDependency(block.id) : enableDependency) && ( + + )}
); }); diff --git a/web/core/components/gantt-chart/root.tsx b/web/core/components/gantt-chart/root.tsx index 3e761477cf5..67930aa58e8 100644 --- a/web/core/components/gantt-chart/root.tsx +++ b/web/core/components/gantt-chart/root.tsx @@ -24,6 +24,7 @@ type GanttChartRootProps = { enableReorder?: boolean | ((blockId: string) => boolean); enableAddBlock?: boolean | ((blockId: string) => boolean); enableSelection?: boolean | ((blockId: string) => boolean); + enableDependency?: boolean | ((blockId: string) => boolean); bottomSpacing?: boolean; showAllBlocks?: boolean; showToday?: boolean; @@ -47,6 +48,7 @@ export const GanttChartRoot: FC = observer((props) => { enableReorder = false, enableAddBlock = false, enableSelection = false, + enableDependency = false, bottomSpacing = false, showAllBlocks = false, showToday = true, @@ -79,6 +81,7 @@ export const GanttChartRoot: FC = observer((props) => { enableReorder={enableReorder} enableAddBlock={enableAddBlock} enableSelection={enableSelection} + enableDependency={enableDependency} bottomSpacing={bottomSpacing} showAllBlocks={showAllBlocks} quickAdd={quickAdd} diff --git a/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 103b49f3517..96cff38ad89 100644 --- a/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -98,14 +98,14 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan target_date?: string; }[] ) => - issues.updateIssueDates(workspaceSlug.toString(), projectId.toString(), updates).catch(() => { + issues.updateIssueDates(workspaceSlug.toString(), updates, projectId.toString()).catch(() => { setToast({ type: TOAST_TYPE.ERROR, title: t("toast.error"), message: "Error while updating work item dates, Please try again Later", }); }), - [issues] + [issues, projectId, workspaceSlug] ); const quickAdd = diff --git a/web/core/components/issues/issue-layouts/properties/labels.tsx b/web/core/components/issues/issue-layouts/properties/labels.tsx index 7c0ed4cd2ef..b8f21bfbad3 100644 --- a/web/core/components/issues/issue-layouts/properties/labels.tsx +++ b/web/core/components/issues/issue-layouts/properties/labels.tsx @@ -82,8 +82,6 @@ export const IssuePropertyLabels: React.FC = observer((pro } }, [isOpen, isMobile]); - if (!value) return null; - let projectLabels: IIssueLabel[] = defaultOptions as IIssueLabel[]; if (storeLabels && storeLabels.length > 0) projectLabels = storeLabels; diff --git a/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 372c5605b55..cb29d55a2f5 100644 --- a/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -1,34 +1,20 @@ import React, { useCallback } from "react"; -import isEmpty from "lodash/isEmpty"; +import { isEmpty } from "lodash"; import { observer } from "mobx-react"; import { useParams, useSearchParams } from "next/navigation"; -import useSWR from "swr"; // plane constants -import { - ALL_ISSUES, - EIssueLayoutTypes, - EIssueFilterType, - EIssuesStoreType, - ISSUE_DISPLAY_FILTERS_BY_PAGE -,EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { IIssueDisplayFilterOptions } from "@plane/types"; +import useSWR from "swr"; +import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; // hooks // components -import { EmptyState } from "@/components/common"; -import { SpreadsheetView } from "@/components/issues/issue-layouts"; -import { AllIssueQuickActions } from "@/components/issues/issue-layouts/quick-action-dropdowns"; -import { SpreadsheetLayoutLoader } from "@/components/ui"; // hooks -import { useGlobalView, useIssues, useUserPermissions } from "@/hooks/store"; +import { EmptyState } from "@/components/common"; +import { WorkspaceActiveLayout } from "@/components/views/helper"; +import { useGlobalView, useIssues } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; -import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; -import { useIssuesActions } from "@/hooks/use-issues-actions"; import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; // store import emptyView from "@/public/empty-state/view.svg"; -import { IssuePeekOverview } from "../../peek-overview"; -import { IssueLayoutHOC } from "../issue-layout-HOC"; -import { TRenderQuickActions } from "../list/list-view-types"; type Props = { isDefaultView: boolean; @@ -38,32 +24,34 @@ type Props = { export const AllIssueLayoutRoot: React.FC = observer((props: Props) => { const { isDefaultView, isLoading = false, toggleLoading } = props; - // router - const { workspaceSlug, globalViewId } = useParams(); + + // Router hooks const router = useAppRouter(); + const { workspaceSlug, globalViewId } = useParams(); const searchParams = useSearchParams(); - const routeFilters: { - [key: string]: string; - } = {}; - searchParams.forEach((value: string, key: string) => { - routeFilters[key] = value; - }); - //swr hook for fetching issue properties - useWorkspaceIssueProperties(workspaceSlug); - // store + + // Store hooks const { issuesFilter: { filters, fetchFilters, updateFilters }, - issues: { clear, getIssueLoader, getPaginationData, groupedIssueIds, fetchIssues, fetchNextIssues }, + issues: { clear, groupedIssueIds, fetchIssues, fetchNextIssues }, } = useIssues(EIssuesStoreType.GLOBAL); - const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL); - - const { allowPermissions } = useUserPermissions(); - const { fetchAllGlobalViews, getViewDetailsById } = useGlobalView(); + // Custom hooks + useWorkspaceIssueProperties(workspaceSlug); + + // Derived values const viewDetails = getViewDetailsById(globalViewId?.toString()); - // filter init from the query params + const issueFilters = globalViewId ? filters?.[globalViewId.toString()] : undefined; + const activeLayout: EIssueLayoutTypes | undefined = issueFilters?.displayFilters?.layout; + + // Route filters + const routeFilters: { [key: string]: string } = {}; + searchParams.forEach((value: string, key: string) => { + routeFilters[key] = value; + }); + // Apply route filters to store const routerFilterParams = () => { if ( workspaceSlug && @@ -89,10 +77,12 @@ export const AllIssueLayoutRoot: React.FC = observer((props: Props) => { } }; + // Fetch next pages callback const fetchNextPages = useCallback(() => { if (workspaceSlug && globalViewId) fetchNextIssues(workspaceSlug.toString(), globalViewId.toString()); }, [fetchNextIssues, workspaceSlug, globalViewId]); + // Fetch global views const { isLoading: globalViewsLoading } = useSWR( workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS_${workspaceSlug}` : null, async () => { @@ -103,6 +93,7 @@ export const AllIssueLayoutRoot: React.FC = observer((props: Props) => { { revalidateIfStale: false, revalidateOnFocus: false } ); + // Fetch issues const { isLoading: issuesLoading } = useSWR( workspaceSlug && globalViewId ? `WORKSPACE_GLOBAL_VIEW_ISSUES_${workspaceSlug}_${globalViewId}` : null, async () => { @@ -126,54 +117,7 @@ export const AllIssueLayoutRoot: React.FC = observer((props: Props) => { { revalidateIfStale: false, revalidateOnFocus: false } ); - const canEditProperties = useCallback( - (projectId: string | undefined) => { - if (!projectId) return false; - return allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.PROJECT, - workspaceSlug.toString(), - projectId - ); - }, - [workspaceSlug] - ); - - const issueFilters = globalViewId ? filters?.[globalViewId.toString()] : undefined; - - const handleDisplayFiltersUpdate = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !globalViewId) return; - - updateFilters( - workspaceSlug.toString(), - undefined, - EIssueFilterType.DISPLAY_FILTERS, - { ...updatedDisplayFilter }, - globalViewId.toString() - ); - }, - [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 ?? undefined)} - placements={placement} - /> - ), - [canEditProperties, removeIssue, updateIssue, archiveIssue] - ); - - // when the call is not loading and the view does not exist and the view is not a default view, show empty state + // Empty state if (!isLoading && !globalViewsLoading && !issuesLoading && !viewDetails && !isDefaultView) { return ( = observer((props: Props) => { ); } - if ((isLoading && issuesLoading && getIssueLoader() === "init-loader") || !globalViewId || !groupedIssueIds) { - return ; - } - - const issueIds = groupedIssueIds[ALL_ISSUES]; - const nextPageResults = getPaginationData(ALL_ISSUES, undefined)?.nextPageResults; - return ( - - - - {/* peek overview */} - - - + ); }); diff --git a/web/core/components/issues/issue-layouts/spreadsheet/roots/workspace-root.tsx b/web/core/components/issues/issue-layouts/spreadsheet/roots/workspace-root.tsx new file mode 100644 index 00000000000..caba1176dad --- /dev/null +++ b/web/core/components/issues/issue-layouts/spreadsheet/roots/workspace-root.tsx @@ -0,0 +1,137 @@ +import React, { useCallback } from "react"; +import { observer } from "mobx-react"; +// plane constants +import { + ALL_ISSUES, + EIssueLayoutTypes, + EIssueFilterType, + EIssuesStoreType, + EUserPermissions, + EUserPermissionsLevel, +} from "@plane/constants"; +import { IIssueDisplayFilterOptions } from "@plane/types"; +// hooks +// components +import { SpreadsheetView } from "@/components/issues/issue-layouts"; +import { AllIssueQuickActions } from "@/components/issues/issue-layouts/quick-action-dropdowns"; +import { SpreadsheetLayoutLoader } from "@/components/ui"; +// hooks +import { useIssues, useUserPermissions } from "@/hooks/store"; +import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; +import { useIssuesActions } from "@/hooks/use-issues-actions"; +import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; +// store +import { IssuePeekOverview } from "../../../peek-overview"; +import { IssueLayoutHOC } from "../../issue-layout-HOC"; +import { TRenderQuickActions } from "../../list/list-view-types"; + +type Props = { + isDefaultView: boolean; + isLoading?: boolean; + toggleLoading: (value: boolean) => void; + workspaceSlug: string; + globalViewId: string; + routeFilters: { + [key: string]: string; + }; + fetchNextPages: () => void; + globalViewsLoading: boolean; + issuesLoading: boolean; +}; + +export const WorkspaceSpreadsheetRoot: React.FC = observer((props: Props) => { + const { isLoading = false, workspaceSlug, globalViewId, fetchNextPages, issuesLoading } = props; + + // Custom hooks + useWorkspaceIssueProperties(workspaceSlug); + + // Store hooks + const { + issuesFilter: { filters, updateFilters }, + issues: { getIssueLoader, getPaginationData, groupedIssueIds }, + } = useIssues(EIssuesStoreType.GLOBAL); + const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL); + const { allowPermissions } = useUserPermissions(); + + // Derived values + const issueFilters = globalViewId ? filters?.[globalViewId.toString()] : undefined; + + // Permission checker + const canEditProperties = useCallback( + (projectId: string | undefined) => { + if (!projectId) return false; + return allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug.toString(), + projectId + ); + }, + [allowPermissions, workspaceSlug] + ); + + // Display filters handler + const handleDisplayFiltersUpdate = useCallback( + (updatedDisplayFilter: Partial) => { + if (!workspaceSlug || !globalViewId) return; + + updateFilters( + workspaceSlug.toString(), + undefined, + EIssueFilterType.DISPLAY_FILTERS, + { ...updatedDisplayFilter }, + globalViewId.toString() + ); + }, + [updateFilters, workspaceSlug, globalViewId] + ); + + // Quick actions renderer + 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 ?? undefined)} + placements={placement} + /> + ), + [canEditProperties, removeIssue, updateIssue, archiveIssue] + ); + + // Loading state + if ((isLoading && issuesLoading && getIssueLoader() === "init-loader") || !globalViewId || !groupedIssueIds) { + return ; + } + + // Computed values + const issueIds = groupedIssueIds[ALL_ISSUES]; + const nextPageResults = getPaginationData(ALL_ISSUES, undefined)?.nextPageResults; + + // Render spreadsheet + return ( + + + + {/* peek overview */} + + + + ); +}); diff --git a/web/core/components/views/helper.tsx b/web/core/components/views/helper.tsx new file mode 100644 index 00000000000..975e103fdba --- /dev/null +++ b/web/core/components/views/helper.tsx @@ -0,0 +1,51 @@ +import { EIssueLayoutTypes } from "@plane/constants"; +import { WorkspaceAdditionalLayouts } from "@/plane-web/components/views/helper"; +import { WorkspaceSpreadsheetRoot } from "../issues/issue-layouts/spreadsheet/roots/workspace-root"; + +export type TWorkspaceLayoutProps = { + activeLayout: EIssueLayoutTypes | undefined; + isDefaultView: boolean; + isLoading?: boolean; + toggleLoading: (value: boolean) => void; + workspaceSlug: string; + globalViewId: string; + routeFilters: { + [key: string]: string; + }; + fetchNextPages: () => void; + globalViewsLoading: boolean; + issuesLoading: boolean; +}; + +export const WorkspaceActiveLayout = (props: TWorkspaceLayoutProps) => { + const { + activeLayout = EIssueLayoutTypes.SPREADSHEET, + isDefaultView, + isLoading, + toggleLoading, + workspaceSlug, + globalViewId, + routeFilters, + fetchNextPages, + globalViewsLoading, + issuesLoading, + } = props; + switch (activeLayout) { + case EIssueLayoutTypes.SPREADSHEET: + return ( + + ); + default: + return ; + } +}; diff --git a/web/core/hooks/store/use-issues.ts b/web/core/hooks/store/use-issues.ts index e5842cd1cb7..bdb442c5f3b 100644 --- a/web/core/hooks/store/use-issues.ts +++ b/web/core/hooks/store/use-issues.ts @@ -9,6 +9,7 @@ import { IProjectEpics, IProjectEpicsFilter } from "@/plane-web/store/issue/epic // types import { ITeamIssues, ITeamIssuesFilter } from "@/plane-web/store/issue/team"; import { ITeamViewIssues, ITeamViewIssuesFilter } from "@/plane-web/store/issue/team-views"; +import { IWorkspaceIssues } from "@/plane-web/store/issue/workspace/issue.store"; import { IArchivedIssues, IArchivedIssuesFilter } from "@/store/issue/archived"; import { ICycleIssues, ICycleIssuesFilter } from "@/store/issue/cycle"; import { IDraftIssues, IDraftIssuesFilter } from "@/store/issue/draft"; @@ -16,7 +17,7 @@ import { IModuleIssues, IModuleIssuesFilter } from "@/store/issue/module"; import { IProfileIssues, IProfileIssuesFilter } from "@/store/issue/profile"; import { IProjectIssues, IProjectIssuesFilter } from "@/store/issue/project"; import { IProjectViewIssues, IProjectViewIssuesFilter } from "@/store/issue/project-views"; -import { IWorkspaceIssues, IWorkspaceIssuesFilter } from "@/store/issue/workspace"; +import { IWorkspaceIssuesFilter } from "@/store/issue/workspace"; import { IWorkspaceDraftIssues, IWorkspaceDraftIssuesFilter } from "@/store/issue/workspace-draft"; // constants diff --git a/web/core/services/workspace.service.ts b/web/core/services/workspace.service.ts index 95ed2c976b3..bb91bc12198 100644 --- a/web/core/services/workspace.service.ts +++ b/web/core/services/workspace.service.ts @@ -263,8 +263,11 @@ export class WorkspaceService extends APIService { } async getViewIssues(workspaceSlug: string, params: any, config = {}): Promise { + const path = params.expand?.includes("issue_relation") + ? `/api/workspaces/${workspaceSlug}/issues-detail/` + : `/api/workspaces/${workspaceSlug}/issues/`; return this.get( - `/api/workspaces/${workspaceSlug}/issues/`, + path, { params, }, diff --git a/web/core/store/issue/helpers/base-issues.store.ts b/web/core/store/issue/helpers/base-issues.store.ts index f7dd980d239..7c84d848b92 100644 --- a/web/core/store/issue/helpers/base-issues.store.ts +++ b/web/core/store/issue/helpers/base-issues.store.ts @@ -113,7 +113,7 @@ export interface IBaseIssuesStore { addModuleIds: string[], removeModuleIds: string[] ): Promise; - updateIssueDates(workspaceSlug: string, projectId: string, updates: IBlockUpdateDependencyData[]): Promise; + updateIssueDates(workspaceSlug: string, updates: IBlockUpdateDependencyData[], projectId?: string): Promise; } // This constant maps the group by keys to the respective issue property that the key relies on @@ -826,9 +826,10 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore { async updateIssueDates( workspaceSlug: string, - projectId: string, - updates: { id: string; start_date?: string; target_date?: string }[] + updates: { id: string; start_date?: string; target_date?: string }[], + projectId?: string ) { + if(!projectId) return; const issueDatesBeforeChange: { id: string; start_date?: string; target_date?: string }[] = []; try { const getIssueById = this.rootIssueStore.issues.getIssueById; diff --git a/web/core/store/issue/issue-details/relation.store.ts b/web/core/store/issue/issue-details/relation.store.ts index edabc1a9f47..cb446e32c5e 100644 --- a/web/core/store/issue/issue-details/relation.store.ts +++ b/web/core/store/issue/issue-details/relation.store.ts @@ -182,7 +182,7 @@ export class IssueRelationStore implements IIssueRelationStore { */ createCurrentRelation = async (issueId: string, relationType: TIssueRelationTypes, relatedIssueId: string) => { const workspaceSlug = this.rootIssueDetailStore.rootIssueStore.workspaceSlug; - const projectId = this.rootIssueDetailStore.rootIssueStore.projectId; + const projectId = this.rootIssueDetailStore.issue.getIssueById(issueId)?.project_id; if (!workspaceSlug || !projectId) return; diff --git a/web/core/store/issue/root.store.ts b/web/core/store/issue/root.store.ts index 31e1db54e05..33710fae15f 100644 --- a/web/core/store/issue/root.store.ts +++ b/web/core/store/issue/root.store.ts @@ -14,6 +14,7 @@ import { TeamViewIssuesFilter, } from "@/plane-web/store/issue/team-views"; // root store +import { IWorkspaceIssues, WorkspaceIssues } from "@/plane-web/store/issue/workspace/issue.store"; import { RootStore } from "@/plane-web/store/root.store"; import { IWorkspaceMembership } from "@/store/member/workspace-member.store"; // issues data store @@ -32,7 +33,7 @@ import { IProjectViewIssues, ProjectViewIssues, } from "./project-views"; -import { WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues, IWorkspaceIssuesFilter } from "./workspace"; +import { WorkspaceIssuesFilter, IWorkspaceIssuesFilter } from "./workspace"; import { IWorkspaceDraftIssues, IWorkspaceDraftIssuesFilter, diff --git a/web/core/store/issue/workspace/filter.store.ts b/web/core/store/issue/workspace/filter.store.ts index 2a98ba03cb6..71ce319d315 100644 --- a/web/core/store/issue/workspace/filter.store.ts +++ b/web/core/store/issue/workspace/filter.store.ts @@ -132,53 +132,49 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo ); fetchFilters = async (workspaceSlug: string, viewId: TWorkspaceFilters) => { - try { - let filters: IIssueFilterOptions; - let displayFilters: IIssueDisplayFilterOptions; - let displayProperties: IIssueDisplayProperties; - let kanbanFilters: TIssueKanbanFilters = { - group_by: [], - sub_group_by: [], - }; - - const _filters = this.handleIssuesLocalFilters.get(EIssuesStoreType.GLOBAL, workspaceSlug, undefined, viewId); + let filters: IIssueFilterOptions; + let displayFilters: IIssueDisplayFilterOptions; + let displayProperties: IIssueDisplayProperties; + let kanbanFilters: TIssueKanbanFilters = { + group_by: [], + sub_group_by: [], + }; + + const _filters = this.handleIssuesLocalFilters.get(EIssuesStoreType.GLOBAL, workspaceSlug, undefined, viewId); + displayFilters = this.computedDisplayFilters(_filters?.display_filters, { + layout: EIssueLayoutTypes.SPREADSHEET, + order_by: "-created_at", + }); + displayProperties = this.computedDisplayProperties(_filters?.display_properties); + kanbanFilters = { + group_by: _filters?.kanban_filters?.group_by || [], + sub_group_by: _filters?.kanban_filters?.sub_group_by || [], + }; + + if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) { + const currentUserId = this.rootIssueStore.currentUserId; + filters = this.getComputedFiltersBasedOnViews(currentUserId, viewId as TStaticViewTypes); + } else { + const _filters = await this.issueFilterService.getViewDetails(workspaceSlug, viewId); + filters = this.computedFilters(_filters?.filters); displayFilters = this.computedDisplayFilters(_filters?.display_filters, { layout: EIssueLayoutTypes.SPREADSHEET, order_by: "-created_at", }); displayProperties = this.computedDisplayProperties(_filters?.display_properties); - kanbanFilters = { - group_by: _filters?.kanban_filters?.group_by || [], - sub_group_by: _filters?.kanban_filters?.sub_group_by || [], - }; - - if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) { - const currentUserId = this.rootIssueStore.currentUserId; - filters = this.getComputedFiltersBasedOnViews(currentUserId, viewId as TStaticViewTypes); - } else { - const _filters = await this.issueFilterService.getViewDetails(workspaceSlug, viewId); - filters = this.computedFilters(_filters?.filters); - displayFilters = this.computedDisplayFilters(_filters?.display_filters, { - layout: EIssueLayoutTypes.SPREADSHEET, - order_by: "-created_at", - }); - displayProperties = this.computedDisplayProperties(_filters?.display_properties); - } - - // override existing order by if ordered by manual sort_order - if (displayFilters.order_by === "sort_order") { - displayFilters.order_by = "-created_at"; - } + } - runInAction(() => { - set(this.filters, [viewId, "filters"], filters); - set(this.filters, [viewId, "displayFilters"], displayFilters); - set(this.filters, [viewId, "displayProperties"], displayProperties); - set(this.filters, [viewId, "kanbanFilters"], kanbanFilters); - }); - } catch (error) { - throw error; + // override existing order by if ordered by manual sort_order + if (displayFilters.order_by === "sort_order") { + displayFilters.order_by = "-created_at"; } + + runInAction(() => { + set(this.filters, [viewId, "filters"], filters); + set(this.filters, [viewId, "displayFilters"], displayFilters); + set(this.filters, [viewId, "displayProperties"], displayProperties); + set(this.filters, [viewId, "kanbanFilters"], kanbanFilters); + }); }; updateFilters = async (