mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: store changes on the filters and display filters
This commit is contained in:
parent
02380c730d
commit
a5fd6f0e8a
18
packages/types/src/view/filter.d.ts
vendored
18
packages/types/src/view/filter.d.ts
vendored
@ -1,9 +1,17 @@
|
||||
declare enum EViewLayouts {
|
||||
LIST = "list",
|
||||
KANBAN = "kanban",
|
||||
CALENDAR = "calendar",
|
||||
SPREADSHEET = "spreadsheet",
|
||||
GANTT = "gantt",
|
||||
}
|
||||
|
||||
export type TViewLayouts =
|
||||
| "list"
|
||||
| "kanban"
|
||||
| "calendar"
|
||||
| "spreadsheet"
|
||||
| "gantt";
|
||||
| EViewLayouts.LIST
|
||||
| EViewLayouts.KANBAN
|
||||
| EViewLayouts.CALENDAR
|
||||
| EViewLayouts.SPREADSHEET
|
||||
| EViewLayouts.GANTT;
|
||||
|
||||
export type TViewDisplayFiltersGrouped =
|
||||
| "project"
|
||||
|
@ -154,7 +154,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={<BreadcrumbLink label="Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />}
|
||||
link={
|
||||
<BreadcrumbLink label="Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
@ -218,7 +220,12 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
)}
|
||||
{canUserCreateIssue && (
|
||||
<>
|
||||
<Button className="hidden md:block" onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
<Button
|
||||
className="hidden md:block"
|
||||
onClick={() => setAnalyticsModal(true)}
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
>
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
|
@ -0,0 +1,72 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useViewDetail, useViewFilter } from "hooks/store";
|
||||
// components
|
||||
import { ViewDisplayFiltersItem, ViewDisplayFilterSelection } from "..";
|
||||
// types
|
||||
import { TViewDisplayFilters, TViewTypes } from "@plane/types";
|
||||
|
||||
type TViewDisplayFiltersItemRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
filterKey: keyof TViewDisplayFilters;
|
||||
};
|
||||
|
||||
export const ViewDisplayFiltersItemRoot: FC<TViewDisplayFiltersItemRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, filterKey } = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
const viewFilterHelper = useViewFilter(workspaceSlug, projectId);
|
||||
|
||||
const filterPropertyIds = viewFilterHelper?.displayFilterIdsWithKey(filterKey) || [];
|
||||
|
||||
const handlePropertySelection = (_propertyId: string) => {
|
||||
viewDetailStore?.setDisplayFilters({ [filterKey]: _propertyId });
|
||||
};
|
||||
|
||||
const renderElement = (_propertyId: string) =>
|
||||
filterKey === "group_by"
|
||||
? viewDetailStore?.appliedFilters?.display_filters?.["sub_group_by"] !== _propertyId
|
||||
? true
|
||||
: false
|
||||
: filterKey === "sub_group_by"
|
||||
? viewDetailStore?.appliedFilters?.display_filters?.["group_by"] !== _propertyId
|
||||
? true
|
||||
: false
|
||||
: true;
|
||||
|
||||
if (filterPropertyIds.length <= 0)
|
||||
return <div className="text-xs italic py-1 text-custom-text-300">No items are available.</div>;
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
{filterPropertyIds.map(
|
||||
(propertyId) =>
|
||||
renderElement(propertyId) && (
|
||||
<button
|
||||
key={`filterKey_${propertyId}`}
|
||||
className="relative w-full flex items-center overflow-hidden gap-2.5 cursor-pointer p-1 py-1.5 rounded hover:bg-custom-background-80 transition-all group"
|
||||
onClick={() => handlePropertySelection(propertyId)}
|
||||
>
|
||||
<ViewDisplayFilterSelection
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
filterKey={filterKey}
|
||||
propertyId={propertyId}
|
||||
/>
|
||||
<ViewDisplayFiltersItem
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
filterKey={filterKey}
|
||||
propertyId={propertyId}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
27
web/components/view/display-filters/display-filter-item.tsx
Normal file
27
web/components/view/display-filters/display-filter-item.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { FC, Fragment } from "react";
|
||||
// hooks
|
||||
import { useViewFilter } from "hooks/store";
|
||||
// types
|
||||
import { TViewDisplayFilters } from "@plane/types";
|
||||
|
||||
type TViewDisplayFiltersItem = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
filterKey: keyof TViewDisplayFilters;
|
||||
propertyId: string;
|
||||
};
|
||||
|
||||
export const ViewDisplayFiltersItem: FC<TViewDisplayFiltersItem> = (props) => {
|
||||
const { workspaceSlug, projectId, filterKey, propertyId } = props;
|
||||
// hooks
|
||||
const viewFilterHelper = useViewFilter(workspaceSlug, projectId);
|
||||
|
||||
const propertyDetail = viewFilterHelper?.displayPropertyDetails(filterKey, propertyId) || undefined;
|
||||
|
||||
if (!propertyDetail) return <></>;
|
||||
return (
|
||||
<div className="text-xs block truncate line-clamp-1 text-custom-text-200 group-hover:text-custom-text-100">
|
||||
{propertyDetail?.label || propertyId}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
import { FC } from "react";
|
||||
import { Check } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useViewDetail } from "hooks/store";
|
||||
// types
|
||||
import { TViewDisplayFilters, TViewTypes } from "@plane/types";
|
||||
|
||||
type TViewDisplayFilterSelection = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
filterKey: keyof TViewDisplayFilters;
|
||||
propertyId: string;
|
||||
};
|
||||
|
||||
export const ViewDisplayFilterSelection: FC<TViewDisplayFilterSelection> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, filterKey, propertyId } = props;
|
||||
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
|
||||
const propertyIds = viewDetailStore?.appliedFilters?.display_filters?.[filterKey] || undefined;
|
||||
|
||||
const isSelected = propertyIds === propertyId || false;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex-shrink-0 w-3 h-3 flex justify-center items-center border rounded-full text-bold ${
|
||||
isSelected
|
||||
? "border-custom-primary-100 bg-custom-primary-100"
|
||||
: "border-custom-border-400 bg-custom-background-100"
|
||||
}`}
|
||||
>
|
||||
{isSelected && <Check size={14} />}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -7,28 +7,34 @@ import { MonitorDot } from "lucide-react";
|
||||
// hooks
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { ViewDisplayPropertiesRoot } from "../";
|
||||
import { ViewDisplayFiltersRoot } from "../";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { TViewTypes } from "@plane/types";
|
||||
// constants
|
||||
import { EViewPageType } from "constants/view";
|
||||
|
||||
type TViewDisplayFiltersDropdown = {
|
||||
type NewType = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
viewPageType: EViewPageType;
|
||||
children?: ReactNode;
|
||||
displayDropdownText?: boolean;
|
||||
dropdownPlacement?: Placement;
|
||||
};
|
||||
|
||||
type TViewDisplayFiltersDropdown = NewType;
|
||||
|
||||
export const ViewDisplayFiltersDropdown: FC<TViewDisplayFiltersDropdown> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
viewId,
|
||||
viewType,
|
||||
viewPageType,
|
||||
children,
|
||||
displayDropdownText = true,
|
||||
dropdownPlacement = "bottom-start",
|
||||
@ -110,19 +116,15 @@ export const ViewDisplayFiltersDropdown: FC<TViewDisplayFiltersDropdown> = obser
|
||||
{...attributes.popper}
|
||||
className="my-1 w-72 p-2 space-y-2 rounded bg-custom-background-100 border-[0.5px] border-custom-border-300 shadow-custom-shadow-rg focus:outline-none"
|
||||
>
|
||||
<div className="max-h-96 space-y-1 overflow-y-scroll">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium text-custom-text-200">Properties</div>
|
||||
<ViewDisplayPropertiesRoot
|
||||
<div className="max-h-[500px] space-y-0.5 overflow-y-scroll mb-2">
|
||||
<ViewDisplayFiltersRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
viewPageType={viewPageType}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border border-red-500">Content</div>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
|
@ -1,17 +1,101 @@
|
||||
import { FC } from "react";
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ChevronUp, ChevronDown } from "lucide-react";
|
||||
import filter from "lodash/filter";
|
||||
import concat from "lodash/concat";
|
||||
import uniq from "lodash/uniq";
|
||||
// hooks
|
||||
import { useViewDetail } from "hooks/store";
|
||||
// components
|
||||
import { ViewDisplayPropertiesRoot, ViewDisplayFiltersItemRoot } from "../";
|
||||
// types
|
||||
import { TViewDisplayFilters, TViewTypes } from "@plane/types";
|
||||
// constants
|
||||
import { EViewPageType, viewDefaultFilterParametersByViewTypeAndLayout } from "constants/view";
|
||||
|
||||
type TViewDisplayFiltersRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
viewPageType: EViewPageType;
|
||||
};
|
||||
|
||||
export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = (props) => {
|
||||
const { workspaceSlug, projectId, viewId } = props;
|
||||
export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, viewPageType } = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
// state
|
||||
const [filterVisibility, setFilterVisibility] = useState<(Partial<keyof TViewDisplayFilters> | "display_property")[]>(
|
||||
[]
|
||||
);
|
||||
const handleFilterVisibility = (key: keyof TViewDisplayFilters | "display_property") => {
|
||||
setFilterVisibility((prevData = []) => {
|
||||
if (prevData.includes(key)) return filter(prevData, (item) => item !== key);
|
||||
return uniq(concat(prevData, [key]));
|
||||
});
|
||||
};
|
||||
|
||||
const layout = viewDetailStore?.appliedFilters?.display_filters?.layout;
|
||||
|
||||
const filtersProperties = layout
|
||||
? viewDefaultFilterParametersByViewTypeAndLayout(viewPageType, layout, "display_filters")
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="space-y-1 divide-y divide-custom-border-300">
|
||||
<div className="relative py-1 first:pt-0">
|
||||
<div className="sticky top-0 z-20 flex justify-between items-center gap-2 bg-custom-background-100 select-none">
|
||||
<div className="font-medium text-xs text-custom-text-300 capitalize py-1">Properties</div>
|
||||
<div
|
||||
className="flex-shrink-0 relative overflow-hidden w-5 h-5 rounded flex justify-center items-center cursor-pointer hover:bg-custom-background-80"
|
||||
onClick={() => handleFilterVisibility("display_property")}
|
||||
>
|
||||
{!filterVisibility.includes("display_property") ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
||||
</div>
|
||||
</div>
|
||||
{!filterVisibility.includes("display_property") && (
|
||||
<div className="py-1">
|
||||
<ViewDisplayPropertiesRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{filtersProperties.map((filterKey) => (
|
||||
<div key={filterKey} className="relative py-1 last:pb-0">
|
||||
<div className="sticky top-0 z-20 flex justify-between items-center gap-2 bg-custom-background-100 select-none">
|
||||
<div className="font-medium text-xs text-custom-text-300 capitalize py-1">
|
||||
{filterKey.replaceAll("_", " ")}
|
||||
</div>
|
||||
<div
|
||||
className="flex-shrink-0 relative overflow-hidden w-5 h-5 rounded flex justify-center items-center cursor-pointer hover:bg-custom-background-80"
|
||||
onClick={() => handleFilterVisibility(filterKey)}
|
||||
>
|
||||
{!filterVisibility.includes(filterKey) ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
||||
</div>
|
||||
</div>
|
||||
{!filterVisibility.includes(filterKey) && (
|
||||
<ViewDisplayFiltersItemRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
filterKey={filterKey}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* extra options */}
|
||||
<div>
|
||||
<div>ViewDisplayFiltersRoot</div>
|
||||
<div>Show sub issues</div>
|
||||
<div>Show Empty groups</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -12,12 +12,14 @@ import { ViewFiltersRoot } from "../";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { TViewTypes } from "@plane/types";
|
||||
import { EViewPageType } from "constants/view";
|
||||
|
||||
type TViewFiltersDropdown = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
viewPageType: EViewPageType;
|
||||
children?: ReactNode;
|
||||
displayDropdownText?: boolean;
|
||||
dropdownPlacement?: Placement;
|
||||
@ -29,6 +31,7 @@ export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) =>
|
||||
projectId,
|
||||
viewId,
|
||||
viewType,
|
||||
viewPageType,
|
||||
children,
|
||||
displayDropdownText = true,
|
||||
dropdownPlacement = "bottom-start",
|
||||
@ -130,6 +133,7 @@ export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) =>
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
viewPageType={viewPageType}
|
||||
dateCustomFilterToggle={dateCustomFilterToggle}
|
||||
setDateCustomFilterToggle={setDateCustomFilterToggle}
|
||||
/>
|
||||
|
@ -50,14 +50,14 @@ export const ViewFiltersEditDropdown: FC<TViewFiltersEditDropdown> = observer((p
|
||||
// dropdown options
|
||||
const dropdownOptions: TViewFilterEditDropdownOptions[] = useMemo(
|
||||
() => [
|
||||
// {
|
||||
// icon: PhotoFilterIcon,
|
||||
// key: "save_as_new",
|
||||
// label: "Save as new view",
|
||||
// onClick: () => {
|
||||
// viewOperations.localViewCreateEdit(undefined, viewDetailStore?.filtersToUpdate);
|
||||
// },
|
||||
// },
|
||||
{
|
||||
icon: PhotoFilterIcon,
|
||||
key: "save_as_new",
|
||||
label: "Save as new view",
|
||||
onClick: () => {
|
||||
viewOperations.localViewCreateEdit(undefined, viewDetailStore?.filtersToUpdate);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: RotateCcw,
|
||||
key: "reset_changes",
|
||||
|
@ -10,19 +10,28 @@ import { useViewDetail } from "hooks/store";
|
||||
import { ViewFiltersItemRoot } from "../";
|
||||
// types
|
||||
import { TViewFilters, TViewTypes } from "@plane/types";
|
||||
import { VIEW_DEFAULT_FILTER_PARAMETERS } from "constants/view";
|
||||
import { EViewPageType, viewDefaultFilterParametersByViewTypeAndLayout } from "constants/view";
|
||||
|
||||
type TViewFiltersRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
viewPageType: EViewPageType;
|
||||
dateCustomFilterToggle: string | undefined;
|
||||
setDateCustomFilterToggle: (value: string | undefined) => void;
|
||||
};
|
||||
|
||||
export const ViewFiltersRoot: FC<TViewFiltersRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, dateCustomFilterToggle, setDateCustomFilterToggle } = props;
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
viewId,
|
||||
viewType,
|
||||
viewPageType,
|
||||
dateCustomFilterToggle,
|
||||
setDateCustomFilterToggle,
|
||||
} = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
// state
|
||||
@ -34,9 +43,11 @@ export const ViewFiltersRoot: FC<TViewFiltersRoot> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const layout = viewDetailStore?.appliedFilters?.display_filters?.layout || "spreadsheet";
|
||||
const layout = viewDetailStore?.appliedFilters?.display_filters?.layout;
|
||||
|
||||
const filtersProperties = VIEW_DEFAULT_FILTER_PARAMETERS?.["all"]?.["spreadsheet"]?.filters || [];
|
||||
const filtersProperties = layout
|
||||
? viewDefaultFilterParametersByViewTypeAndLayout(viewPageType, layout, "filters")
|
||||
: [];
|
||||
|
||||
if (!layout || filtersProperties.length <= 0) return <></>;
|
||||
return (
|
||||
@ -45,7 +56,7 @@ export const ViewFiltersRoot: FC<TViewFiltersRoot> = observer((props) => {
|
||||
<div key={filterKey} className="relative py-1 first:pt-0 last:pb-0">
|
||||
<div className="sticky top-0 z-20 flex justify-between items-center gap-2 bg-custom-background-100 select-none">
|
||||
<div className="font-medium text-xs text-custom-text-300 capitalize py-1">
|
||||
{filterKey.replace("_", " ")}
|
||||
{filterKey.replaceAll("_", " ")}
|
||||
</div>
|
||||
<div
|
||||
className="flex-shrink-0 relative overflow-hidden w-5 h-5 rounded flex justify-center items-center cursor-pointer hover:bg-custom-background-80"
|
||||
|
@ -22,6 +22,9 @@ export * from "./filters/edit-dropdown";
|
||||
// view display filters
|
||||
export * from "./display-filters/dropdown";
|
||||
export * from "./display-filters/root";
|
||||
export * from "./display-filters/display-filter-item-root";
|
||||
export * from "./display-filters/display-filter-item";
|
||||
export * from "./display-filters/display-filter-selection";
|
||||
|
||||
// view display properties
|
||||
export * from "./display-properties/root";
|
||||
|
@ -6,32 +6,39 @@ import { useViewDetail } from "hooks/store";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { TViewLayouts, TViewTypes } from "@plane/types";
|
||||
import { TViewTypes } from "@plane/types";
|
||||
// constants
|
||||
import { EViewLayouts, EViewPageType, viewPageDefaultLayoutsByPageType } from "constants/view";
|
||||
|
||||
type TViewLayoutRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
viewPageType: EViewPageType;
|
||||
};
|
||||
|
||||
const LAYOUTS_DATA: { key: TViewLayouts; title: string; icon: LucideIcon }[] = [
|
||||
{ key: "list", title: "List Layout", icon: List },
|
||||
{ key: "kanban", title: "Kanban Layout", icon: Kanban },
|
||||
{ key: "calendar", title: "Calendar Layout", icon: Calendar },
|
||||
{ key: "spreadsheet", title: "Spreadsheet Layout", icon: Sheet },
|
||||
{ key: "gantt", title: "Gantt Chart layout", icon: GanttChartSquare },
|
||||
const LAYOUTS_DATA: { key: EViewLayouts; title: string; icon: LucideIcon }[] = [
|
||||
{ key: EViewLayouts.LIST, title: "List Layout", icon: List },
|
||||
{ key: EViewLayouts.KANBAN, title: "Kanban Layout", icon: Kanban },
|
||||
{ key: EViewLayouts.CALENDAR, title: "Calendar Layout", icon: Calendar },
|
||||
{ key: EViewLayouts.SPREADSHEET, title: "Spreadsheet Layout", icon: Sheet },
|
||||
{ key: EViewLayouts.GANTT, title: "Gantt Chart layout", icon: GanttChartSquare },
|
||||
];
|
||||
|
||||
export const ViewLayoutRoot: FC<TViewLayoutRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType } = props;
|
||||
const { workspaceSlug, projectId, viewId, viewType, viewPageType } = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
|
||||
if (!viewDetailStore) return <></>;
|
||||
const validLayouts = viewPageDefaultLayoutsByPageType(viewPageType);
|
||||
|
||||
if (!viewDetailStore || validLayouts.length <= 1) return <></>;
|
||||
return (
|
||||
<div className="relative flex gap-0.5 items-center bg-custom-background-80 rounded p-1 shadow-custom-shadow-2xs">
|
||||
{LAYOUTS_DATA.map((layout) => (
|
||||
{LAYOUTS_DATA.map((layout) => {
|
||||
if (!validLayouts.includes(layout.key)) return <Fragment key={layout.key} />;
|
||||
return (
|
||||
<Fragment key={layout.key}>
|
||||
<Tooltip tooltipContent={layout.title} position="bottom">
|
||||
<div
|
||||
@ -48,7 +55,8 @@ export const ViewLayoutRoot: FC<TViewLayoutRoot> = observer((props) => {
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Fragment>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// constants
|
||||
import { viewLocalPayload } from "constants/view";
|
||||
import { EViewPageType, viewLocalPayload } from "constants/view";
|
||||
// types
|
||||
import { TViewOperations } from "./types";
|
||||
import { TView, TViewTypes } from "@plane/types";
|
||||
@ -33,6 +33,7 @@ type TGlobalViewRoot = {
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
viewPageType: EViewPageType;
|
||||
baseRoute: string;
|
||||
workspaceViewTabOptions: { key: TViewTypes; title: string; href: string }[];
|
||||
};
|
||||
@ -43,7 +44,7 @@ type TViewOperationsToggle = {
|
||||
};
|
||||
|
||||
export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, baseRoute, workspaceViewTabOptions } = props;
|
||||
const { workspaceSlug, projectId, viewId, viewType, viewPageType, baseRoute, workspaceViewTabOptions } = props;
|
||||
// hooks
|
||||
const viewStore = useView(workspaceSlug, projectId, viewType);
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
@ -232,7 +233,13 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<ViewLayoutRoot workspaceSlug={workspaceSlug} projectId={projectId} viewId={viewId} viewType={viewType} />
|
||||
<ViewLayoutRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
viewPageType={viewPageType}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
@ -241,6 +248,7 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
viewPageType={viewPageType}
|
||||
displayDropdownText={false}
|
||||
/>
|
||||
</div>
|
||||
@ -251,17 +259,21 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
|
||||
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">
|
||||
<ViewFiltersEditDropdown
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
@ -270,6 +282,7 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
|
||||
viewOperations={viewOperations}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -282,6 +295,7 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
|
||||
projectId={projectId}
|
||||
viewId={viewOperationsToggle.viewId}
|
||||
viewType={viewType}
|
||||
viewPageType={viewPageType}
|
||||
viewOperations={viewOperations}
|
||||
/>
|
||||
)}
|
||||
|
@ -11,17 +11,20 @@ import { Input, Button } from "@plane/ui";
|
||||
// types
|
||||
import { TViewTypes } from "@plane/types";
|
||||
import { TViewOperations } from "../types";
|
||||
// constants
|
||||
import { EViewPageType } from "constants/view";
|
||||
|
||||
type TViewCreateEditForm = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
viewPageType: EViewPageType;
|
||||
viewOperations: TViewOperations;
|
||||
};
|
||||
|
||||
export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
|
||||
const { workspaceSlug, projectId, viewId, viewType, viewPageType, viewOperations } = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
const { getProjectById } = useProject();
|
||||
@ -126,6 +129,7 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
viewPageType={viewPageType}
|
||||
dropdownPlacement="right"
|
||||
>
|
||||
<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">
|
||||
|
@ -49,49 +49,49 @@ export const ViewEditDropdown: FC<TViewEditDropdown> = observer((props) => {
|
||||
onClick: () => viewOperations.localViewCreateEdit(viewId),
|
||||
children: undefined,
|
||||
},
|
||||
// {
|
||||
// icon: Eye,
|
||||
// key: "accessability",
|
||||
// label: "Change Accessability",
|
||||
// onClick: () => {},
|
||||
// children: [
|
||||
// {
|
||||
// icon: Eye,
|
||||
// key: "private",
|
||||
// label: "Private",
|
||||
// onClick: () => viewOperations.create({}),
|
||||
// children: undefined,
|
||||
// },
|
||||
// {
|
||||
// icon: Globe2,
|
||||
// key: "public",
|
||||
// label: "Public",
|
||||
// onClick: () => viewOperations.create({}),
|
||||
// children: undefined,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// icon: Copy,
|
||||
// key: "duplicate",
|
||||
// label: "Duplicate view",
|
||||
// onClick: () => viewOperations.remove(viewId),
|
||||
// children: undefined,
|
||||
// },
|
||||
// {
|
||||
// icon: Link2,
|
||||
// key: "copy_link",
|
||||
// label: "Copy view link",
|
||||
// onClick: () => viewOperations.remove(viewId),
|
||||
// children: undefined,
|
||||
// },
|
||||
// {
|
||||
// icon: Trash,
|
||||
// key: "delete",
|
||||
// label: "Delete view",
|
||||
// onClick: () => viewOperations.remove(viewId),
|
||||
// children: undefined,
|
||||
// },
|
||||
{
|
||||
icon: Eye,
|
||||
key: "accessability",
|
||||
label: "Change Accessability",
|
||||
onClick: () => {},
|
||||
children: [
|
||||
{
|
||||
icon: Eye,
|
||||
key: "private",
|
||||
label: "Private",
|
||||
onClick: () => viewOperations.create({}),
|
||||
children: undefined,
|
||||
},
|
||||
{
|
||||
icon: Globe2,
|
||||
key: "public",
|
||||
label: "Public",
|
||||
onClick: () => viewOperations.create({}),
|
||||
children: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Copy,
|
||||
key: "duplicate",
|
||||
label: "Duplicate view",
|
||||
onClick: () => viewOperations.remove(viewId),
|
||||
children: undefined,
|
||||
},
|
||||
{
|
||||
icon: Link2,
|
||||
key: "copy_link",
|
||||
label: "Copy view link",
|
||||
onClick: () => viewOperations.remove(viewId),
|
||||
children: undefined,
|
||||
},
|
||||
{
|
||||
icon: Trash,
|
||||
key: "delete",
|
||||
label: "Delete view",
|
||||
onClick: () => viewOperations.remove(viewId),
|
||||
children: undefined,
|
||||
},
|
||||
],
|
||||
[viewOperations, viewId]
|
||||
);
|
||||
|
@ -1,40 +1,24 @@
|
||||
// types
|
||||
import { TStateGroups, TIssuePriorities, TViewFilters, TViewDisplayFilters, TViewLayouts } from "@plane/types";
|
||||
import {
|
||||
TStateGroups,
|
||||
TIssuePriorities,
|
||||
TViewFilters,
|
||||
TViewDisplayFilters,
|
||||
TViewDisplayFiltersGrouped,
|
||||
TViewDisplayFiltersOrderBy,
|
||||
TViewDisplayFiltersType,
|
||||
} from "@plane/types";
|
||||
|
||||
// filters constants
|
||||
export const STATE_GROUP_PROPERTY: {
|
||||
[key in TStateGroups]: {
|
||||
label: string;
|
||||
color: string;
|
||||
};
|
||||
} = {
|
||||
backlog: {
|
||||
label: "Backlog",
|
||||
color: "#d9d9d9",
|
||||
},
|
||||
unstarted: {
|
||||
label: "Unstarted",
|
||||
color: "#3f76ff",
|
||||
},
|
||||
started: {
|
||||
label: "Started",
|
||||
color: "#f59e0b",
|
||||
},
|
||||
completed: {
|
||||
label: "Completed",
|
||||
color: "#16a34a",
|
||||
},
|
||||
cancelled: {
|
||||
label: "Canceled",
|
||||
color: "#dc2626",
|
||||
},
|
||||
export const STATE_GROUP_PROPERTY: Record<TStateGroups, { label: string; color: string }> = {
|
||||
backlog: { label: "Backlog", color: "#d9d9d9" },
|
||||
unstarted: { label: "Unstarted", color: "#3f76ff" },
|
||||
started: { label: "Started", color: "#f59e0b" },
|
||||
completed: { label: "Completed", color: "#16a34a" },
|
||||
cancelled: { label: "Canceled", color: "#dc2626" },
|
||||
};
|
||||
|
||||
export const PRIORITIES_PROPERTY: {
|
||||
[key in TIssuePriorities]: {
|
||||
label: string;
|
||||
};
|
||||
} = {
|
||||
export const PRIORITIES_PROPERTY: Record<TIssuePriorities, { label: string }> = {
|
||||
urgent: { label: "Urgent" },
|
||||
high: { label: "High" },
|
||||
medium: { label: "Medium" },
|
||||
@ -42,11 +26,7 @@ export const PRIORITIES_PROPERTY: {
|
||||
none: { label: "None" },
|
||||
};
|
||||
|
||||
export const DATE_PROPERTY: {
|
||||
[key in string]: {
|
||||
label: string;
|
||||
};
|
||||
} = {
|
||||
export const DATE_PROPERTY: Record<string, { label: string }> = {
|
||||
"1_weeks;after;fromnow": { label: "1 week from now" },
|
||||
"2_weeks;after;fromnow": { label: "2 weeks from now" },
|
||||
"1_months;after;fromnow": { label: "1 month from now" },
|
||||
@ -55,77 +35,98 @@ export const DATE_PROPERTY: {
|
||||
};
|
||||
|
||||
// display filter constants
|
||||
export const GROUP_BY_PROPERTY: Partial<Record<TViewDisplayFiltersGrouped | "null", { label: string }>> = {
|
||||
state: { label: "states" },
|
||||
priority: { label: "Priority" },
|
||||
labels: { label: "labels" },
|
||||
assignees: { label: "Assignees" },
|
||||
created_by: { label: "Created By" },
|
||||
cycles: { label: "Cycles" },
|
||||
modules: { label: "Modules" },
|
||||
null: { label: "None" },
|
||||
};
|
||||
|
||||
// layout, filter, display filter and display properties permissions for views
|
||||
type TViewLayoutFilterProperties = {
|
||||
export const ORDER_BY_PROPERTY: Partial<Record<TViewDisplayFiltersOrderBy, Record<string, string>>> = {
|
||||
sort_order: { label: "Manual" },
|
||||
"-created_at": { label: "Last Created" },
|
||||
"-updated_at": { label: "Last Updated" },
|
||||
start_date: { label: "Start Date" },
|
||||
target_date: { label: "Due Date" },
|
||||
"-priority": { label: "Priority" },
|
||||
};
|
||||
|
||||
export const TYPE_PROPERTY: Record<TViewDisplayFiltersType | "null", { label: string }> = {
|
||||
null: { label: "All" },
|
||||
active: { label: "Active issues" },
|
||||
backlog: { label: "Backlog issues" },
|
||||
};
|
||||
|
||||
export const EXTRA_OPTIONS_PROPERTY: Record<string, { label: string }> = {
|
||||
sub_issue: { label: "Sub Issues" },
|
||||
show_empty_groups: { label: "Show Empty Groups" },
|
||||
};
|
||||
|
||||
export enum EViewPageType {
|
||||
ALL = "all",
|
||||
PROFILE = "profile",
|
||||
PROJECT = "project",
|
||||
ARCHIVED = "archived",
|
||||
DRAFT = "draft",
|
||||
}
|
||||
|
||||
export enum EViewLayouts {
|
||||
LIST = "list",
|
||||
KANBAN = "kanban",
|
||||
CALENDAR = "calendar",
|
||||
SPREADSHEET = "spreadsheet",
|
||||
GANTT = "gantt",
|
||||
}
|
||||
|
||||
export type TViewLayoutFilterProperties = {
|
||||
filters: Partial<keyof TViewFilters>[];
|
||||
readonlyFilters?: Partial<keyof TViewFilters>[];
|
||||
display_filters: Partial<keyof TViewDisplayFilters>[];
|
||||
extra_options: ("sub_issue" | "show_empty_groups")[];
|
||||
display_properties: boolean;
|
||||
readonlyFilters?: Partial<keyof TViewFilters>[];
|
||||
};
|
||||
|
||||
type TViewLayoutFilters = {
|
||||
list: TViewLayoutFilterProperties;
|
||||
kanban: TViewLayoutFilterProperties;
|
||||
calendar: TViewLayoutFilterProperties;
|
||||
spreadsheet: TViewLayoutFilterProperties;
|
||||
gantt: TViewLayoutFilterProperties;
|
||||
export type TViewLayoutFilters = {
|
||||
layouts: Partial<EViewLayouts>[];
|
||||
[EViewLayouts.LIST]: TViewLayoutFilterProperties;
|
||||
[EViewLayouts.KANBAN]: TViewLayoutFilterProperties;
|
||||
[EViewLayouts.CALENDAR]: TViewLayoutFilterProperties;
|
||||
[EViewLayouts.SPREADSHEET]: TViewLayoutFilterProperties;
|
||||
[EViewLayouts.GANTT]: TViewLayoutFilterProperties;
|
||||
};
|
||||
|
||||
type TFilterPermissions = {
|
||||
all: Omit<TViewLayoutFilters, "list" | "kanban" | "calendar" | "gantt"> & {
|
||||
layouts: Omit<TViewLayouts, "list" | "kanban" | "calendar" | "gantt">[];
|
||||
};
|
||||
profile: Omit<TViewLayoutFilters, "spreadsheet" | "calendar" | "gantt"> & {
|
||||
layouts: Omit<TViewLayouts, "spreadsheet" | "calendar" | "gantt">[];
|
||||
};
|
||||
project: TViewLayoutFilters & {
|
||||
layouts: TViewLayouts[];
|
||||
};
|
||||
archived: Omit<TViewLayoutFilters, "kanban" | "spreadsheet" | "calendar" | "gantt"> & {
|
||||
layouts: Omit<TViewLayouts, "kanban" | "spreadsheet" | "calendar" | "gantt">[];
|
||||
};
|
||||
draft: Omit<TViewLayoutFilters, "spreadsheet" | "calendar" | "gantt"> & {
|
||||
layouts: Omit<TViewLayouts, "kanban" | "spreadsheet" | "calendar" | "gantt">[];
|
||||
};
|
||||
export type TFilterPermissions = {
|
||||
[EViewPageType.ALL]: Partial<TViewLayoutFilters>;
|
||||
[EViewPageType.PROFILE]: Partial<TViewLayoutFilters>;
|
||||
[EViewPageType.PROJECT]: TViewLayoutFilters;
|
||||
[EViewPageType.ARCHIVED]: Partial<TViewLayoutFilters>;
|
||||
[EViewPageType.DRAFT]: Partial<TViewLayoutFilters>;
|
||||
};
|
||||
|
||||
const ALL_FILTER_PERMISSIONS: TFilterPermissions["all"] = {
|
||||
layouts: ["spreadsheet"],
|
||||
spreadsheet: {
|
||||
// filters: ["project", "priority", "state_group", "assignees", "created_by", "labels", "start_date", "target_date"],
|
||||
filters: [
|
||||
"project",
|
||||
"module",
|
||||
"cycle",
|
||||
"priority",
|
||||
"state",
|
||||
"state_group",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"subscriber",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
],
|
||||
// display_filters: ["type"],
|
||||
display_filters: ["group_by", "sub_group_by", "order_by", "type"],
|
||||
extra_options: [],
|
||||
layouts: [EViewLayouts.SPREADSHEET],
|
||||
[EViewLayouts.SPREADSHEET]: {
|
||||
filters: ["project", "priority", "state_group", "assignees", "created_by", "labels", "start_date", "target_date"],
|
||||
display_filters: ["type"],
|
||||
// extra_options: [],
|
||||
extra_options: ["sub_issue", "show_empty_groups"],
|
||||
display_properties: true,
|
||||
},
|
||||
};
|
||||
|
||||
const PROFILE_FILTER_PERMISSIONS: TFilterPermissions["profile"] = {
|
||||
layouts: ["list", "kanban"],
|
||||
list: {
|
||||
layouts: [EViewLayouts.LIST, EViewLayouts.KANBAN],
|
||||
[EViewLayouts.LIST]: {
|
||||
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
|
||||
display_filters: ["group_by", "order_by", "type"],
|
||||
extra_options: [],
|
||||
display_properties: true,
|
||||
},
|
||||
kanban: {
|
||||
[EViewLayouts.KANBAN]: {
|
||||
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
|
||||
display_filters: ["group_by", "order_by", "type"],
|
||||
extra_options: [],
|
||||
@ -134,8 +135,14 @@ const PROFILE_FILTER_PERMISSIONS: TFilterPermissions["profile"] = {
|
||||
};
|
||||
|
||||
const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
|
||||
layouts: ["list", "kanban", "spreadsheet", "calendar", "gantt"],
|
||||
list: {
|
||||
layouts: [
|
||||
EViewLayouts.LIST,
|
||||
EViewLayouts.KANBAN,
|
||||
EViewLayouts.CALENDAR,
|
||||
EViewLayouts.SPREADSHEET,
|
||||
EViewLayouts.GANTT,
|
||||
],
|
||||
[EViewLayouts.LIST]: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
@ -152,7 +159,7 @@ const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
|
||||
extra_options: ["sub_issue", "show_empty_groups"],
|
||||
display_properties: true,
|
||||
},
|
||||
kanban: {
|
||||
[EViewLayouts.KANBAN]: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
@ -169,7 +176,7 @@ const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
|
||||
extra_options: ["sub_issue", "show_empty_groups"],
|
||||
display_properties: true,
|
||||
},
|
||||
calendar: {
|
||||
[EViewLayouts.CALENDAR]: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
@ -186,7 +193,7 @@ const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
|
||||
extra_options: ["sub_issue"],
|
||||
display_properties: true,
|
||||
},
|
||||
spreadsheet: {
|
||||
[EViewLayouts.SPREADSHEET]: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
@ -203,8 +210,7 @@ const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
|
||||
extra_options: [],
|
||||
display_properties: true,
|
||||
},
|
||||
|
||||
gantt: {
|
||||
[EViewLayouts.GANTT]: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
@ -224,8 +230,8 @@ const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
|
||||
};
|
||||
|
||||
const ARCHIVED_FILTER_PERMISSIONS: TFilterPermissions["archived"] = {
|
||||
layouts: ["list"],
|
||||
list: {
|
||||
layouts: [EViewLayouts.LIST],
|
||||
[EViewLayouts.LIST]: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
@ -245,8 +251,8 @@ const ARCHIVED_FILTER_PERMISSIONS: TFilterPermissions["archived"] = {
|
||||
};
|
||||
|
||||
const DRAFT_FILTER_PERMISSIONS: TFilterPermissions["draft"] = {
|
||||
layouts: ["list", "kanban"],
|
||||
list: {
|
||||
layouts: [EViewLayouts.LIST, EViewLayouts.KANBAN],
|
||||
[EViewLayouts.LIST]: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
@ -263,7 +269,7 @@ const DRAFT_FILTER_PERMISSIONS: TFilterPermissions["draft"] = {
|
||||
extra_options: ["sub_issue", "show_empty_groups"],
|
||||
display_properties: true,
|
||||
},
|
||||
kanban: {
|
||||
[EViewLayouts.KANBAN]: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
@ -283,9 +289,19 @@ const DRAFT_FILTER_PERMISSIONS: TFilterPermissions["draft"] = {
|
||||
};
|
||||
|
||||
export const VIEW_DEFAULT_FILTER_PARAMETERS: TFilterPermissions = {
|
||||
all: ALL_FILTER_PERMISSIONS,
|
||||
profile: PROFILE_FILTER_PERMISSIONS,
|
||||
project: PROJECT_FILTER_PERMISSIONS,
|
||||
archived: ARCHIVED_FILTER_PERMISSIONS,
|
||||
draft: DRAFT_FILTER_PERMISSIONS,
|
||||
[EViewPageType.ALL]: ALL_FILTER_PERMISSIONS,
|
||||
[EViewPageType.PROFILE]: PROFILE_FILTER_PERMISSIONS,
|
||||
[EViewPageType.PROJECT]: PROJECT_FILTER_PERMISSIONS,
|
||||
[EViewPageType.ARCHIVED]: ARCHIVED_FILTER_PERMISSIONS,
|
||||
[EViewPageType.DRAFT]: DRAFT_FILTER_PERMISSIONS,
|
||||
};
|
||||
|
||||
export const viewPageDefaultLayoutsByPageType = (_viewPageType: EViewPageType) =>
|
||||
VIEW_DEFAULT_FILTER_PARAMETERS?.[_viewPageType]?.layouts || [];
|
||||
|
||||
export const viewDefaultFilterParametersByViewTypeAndLayout = <K extends keyof TViewLayoutFilterProperties>(
|
||||
_viewPageType: EViewPageType,
|
||||
_layout: EViewLayouts,
|
||||
property: K
|
||||
): TViewLayoutFilterProperties[K] =>
|
||||
VIEW_DEFAULT_FILTER_PARAMETERS?.[_viewPageType]?.[_layout]?.[property] as TViewLayoutFilterProperties[K];
|
||||
|
@ -13,9 +13,24 @@ import {
|
||||
StateGroupIcon,
|
||||
} from "@plane/ui";
|
||||
// types
|
||||
import { TIssuePriorities, TStateGroups, TViewFilters } from "@plane/types";
|
||||
import {
|
||||
TIssuePriorities,
|
||||
TStateGroups,
|
||||
TViewFilters,
|
||||
TViewDisplayFilters,
|
||||
TViewDisplayFiltersGrouped,
|
||||
TViewDisplayFiltersOrderBy,
|
||||
TViewDisplayFiltersType,
|
||||
} from "@plane/types";
|
||||
// constants
|
||||
import { STATE_GROUP_PROPERTY, PRIORITIES_PROPERTY, DATE_PROPERTY } from "constants/view/filters";
|
||||
import {
|
||||
STATE_GROUP_PROPERTY,
|
||||
PRIORITIES_PROPERTY,
|
||||
DATE_PROPERTY,
|
||||
GROUP_BY_PROPERTY,
|
||||
ORDER_BY_PROPERTY,
|
||||
TYPE_PROPERTY,
|
||||
} from "constants/view/filters";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
@ -30,6 +45,11 @@ type TFilterPropertyDefaultDetails = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
type TDisplayFilterPropertyDetails = {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const useViewFilter = (workspaceSlug: string, projectId: string | undefined) => {
|
||||
const { projectMap, getProjectById } = useProject();
|
||||
const { getProjectModuleIds, getModuleById } = useModule();
|
||||
@ -326,9 +346,68 @@ export const useViewFilter = (workspaceSlug: string, projectId: string | undefin
|
||||
}
|
||||
};
|
||||
|
||||
const displayFilterIdsWithKey = (displayFilterKey: keyof TViewDisplayFilters): string[] | undefined => {
|
||||
if (!displayFilterKey) return undefined;
|
||||
|
||||
switch (displayFilterKey) {
|
||||
case "group_by":
|
||||
return Object.keys(GROUP_BY_PROPERTY) || undefined;
|
||||
case "sub_group_by":
|
||||
return Object.keys(GROUP_BY_PROPERTY) || undefined;
|
||||
case "order_by":
|
||||
return Object.keys(ORDER_BY_PROPERTY) || undefined;
|
||||
case "type":
|
||||
return Object.keys(TYPE_PROPERTY) || undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const displayPropertyDetails = (
|
||||
displayFilterKey: keyof TViewDisplayFilters,
|
||||
propertyId: string
|
||||
): TDisplayFilterPropertyDetails | undefined => {
|
||||
if (!displayFilterKey) return undefined;
|
||||
|
||||
switch (displayFilterKey) {
|
||||
case "group_by":
|
||||
const groupBy = GROUP_BY_PROPERTY?.[propertyId as TViewDisplayFiltersGrouped | "null"];
|
||||
if (!groupBy) return undefined;
|
||||
return {
|
||||
icon: undefined,
|
||||
label: groupBy.label,
|
||||
};
|
||||
case "sub_group_by":
|
||||
const subGroupBy = GROUP_BY_PROPERTY?.[propertyId as TViewDisplayFiltersGrouped | "null"];
|
||||
if (!subGroupBy) return undefined;
|
||||
return {
|
||||
icon: undefined,
|
||||
label: subGroupBy.label,
|
||||
};
|
||||
case "order_by":
|
||||
const orderBy = ORDER_BY_PROPERTY?.[propertyId as TViewDisplayFiltersOrderBy];
|
||||
if (!orderBy) return undefined;
|
||||
return {
|
||||
icon: undefined,
|
||||
label: orderBy.label,
|
||||
};
|
||||
case "type":
|
||||
const type = TYPE_PROPERTY?.[propertyId as TViewDisplayFiltersType | "null"];
|
||||
if (!type) return undefined;
|
||||
return {
|
||||
icon: undefined,
|
||||
label: type.label,
|
||||
};
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
filterIdsWithKey,
|
||||
propertyDefaultDetails,
|
||||
propertyDetails,
|
||||
displayFilterIdsWithKey,
|
||||
displayPropertyDetails,
|
||||
};
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ import { GlobalViewRoot } from "components/view";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { VIEW_TYPES } from "constants/view";
|
||||
import { EViewPageType, VIEW_TYPES } from "constants/view";
|
||||
|
||||
const ProjectPrivateViewPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
@ -38,6 +38,7 @@ const ProjectPrivateViewPage: NextPageWithLayout = () => {
|
||||
projectId={projectId.toString()}
|
||||
viewId={viewId.toString()}
|
||||
viewType={VIEW_TYPES.PROJECT_PRIVATE_VIEWS}
|
||||
viewPageType={EViewPageType.PROJECT}
|
||||
baseRoute={`/${workspaceSlug?.toString()}/projects/${projectId}/views/private`}
|
||||
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||
/>
|
||||
|
@ -1,49 +1,16 @@
|
||||
import { ReactElement, useMemo } from "react";
|
||||
import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { GlobalViewRoot } from "components/view";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { VIEW_TYPES } from "constants/view";
|
||||
|
||||
const ProjectPrivateViewPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
|
||||
const workspaceViewTabOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS,
|
||||
title: "Private",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/views/private`,
|
||||
},
|
||||
{
|
||||
key: VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS,
|
||||
title: "Public",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/views/public`,
|
||||
},
|
||||
],
|
||||
[workspaceSlug, projectId]
|
||||
);
|
||||
|
||||
if (!workspaceSlug || !projectId || !viewId) return <></>;
|
||||
return (
|
||||
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||
<GlobalViewRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
viewId={viewId.toString()}
|
||||
viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS}
|
||||
baseRoute={`/${workspaceSlug?.toString()}/projects/${projectId}/views/private`}
|
||||
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <div />;
|
||||
};
|
||||
|
||||
ProjectPrivateViewPage.getLayout = function getLayout(page: ReactElement) {
|
||||
|
@ -7,7 +7,7 @@ import { GlobalViewRoot } from "components/view";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { VIEW_TYPES } from "constants/view";
|
||||
import { EViewPageType, VIEW_TYPES } from "constants/view";
|
||||
|
||||
const ProjectPublicViewPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
@ -38,6 +38,7 @@ const ProjectPublicViewPage: NextPageWithLayout = () => {
|
||||
projectId={undefined}
|
||||
viewId={viewId.toString()}
|
||||
viewType={VIEW_TYPES.PROJECT_PUBLIC_VIEWS}
|
||||
viewPageType={EViewPageType.PROJECT}
|
||||
baseRoute={`/${workspaceSlug?.toString()}/views/public`}
|
||||
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||
/>
|
||||
|
@ -7,7 +7,7 @@ import { GlobalViewRoot } from "components/view";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { VIEW_TYPES } from "constants/view";
|
||||
import { EViewPageType, VIEW_TYPES } from "constants/view";
|
||||
|
||||
const WorkspacePrivateViewPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
@ -38,6 +38,7 @@ const WorkspacePrivateViewPage: NextPageWithLayout = () => {
|
||||
projectId={undefined}
|
||||
viewId={viewId.toString()}
|
||||
viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS}
|
||||
viewPageType={EViewPageType.ALL}
|
||||
baseRoute={`/${workspaceSlug?.toString()}/views/private`}
|
||||
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||
/>
|
||||
|
@ -7,7 +7,7 @@ import { GlobalViewRoot } from "components/view";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { VIEW_TYPES } from "constants/view";
|
||||
import { EViewPageType, VIEW_TYPES } from "constants/view";
|
||||
|
||||
const WorkspacePublicViewPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
@ -38,6 +38,7 @@ const WorkspacePublicViewPage: NextPageWithLayout = () => {
|
||||
projectId={undefined}
|
||||
viewId={viewId.toString()}
|
||||
viewType={VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS}
|
||||
viewPageType={EViewPageType.ALL}
|
||||
baseRoute={`/${workspaceSlug?.toString()}/views/public`}
|
||||
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||
/>
|
||||
|
@ -8,6 +8,8 @@ import {
|
||||
TViewFilterProps,
|
||||
TViewFilterQueryParams,
|
||||
} from "@plane/types";
|
||||
// constants
|
||||
import { EViewPageType, viewPageDefaultLayoutsByPageType } from "constants/view";
|
||||
|
||||
export class FiltersHelper {
|
||||
// computed filters
|
||||
@ -29,10 +31,14 @@ export class FiltersHelper {
|
||||
|
||||
// computed display filters
|
||||
computedDisplayFilters = (
|
||||
viewPageType: EViewPageType,
|
||||
displayFilters: TViewDisplayFilters,
|
||||
defaultValues?: Partial<TViewDisplayFilters>
|
||||
): TViewDisplayFilters => ({
|
||||
layout: defaultValues?.layout || displayFilters?.layout || "list",
|
||||
): TViewDisplayFilters => {
|
||||
const viewPageDefaultLayout = viewPageDefaultLayoutsByPageType(viewPageType)?.[0] || "list";
|
||||
|
||||
return {
|
||||
layout: defaultValues?.layout || displayFilters?.layout || viewPageDefaultLayout,
|
||||
group_by: defaultValues?.group_by || displayFilters?.group_by || undefined,
|
||||
sub_group_by: defaultValues?.sub_group_by || displayFilters?.sub_group_by || undefined,
|
||||
order_by: defaultValues?.order_by || displayFilters?.order_by || "sort_order",
|
||||
@ -43,7 +49,8 @@ export class FiltersHelper {
|
||||
show_weekends: defaultValues?.calendar?.show_weekends || displayFilters?.calendar?.show_weekends || false,
|
||||
layout: defaultValues?.calendar?.layout || displayFilters?.calendar?.layout || "month",
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// computed display properties
|
||||
computedDisplayProperties = (
|
||||
|
@ -11,6 +11,8 @@ import {
|
||||
} from "services/view";
|
||||
// types
|
||||
import { RootStore } from "store/root.store";
|
||||
// constants
|
||||
import { EViewPageType } from "constants/view";
|
||||
|
||||
export class GlobalViewRootStore {
|
||||
workspacePrivateViewStore: ViewRootStore;
|
||||
@ -59,25 +61,29 @@ export class GlobalViewRootStore {
|
||||
this.store,
|
||||
workspacePrivateDefaultViews,
|
||||
new WorkspacePrivateViewService(),
|
||||
new WorkspaceFiltersService()
|
||||
new WorkspaceFiltersService(),
|
||||
EViewPageType.ALL
|
||||
);
|
||||
this.workspacePublicViewStore = new ViewRootStore(
|
||||
this.store,
|
||||
workspacePublicDefaultViews,
|
||||
new WorkspacePublicViewService(),
|
||||
new WorkspaceFiltersService()
|
||||
new WorkspaceFiltersService(),
|
||||
EViewPageType.ALL
|
||||
);
|
||||
this.projectPrivateViewStore = new ViewRootStore(
|
||||
this.store,
|
||||
undefined,
|
||||
new ProjectPrivateViewService(),
|
||||
new ProjectFiltersService()
|
||||
new ProjectFiltersService(),
|
||||
EViewPageType.PROJECT
|
||||
);
|
||||
this.projectPublicViewStore = new ViewRootStore(
|
||||
this.store,
|
||||
undefined,
|
||||
new ProjectPublicViewService(),
|
||||
new ProjectFiltersService()
|
||||
new ProjectFiltersService(),
|
||||
EViewPageType.PROJECT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ import { ViewStore } from "./view.store";
|
||||
// types
|
||||
import { TUserViewService, TViewService } from "services/view/types";
|
||||
import { TView } from "@plane/types";
|
||||
// constants
|
||||
import { EViewPageType } from "constants/view";
|
||||
|
||||
export type TLoader = "init-loader" | "mutation-loader" | "submitting" | undefined;
|
||||
|
||||
@ -37,7 +39,8 @@ export class ViewRootStore implements TViewRootStore {
|
||||
private store: RootStore,
|
||||
private defaultViews: TView[] = [],
|
||||
private service: TViewService,
|
||||
private userService: TUserViewService
|
||||
private userService: TUserViewService,
|
||||
private viewPageType: EViewPageType
|
||||
) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
@ -70,7 +73,12 @@ export class ViewRootStore implements TViewRootStore {
|
||||
// actions
|
||||
localViewCreate = async (view: TView) => {
|
||||
runInAction(() => {
|
||||
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||
if (view.id)
|
||||
set(
|
||||
this.viewMap,
|
||||
[view.id],
|
||||
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@ -84,7 +92,12 @@ export class ViewRootStore implements TViewRootStore {
|
||||
if (this.defaultViews && this.defaultViews.length > 0)
|
||||
runInAction(() => {
|
||||
this.defaultViews?.forEach((view) => {
|
||||
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||
if (view.id)
|
||||
set(
|
||||
this.viewMap,
|
||||
[view.id],
|
||||
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -93,7 +106,12 @@ export class ViewRootStore implements TViewRootStore {
|
||||
|
||||
runInAction(() => {
|
||||
views.forEach((view) => {
|
||||
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||
if (view.id)
|
||||
set(
|
||||
this.viewMap,
|
||||
[view.id],
|
||||
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
|
||||
);
|
||||
});
|
||||
this.loader = undefined;
|
||||
});
|
||||
@ -105,9 +123,13 @@ export class ViewRootStore implements TViewRootStore {
|
||||
const { workspaceSlug, projectId } = this.store.app.router;
|
||||
if (!workspaceSlug || !viewId) return;
|
||||
|
||||
// fetching display properties and display_filters
|
||||
const userView = await this.userService.fetch(workspaceSlug, projectId);
|
||||
if (!userView) return;
|
||||
|
||||
// fetching kanban display filters from local
|
||||
|
||||
// fetching display filters from local and from the view
|
||||
if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) {
|
||||
const view = { ...this.viewById(viewId) };
|
||||
if (!view) return;
|
||||
@ -124,7 +146,12 @@ export class ViewRootStore implements TViewRootStore {
|
||||
view?.display_properties && (view.display_properties = userView.display_properties);
|
||||
|
||||
runInAction(() => {
|
||||
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||
if (view.id)
|
||||
set(
|
||||
this.viewMap,
|
||||
[view.id],
|
||||
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
@ -139,7 +166,12 @@ export class ViewRootStore implements TViewRootStore {
|
||||
if (!view) return;
|
||||
|
||||
runInAction(() => {
|
||||
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||
if (view.id)
|
||||
set(
|
||||
this.viewMap,
|
||||
[view.id],
|
||||
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
|
||||
);
|
||||
});
|
||||
|
||||
if (data.id) this.remove(data.id);
|
||||
@ -169,7 +201,12 @@ export class ViewRootStore implements TViewRootStore {
|
||||
if (!view) return;
|
||||
|
||||
runInAction(() => {
|
||||
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||
if (view.id)
|
||||
set(
|
||||
this.viewMap,
|
||||
[view.id],
|
||||
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
|
||||
);
|
||||
});
|
||||
} catch {}
|
||||
};
|
||||
|
@ -20,6 +20,8 @@ import {
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { FiltersHelper } from "./helpers/filters_helpers";
|
||||
// constants
|
||||
import { EViewPageType, viewDefaultFilterParametersByViewTypeAndLayout } from "constants/view";
|
||||
|
||||
type TLoader = "updating" | undefined;
|
||||
|
||||
@ -79,7 +81,8 @@ export class ViewStore extends FiltersHelper implements TViewStore {
|
||||
private store: RootStore,
|
||||
_view: TView,
|
||||
private service: TViewService,
|
||||
private userService: TUserViewService
|
||||
private userService: TUserViewService,
|
||||
private viewPageType: EViewPageType
|
||||
) {
|
||||
super();
|
||||
this.id = _view.id;
|
||||
@ -89,7 +92,7 @@ export class ViewStore extends FiltersHelper implements TViewStore {
|
||||
this.description = _view.description;
|
||||
this.query = _view.query;
|
||||
this.filters = this.computedFilters(_view.filters);
|
||||
this.display_filters = this.computedDisplayFilters(_view.display_filters);
|
||||
this.display_filters = this.computedDisplayFilters(this.viewPageType, _view.display_filters);
|
||||
this.display_properties = this.computedDisplayProperties(_view.display_properties);
|
||||
this.access = _view.access;
|
||||
this.owned_by = _view.owned_by;
|
||||
@ -108,7 +111,7 @@ export class ViewStore extends FiltersHelper implements TViewStore {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
filters: this.computedFilters(_view.filters),
|
||||
display_filters: this.computedDisplayFilters(_view.display_filters),
|
||||
display_filters: this.computedDisplayFilters(this.viewPageType, _view.display_filters),
|
||||
display_properties: this.computedDisplayProperties(_view.display_properties),
|
||||
};
|
||||
|
||||
@ -164,7 +167,11 @@ export class ViewStore extends FiltersHelper implements TViewStore {
|
||||
get appliedFilters() {
|
||||
return {
|
||||
filters: this.computedFilters(this.filters, this.filtersToUpdate.filters),
|
||||
display_filters: this.computedDisplayFilters(this.display_filters, this.filtersToUpdate.display_filters),
|
||||
display_filters: this.computedDisplayFilters(
|
||||
this.viewPageType,
|
||||
this.display_filters,
|
||||
this.filtersToUpdate.display_filters
|
||||
),
|
||||
display_properties: this.computedDisplayProperties(
|
||||
this.display_properties,
|
||||
this.filtersToUpdate.display_properties
|
||||
@ -173,9 +180,17 @@ export class ViewStore extends FiltersHelper implements TViewStore {
|
||||
}
|
||||
|
||||
get appliedFiltersQueryParams() {
|
||||
const filters = this.appliedFilters;
|
||||
if (!filters) return undefined;
|
||||
return this.computeAppliedFiltersQueryParameters(filters, [])?.query || undefined;
|
||||
const appliedFilters = this.appliedFilters;
|
||||
if (!appliedFilters) return undefined;
|
||||
|
||||
const layout = appliedFilters?.display_filters?.layout;
|
||||
const requiredFilterProperties = viewDefaultFilterParametersByViewTypeAndLayout(
|
||||
this.viewPageType,
|
||||
layout,
|
||||
"filters"
|
||||
);
|
||||
|
||||
return this.computeAppliedFiltersQueryParameters(appliedFilters, requiredFilterProperties)?.query || undefined;
|
||||
}
|
||||
|
||||
get isFiltersApplied() {
|
||||
@ -247,12 +262,18 @@ export class ViewStore extends FiltersHelper implements TViewStore {
|
||||
set(this.filtersToUpdate, ["display_filters", _key], display_filters[_key]);
|
||||
});
|
||||
});
|
||||
|
||||
// update display properties globally
|
||||
|
||||
// updating display properties locally for kanban filters
|
||||
};
|
||||
|
||||
setDisplayProperties = async (displayPropertyKey: keyof TViewDisplayProperties) => {
|
||||
runInAction(() => {
|
||||
update(this.filtersToUpdate, ["display_properties", displayPropertyKey], (_value: boolean = true) => !_value);
|
||||
});
|
||||
|
||||
// update display properties globally
|
||||
};
|
||||
|
||||
setIsEditable = (is_editable: boolean) => {
|
||||
|
Loading…
Reference in New Issue
Block a user