[WEB-4321]chore: workspace views refactor (#7214)

* chore: workspace views reafactor

* chore: resolved coderabbit suggestions

* chore: added project level workspace filter

* chore: added enum for roles

* chore: removed redundant type definition

* chore: optimised the query

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
Vamsi Krishna 2025-06-19 16:26:32 +05:30 committed by GitHub
parent 8988cf9a85
commit 64fd0b2830
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 381 additions and 172 deletions

View file

@ -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 @@ class IssueDetailEndpoint(BaseAPIView):
.values("count")
)
)
issue = issue.filter(**filters)
order_by_param = request.GET.get("order_by", "-created_at")
# Issue queryset

View file

@ -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";

View file

@ -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: {

View file

@ -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 (
<>
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
@ -113,13 +134,18 @@ export const GlobalIssuesHeader = observer(() => {
<Header.RightItem>
{!isLocked ? (
<>
<GlobalViewLayoutSelection
onChange={handleLayoutChange}
selectedLayout={activeLayout ?? EIssueLayoutTypes.SPREADSHEET}
workspaceSlug={workspaceSlug.toString()}
/>
<FiltersDropdown
title={t("common.filters")}
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.spreadsheet}
layoutDisplayFiltersOptions={currentLayoutFilters}
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}}
@ -130,7 +156,7 @@ export const GlobalIssuesHeader = observer(() => {
</FiltersDropdown>
<FiltersDropdown title={t("common.display")} placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.spreadsheet}
layoutDisplayFiltersOptions={currentLayoutFilters}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}

View file

@ -0,0 +1,12 @@
import { EIssueLayoutTypes } from "@plane/constants";
import { TWorkspaceLayoutProps } from "@/components/views/helper";
export type TLayoutSelectionProps = {
onChange: (layout: EIssueLayoutTypes) => void;
selectedLayout: EIssueLayoutTypes;
workspaceSlug: string;
};
export const GlobalViewLayoutSelection = (props: TLayoutSelectionProps) => <></>;
export const WorkspaceAdditionalLayouts = (props: TWorkspaceLayoutProps) => <></>;

View file

@ -0,0 +1 @@
export * from "@/store/issue/workspace/issue.store";

View file

@ -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);

View file

@ -20,6 +20,7 @@ type Props = {
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
enableBlockMove: boolean;
enableDependency: boolean;
ganttContainerRef: RefObject<HTMLDivElement>;
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>;
};
@ -33,6 +34,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
enableBlockRightResize,
enableBlockMove,
ganttContainerRef,
enableDependency,
updateBlockDates,
} = props;
// store hooks
@ -90,6 +92,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove && !!isBlockComplete}
enableDependency={enableDependency}
isMoving={isMoving}
ganttContainerRef={ganttContainerRef}
/>

View file

@ -12,6 +12,7 @@ export type GanttChartBlocksProps = {
ganttContainerRef: React.RefObject<HTMLDivElement>;
showAllBlocks: boolean;
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>;
enableDependency: boolean | ((blockId: string) => boolean);
};
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
@ -24,6 +25,7 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
ganttContainerRef,
showAllBlocks,
updateBlockDates,
enableDependency,
} = props;
return (
@ -41,6 +43,7 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (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}
/>

View file

@ -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<Props> = observer((props) => {
enableReorder,
enableAddBlock,
enableSelection,
enableDependency,
itemsContainerWidth,
showAllBlocks,
sidebarToRender,
@ -215,6 +217,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
ganttContainerRef={ganttContainerRef}
enableDependency={enableDependency}
showAllBlocks={showAllBlocks}
updateBlockDates={updateBlockDates}
/>

View file

@ -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<ChartViewRootProps> = observer((props) => {
enableReorder,
enableAddBlock,
enableSelection,
enableDependency,
bottomSpacing,
showAllBlocks,
quickAdd,
@ -204,6 +206,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
enableReorder={enableReorder}
enableSelection={enableSelection}
enableAddBlock={enableAddBlock}
enableDependency={enableDependency}
itemsContainerWidth={itemsContainerWidth}
showAllBlocks={showAllBlocks}
sidebarToRender={sidebarToRender}

View file

@ -18,6 +18,7 @@ type Props = {
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
enableBlockMove: boolean;
enableDependency: boolean | ((blockId: string) => boolean);
ganttContainerRef: RefObject<HTMLDivElement>;
};
@ -29,6 +30,7 @@ export const ChartDraggable: React.FC<Props> = observer((props) => {
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
enableDependency,
isMoving,
ganttContainerRef,
} = props;
@ -36,7 +38,9 @@ export const ChartDraggable: React.FC<Props> = observer((props) => {
return (
<div className="group w-full z-[5] relative inline-flex h-full cursor-pointer items-center font-medium transition-all">
{/* left resize drag handle */}
<LeftDependencyDraggable block={block} ganttContainerRef={ganttContainerRef} />
{(typeof enableDependency === "function" ? enableDependency(block.id) : enableDependency) && (
<LeftDependencyDraggable block={block} ganttContainerRef={ganttContainerRef} />
)}
<LeftResizable
enableBlockLeftResize={enableBlockLeftResize}
handleBlockDrag={handleBlockDrag}
@ -58,7 +62,9 @@ export const ChartDraggable: React.FC<Props> = observer((props) => {
isMoving={isMoving}
position={block.position}
/>
<RightDependencyDraggable block={block} ganttContainerRef={ganttContainerRef} />
{(typeof enableDependency === "function" ? enableDependency(block.id) : enableDependency) && (
<RightDependencyDraggable block={block} ganttContainerRef={ganttContainerRef} />
)}
</div>
);
});

View file

@ -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<GanttChartRootProps> = observer((props) => {
enableReorder = false,
enableAddBlock = false,
enableSelection = false,
enableDependency = false,
bottomSpacing = false,
showAllBlocks = false,
showToday = true,
@ -79,6 +81,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = observer((props) => {
enableReorder={enableReorder}
enableAddBlock={enableAddBlock}
enableSelection={enableSelection}
enableDependency={enableDependency}
bottomSpacing={bottomSpacing}
showAllBlocks={showAllBlocks}
quickAdd={quickAdd}

View file

@ -98,14 +98,14 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = 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 =

View file

@ -82,8 +82,6 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
}
}, [isOpen, isMobile]);
if (!value) return null;
let projectLabels: IIssueLabel[] = defaultOptions as IIssueLabel[];
if (storeLabels && storeLabels.length > 0) projectLabels = storeLabels;

View file

@ -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<Props> = 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;
} = {};
// Store hooks
const {
issuesFilter: { filters, fetchFilters, updateFilters },
issues: { clear, groupedIssueIds, fetchIssues, fetchNextIssues },
} = useIssues(EIssuesStoreType.GLOBAL);
const { fetchAllGlobalViews, getViewDetailsById } = useGlobalView();
// Custom hooks
useWorkspaceIssueProperties(workspaceSlug);
// Derived values
const viewDetails = getViewDetailsById(globalViewId?.toString());
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;
});
//swr hook for fetching issue properties
useWorkspaceIssueProperties(workspaceSlug);
// store
const {
issuesFilter: { filters, fetchFilters, updateFilters },
issues: { clear, getIssueLoader, getPaginationData, groupedIssueIds, fetchIssues, fetchNextIssues },
} = useIssues(EIssuesStoreType.GLOBAL);
const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL);
const { allowPermissions } = useUserPermissions();
const { fetchAllGlobalViews, getViewDetailsById } = useGlobalView();
const viewDetails = getViewDetailsById(globalViewId?.toString());
// filter init from the query params
// Apply route filters to store
const routerFilterParams = () => {
if (
workspaceSlug &&
@ -89,10 +77,12 @@ export const AllIssueLayoutRoot: React.FC<Props> = 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<Props> = 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<Props> = 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<IIssueDisplayFilterOptions>) => {
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 }) => (
<AllIssueQuickActions
parentRef={parentRef}
customActionButton={customActionButton}
issue={issue}
handleDelete={async () => 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 (
<EmptyState
@ -188,31 +132,18 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props: Props) => {
);
}
if ((isLoading && issuesLoading && getIssueLoader() === "init-loader") || !globalViewId || !groupedIssueIds) {
return <SpreadsheetLayoutLoader />;
}
const issueIds = groupedIssueIds[ALL_ISSUES];
const nextPageResults = getPaginationData(ALL_ISSUES, undefined)?.nextPageResults;
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.GLOBAL}>
<IssueLayoutHOC layout={EIssueLayoutTypes.SPREADSHEET}>
<SpreadsheetView
displayProperties={issueFilters?.displayProperties ?? {}}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issueIds={Array.isArray(issueIds) ? issueIds : []}
quickActions={renderQuickActions}
updateIssue={updateIssue}
canEditProperties={canEditProperties}
canLoadMoreIssues={!!nextPageResults}
loadMoreIssues={fetchNextPages}
isWorkspaceLevel
/>
{/* peek overview */}
<IssuePeekOverview />
</IssueLayoutHOC>
</IssuesStoreContext.Provider>
<WorkspaceActiveLayout
activeLayout={activeLayout}
isDefaultView={isDefaultView}
isLoading={isLoading}
toggleLoading={toggleLoading}
workspaceSlug={workspaceSlug?.toString()}
globalViewId={globalViewId?.toString()}
routeFilters={routeFilters}
fetchNextPages={fetchNextPages}
globalViewsLoading={globalViewsLoading}
issuesLoading={issuesLoading}
/>
);
});

View file

@ -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<Props> = 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<IIssueDisplayFilterOptions>) => {
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 }) => (
<AllIssueQuickActions
parentRef={parentRef}
customActionButton={customActionButton}
issue={issue}
handleDelete={async () => 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 <SpreadsheetLayoutLoader />;
}
// Computed values
const issueIds = groupedIssueIds[ALL_ISSUES];
const nextPageResults = getPaginationData(ALL_ISSUES, undefined)?.nextPageResults;
// Render spreadsheet
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.GLOBAL}>
<IssueLayoutHOC layout={EIssueLayoutTypes.SPREADSHEET}>
<SpreadsheetView
displayProperties={issueFilters?.displayProperties ?? {}}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issueIds={Array.isArray(issueIds) ? issueIds : []}
quickActions={renderQuickActions}
updateIssue={updateIssue}
canEditProperties={canEditProperties}
canLoadMoreIssues={!!nextPageResults}
loadMoreIssues={fetchNextPages}
isWorkspaceLevel
/>
{/* peek overview */}
<IssuePeekOverview />
</IssueLayoutHOC>
</IssuesStoreContext.Provider>
);
});

View file

@ -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 (
<WorkspaceSpreadsheetRoot
isDefaultView={isDefaultView}
isLoading={isLoading}
toggleLoading={toggleLoading}
workspaceSlug={workspaceSlug}
globalViewId={globalViewId}
routeFilters={routeFilters}
fetchNextPages={fetchNextPages}
globalViewsLoading={globalViewsLoading}
issuesLoading={issuesLoading}
/>
);
default:
return <WorkspaceAdditionalLayouts {...props} />;
}
};

View file

@ -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

View file

@ -263,8 +263,11 @@ export class WorkspaceService extends APIService {
}
async getViewIssues(workspaceSlug: string, params: any, config = {}): Promise<TIssuesResponse> {
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,
},

View file

@ -113,7 +113,7 @@ export interface IBaseIssuesStore {
addModuleIds: string[],
removeModuleIds: string[]
): Promise<void>;
updateIssueDates(workspaceSlug: string, projectId: string, updates: IBlockUpdateDependencyData[]): Promise<void>;
updateIssueDates(workspaceSlug: string, updates: IBlockUpdateDependencyData[], projectId?: string): Promise<void>;
}
// 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;

View file

@ -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;

View file

@ -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,

View file

@ -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: [],
};
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);
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 (