chore: view ui and new store workflow

This commit is contained in:
gurusainath 2024-02-16 18:28:21 +05:30
parent 5661ae730a
commit 61f184a2ef
38 changed files with 957 additions and 356 deletions

View File

@ -43,8 +43,6 @@ export type TView = {
updated_at: Date | undefined;
// local view variables
is_local_view: boolean;
is_create: boolean;
is_editable: boolean;
};
export type TUpdateView = {

View File

@ -78,7 +78,7 @@ export type TViewDisplayFilters = {
layout: TViewLayouts;
group_by: TViewDisplayFiltersGrouped | undefined;
sub_group_by: TViewDisplayFiltersGrouped | undefined;
order_by: TViewDisplayFiltersOrderBy | string;
order_by: TViewDisplayFiltersOrderBy;
type: TViewDisplayFiltersType | undefined;
sub_issue: boolean;
show_empty_groups: boolean;

View File

@ -0,0 +1,128 @@
import React, { Fragment, useCallback, useMemo } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useIssues, useUser, useViewDetail } from "hooks/store";
import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties";
// components
import { IssuePeekOverview } from "components/issues";
import { SpreadsheetView } from "components/issues/issue-layouts";
import { AllIssueQuickActions } from "components/issues/issue-layouts/quick-action-dropdowns";
// ui
import { SpreadsheetLayoutLoader } from "components/ui";
// types
import { TIssue, IIssueDisplayFilterOptions, TViewTypes } from "@plane/types";
import { EIssueActions } from "../types";
// constants
import { EUserProjectRoles } from "constants/project";
import { EIssuesStoreType } from "constants/issue";
import { EViewPageType } from "constants/view";
type TGlobalViewIssueLayoutRoot = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string;
viewType: TViewTypes;
viewPageType: EViewPageType;
};
export const GlobalViewIssueLayoutRoot: React.FC<TGlobalViewIssueLayoutRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType } = props;
// hooks
const {
issues: { loader, groupedIssueIds, updateIssue, removeIssue },
} = useIssues(EIssuesStoreType.GLOBAL);
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const {
membership: { currentWorkspaceAllProjectsRole },
} = useUser();
const { issueIds } = groupedIssueIds;
//swr hook for fetching issue properties
useWorkspaceIssueProperties(workspaceSlug);
const issueActions = useMemo(
() => ({
[EIssueActions.UPDATE]: async (issue: TIssue) => {
const projectId = issue.project_id;
if (!workspaceSlug || !projectId || !viewId) return;
await updateIssue(workspaceSlug.toString(), projectId, issue.id, issue, viewId.toString());
},
[EIssueActions.DELETE]: async (issue: TIssue) => {
const projectId = issue.project_id;
if (!workspaceSlug || !projectId || !viewId) return;
await removeIssue(workspaceSlug.toString(), projectId, issue.id, viewId.toString());
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[updateIssue, removeIssue, workspaceSlug]
);
const handleIssues = useCallback(
async (issue: TIssue, action: EIssueActions) => {
if (action === EIssueActions.UPDATE) await issueActions[action]!(issue);
if (action === EIssueActions.DELETE) await issueActions[action]!(issue);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const handleDisplayFiltersUpdate = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !viewId) return;
viewDetailStore?.setDisplayFilters({ order_by: updatedDisplayFilter?.order_by });
},
[viewDetailStore, workspaceSlug, viewId]
);
const renderQuickActions = useCallback(
(issue: TIssue, customActionButton?: React.ReactElement, portalElement?: HTMLDivElement | null) => (
<AllIssueQuickActions
customActionButton={customActionButton}
issue={issue}
handleUpdate={async () => handleIssues({ ...issue }, EIssueActions.UPDATE)}
handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)}
portalElement={portalElement}
/>
),
[handleIssues]
);
const canEditProperties = useCallback(
(projectId: string | undefined) => {
if (!projectId) return false;
const currentProjectRole = currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId];
return !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
},
[currentWorkspaceAllProjectsRole]
);
if (loader === "init-loader" || !issueIds) {
return <SpreadsheetLayoutLoader />;
}
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
{issueIds.length === 0 ? (
<div>Empty state</div>
) : (
<Fragment>
<SpreadsheetView
displayProperties={viewDetailStore?.appliedFilters?.display_properties ?? {}}
displayFilters={viewDetailStore?.appliedFilters?.filters as IIssueDisplayFilterOptions} // Fix: Update the type of displayFilters prop
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issueIds={issueIds}
quickActions={renderQuickActions}
handleIssues={handleIssues}
canEditProperties={canEditProperties}
viewId={viewId}
/>
{/* peek overview */}
<IssuePeekOverview />
</Fragment>
)}
</div>
);
});

View File

@ -5,3 +5,4 @@ export * from "./project-view-layout-root";
export * from "./archived-issue-layout-root";
export * from "./draft-issue-layout-root";
export * from "./all-issue-layout-root";
export * from "./global-view-issue-layout-root";

View File

@ -12,12 +12,13 @@ type TViewAppliedFiltersItem = {
viewType: TViewTypes;
filterKey: keyof TViewFilters;
propertyId: string;
isLocalView: boolean;
};
export const ViewAppliedFiltersItem: FC<TViewAppliedFiltersItem> = (props) => {
const { workspaceSlug, projectId, viewId, viewType, filterKey, propertyId } = props;
const { workspaceSlug, projectId, viewId, viewType, filterKey, propertyId, isLocalView } = props;
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType, isLocalView);
const viewFilterHelper = useViewFilter(workspaceSlug, projectId);
const propertyDetail = viewFilterHelper?.propertyDetails(filterKey, propertyId) || undefined;

View File

@ -16,12 +16,13 @@ type TViewAppliedFilters = {
viewType: TViewTypes;
filterKey: keyof TViewFilters;
propertyVisibleCount?: number | undefined;
isLocalView: boolean;
};
export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, filterKey, propertyVisibleCount } = props;
const { workspaceSlug, projectId, viewId, viewType, filterKey, propertyVisibleCount, isLocalView } = props;
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType, isLocalView);
const viewFilterStore = useViewFilter(workspaceSlug, projectId);
const currentDefaultFilterDetails = useMemo(
@ -44,7 +45,7 @@ export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => {
if (!propertyValues || propertyValues.length <= 0) return <></>;
return (
<div className="relative flex items-center gap-2 border border-custom-border-300 rounded p-1 px-2 min-h-[32px]">
<div className="relative flex items-center gap-2 border border-custom-border-200 rounded p-1 px-2 min-h-[32px]">
<div className="flex-shrink-0 text-xs text-custom-text-200 capitalize">{filterKey.replaceAll("_", " ")}</div>
<div className="relative flex items-center gap-1.5 flex-wrap">
{propertyVisibleCount && propertyValues.length >= propertyVisibleCount ? (
@ -65,6 +66,7 @@ export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => {
viewType={viewType}
filterKey={filterKey}
propertyId={propertyId}
isLocalView={isLocalView}
/>
</Fragment>
))}

View File

@ -16,12 +16,21 @@ type TViewAppliedFiltersRoot = {
viewType: TViewTypes;
propertyVisibleCount?: number | undefined;
showClearAll?: boolean;
isLocalView?: boolean;
};
export const ViewAppliedFiltersRoot: FC<TViewAppliedFiltersRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, propertyVisibleCount = undefined, showClearAll = false } = props;
const {
workspaceSlug,
projectId,
viewId,
viewType,
propertyVisibleCount = undefined,
showClearAll = false,
isLocalView = false,
} = props;
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType, isLocalView);
const filterKeys = useMemo(
() =>
@ -52,6 +61,7 @@ export const ViewAppliedFiltersRoot: FC<TViewAppliedFiltersRoot> = observer((pro
viewType={viewType}
filterKey={filterKey}
propertyVisibleCount={propertyVisibleCount}
isLocalView={isLocalView}
/>
</Fragment>
);

View File

@ -32,7 +32,7 @@ export const ViewDisplayFilterSelection: FC<TViewDisplayFilterSelection> = obser
: "border-custom-border-400 bg-custom-background-100"
}`}
>
{isSelected && <Check size={14} />}
{isSelected && <Check size={14} className="text-white" />}
</div>
);
});

View File

@ -43,7 +43,7 @@ export const DisplayFilterExtraOptions: FC<TDisplayFilterExtraOptions> = observe
: "border-custom-border-400 bg-custom-background-100"
}`}
>
{isSelected && <Check size={14} />}
{isSelected && <Check size={14} className="text-white" />}
</div>
<div className="text-xs block truncate line-clamp-1 text-custom-text-200 group-hover:text-custom-text-100">
{optionTitle || "Extra Option"}

View File

@ -27,10 +27,10 @@ export const ViewDisplayPropertySelection: FC<TViewDisplayPropertySelection> = o
return (
<div
className={`relative flex items-center gap-1 text-xs rounded p-0.5 px-2 border transition-all capitalize cursor-pointer
className={`relative flex items-center gap-1 text-xs rounded p-0.5 px-2 border transition-all capitalize cursor-pointer
${
propertyIsSelected
? `border-custom-primary-100 bg-custom-primary-100`
? `border-custom-primary-100 bg-custom-primary-100 text-white`
: `border-custom-border-300 hover:bg-custom-background-80`
}`}
onClick={handlePropertySelection}

View File

@ -0,0 +1,103 @@
import { FC, ReactNode } from "react";
import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
import useSWR from "swr";
// hooks
import { useApplication, useEventTracker, useIssues, useProject, useUser, useView } from "hooks/store";
// components
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// types
import { TViewTypes } from "@plane/types";
// constants
import { ALL_ISSUES_EMPTY_STATE_DETAILS } from "constants/empty-state";
import { EUserWorkspaceRoles } from "constants/workspace";
import { EIssuesStoreType } from "constants/issue";
type TViewEmptyStateRoot = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string;
viewType: TViewTypes;
children: ReactNode;
};
export const ViewEmptyStateRoot: FC<TViewEmptyStateRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, children } = props;
// hooks
const { commandPalette } = useApplication();
const { resolvedTheme } = useTheme();
const { workspaceProjectIds } = useProject();
const {
currentUser,
membership: { currentWorkspaceRole },
} = useUser();
const { setTrackElement } = useEventTracker();
const { issueMap } = useIssues();
const viewStore = useView(workspaceSlug, projectId, viewType);
const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(viewId);
const currentView = isDefaultView ? viewId : "custom-view";
const currentViewDetails = ALL_ISSUES_EMPTY_STATE_DETAILS[currentView as keyof typeof ALL_ISSUES_EMPTY_STATE_DETAILS];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("all-issues", currentView, isLightMode);
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
useSWR(
workspaceSlug && viewId && viewType && viewStore ? `WORKSPACE_VIEWS_${workspaceSlug}_${viewType}` : null,
async () => {
if (workspaceSlug && viewType && viewStore)
await viewStore?.fetch(
workspaceSlug,
projectId,
viewStore?.viewIds.length > 0 ? "view-mutation-loader" : "view-loader"
);
await viewStore?.fetchById(workspaceSlug, projectId, viewId);
}
);
const issueIds = projectId ? true : (Object.values(issueMap) ?? []).length === 0;
if (!workspaceSlug) return <></>;
return (
<>
{(workspaceProjectIds ?? []).length === 0 || issueIds ? (
<div className="relative w-full h-full">
<EmptyState
image={emptyStateImage}
title={(workspaceProjectIds ?? []).length > 0 ? currentViewDetails.title : "No project"}
description={
(workspaceProjectIds ?? []).length > 0
? currentViewDetails.description
: "To create issues or manage your work, you need to create a project or be a part of one."
}
size="sm"
primaryButton={
(workspaceProjectIds ?? []).length > 0
? currentView !== "custom-view" && currentView !== "subscribed"
? {
text: "Create new issue",
onClick: () => {
setTrackElement("All issues empty state");
commandPalette.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
}
: undefined
: {
text: "Start your first project",
onClick: () => {
setTrackElement("All issues empty state");
commandPalette.toggleCreateProjectModal(true);
},
}
}
disabled={!isEditingAllowed}
/>
</div>
) : (
<>{children}</>
)}
</>
);
});

View File

@ -0,0 +1,103 @@
import { FC, ReactNode } from "react";
import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
import useSWR from "swr";
// hooks
import { useApplication, useEventTracker, useIssues, useProject, useUser, useView } from "hooks/store";
// components
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// types
import { TViewTypes } from "@plane/types";
// constants
import { ALL_ISSUES_EMPTY_STATE_DETAILS } from "constants/empty-state";
import { EUserWorkspaceRoles } from "constants/workspace";
import { EIssuesStoreType } from "constants/issue";
type TViewEmptyStateRoot = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string;
viewType: TViewTypes;
children: ReactNode;
};
export const ViewEmptyStateRoot: FC<TViewEmptyStateRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, children } = props;
// hooks
const { commandPalette } = useApplication();
const { resolvedTheme } = useTheme();
const { workspaceProjectIds } = useProject();
const {
currentUser,
membership: { currentWorkspaceRole },
} = useUser();
const { setTrackElement } = useEventTracker();
const { issueMap } = useIssues();
const viewStore = useView(workspaceSlug, projectId, viewType);
const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(viewId);
const currentView = isDefaultView ? viewId : "custom-view";
const currentViewDetails = ALL_ISSUES_EMPTY_STATE_DETAILS[currentView as keyof typeof ALL_ISSUES_EMPTY_STATE_DETAILS];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("all-issues", currentView, isLightMode);
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
useSWR(
workspaceSlug && viewId && viewType && viewStore ? `WORKSPACE_VIEWS_${workspaceSlug}_${viewType}` : null,
async () => {
if (workspaceSlug && viewType && viewStore)
await viewStore?.fetch(
workspaceSlug,
projectId,
viewStore?.viewIds.length > 0 ? "view-mutation-loader" : "view-loader"
);
await viewStore?.fetchById(workspaceSlug, projectId, viewId);
}
);
const issueIds = projectId ? true : (Object.values(issueMap) ?? []).length === 0;
if (!workspaceSlug) return <></>;
return (
<>
{(workspaceProjectIds ?? []).length === 0 || issueIds ? (
<div className="relative w-full h-full">
<EmptyState
image={emptyStateImage}
title={(workspaceProjectIds ?? []).length > 0 ? currentViewDetails.title : "No project"}
description={
(workspaceProjectIds ?? []).length > 0
? currentViewDetails.description
: "To create issues or manage your work, you need to create a project or be a part of one."
}
size="sm"
primaryButton={
(workspaceProjectIds ?? []).length > 0
? currentView !== "custom-view" && currentView !== "subscribed"
? {
text: "Create new issue",
onClick: () => {
setTrackElement("All issues empty state");
commandPalette.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
}
: undefined
: {
text: "Start your first project",
onClick: () => {
setTrackElement("All issues empty state");
commandPalette.toggleCreateProjectModal(true);
},
}
}
disabled={!isEditingAllowed}
/>
</div>
) : (
<>{children}</>
)}
</>
);
});

View File

@ -0,0 +1,37 @@
import { FC, ReactNode } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useIssues, useProject } from "hooks/store";
// types
import { TViewTypes } from "@plane/types";
type TViewEmptyStateRoot = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string;
viewType: TViewTypes;
children: ReactNode;
};
export const ViewEmptyStateRoot: FC<TViewEmptyStateRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, children } = props;
// hooks
const { workspaceProjectIds } = useProject();
const { issueMap } = useIssues();
const areIssueAvailable = projectId ? true : (Object.values(issueMap) ?? []).length === 0 ? true : false;
if (!workspaceSlug) return <></>;
return (
<>
{(workspaceProjectIds ?? []).length === 0 ? (
<div className="relative w-full h-full">No Projects are available.</div>
) : (
<>
{areIssueAvailable ? <>{children}</> : <div className="relative w-full h-full">No issues are available.</div>}
</>
)}
</>
);
});

View File

@ -23,6 +23,7 @@ type TViewFiltersDropdown = {
children?: ReactNode;
displayDropdownText?: boolean;
dropdownPlacement?: Placement;
isLocalView?: boolean;
};
export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) => {
@ -35,6 +36,7 @@ export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) =>
children,
displayDropdownText = true,
dropdownPlacement = "bottom-start",
isLocalView = false,
} = props;
// state
const [dropdownToggle, setDropdownToggle] = useState(false);
@ -139,6 +141,7 @@ export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) =>
viewPageType={viewPageType}
dateCustomFilterToggle={dateCustomFilterToggle}
setDateCustomFilterToggle={setDateCustomFilterToggle}
isLocalView={isLocalView}
/>
</div>
</div>

View File

@ -55,19 +55,20 @@ export const ViewFiltersEditDropdown: FC<TViewFiltersEditDropdown> = observer((p
key: "save_as_new",
label: "Save as new view",
onClick: () => {
viewOperations.localViewCreateEdit(undefined, viewDetailStore?.filtersToUpdate);
viewOperations.localViewCreateEdit(undefined, "SAVE_AS_NEW");
},
},
{
icon: RotateCcw,
key: "reset_changes",
label: "Reset changes",
label: "Reset filter changes",
onClick: () => viewDetailStore?.resetChanges(),
},
],
[viewOperations, viewDetailStore]
);
if (viewDetailStore?.is_local_view) return <></>;
if (!viewDetailStore?.isFiltersUpdateEnabled) return <></>;
return (
<Menu as="div" className="relative flex-shrink-0" ref={dropdownRef}>

View File

@ -16,13 +16,22 @@ type TViewFiltersItemRoot = {
filterKey: keyof TViewFilters;
dateCustomFilterToggle: string | undefined;
setDateCustomFilterToggle: (value: string | undefined) => void;
isLocalView: boolean;
};
export const ViewFiltersItemRoot: FC<TViewFiltersItemRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, filterKey, dateCustomFilterToggle, setDateCustomFilterToggle } =
props;
const {
workspaceSlug,
projectId,
viewId,
viewType,
filterKey,
dateCustomFilterToggle,
setDateCustomFilterToggle,
isLocalView,
} = props;
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType, isLocalView);
const viewFilterHelper = useViewFilter(workspaceSlug, projectId);
// state
const [viewAll, setViewAll] = useState(false);
@ -76,6 +85,7 @@ export const ViewFiltersItemRoot: FC<TViewFiltersItemRoot> = observer((props) =>
viewType={viewType}
filterKey={filterKey}
propertyId={propertyId}
isLocalView={isLocalView}
/>
<ViewFiltersItem
workspaceSlug={workspaceSlug}

View File

@ -8,7 +8,6 @@ import { TViewFilters } from "@plane/types";
type TViewFiltersItem = {
workspaceSlug: string;
projectId: string | undefined;
filterKey: keyof TViewFilters;
propertyId: string;
};

View File

@ -13,12 +13,13 @@ type TViewFilterSelection = {
viewType: TViewTypes;
filterKey: keyof TViewFilters;
propertyId: string;
isLocalView: boolean;
};
export const ViewFilterSelection: FC<TViewFilterSelection> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, filterKey, propertyId } = props;
const { workspaceSlug, projectId, viewId, viewType, filterKey, propertyId, isLocalView } = props;
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType, isLocalView);
const propertyIds = useMemo(
() => viewDetailStore?.appliedFilters?.filters?.[filterKey] || [],
@ -33,18 +34,6 @@ export const ViewFilterSelection: FC<TViewFilterSelection> = observer((props) =>
: propertyIds?.includes(propertyId)
: propertyIds?.includes(propertyId) || false;
// const isSelected = useMemo(
// () =>
// ["start_date", "target_date"].includes(filterKey)
// ? propertyId === "custom"
// ? propertyIds.filter((id) => id.includes("-")).length > 0
// ? true
// : false
// : propertyIds?.includes(propertyId)
// : propertyIds?.includes(propertyId) || false,
// [filterKey, propertyId, propertyIds]
// );
return (
<div
className={`flex-shrink-0 w-3 h-3 flex justify-center items-center border rounded text-bold ${
@ -53,7 +42,7 @@ export const ViewFilterSelection: FC<TViewFilterSelection> = observer((props) =>
: "border-custom-border-400 bg-custom-background-100"
}`}
>
{isSelected && <Check size={14} />}
{isSelected && <Check size={14} className="text-white" />}
</div>
);
});

View File

@ -20,6 +20,7 @@ type TViewFiltersRoot = {
viewPageType: EViewPageType;
dateCustomFilterToggle: string | undefined;
setDateCustomFilterToggle: (value: string | undefined) => void;
isLocalView: boolean;
};
export const ViewFiltersRoot: FC<TViewFiltersRoot> = observer((props) => {
@ -31,9 +32,10 @@ export const ViewFiltersRoot: FC<TViewFiltersRoot> = observer((props) => {
viewPageType,
dateCustomFilterToggle,
setDateCustomFilterToggle,
isLocalView,
} = props;
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType, isLocalView);
// state
const [filterVisibility, setFilterVisibility] = useState<Partial<keyof TViewFilters>[]>([]);
const handleFilterVisibility = useCallback((key: keyof TViewFilters) => {
@ -73,6 +75,7 @@ export const ViewFiltersRoot: FC<TViewFiltersRoot> = observer((props) => {
filterKey={filterKey}
dateCustomFilterToggle={dateCustomFilterToggle}
setDateCustomFilterToggle={setDateCustomFilterToggle}
isLocalView={isLocalView}
/>
)}
</div>

View File

@ -2,6 +2,9 @@ export * from "./root";
export * from "./header-tabs";
// empty states
export * from "./empty-states/empty-state";
// views
export * from "./views/root";
export * from "./views/view-item";

View File

@ -1,7 +1,6 @@
import { FC, Fragment, useCallback, useEffect, useMemo, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { v4 as uuidV4 } from "uuid";
import cloneDeep from "lodash/cloneDeep";
// hooks
import { useView, useViewDetail } from "hooks/store";
import useToast from "hooks/use-toast";
@ -17,14 +16,20 @@ import {
ViewAppliedFiltersRoot,
ViewDuplicateConfirmationModal,
ViewDeleteConfirmationModal,
} from ".";
} from "./";
// ui
import { Spinner } from "@plane/ui";
import { Loader } from "@plane/ui";
// constants
import { EViewPageType, viewLocalPayload } from "constants/view";
import {
ELocalViews,
EViewLayouts,
EViewPageType,
TViewCRUD,
viewDefaultFilterParametersByViewTypeAndLayout,
} from "constants/view";
// types
import { TViewOperations } from "./types";
import { TView, TViewTypes } from "@plane/types";
import { TViewTypes } from "@plane/types";
type TGlobalViewRoot = {
workspaceSlug: string;
@ -36,11 +41,13 @@ type TGlobalViewRoot = {
};
type TViewOperationsToggle = {
type: "CREATE" | "EDIT" | "DUPLICATE" | "DELETE" | undefined;
type: "DUPLICATE" | "DELETE" | undefined;
viewId: string | undefined;
};
export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
// router
const router = useRouter();
const { workspaceSlug, projectId, viewId, viewType, viewPageType, baseRoute } = props;
// hooks
const viewStore = useView(workspaceSlug, projectId, viewType);
@ -55,38 +62,11 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
(type: TViewOperationsToggle["type"], viewId: string | undefined) => setViewOperationsToggle({ type, viewId }),
[]
);
// hooks
const viewDetailCreateEditStore = useViewDetail(
workspaceSlug,
projectId,
viewOperationsToggle?.viewId || viewId,
viewType
);
const viewOperations: TViewOperations = useMemo(
() => ({
localViewCreateEdit: (viewId: string | undefined, currentView = undefined) => {
if (viewId === undefined) {
if (currentView !== undefined) {
// creating new view
const currentViewPayload = cloneDeep({ ...currentView, id: uuidV4() });
handleViewOperationsToggle("CREATE", currentViewPayload.id);
viewStore?.localViewCreate(workspaceSlug, projectId, currentViewPayload as TView);
} else {
// if current view is available, create a new view with the same data
const viewPayload = viewLocalPayload;
handleViewOperationsToggle("CREATE", viewPayload.id);
viewStore?.localViewCreate(workspaceSlug, projectId, viewPayload as TView);
}
} else {
handleViewOperationsToggle("EDIT", viewId);
viewDetailCreateEditStore?.setIsEditable(true);
}
},
localViewCreateEditClear: async (viewId: string | undefined) => {
if (viewDetailCreateEditStore?.is_create && viewId) viewStore?.remove(workspaceSlug, projectId, viewId);
if (viewDetailCreateEditStore?.is_editable && viewId) viewDetailCreateEditStore.resetChanges();
handleViewOperationsToggle(undefined, undefined);
localViewCreateEdit: (viewId: string | undefined, status: TViewCRUD) => {
viewStore?.localViewHandler(viewId, status);
},
fetch: async () => {
@ -100,9 +80,9 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
});
}
},
create: async (data: Partial<TView>) => {
create: async () => {
try {
await viewStore?.create(workspaceSlug, projectId, data);
await viewStore?.create();
handleViewOperationsToggle(undefined, undefined);
setToastAlert({
type: "success",
@ -117,23 +97,6 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
});
}
},
remove: async (viewId: string) => {
try {
await viewStore?.remove(workspaceSlug, projectId, viewId);
handleViewOperationsToggle(undefined, undefined);
setToastAlert({
type: "success",
title: "Success!",
message: "View removed successfully.",
});
} catch {
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong. Please try again later or contact the support team.",
});
}
},
update: async () => {
try {
await viewDetailStore?.saveChanges();
@ -151,25 +114,80 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
});
}
},
remove: async (viewId: string) => {
try {
await viewStore?.remove(viewId);
handleViewOperationsToggle(undefined, undefined);
setToastAlert({
type: "success",
title: "Success!",
message: "View removed successfully.",
});
} catch {
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong. Please try again later or contact the support team.",
});
}
},
duplicate: async (viewId: string) => {
try {
await viewStore?.duplicate(viewId);
handleViewOperationsToggle(undefined, undefined);
setToastAlert({
type: "success",
title: "Success!",
message: "View removed successfully.",
});
} catch {
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong. Please try again later or contact the support team.",
});
}
},
}),
[
workspaceSlug,
projectId,
viewStore,
viewDetailStore,
setToastAlert,
viewDetailCreateEditStore,
handleViewOperationsToggle,
]
[viewStore, viewDetailStore, handleViewOperationsToggle, setToastAlert, workspaceSlug, projectId]
);
const applyFIltersFromRouter = () => {
if (workspaceSlug && viewId && Object.values(ELocalViews).includes(viewId as ELocalViews)) {
const routerQueryParams = { ...router.query };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { ["workspaceSlug"]: _workspaceSlug, ["viewId"]: _viewId, ...filters } = routerQueryParams;
const acceptedFilters = viewDefaultFilterParametersByViewTypeAndLayout(
viewPageType,
EViewLayouts.SPREADSHEET,
"filters"
);
Object.keys(filters).forEach((key) => {
const filterKey: any = key;
const filterValue = filters[key]?.toString() || undefined;
if (filterKey && filterValue && acceptedFilters.includes(filterKey)) {
const _filterValues = filterValue.split(",");
_filterValues.forEach((element) => {
console.log("filterKey", filterKey);
console.log("element", element);
viewDetailStore?.setFilters(filterKey, element);
});
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
};
// fetch all views
useEffect(() => {
const fetchViews = async () => {
await viewStore?.fetch(
workspaceSlug,
projectId,
viewStore?.viewIds.length > 0 ? "mutation-loader" : "init-loader"
viewStore?.viewIds.length > 0 ? "view-mutation-loader" : "view-loader"
);
};
if (workspaceSlug && viewType && viewStore) fetchViews();
@ -177,18 +195,34 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
// fetch view by id
useEffect(() => {
const fetchViews = async () => {
viewId && (await viewStore?.fetchById(workspaceSlug, projectId, viewId));
const fetchViewByViewId = async () => {
await viewStore?.fetchById(workspaceSlug, projectId, viewId);
// applyFIltersFromRouter();
};
if (workspaceSlug && viewId && viewType && viewStore) fetchViews();
if (workspaceSlug && viewId && viewType && viewStore) {
fetchViewByViewId();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [workspaceSlug, projectId, viewId, viewType, viewStore]);
console.log("viewStore? -->", viewStore?.viewMapCEN?.id);
return (
<div className="relative w-full h-full">
{viewStore?.loader && viewStore?.loader === "init-loader" ? (
<div className="relative w-full h-full flex justify-center items-center">
<Spinner />
</div>
{viewStore?.loader && viewStore?.loader === "view-loader" ? (
<Loader className="relative w-full flex items-center gap-2 pt-2 pb-1 px-5 border-b border-custom-border-300">
<div>
<Loader.Item height="30px" width="120px" />
<div className="border-t-2 rounded-t border-custom-primary-100" />
</div>
<Loader.Item height="30px" width="120px" />
<Loader.Item height="30px" width="120px" />
<Loader.Item height="30px" width="120px" />
<Loader.Item height="30px" width="120px" />
<div className="ml-auto">
<Loader.Item height="30px" width="120px" />
</div>
</Loader>
) : (
<>
<div className="border-b border-custom-border-200 pt-2">
@ -202,85 +236,102 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
/>
</div>
<div className="p-5 py-2 border-b border-custom-border-200 relative flex items-start gap-1">
<div className="w-full overflow-hidden">
<ViewAppliedFiltersRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
propertyVisibleCount={5}
/>
</div>
{viewStore?.loader === "view-detail-loader" ? (
<Loader className="relative w-full flex items-center gap-2 py-3 px-5 border-b border-custom-border-300">
<div className="mr-auto relative flex items-center gap-2">
<Loader.Item width="140px" height="30px" />
<Loader.Item width="140px" height="30px" />
<Loader.Item width="140px" height="30px" />
<Loader.Item width="140px" height="30px" />
</div>
<Loader.Item width="30px" height="30px" />
<Loader.Item width="30px" height="30px" />
<Loader.Item width="30px" height="30px" />
<Loader.Item width="120px" height="30px" />
</Loader>
) : (
<div className="p-5 py-2 border-b border-custom-border-200 relative flex items-start gap-1">
<div className="w-full overflow-hidden">
<ViewAppliedFiltersRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
propertyVisibleCount={5}
/>
</div>
<div className="flex-shrink-0">
<ViewLayoutRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewPageType={viewPageType}
/>
</div>
<div className="flex-shrink-0">
<ViewLayoutRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewPageType={viewPageType}
/>
</div>
<div className="flex-shrink-0">
<ViewFiltersDropdown
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewPageType={viewPageType}
displayDropdownText={false}
/>
</div>
<div className="flex-shrink-0">
<ViewFiltersDropdown
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewPageType={viewPageType}
displayDropdownText={false}
/>
</div>
<div className="flex-shrink-0">
<ViewDisplayFiltersDropdown
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewPageType={viewPageType}
displayDropdownText={false}
/>
</div>
<div className="flex-shrink-0">
<ViewDisplayFiltersDropdown
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewPageType={viewPageType}
displayDropdownText={false}
/>
</div>
<div className="flex-shrink-0">
<ViewEditDropdown
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewOperations={viewOperations}
/>
</div>
<div className="flex-shrink-0">
<ViewEditDropdown
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewOperations={viewOperations}
/>
</div>
<div className="flex-shrink-0">
<ViewFiltersEditDropdown
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewOperations={viewOperations}
/>
<div className="flex-shrink-0">
<ViewFiltersEditDropdown
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewOperations={viewOperations}
/>
</div>
</div>
</div>
)}
</>
)}
{/* create edit modal */}
{viewStore?.viewMapCEN?.id && (
<ViewCreateEditForm
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewStore?.viewMapCEN?.id}
viewType={viewType}
viewPageType={viewPageType}
viewOperations={viewOperations}
isLocalView={true}
/>
)}
{viewOperationsToggle.type && viewOperationsToggle.viewId && (
<Fragment>
{["CREATE", "EDIT"].includes(viewOperationsToggle.type) && (
<ViewCreateEditForm
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewOperationsToggle.viewId}
viewType={viewType}
viewPageType={viewPageType}
viewOperations={viewOperations}
/>
)}
{["DUPLICATE"].includes(viewOperationsToggle.type) && (
<ViewDuplicateConfirmationModal viewId={viewOperationsToggle.viewId} viewOperations={viewOperations} />
)}

View File

@ -1,15 +1,17 @@
import { LucideIcon } from "lucide-react";
// types
import { TView, TUpdateView } from "@plane/types";
// constants
import { TViewCRUD } from "constants/view";
export type TViewOperations = {
localViewCreateEdit: (viewId: string | undefined, currentView?: TUpdateView | undefined) => void;
localViewCreateEditClear: (viewId: string | undefined) => Promise<void>;
localViewCreateEdit: (viewId: string | undefined, status: TViewCRUD) => void;
fetch: () => Promise<void>;
create: (data: Partial<TView>) => Promise<void>;
update: () => Promise<void>;
remove: (viewId: string) => Promise<void>;
duplicate: (viewId: string) => Promise<void>;
};
// view and view filter edit dropdowns

View File

@ -21,12 +21,13 @@ type TViewCreateEditForm = {
viewType: TViewTypes;
viewPageType: EViewPageType;
viewOperations: TViewOperations;
isLocalView: boolean;
};
export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewPageType, viewOperations } = props;
const { workspaceSlug, projectId, viewId, viewType, viewPageType, viewOperations, isLocalView } = props;
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType, isLocalView);
const { getProjectById } = useProject();
// states
const [modalToggle, setModalToggle] = useState(false);
@ -36,9 +37,9 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
const modalClose = useCallback(() => {
setModalToggle(false);
setTimeout(() => {
viewOperations.localViewCreateEditClear(viewId);
viewOperations.localViewCreateEdit(undefined, "CLEAR");
}, 200);
}, [viewId, setModalToggle, viewOperations]);
}, [setModalToggle, viewOperations]);
useEffect(() => {
if (viewId) modalOpen();
@ -46,16 +47,16 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
const onContinue = async () => {
setLoader(true);
if (viewDetailStore?.is_create) {
const payload = viewDetailStore?.filtersToUpdate;
await viewOperations.create(payload);
modalClose();
} else {
const payload = viewDetailStore?.filtersToUpdate;
if (!payload) return;
await viewOperations.update();
modalClose();
}
// if (viewDetailStore?.id != "create") {
// const payload = viewDetailStore?.filtersToUpdate;
// await viewOperations.create(payload);
// modalClose();
// } else {
// const payload = viewDetailStore?.filtersToUpdate;
// if (!payload) return;
// await viewOperations.update();
// modalClose();
// }
setLoader(false);
};
@ -131,6 +132,7 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
viewType={viewType}
viewPageType={viewPageType}
dropdownPlacement="right"
isLocalView={isLocalView}
>
<div className="cursor-pointer relative rounded p-1.5 px-2 flex items-center gap-1 border border-custom-border-100 bg-custom-background-80">
<div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4 overflow-hidden">
@ -161,6 +163,7 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
viewId={viewId}
viewType={viewType}
propertyVisibleCount={undefined}
isLocalView={isLocalView}
/>
</div>
@ -169,7 +172,7 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
Cancel
</Button>
<Button variant="primary" onClick={onContinue} disabled={loader}>
{loader ? `Saving...` : `${viewDetailStore?.is_create ? `Create` : `Update`} View`}
{loader ? `Saving...` : `${viewDetailStore?.id === "create" ? `Create` : `Update`} View`}
</Button>
</div>
</Dialog.Panel>

View File

@ -3,18 +3,24 @@ import { observer } from "mobx-react-lite";
import { Menu, Transition } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Copy, Eye, Globe2, Link2, Pencil, Trash } from "lucide-react";
// hooks
import { useViewDetail } from "hooks/store";
// types
import { TViewEditDropdownOptions, TViewOperations } from "../types";
import { TViewTypes } from "@plane/types";
type TViewEditDropdown = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string;
viewType: TViewTypes;
viewOperations: TViewOperations;
};
export const ViewEditDropdown: FC<TViewEditDropdown> = observer((props) => {
const { viewId, viewOperations } = props;
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
// popper-js refs
@ -46,7 +52,7 @@ export const ViewEditDropdown: FC<TViewEditDropdown> = observer((props) => {
icon: Pencil,
key: "rename",
label: "Rename",
onClick: () => viewOperations.localViewCreateEdit(viewId),
onClick: () => viewOperations.localViewCreateEdit(viewId, "EDIT"),
children: undefined,
},
{
@ -96,6 +102,7 @@ export const ViewEditDropdown: FC<TViewEditDropdown> = observer((props) => {
[viewOperations, viewId]
);
if (viewDetailStore?.is_local_view) return <></>;
return (
<Menu as="div" className="relative flex-shrink-0" ref={dropdownRef}>
<Menu.Button

View File

@ -1,4 +1,4 @@
import { FC, Fragment, useEffect, useMemo, useState } from "react";
import { FC, Fragment, useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// hooks
@ -31,7 +31,7 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
const handleViewTabsVisibility = () => {
const tabContainer = document.getElementById("tab-container");
const tabItemViewMore = document.getElementById("tab-item-view-more");
const itemWidth = 124;
const itemWidth = 122;
if (!tabContainer || !tabItemViewMore) return;
const containerWidth = tabContainer.clientWidth;
@ -49,14 +49,11 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
return () => window.removeEventListener("resize", () => handleViewTabsVisibility());
}, [viewStore?.viewIds]);
const viewIds = useMemo(() => {
const ids = viewStore?.viewIds?.slice(0, itemsToRenderViewsCount || viewStore?.viewIds.length) || [];
if (!ids.includes(viewId)) {
ids.pop();
ids.push(viewId);
}
return ids;
}, [viewId, viewStore, itemsToRenderViewsCount]);
const viewIds = viewStore?.viewIds?.slice(0, itemsToRenderViewsCount || viewStore?.viewIds.length) || [];
if (!viewIds.includes(viewId)) {
viewIds.pop();
viewIds.push(viewId);
}
return (
<div className="relative flex justify-between px-5 gap-2">
@ -102,7 +99,11 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
</div>
<div className="flex-shrink-0 my-auto pb-1">
<Button size="sm" prependIcon={<Plus />} onClick={() => viewOperations?.localViewCreateEdit(undefined)}>
<Button
size="sm"
prependIcon={<Plus />}
onClick={() => viewOperations?.localViewCreateEdit(undefined, "CREATE")}
>
New View
</Button>
</div>

View File

@ -134,7 +134,7 @@ export const ViewDropdown: FC<TViewDropdown> = (props) => {
<div
className="relative flex justify-center items-center gap-1 rounded p-1 py-1.5 transition-all border border-custom-border-200 bg-custom-background-90 hover:bg-custom-background-80 text-custom-text-300 hover:text-custom-text-200 cursor-pointer"
onClick={() => viewOperations?.localViewCreateEdit(undefined)}
onClick={() => viewOperations?.localViewCreateEdit(undefined, "CREATE")}
>
<Plus className="w-3 h-3" />
<div className="text-sm">New view</div>

View File

@ -1,4 +1,3 @@
import { v4 as uuidV4 } from "uuid";
// types
import { TViewTypes, TView } from "@plane/types";
@ -9,13 +8,20 @@ export const VIEW_TYPES: Record<TViewTypes, TViewTypes> = {
PROJECT_PUBLIC_VIEWS: "PROJECT_PUBLIC_VIEWS",
};
export type TViewCRUD = "CREATE" | "EDIT" | "SAVE_AS_NEW" | "CLEAR";
export const viewLocalPayload: Partial<TView> = {
id: uuidV4(),
id: "create",
name: "",
description: "",
filters: undefined,
display_filters: undefined,
display_properties: undefined,
is_local_view: false,
is_create: true,
};
export const generateViewStoreKey = (
workspaceSlug: string,
projectId: string | undefined,
viewType: TViewTypes
): string => `${workspaceSlug}_${projectId}_${viewType}`;

View File

@ -11,8 +11,9 @@ import { VIEW_TYPES } from "constants/view";
export const useViewDetail = (
workspaceSlug: string,
projectId: string | undefined,
viewId: string,
viewType: TViewTypes | undefined
viewId: string | undefined,
viewType: TViewTypes | undefined,
isEditable: boolean = false
): TViewStore | undefined => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useViewDetail must be used within StoreProvider");
@ -21,14 +22,18 @@ export const useViewDetail = (
switch (viewType) {
case VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS:
if (isEditable) return context.view.workspacePrivateViewStore.viewMapCEN;
return context.view.workspacePrivateViewStore.viewById(viewId);
case VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS:
if (isEditable) return context.view.workspacePrivateViewStore.viewMapCEN;
return context.view.workspacePublicViewStore.viewById(viewId);
case VIEW_TYPES.PROJECT_PRIVATE_VIEWS:
if (!projectId) return undefined;
if (isEditable) return context.view.workspacePrivateViewStore.viewMapCEN;
return context.view.projectPrivateViewStore.viewById(viewId);
case VIEW_TYPES.PROJECT_PUBLIC_VIEWS:
if (!projectId) return undefined;
if (isEditable) return context.view.workspacePrivateViewStore.viewMapCEN;
return context.view.projectPublicViewStore.viewById(viewId);
default:
return undefined;

View File

@ -8,7 +8,7 @@ import { TViewTypes } from "@plane/types";
import { VIEW_TYPES } from "constants/view";
export const useView = (
workspaceSlug: string,
workspaceSlug: string | undefined,
projectId: string | undefined,
viewType: TViewTypes | undefined
): ViewRootStore | undefined => {

View File

@ -36,8 +36,10 @@ const ProjectPublicViewPage: NextPageWithLayout = observer(() => {
? `PROJECT_VIEWS_${VIEW_TYPES.PROJECT_PUBLIC_VIEWS}_${workspaceSlug.toString()}_${projectId.toString()}`
: null,
async () => {
await viewStore?.fetch();
console.log("viewStore", viewStore?.viewIds);
if (workspaceSlug && projectId) {
await viewStore?.fetch(workspaceSlug?.toString(), projectId?.toString());
console.log("viewStore", viewStore?.viewIds);
}
}
);
@ -66,7 +68,7 @@ const ProjectPublicViewPage: NextPageWithLayout = observer(() => {
if (!workspaceSlug || !projectId) return <></>;
return (
<div className="relative w-full h-full flex flex-col overflow-hidden">
{viewStore?.loader === "init-loader" ? (
{viewStore?.loader === "view-loader" ? (
<div className="relative w-full h-full flex justify-center items-center">
<Spinner />
</div>

View File

@ -36,8 +36,10 @@ const ProjectPublicViewPage: NextPageWithLayout = observer(() => {
? `PROJECT_VIEWS_${VIEW_TYPES.PROJECT_PUBLIC_VIEWS}_${workspaceSlug.toString()}_${projectId.toString()}`
: null,
async () => {
await viewStore?.fetch();
console.log("viewStore", viewStore?.viewIds);
if (workspaceSlug && projectId) {
await viewStore?.fetch(workspaceSlug.toString(), projectId.toString());
console.log("viewStore", viewStore?.viewIds);
}
}
);
@ -66,7 +68,7 @@ const ProjectPublicViewPage: NextPageWithLayout = observer(() => {
if (!workspaceSlug || !projectId) return <></>;
return (
<div className="relative w-full h-full flex flex-col overflow-hidden">
{viewStore?.loader === "init-loader" ? (
{viewStore?.loader === "view-loader" ? (
<div className="relative w-full h-full flex justify-center items-center">
<Spinner />
</div>

View File

@ -5,10 +5,11 @@ import { CheckCircle } from "lucide-react";
import { AppLayout } from "layouts/app-layout";
// components
import { GlobalViewRoot, ViewHeader } from "components/view";
import { GlobalViewIssueLayoutRoot } from "components/issues";
// types
import { NextPageWithLayout } from "lib/types";
// constants
import { EViewPageType, VIEW_TYPES } from "constants/view";
import { ELocalViews, EViewPageType, VIEW_TYPES } from "constants/view";
const WorkspacePrivateViewPage: NextPageWithLayout = () => {
const router = useRouter();
@ -19,12 +20,12 @@ const WorkspacePrivateViewPage: NextPageWithLayout = () => {
{
key: VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS,
title: "Private",
href: `/${workspaceSlug}/views/private/assigned`,
href: `/${workspaceSlug}/views/private/${ELocalViews.ASSIGNED}`,
},
{
key: VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS,
title: "Public",
href: `/${workspaceSlug}/views/public/all-issues`,
href: `/${workspaceSlug}/views/public/${ELocalViews.ALL_ISSUES}`,
},
],
[workspaceSlug]
@ -44,7 +45,9 @@ const WorkspacePrivateViewPage: NextPageWithLayout = () => {
workspaceViewTabOptions={workspaceViewTabOptions}
/>
</div>
</div>
<div className="flex-shrink-0 w-full">
{/* content */}
<GlobalViewRoot
workspaceSlug={workspaceSlug.toString()}
@ -56,9 +59,24 @@ const WorkspacePrivateViewPage: NextPageWithLayout = () => {
/>
</div>
<div className="w-full h-full overflow-hidden relative flex justify-center items-center text-sm text-custom-text-300">
Issues render placeholder
{/* issues */}
<div className="relative w-full h-full overflow-hidden">
<GlobalViewIssueLayoutRoot
workspaceSlug={workspaceSlug.toString()}
projectId={undefined}
viewId={viewId.toString()}
viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS}
viewPageType={EViewPageType.ALL}
/>
</div>
{/* TODO: once the functionality is done implement the empty states */}
{/* <ViewEmptyStateRoot
workspaceSlug={workspaceSlug.toString()}
projectId={undefined}
viewId={viewId.toString()}
viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS}
></ViewEmptyStateRoot> */}
</div>
);
};

View File

@ -5,6 +5,7 @@ import { CheckCircle } from "lucide-react";
import { AppLayout } from "layouts/app-layout";
// components
import { GlobalViewRoot, ViewHeader } from "components/view";
import { GlobalViewIssueLayoutRoot } from "components/issues";
// types
import { NextPageWithLayout } from "lib/types";
// constants
@ -44,7 +45,9 @@ const WorkspacePublicViewPage: NextPageWithLayout = () => {
workspaceViewTabOptions={workspaceViewTabOptions}
/>
</div>
</div>
<div className="flex-shrink-0 w-full">
{/* content */}
<GlobalViewRoot
workspaceSlug={workspaceSlug.toString()}
@ -56,8 +59,15 @@ const WorkspacePublicViewPage: NextPageWithLayout = () => {
/>
</div>
<div className="w-full h-full overflow-hidden relative flex justify-center items-center text-sm text-custom-text-300">
Issues render placeholder
{/* issues */}
<div className="relative w-full h-full overflow-hidden">
<GlobalViewIssueLayoutRoot
workspaceSlug={workspaceSlug.toString()}
projectId={undefined}
viewId={viewId.toString()}
viewType={VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS}
viewPageType={EViewPageType.ALL}
/>
</div>
</div>
);

View File

@ -7,7 +7,8 @@ import { WorkspaceService } from "services/workspace.service";
import { IssueService } from "services/issue";
// types
import { IIssueRootStore } from "../root.store";
import { TIssue, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types";
import { TIssue, TLoader, TUnGroupedIssues, TViewDisplayFilters, ViewFlags } from "@plane/types";
import { VIEW_TYPES, generateViewStoreKey } from "constants/view";
export interface IWorkspaceIssues {
// observable
@ -17,7 +18,7 @@ export interface IWorkspaceIssues {
// computed
groupedIssueIds: { dataViewId: string; issueIds: TUnGroupedIssues | undefined };
// actions
fetchIssues: (workspaceSlug: string, viewId: string, loadType: TLoader) => Promise<TIssue[]>;
fetchIssues: (workspaceSlug: string, viewId: string, loadType: TLoader, query?: any) => Promise<TIssue[]>;
createIssue: (
workspaceSlug: string,
projectId: string,
@ -76,39 +77,52 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
}
get groupedIssueIds() {
const viewId = this.rootIssueStore.globalViewId;
const workspaceSlug = this.rootIssueStore.workspaceSlug;
if (!workspaceSlug || !viewId) return { dataViewId: "", issueIds: undefined };
const { workspaceSlug, projectId, currentViewType, currentViewId } = this.rootIssueStore.rootStore.view;
if (!workspaceSlug || !currentViewId || !currentViewType) return { dataViewId: "", issueIds: undefined };
const uniqueViewId = `${workspaceSlug}_${viewId}`;
const viewRootKey = generateViewStoreKey(workspaceSlug, projectId, currentViewType);
let displayFilters: TViewDisplayFilters | undefined = undefined;
const displayFilters = this.rootIssueStore?.workspaceIssuesFilter?.filters?.[viewId]?.displayFilters;
if (!displayFilters) return { dataViewId: viewId, issueIds: undefined };
if (currentViewType === VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS)
displayFilters =
this.rootIssueStore?.rootStore?.view?.workspacePrivateViewStore?.viewMap?.[viewRootKey]?.[currentViewId]
?.appliedFilters?.display_filters;
else if (currentViewType === VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS)
displayFilters =
this.rootIssueStore?.rootStore?.view?.workspacePublicViewStore?.viewMap?.[viewRootKey]?.[currentViewId]
?.appliedFilters?.display_filters;
if (!displayFilters) return { dataViewId: currentViewId, issueIds: undefined };
const orderBy = displayFilters?.order_by;
const uniqueViewId = `${workspaceSlug}_${currentViewId}`;
const viewIssueIds = this.issues[uniqueViewId];
if (!viewIssueIds) return { dataViewId: viewId, issueIds: undefined };
if (!viewIssueIds) return { dataViewId: currentViewId, issueIds: undefined };
const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds);
if (!_issues) return { dataViewId: viewId, issueIds: [] };
if (!_issues) return { dataViewId: currentViewId, issueIds: [] };
let issueIds: TIssue | TUnGroupedIssues | undefined = undefined;
issueIds = this.unGroupedIssues(orderBy ?? "-created_at", _issues);
return { dataViewId: viewId, issueIds };
return { dataViewId: currentViewId, issueIds };
}
fetchIssues = async (workspaceSlug: string, viewId: string, loadType: TLoader = "init-loader") => {
fetchIssues = async (
workspaceSlug: string,
viewId: string,
loadType: TLoader = "init-loader",
query: any = undefined
) => {
try {
this.loader = loadType;
const uniqueViewId = `${workspaceSlug}_${viewId}`;
const params = this.rootIssueStore?.workspaceIssuesFilter?.getAppliedFilters(viewId);
const response = await this.workspaceService.getViewIssues(workspaceSlug, params);
const response = await this.workspaceService.getViewIssues(workspaceSlug, query || {});
runInAction(() => {
set(

View File

@ -101,8 +101,10 @@ export class FiltersHelper {
Object.keys(filteredParams).forEach((key) => {
const _key = key as TViewFilterQueryParams;
const _value: string | boolean | string[] | undefined = filteredParams[_key];
if (_value != undefined && acceptableParamsByLayout.includes(_key))
paramsObject[_key] = Array.isArray(_value) ? _value.join(",") : _value;
if (_value != undefined && acceptableParamsByLayout.includes(_key)) {
if (Array.isArray(_value)) _value.length > 0 && (paramsObject[_key] = _value.join(","));
else paramsObject[_key] = _value;
}
});
if (paramsObject && !isEmpty(paramsObject)) {
@ -110,7 +112,7 @@ export class FiltersHelper {
.map((key) => {
const _key = key as TViewFilterQueryParams;
const _value: string | boolean | undefined = paramsObject[_key];
if (!undefined) return `${_key}=${_value}`;
if (_value) return `${_key}=${_value}`;
})
.join("&");
}

View File

@ -1,3 +1,4 @@
import { action, autorun, makeObservable, observable, runInAction } from "mobx";
// stores
import { ViewRootStore } from "./view-root.store";
// services
@ -13,14 +14,39 @@ import {
import { RootStore } from "store/root.store";
// constants
import { EViewPageType, VIEW_TYPES } from "constants/view";
import { TViewTypes } from "@plane/types";
export class GlobalViewRootStore {
workspaceSlug: string | undefined = undefined;
projectId: string | undefined = undefined;
currentViewId: string | undefined = undefined;
currentViewType: TViewTypes | undefined = undefined;
currentUserId: string | undefined = undefined;
workspacePrivateViewStore: ViewRootStore;
workspacePublicViewStore: ViewRootStore;
projectPrivateViewStore: ViewRootStore;
projectPublicViewStore: ViewRootStore;
constructor(private store: RootStore) {
makeObservable(this, {
workspaceSlug: observable.ref,
projectId: observable.ref,
currentViewId: observable.ref,
currentViewType: observable.ref,
currentUserId: observable.ref,
// actions
setWorkspaceSlug: action,
setProjectId: action,
setCurrentViewId: action,
setCurrentViewType: action,
setCurrentUserId: action,
});
autorun(() => {
this.currentUserId = store.user.currentUser?.id;
});
const workspacePrivateDefaultViews: any[] = [
{
id: "assigned",
@ -80,4 +106,11 @@ export class GlobalViewRootStore {
VIEW_TYPES.PROJECT_PUBLIC_VIEWS
);
}
// helper actions
setWorkspaceSlug = (workspaceSlug: string | undefined) => runInAction(() => (this.workspaceSlug = workspaceSlug));
setProjectId = (projectId: string | undefined) => runInAction(() => (this.projectId = projectId));
setCurrentViewId = (viewId: string | undefined) => runInAction(() => (this.currentViewId = viewId));
setCurrentViewType = (viewType: TViewTypes | undefined) => runInAction(() => (this.currentViewType = viewType));
setCurrentUserId = (userId: string | undefined) => runInAction(() => (this.currentUserId = userId));
}

View File

@ -3,6 +3,7 @@ import { computedFn } from "mobx-utils";
import set from "lodash/set";
import sortBy from "lodash/sortBy";
import reverse from "lodash/reverse";
import cloneDeep from "lodash/cloneDeep";
// stores
import { RootStore } from "store/root.store";
import { ViewStore } from "./view.store";
@ -10,30 +11,40 @@ import { ViewStore } from "./view.store";
import { TUserViewService, TViewService } from "services/view/types";
import { TView, TViewTypes } from "@plane/types";
// constants
import { EViewPageType } from "constants/view";
import { EViewPageType, TViewCRUD, generateViewStoreKey, viewLocalPayload } from "constants/view";
export type TLoader = "init-loader" | "mutation-loader" | "submitting" | undefined;
export type TLoader =
| "view-loader"
| "view-mutation-loader"
| "view-detail-loader"
| "create-submitting"
| "delete-submitting"
| "duplicate-submitting"
| undefined;
type TViewRootStore = {
// observables
loader: TLoader;
viewMap: Record<string, Record<string, Record<string, ViewStore>>>; // workspaceSlug/projectId, public/private, viewId -> ViewStore
viewMapCEN: ViewStore | undefined; // view map Create, Edit, and save as New
viewMap: Record<string, Record<string, ViewStore>>; // workspaceSlug/projectId/TViewType.toString(), viewId -> ViewStore
// computed
viewIds: string[];
viewById: (viewId: string) => ViewStore | undefined;
localView: () => ViewStore | undefined;
// actions
localViewCreate: (workspaceSlug: string, projectId: string | undefined, view: TView) => Promise<void>;
fetch: (workspaceSlug: string, projectId: string | undefined, _loader?: TLoader) => Promise<void>;
fetchById: (workspaceSlug: string, projectId: string | undefined, viewId: string) => Promise<void>;
create: (workspaceSlug: string, projectId: string | undefined, view: Partial<TView>) => Promise<void>;
remove: (workspaceSlug: string, projectId: string | undefined, viewId: string) => Promise<void>;
duplicate: (workspaceSlug: string, projectId: string | undefined, viewId: string) => Promise<void>;
remove: (viewId: string) => Promise<void>;
localViewHandler: (viewId: string | undefined, status: TViewCRUD) => void;
create: () => Promise<void>;
duplicate: (viewId: string) => Promise<void>;
};
export class ViewRootStore implements TViewRootStore {
// observables
loader: TLoader = "init-loader";
viewMap: Record<string, Record<string, Record<string, ViewStore>>> = {};
loader: TLoader = "view-loader";
viewMapCEN: ViewStore | undefined = undefined;
viewMap: Record<string, Record<string, ViewStore>> = {};
constructor(
private store: RootStore,
@ -46,11 +57,12 @@ export class ViewRootStore implements TViewRootStore {
makeObservable(this, {
// observables
loader: observable.ref,
viewMapCEN: observable,
viewMap: observable,
// computed
viewIds: computed,
// actions
localViewCreate: action,
localViewHandler: action,
fetch: action,
fetchById: action,
create: action,
@ -61,56 +73,46 @@ export class ViewRootStore implements TViewRootStore {
// computed
get viewIds() {
const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug) return [];
const { workspaceSlug, projectId, currentViewType } = this.store.view;
if (!workspaceSlug || !currentViewType) return [];
const viewRootSlug = projectId ? projectId : workspaceSlug;
const views = this.viewMap?.[viewRootSlug]?.[this.viewType]
? Object.values(this.viewMap?.[viewRootSlug]?.[this.viewType])
: [];
const viewRootSlug = generateViewStoreKey(workspaceSlug, projectId, currentViewType);
const views = this.viewMap?.[viewRootSlug] ? Object.values(this.viewMap?.[viewRootSlug]) : [];
const localViews = views.filter((view) => view.is_local_view);
let apiViews = views.filter((view) => !view.is_local_view && !view.is_create);
let apiViews = views.filter((view) => !view.is_local_view);
apiViews = reverse(sortBy(apiViews, "sort_order"));
const _viewIds = [...localViews.map((view) => view.id), ...apiViews.map((view) => view.id)];
return _viewIds as string[];
}
viewById = computedFn((viewId: string) => {
const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug) return undefined;
const { workspaceSlug, projectId, currentViewType } = this.store.view;
if (!workspaceSlug || !currentViewType) return undefined;
const viewRootSlug = projectId ? projectId : workspaceSlug;
return this.viewMap?.[viewRootSlug]?.[this.viewType]?.[viewId] || undefined;
const viewRootSlug = generateViewStoreKey(workspaceSlug, projectId, currentViewType);
return this.viewMap?.[viewRootSlug]?.[viewId] || undefined;
});
localView = computedFn(() => this.viewMapCEN);
// actions
localViewCreate = async (workspaceSlug: string, projectId: string | undefined, view: TView) => {
const viewRootSlug = projectId ? projectId : workspaceSlug;
runInAction(() => {
if (view.id)
set(
this.viewMap,
[viewRootSlug, this.viewType, view.id],
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
);
});
};
fetch = async (workspaceSlug: string, projectId: string | undefined, _loader: TLoader = "init-loader") => {
fetch = async (workspaceSlug: string, projectId: string | undefined, _loader: TLoader = "view-loader") => {
try {
const viewRootSlug = projectId ? projectId : workspaceSlug;
runInAction(() => (this.loader = _loader));
this.loader = _loader;
this.store.view.setWorkspaceSlug(workspaceSlug);
this.store.view.setProjectId(projectId);
this.store.view.setCurrentViewType(this.viewType);
const viewRootSlug = generateViewStoreKey(workspaceSlug, projectId, this.viewType);
if (this.defaultViews && this.defaultViews.length > 0)
runInAction(() => {
this.defaultViews?.forEach((view) => {
if (view.id)
set(
this.viewMap,
[viewRootSlug, this.viewType, view.id],
[viewRootSlug, view.id],
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
);
});
@ -118,100 +120,155 @@ export class ViewRootStore implements TViewRootStore {
const views = await this.service.fetch(workspaceSlug, projectId);
if (!views) return;
runInAction(() => {
views.forEach((view) => {
if (view.id)
set(
this.viewMap,
[viewRootSlug, this.viewType, view.id],
[viewRootSlug, view.id],
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
);
});
this.loader = undefined;
});
} catch {}
} catch {
runInAction(() => (this.loader = undefined));
}
};
fetchById = async (workspaceSlug: string, projectId: string | undefined, viewId: string) => {
try {
const viewRootSlug = projectId ? projectId : workspaceSlug;
runInAction(() => (this.loader = "view-detail-loader"));
this.store.view.setWorkspaceSlug(workspaceSlug);
this.store.view.setProjectId(projectId);
this.store.view.setCurrentViewId(viewId);
this.store.view.setCurrentViewType(this.viewType);
const viewRootSlug = generateViewStoreKey(workspaceSlug, projectId, this.viewType);
const userView = await this.userService.fetch(workspaceSlug, projectId);
if (!userView) return;
let view: TView | undefined = undefined;
if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) {
const currentView = { ...this.viewById(viewId) } as TView;
if (!currentView) return;
view = currentView;
view.filters = userView.filters;
view.display_filters = userView.display_filters;
view.display_properties = userView.display_properties;
} else {
const currentView = await this.service.fetchById(workspaceSlug, viewId, projectId);
if (!currentView) return;
view = currentView;
view?.display_filters && (view.display_filters = userView.display_filters);
view?.display_properties && (view.display_properties = userView.display_properties);
}
view?.display_filters && (view.display_filters = userView.display_filters);
view?.display_properties && (view.display_properties = userView.display_properties);
if (!view) return;
runInAction(() => {
if (view?.id)
set(
this.viewMap,
[viewRootSlug, this.viewType, view.id],
[viewRootSlug, view.id],
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
);
});
} catch {}
// fetching the issues
const filterParams = this.viewMap?.[viewRootSlug]?.[viewId]?.appliedFiltersQueryParams?.params;
this.store.issue.workspaceIssues.fetchIssues(workspaceSlug, viewId, "init-loader", filterParams);
runInAction(() => (this.loader = undefined));
} catch {
runInAction(() => (this.loader = undefined));
}
};
create = async (workspaceSlug: string, projectId: string | undefined, data: Partial<TView>) => {
remove = async (viewId: string) => {
try {
const viewRootSlug = projectId ? projectId : workspaceSlug;
const { workspaceSlug, projectId, currentViewType } = this.store.view;
if (!workspaceSlug || !currentViewType) return undefined;
const view = await this.service.create(workspaceSlug, data, projectId);
runInAction(() => (this.loader = "delete-submitting"));
const viewRootSlug = generateViewStoreKey(workspaceSlug, projectId, this.viewType);
await this.service.remove?.(workspaceSlug, viewId, projectId);
runInAction(() => {
delete this.viewMap?.[viewRootSlug]?.[viewId];
this.loader = undefined;
});
} catch {
runInAction(() => (this.loader = undefined));
}
};
localViewHandler = (viewId: string | undefined, status: TViewCRUD) => {
const { workspaceSlug, projectId, currentViewType } = this.store.view;
if (!workspaceSlug || !currentViewType) return undefined;
if (status === "CLEAR") {
runInAction(() => (this.viewMapCEN = undefined));
return;
}
const viewRootSlug = generateViewStoreKey(workspaceSlug, projectId, currentViewType);
let _view: Partial<TView> = {};
if (status === "CREATE") _view = cloneDeep(viewLocalPayload);
else if (status === "EDIT") {
if (!viewId) return;
_view = cloneDeep(this.viewMap?.[viewRootSlug]?.[viewId]);
} else if (status === "SAVE_AS_NEW") {
if (!viewId) return;
_view = cloneDeep(this.viewMap?.[viewRootSlug]?.[viewId]);
} else return;
runInAction(() => {
if (_view.id)
set(
this,
["viewMapCEN"],
new ViewStore(this.store, _view as TView, this.service, this.userService, this.viewPageType)
);
});
};
create = async () => {
try {
const { workspaceSlug, projectId } = this.store.view;
if (!workspaceSlug || !this.viewMapCEN) return;
runInAction(() => (this.loader = "create-submitting"));
const viewRootSlug = generateViewStoreKey(workspaceSlug, projectId, this.viewType);
const view = await this.service.create(workspaceSlug, this.viewMapCEN, projectId);
if (!view) return;
runInAction(() => {
if (view.id)
set(
this.viewMap,
[viewRootSlug, this.viewType, view.id],
[viewRootSlug, view.id],
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
);
this.viewMapCEN = undefined;
this.loader = undefined;
});
if (data.id) this.remove(workspaceSlug, projectId, data.id);
} catch {}
} catch {
runInAction(() => (this.loader = undefined));
}
};
remove = async (workspaceSlug: string, projectId: string | undefined, viewId: string) => {
duplicate = async (viewId: string) => {
try {
const viewRootSlug = projectId ? projectId : workspaceSlug;
const { workspaceSlug, projectId } = this.store.view;
if (!workspaceSlug || !this.service.duplicate) return;
if (
this.viewMap?.[viewRootSlug]?.[this.viewType]?.[viewId] != undefined &&
!this.viewMap?.[viewRootSlug]?.[this.viewType]?.[viewId]?.is_create
)
await this.service.remove?.(workspaceSlug, viewId, projectId);
runInAction(() => (this.loader = "duplicate-submitting"));
runInAction(() => {
delete this.viewMap?.[viewRootSlug]?.[this.viewType]?.[viewId];
});
} catch {}
};
duplicate = async (workspaceSlug: string, projectId: string | undefined, viewId: string) => {
try {
if (!this.service.duplicate) return;
const viewRootSlug = projectId ? projectId : workspaceSlug;
const viewRootSlug = generateViewStoreKey(workspaceSlug, projectId, this.viewType);
const view = await this.service.duplicate(workspaceSlug, viewId, projectId);
if (!view) return;
@ -220,10 +277,13 @@ export class ViewRootStore implements TViewRootStore {
if (view.id)
set(
this.viewMap,
[viewRootSlug, this.viewType, view.id],
[viewRootSlug, view.id],
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
);
this.loader = undefined;
});
} catch {}
} catch {
runInAction(() => (this.loader = undefined));
}
};
}

View File

@ -36,7 +36,7 @@ export type TViewStore = TView & {
filtersToUpdate: TUpdateView;
// computed
appliedFilters: TViewFilterProps | undefined;
appliedFiltersQueryParams: string | undefined;
appliedFiltersQueryParams: { params: Object; query: string } | undefined;
isFiltersApplied: boolean;
isFiltersUpdateEnabled: boolean;
// helper actions
@ -45,7 +45,6 @@ export type TViewStore = TView & {
setFilters: (filterKey: keyof TViewFilters | undefined, filterValue: "clear_all" | string) => void;
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) => void;
setIsEditable: (id_editable: boolean) => void;
resetChanges: () => void;
saveChanges: () => Promise<void>;
// actions
@ -77,8 +76,6 @@ export class ViewStore extends FiltersHelper implements TViewStore {
created_at: Date | undefined;
updated_at: Date | undefined;
is_local_view: boolean = false;
is_create: boolean = false;
is_editable: boolean = false;
loader: TLoader = undefined;
filtersToUpdate: TUpdateView;
@ -110,8 +107,6 @@ export class ViewStore extends FiltersHelper implements TViewStore {
this.created_at = _view.created_at;
this.updated_at = _view.updated_at;
this.is_local_view = _view.is_local_view;
this.is_create = _view.is_create;
this.is_editable = _view.is_editable;
this.filtersToUpdate = {
name: this.name,
description: this.description,
@ -142,8 +137,7 @@ export class ViewStore extends FiltersHelper implements TViewStore {
created_at: observable.ref,
updated_at: observable.ref,
is_local_view: observable.ref,
is_create: observable.ref,
is_editable: observable.ref,
loader: observable.ref,
filtersToUpdate: observable,
// computed
@ -156,7 +150,6 @@ export class ViewStore extends FiltersHelper implements TViewStore {
setFilters: action,
setDisplayFilters: action,
setDisplayProperties: action,
setIsEditable: action,
resetChanges: action,
saveChanges: action,
// actions
@ -197,8 +190,7 @@ export class ViewStore extends FiltersHelper implements TViewStore {
layout,
EFilterTypes.FILTERS
);
return this.computeAppliedFiltersQueryParameters(appliedFilters, requiredFilterProperties)?.query || undefined;
return this.computeAppliedFiltersQueryParameters(appliedFilters, requiredFilterProperties) || undefined;
}
get isFiltersApplied() {
@ -247,6 +239,12 @@ export class ViewStore extends FiltersHelper implements TViewStore {
return concat(_values, filterValue);
});
});
const { workspaceSlug } = this.store.view;
if (workspaceSlug && this.id) {
const filterParams = this.appliedFiltersQueryParams?.params;
this.store.issue.workspaceIssues.fetchIssues(workspaceSlug, this.id, "mutation", filterParams);
}
};
setDisplayFilters = async (display_filters: Partial<TViewDisplayFilters>) => {
@ -288,12 +286,6 @@ export class ViewStore extends FiltersHelper implements TViewStore {
this.updateUserDisplayProperties({ [EFilterTypes.DISPLAY_PROPERTIES]: this.filtersToUpdate.display_properties });
};
setIsEditable = (is_editable: boolean) => {
runInAction(() => {
this.is_editable = is_editable;
});
};
resetChanges = () => {
runInAction(() => {
const _view = cloneDeep(this);
@ -310,11 +302,8 @@ export class ViewStore extends FiltersHelper implements TViewStore {
saveChanges = async () => {
try {
if (!this.id) return;
if (["all-issues", "assigned", "created", "subscribed"].includes(this.id)) {
const payload = this.filtersToUpdate.filters;
await this.updateUserFilters({ [EFilterTypes.FILTERS]: payload });
} else await this.update(this.filtersToUpdate);
console.log("coming here");
await this.update(this.filtersToUpdate);
} catch {
Object.keys(this.filtersToUpdate).forEach((key) => {
const _key = key as keyof TUpdateView;
@ -326,13 +315,14 @@ export class ViewStore extends FiltersHelper implements TViewStore {
// actions
update = async (viewData: TUpdateView) => {
try {
if (!this.workspace || !this.id) return;
const { workspaceSlug, projectId } = this.store.view;
if (!workspaceSlug || !this.id) return;
runInAction(() => {
this.loader = "updating";
});
const view = await this.service.update(this.workspace, this.id, viewData, this.project);
const view = await this.service.update(workspaceSlug, this.id, viewData, projectId);
if (!view) return;
runInAction(() => {
@ -349,9 +339,10 @@ export class ViewStore extends FiltersHelper implements TViewStore {
lockView = async () => {
try {
if (!this.workspace || !this.id || !this.service.lock) return;
const { workspaceSlug, projectId } = this.store.view;
if (!workspaceSlug || !this.id || !this.service.lock) return;
const view = await this.service.lock(this.workspace, this.id, this.project);
const view = await this.service.lock(workspaceSlug, this.id, projectId);
if (!view) return;
runInAction(() => {
@ -364,9 +355,10 @@ export class ViewStore extends FiltersHelper implements TViewStore {
unlockView = async () => {
try {
if (!this.workspace || !this.id || !this.service.unlock) return;
const { workspaceSlug, projectId } = this.store.view;
if (!workspaceSlug || !this.id || !this.service.unlock) return;
const view = await this.service.unlock(this.workspace, this.id, this.project);
const view = await this.service.unlock(workspaceSlug, this.id, projectId);
if (!view) return;
runInAction(() => {
@ -379,9 +371,10 @@ export class ViewStore extends FiltersHelper implements TViewStore {
makeFavorite = async () => {
try {
if (!this.workspace || !this.id || !this.service.makeFavorite) return;
const { workspaceSlug, projectId } = this.store.view;
if (!workspaceSlug || !this.id || !this.service.makeFavorite) return;
const view = await this.service.makeFavorite(this.workspace, this.id, this.project);
const view = await this.service.makeFavorite(workspaceSlug, this.id, projectId);
if (!view) return;
runInAction(() => {
@ -394,9 +387,10 @@ export class ViewStore extends FiltersHelper implements TViewStore {
removeFavorite = async () => {
try {
if (!this.workspace || !this.id || !this.service.removeFavorite) return;
const { workspaceSlug, projectId } = this.store.view;
if (!workspaceSlug || !this.id || !this.service.removeFavorite) return;
const view = await this.service.removeFavorite(this.workspace, this.id, this.project);
const view = await this.service.removeFavorite(workspaceSlug, this.id, projectId);
if (!view) return;
runInAction(() => {
@ -410,7 +404,7 @@ export class ViewStore extends FiltersHelper implements TViewStore {
// updating the user specific filters, display filters, and display properties
updateUserFilters = async (filters: { [EFilterTypes.FILTERS]: TViewFilters }) => {
try {
const { workspaceSlug, projectId } = this.store.app.router;
const { workspaceSlug, projectId } = this.store.view;
if (!workspaceSlug) return;
const userView = await this.userService.update(workspaceSlug, filters, projectId);
@ -428,7 +422,7 @@ export class ViewStore extends FiltersHelper implements TViewStore {
updateUserDisplayFilters = async (display_filters: { [EFilterTypes.DISPLAY_FILTERS]: TViewDisplayFilters }) => {
try {
const { workspaceSlug, projectId } = this.store.app.router;
const { workspaceSlug, projectId } = this.store.view;
if (!workspaceSlug) return;
const userView = await this.userService.update(workspaceSlug, display_filters, projectId);
@ -448,7 +442,7 @@ export class ViewStore extends FiltersHelper implements TViewStore {
[EFilterTypes.DISPLAY_PROPERTIES]: TViewDisplayProperties;
}) => {
try {
const { workspaceSlug, projectId } = this.store.app.router;
const { workspaceSlug, projectId } = this.store.view;
if (!workspaceSlug) return;
const userView = await this.userService.update(workspaceSlug, display_properties, projectId);