chore: store changes on the filters and display filters

This commit is contained in:
gurusainath 2024-02-13 18:41:34 +05:30
parent 02380c730d
commit a5fd6f0e8a
27 changed files with 717 additions and 298 deletions

View File

@ -1,9 +1,17 @@
declare enum EViewLayouts {
LIST = "list",
KANBAN = "kanban",
CALENDAR = "calendar",
SPREADSHEET = "spreadsheet",
GANTT = "gantt",
}
export type TViewLayouts = export type TViewLayouts =
| "list" | EViewLayouts.LIST
| "kanban" | EViewLayouts.KANBAN
| "calendar" | EViewLayouts.CALENDAR
| "spreadsheet" | EViewLayouts.SPREADSHEET
| "gantt"; | EViewLayouts.GANTT;
export type TViewDisplayFiltersGrouped = export type TViewDisplayFiltersGrouped =
| "project" | "project"

View File

@ -154,7 +154,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" 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> </Breadcrumbs>
</div> </div>
@ -203,7 +205,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
</div> </div>
{currentProjectDetails?.inbox_view && inboxDetails && ( {currentProjectDetails?.inbox_view && inboxDetails && (
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxDetails?.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxDetails?.id}`}>
<span className="hidden md:block" > <span className="hidden md:block">
<Button variant="neutral-primary" size="sm" className="relative"> <Button variant="neutral-primary" size="sm" className="relative">
Inbox Inbox
{inboxDetails?.pending_issue_count > 0 && ( {inboxDetails?.pending_issue_count > 0 && (
@ -218,7 +220,12 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
)} )}
{canUserCreateIssue && ( {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 Analytics
</Button> </Button>
<Button <Button

View File

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

View 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>
);
};

View File

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

View File

@ -7,28 +7,34 @@ import { MonitorDot } from "lucide-react";
// hooks // hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components // components
import { ViewDisplayPropertiesRoot } from "../"; import { ViewDisplayFiltersRoot } from "../";
// ui // ui
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// types // types
import { TViewTypes } from "@plane/types"; import { TViewTypes } from "@plane/types";
// constants
import { EViewPageType } from "constants/view";
type TViewDisplayFiltersDropdown = { type NewType = {
workspaceSlug: string; workspaceSlug: string;
projectId: string | undefined; projectId: string | undefined;
viewId: string; viewId: string;
viewType: TViewTypes; viewType: TViewTypes;
viewPageType: EViewPageType;
children?: ReactNode; children?: ReactNode;
displayDropdownText?: boolean; displayDropdownText?: boolean;
dropdownPlacement?: Placement; dropdownPlacement?: Placement;
}; };
type TViewDisplayFiltersDropdown = NewType;
export const ViewDisplayFiltersDropdown: FC<TViewDisplayFiltersDropdown> = observer((props) => { export const ViewDisplayFiltersDropdown: FC<TViewDisplayFiltersDropdown> = observer((props) => {
const { const {
workspaceSlug, workspaceSlug,
projectId, projectId,
viewId, viewId,
viewType, viewType,
viewPageType,
children, children,
displayDropdownText = true, displayDropdownText = true,
dropdownPlacement = "bottom-start", dropdownPlacement = "bottom-start",
@ -110,18 +116,14 @@ export const ViewDisplayFiltersDropdown: FC<TViewDisplayFiltersDropdown> = obser
{...attributes.popper} {...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" 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="max-h-[500px] space-y-0.5 overflow-y-scroll mb-2">
<div className="space-y-2"> <ViewDisplayFiltersRoot
<div className="text-sm font-medium text-custom-text-200">Properties</div> workspaceSlug={workspaceSlug}
<ViewDisplayPropertiesRoot projectId={projectId}
workspaceSlug={workspaceSlug} viewId={viewId}
projectId={projectId} viewType={viewType}
viewId={viewId} viewPageType={viewPageType}
viewType={viewType} />
/>
</div>
<div className="border border-red-500">Content</div>
</div> </div>
</div> </div>
</Combobox.Options> </Combobox.Options>

View File

@ -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 = { type TViewDisplayFiltersRoot = {
workspaceSlug: string; workspaceSlug: string;
projectId: string | undefined; projectId: string | undefined;
viewId: string; viewId: string;
viewType: TViewTypes;
viewPageType: EViewPageType;
}; };
export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = (props) => { export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId } = 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 ( return (
<div> <div className="space-y-1 divide-y divide-custom-border-300">
<div>ViewDisplayFiltersRoot</div> <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>Show sub issues</div>
<div>Show Empty groups</div>
</div>
</div> </div>
); );
}; });

View File

@ -12,12 +12,14 @@ import { ViewFiltersRoot } from "../";
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// types // types
import { TViewTypes } from "@plane/types"; import { TViewTypes } from "@plane/types";
import { EViewPageType } from "constants/view";
type TViewFiltersDropdown = { type TViewFiltersDropdown = {
workspaceSlug: string; workspaceSlug: string;
projectId: string | undefined; projectId: string | undefined;
viewId: string; viewId: string;
viewType: TViewTypes; viewType: TViewTypes;
viewPageType: EViewPageType;
children?: ReactNode; children?: ReactNode;
displayDropdownText?: boolean; displayDropdownText?: boolean;
dropdownPlacement?: Placement; dropdownPlacement?: Placement;
@ -29,6 +31,7 @@ export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) =>
projectId, projectId,
viewId, viewId,
viewType, viewType,
viewPageType,
children, children,
displayDropdownText = true, displayDropdownText = true,
dropdownPlacement = "bottom-start", dropdownPlacement = "bottom-start",
@ -130,6 +133,7 @@ export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) =>
projectId={projectId} projectId={projectId}
viewId={viewId} viewId={viewId}
viewType={viewType} viewType={viewType}
viewPageType={viewPageType}
dateCustomFilterToggle={dateCustomFilterToggle} dateCustomFilterToggle={dateCustomFilterToggle}
setDateCustomFilterToggle={setDateCustomFilterToggle} setDateCustomFilterToggle={setDateCustomFilterToggle}
/> />

View File

@ -50,14 +50,14 @@ export const ViewFiltersEditDropdown: FC<TViewFiltersEditDropdown> = observer((p
// dropdown options // dropdown options
const dropdownOptions: TViewFilterEditDropdownOptions[] = useMemo( const dropdownOptions: TViewFilterEditDropdownOptions[] = useMemo(
() => [ () => [
// { {
// icon: PhotoFilterIcon, icon: PhotoFilterIcon,
// key: "save_as_new", key: "save_as_new",
// label: "Save as new view", label: "Save as new view",
// onClick: () => { onClick: () => {
// viewOperations.localViewCreateEdit(undefined, viewDetailStore?.filtersToUpdate); viewOperations.localViewCreateEdit(undefined, viewDetailStore?.filtersToUpdate);
// }, },
// }, },
{ {
icon: RotateCcw, icon: RotateCcw,
key: "reset_changes", key: "reset_changes",

View File

@ -10,19 +10,28 @@ import { useViewDetail } from "hooks/store";
import { ViewFiltersItemRoot } from "../"; import { ViewFiltersItemRoot } from "../";
// types // types
import { TViewFilters, TViewTypes } from "@plane/types"; import { TViewFilters, TViewTypes } from "@plane/types";
import { VIEW_DEFAULT_FILTER_PARAMETERS } from "constants/view"; import { EViewPageType, viewDefaultFilterParametersByViewTypeAndLayout } from "constants/view";
type TViewFiltersRoot = { type TViewFiltersRoot = {
workspaceSlug: string; workspaceSlug: string;
projectId: string | undefined; projectId: string | undefined;
viewId: string; viewId: string;
viewType: TViewTypes; viewType: TViewTypes;
viewPageType: EViewPageType;
dateCustomFilterToggle: string | undefined; dateCustomFilterToggle: string | undefined;
setDateCustomFilterToggle: (value: string | undefined) => void; setDateCustomFilterToggle: (value: string | undefined) => void;
}; };
export const ViewFiltersRoot: FC<TViewFiltersRoot> = observer((props) => { 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 // hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
// state // 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 <></>; if (!layout || filtersProperties.length <= 0) return <></>;
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 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="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"> <div className="font-medium text-xs text-custom-text-300 capitalize py-1">
{filterKey.replace("_", " ")} {filterKey.replaceAll("_", " ")}
</div> </div>
<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" className="flex-shrink-0 relative overflow-hidden w-5 h-5 rounded flex justify-center items-center cursor-pointer hover:bg-custom-background-80"

View File

@ -22,6 +22,9 @@ export * from "./filters/edit-dropdown";
// view display filters // view display filters
export * from "./display-filters/dropdown"; export * from "./display-filters/dropdown";
export * from "./display-filters/root"; 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 // view display properties
export * from "./display-properties/root"; export * from "./display-properties/root";

View File

@ -6,49 +6,57 @@ import { useViewDetail } from "hooks/store";
// ui // ui
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// types // types
import { TViewLayouts, TViewTypes } from "@plane/types"; import { TViewTypes } from "@plane/types";
// constants
import { EViewLayouts, EViewPageType, viewPageDefaultLayoutsByPageType } from "constants/view";
type TViewLayoutRoot = { type TViewLayoutRoot = {
workspaceSlug: string; workspaceSlug: string;
projectId: string | undefined; projectId: string | undefined;
viewId: string; viewId: string;
viewType: TViewTypes; viewType: TViewTypes;
viewPageType: EViewPageType;
}; };
const LAYOUTS_DATA: { key: TViewLayouts; title: string; icon: LucideIcon }[] = [ const LAYOUTS_DATA: { key: EViewLayouts; title: string; icon: LucideIcon }[] = [
{ key: "list", title: "List Layout", icon: List }, { key: EViewLayouts.LIST, title: "List Layout", icon: List },
{ key: "kanban", title: "Kanban Layout", icon: Kanban }, { key: EViewLayouts.KANBAN, title: "Kanban Layout", icon: Kanban },
{ key: "calendar", title: "Calendar Layout", icon: Calendar }, { key: EViewLayouts.CALENDAR, title: "Calendar Layout", icon: Calendar },
{ key: "spreadsheet", title: "Spreadsheet Layout", icon: Sheet }, { key: EViewLayouts.SPREADSHEET, title: "Spreadsheet Layout", icon: Sheet },
{ key: "gantt", title: "Gantt Chart layout", icon: GanttChartSquare }, { key: EViewLayouts.GANTT, title: "Gantt Chart layout", icon: GanttChartSquare },
]; ];
export const ViewLayoutRoot: FC<TViewLayoutRoot> = observer((props) => { export const ViewLayoutRoot: FC<TViewLayoutRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType } = props; const { workspaceSlug, projectId, viewId, viewType, viewPageType } = props;
// hooks // hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
if (!viewDetailStore) return <></>; const validLayouts = viewPageDefaultLayoutsByPageType(viewPageType);
if (!viewDetailStore || validLayouts.length <= 1) return <></>;
return ( return (
<div className="relative flex gap-0.5 items-center bg-custom-background-80 rounded p-1 shadow-custom-shadow-2xs"> <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) => {
<Fragment key={layout.key}> if (!validLayouts.includes(layout.key)) return <Fragment key={layout.key} />;
<Tooltip tooltipContent={layout.title} position="bottom"> return (
<div <Fragment key={layout.key}>
className={`relative h-6 w-7 flex justify-center items-center overflow-hidden rounded transition-all cursor-pointer <Tooltip tooltipContent={layout.title} position="bottom">
<div
className={`relative h-6 w-7 flex justify-center items-center overflow-hidden rounded transition-all cursor-pointer
${ ${
viewDetailStore?.filtersToUpdate?.display_filters?.layout === layout.key viewDetailStore?.filtersToUpdate?.display_filters?.layout === layout.key
? `bg-custom-background-100 shadow-custom-shadow-2xs` ? `bg-custom-background-100 shadow-custom-shadow-2xs`
: `hover:bg-custom-background-100` : `hover:bg-custom-background-100`
} }
`} `}
onClick={() => viewDetailStore.setDisplayFilters({ layout: layout.key })} onClick={() => viewDetailStore.setDisplayFilters({ layout: layout.key })}
> >
<layout.icon size={12} /> <layout.icon size={12} />
</div> </div>
</Tooltip> </Tooltip>
</Fragment> </Fragment>
))} );
})}
</div> </div>
); );
}); });

View File

@ -23,7 +23,7 @@ import {
// ui // ui
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// constants // constants
import { viewLocalPayload } from "constants/view"; import { EViewPageType, viewLocalPayload } from "constants/view";
// types // types
import { TViewOperations } from "./types"; import { TViewOperations } from "./types";
import { TView, TViewTypes } from "@plane/types"; import { TView, TViewTypes } from "@plane/types";
@ -33,6 +33,7 @@ type TGlobalViewRoot = {
projectId: string | undefined; projectId: string | undefined;
viewId: string; viewId: string;
viewType: TViewTypes; viewType: TViewTypes;
viewPageType: EViewPageType;
baseRoute: string; baseRoute: string;
workspaceViewTabOptions: { key: TViewTypes; title: string; href: string }[]; workspaceViewTabOptions: { key: TViewTypes; title: string; href: string }[];
}; };
@ -43,7 +44,7 @@ type TViewOperationsToggle = {
}; };
export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => { 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 // hooks
const viewStore = useView(workspaceSlug, projectId, viewType); const viewStore = useView(workspaceSlug, projectId, viewType);
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
@ -232,7 +233,13 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
</div> </div>
<div className="flex-shrink-0"> <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>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
@ -241,6 +248,7 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
projectId={projectId} projectId={projectId}
viewId={viewId} viewId={viewId}
viewType={viewType} viewType={viewType}
viewPageType={viewPageType}
displayDropdownText={false} displayDropdownText={false}
/> />
</div> </div>
@ -251,24 +259,29 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
projectId={projectId} projectId={projectId}
viewId={viewId} viewId={viewId}
viewType={viewType} viewType={viewType}
viewPageType={viewPageType}
displayDropdownText={false} displayDropdownText={false}
/> />
</div> </div>
<ViewEditDropdown <div className="flex-shrink-0">
workspaceSlug={workspaceSlug} <ViewEditDropdown
projectId={projectId} workspaceSlug={workspaceSlug}
viewId={viewId} projectId={projectId}
viewOperations={viewOperations} viewId={viewId}
/> viewOperations={viewOperations}
/>
</div>
<ViewFiltersEditDropdown <div className="flex-shrink-0">
workspaceSlug={workspaceSlug} <ViewFiltersEditDropdown
projectId={projectId} workspaceSlug={workspaceSlug}
viewId={viewId} projectId={projectId}
viewType={viewType} viewId={viewId}
viewOperations={viewOperations} viewType={viewType}
/> viewOperations={viewOperations}
/>
</div>
</div> </div>
</> </>
)} )}
@ -282,6 +295,7 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
projectId={projectId} projectId={projectId}
viewId={viewOperationsToggle.viewId} viewId={viewOperationsToggle.viewId}
viewType={viewType} viewType={viewType}
viewPageType={viewPageType}
viewOperations={viewOperations} viewOperations={viewOperations}
/> />
)} )}

View File

@ -11,17 +11,20 @@ import { Input, Button } from "@plane/ui";
// types // types
import { TViewTypes } from "@plane/types"; import { TViewTypes } from "@plane/types";
import { TViewOperations } from "../types"; import { TViewOperations } from "../types";
// constants
import { EViewPageType } from "constants/view";
type TViewCreateEditForm = { type TViewCreateEditForm = {
workspaceSlug: string; workspaceSlug: string;
projectId: string | undefined; projectId: string | undefined;
viewId: string; viewId: string;
viewType: TViewTypes; viewType: TViewTypes;
viewPageType: EViewPageType;
viewOperations: TViewOperations; viewOperations: TViewOperations;
}; };
export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => { export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props; const { workspaceSlug, projectId, viewId, viewType, viewPageType, viewOperations } = props;
// hooks // hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const { getProjectById } = useProject(); const { getProjectById } = useProject();
@ -126,6 +129,7 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
projectId={projectId} projectId={projectId}
viewId={viewId} viewId={viewId}
viewType={viewType} viewType={viewType}
viewPageType={viewPageType}
dropdownPlacement="right" 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"> <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">

View File

@ -49,49 +49,49 @@ export const ViewEditDropdown: FC<TViewEditDropdown> = observer((props) => {
onClick: () => viewOperations.localViewCreateEdit(viewId), onClick: () => viewOperations.localViewCreateEdit(viewId),
children: undefined, children: undefined,
}, },
// { {
// icon: Eye, icon: Eye,
// key: "accessability", key: "accessability",
// label: "Change Accessability", label: "Change Accessability",
// onClick: () => {}, onClick: () => {},
// children: [ children: [
// { {
// icon: Eye, icon: Eye,
// key: "private", key: "private",
// label: "Private", label: "Private",
// onClick: () => viewOperations.create({}), onClick: () => viewOperations.create({}),
// children: undefined, children: undefined,
// }, },
// { {
// icon: Globe2, icon: Globe2,
// key: "public", key: "public",
// label: "Public", label: "Public",
// onClick: () => viewOperations.create({}), onClick: () => viewOperations.create({}),
// children: undefined, children: undefined,
// }, },
// ], ],
// }, },
// { {
// icon: Copy, icon: Copy,
// key: "duplicate", key: "duplicate",
// label: "Duplicate view", label: "Duplicate view",
// onClick: () => viewOperations.remove(viewId), onClick: () => viewOperations.remove(viewId),
// children: undefined, children: undefined,
// }, },
// { {
// icon: Link2, icon: Link2,
// key: "copy_link", key: "copy_link",
// label: "Copy view link", label: "Copy view link",
// onClick: () => viewOperations.remove(viewId), onClick: () => viewOperations.remove(viewId),
// children: undefined, children: undefined,
// }, },
// { {
// icon: Trash, icon: Trash,
// key: "delete", key: "delete",
// label: "Delete view", label: "Delete view",
// onClick: () => viewOperations.remove(viewId), onClick: () => viewOperations.remove(viewId),
// children: undefined, children: undefined,
// }, },
], ],
[viewOperations, viewId] [viewOperations, viewId]
); );

View File

@ -449,4 +449,4 @@ export const groupReactionEmojis = (reactions: any) => {
} }
return _groupedEmojis; return _groupedEmojis;
}; };

View File

@ -1,40 +1,24 @@
// types // types
import { TStateGroups, TIssuePriorities, TViewFilters, TViewDisplayFilters, TViewLayouts } from "@plane/types"; import {
TStateGroups,
TIssuePriorities,
TViewFilters,
TViewDisplayFilters,
TViewDisplayFiltersGrouped,
TViewDisplayFiltersOrderBy,
TViewDisplayFiltersType,
} from "@plane/types";
// filters constants // filters constants
export const STATE_GROUP_PROPERTY: { export const STATE_GROUP_PROPERTY: Record<TStateGroups, { label: string; color: string }> = {
[key in TStateGroups]: { backlog: { label: "Backlog", color: "#d9d9d9" },
label: string; unstarted: { label: "Unstarted", color: "#3f76ff" },
color: string; started: { label: "Started", color: "#f59e0b" },
}; completed: { label: "Completed", color: "#16a34a" },
} = { cancelled: { label: "Canceled", color: "#dc2626" },
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: { export const PRIORITIES_PROPERTY: Record<TIssuePriorities, { label: string }> = {
[key in TIssuePriorities]: {
label: string;
};
} = {
urgent: { label: "Urgent" }, urgent: { label: "Urgent" },
high: { label: "High" }, high: { label: "High" },
medium: { label: "Medium" }, medium: { label: "Medium" },
@ -42,11 +26,7 @@ export const PRIORITIES_PROPERTY: {
none: { label: "None" }, none: { label: "None" },
}; };
export const DATE_PROPERTY: { export const DATE_PROPERTY: Record<string, { label: string }> = {
[key in string]: {
label: string;
};
} = {
"1_weeks;after;fromnow": { label: "1 week from now" }, "1_weeks;after;fromnow": { label: "1 week from now" },
"2_weeks;after;fromnow": { label: "2 weeks from now" }, "2_weeks;after;fromnow": { label: "2 weeks from now" },
"1_months;after;fromnow": { label: "1 month from now" }, "1_months;after;fromnow": { label: "1 month from now" },
@ -55,77 +35,98 @@ export const DATE_PROPERTY: {
}; };
// display filter constants // 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 export const ORDER_BY_PROPERTY: Partial<Record<TViewDisplayFiltersOrderBy, Record<string, string>>> = {
type TViewLayoutFilterProperties = { 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>[]; filters: Partial<keyof TViewFilters>[];
readonlyFilters?: Partial<keyof TViewFilters>[];
display_filters: Partial<keyof TViewDisplayFilters>[]; display_filters: Partial<keyof TViewDisplayFilters>[];
extra_options: ("sub_issue" | "show_empty_groups")[]; extra_options: ("sub_issue" | "show_empty_groups")[];
display_properties: boolean; display_properties: boolean;
readonlyFilters?: Partial<keyof TViewFilters>[];
}; };
type TViewLayoutFilters = { export type TViewLayoutFilters = {
list: TViewLayoutFilterProperties; layouts: Partial<EViewLayouts>[];
kanban: TViewLayoutFilterProperties; [EViewLayouts.LIST]: TViewLayoutFilterProperties;
calendar: TViewLayoutFilterProperties; [EViewLayouts.KANBAN]: TViewLayoutFilterProperties;
spreadsheet: TViewLayoutFilterProperties; [EViewLayouts.CALENDAR]: TViewLayoutFilterProperties;
gantt: TViewLayoutFilterProperties; [EViewLayouts.SPREADSHEET]: TViewLayoutFilterProperties;
[EViewLayouts.GANTT]: TViewLayoutFilterProperties;
}; };
type TFilterPermissions = { export type TFilterPermissions = {
all: Omit<TViewLayoutFilters, "list" | "kanban" | "calendar" | "gantt"> & { [EViewPageType.ALL]: Partial<TViewLayoutFilters>;
layouts: Omit<TViewLayouts, "list" | "kanban" | "calendar" | "gantt">[]; [EViewPageType.PROFILE]: Partial<TViewLayoutFilters>;
}; [EViewPageType.PROJECT]: TViewLayoutFilters;
profile: Omit<TViewLayoutFilters, "spreadsheet" | "calendar" | "gantt"> & { [EViewPageType.ARCHIVED]: Partial<TViewLayoutFilters>;
layouts: Omit<TViewLayouts, "spreadsheet" | "calendar" | "gantt">[]; [EViewPageType.DRAFT]: Partial<TViewLayoutFilters>;
};
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">[];
};
}; };
const ALL_FILTER_PERMISSIONS: TFilterPermissions["all"] = { const ALL_FILTER_PERMISSIONS: TFilterPermissions["all"] = {
layouts: ["spreadsheet"], layouts: [EViewLayouts.SPREADSHEET],
spreadsheet: { [EViewLayouts.SPREADSHEET]: {
// filters: ["project", "priority", "state_group", "assignees", "created_by", "labels", "start_date", "target_date"], filters: ["project", "priority", "state_group", "assignees", "created_by", "labels", "start_date", "target_date"],
filters: [ display_filters: ["type"],
"project", // extra_options: [],
"module", extra_options: ["sub_issue", "show_empty_groups"],
"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: [],
display_properties: true, display_properties: true,
}, },
}; };
const PROFILE_FILTER_PERMISSIONS: TFilterPermissions["profile"] = { const PROFILE_FILTER_PERMISSIONS: TFilterPermissions["profile"] = {
layouts: ["list", "kanban"], layouts: [EViewLayouts.LIST, EViewLayouts.KANBAN],
list: { [EViewLayouts.LIST]: {
filters: ["priority", "state_group", "labels", "start_date", "target_date"], filters: ["priority", "state_group", "labels", "start_date", "target_date"],
display_filters: ["group_by", "order_by", "type"], display_filters: ["group_by", "order_by", "type"],
extra_options: [], extra_options: [],
display_properties: true, display_properties: true,
}, },
kanban: { [EViewLayouts.KANBAN]: {
filters: ["priority", "state_group", "labels", "start_date", "target_date"], filters: ["priority", "state_group", "labels", "start_date", "target_date"],
display_filters: ["group_by", "order_by", "type"], display_filters: ["group_by", "order_by", "type"],
extra_options: [], extra_options: [],
@ -134,8 +135,14 @@ const PROFILE_FILTER_PERMISSIONS: TFilterPermissions["profile"] = {
}; };
const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = { const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
layouts: ["list", "kanban", "spreadsheet", "calendar", "gantt"], layouts: [
list: { EViewLayouts.LIST,
EViewLayouts.KANBAN,
EViewLayouts.CALENDAR,
EViewLayouts.SPREADSHEET,
EViewLayouts.GANTT,
],
[EViewLayouts.LIST]: {
filters: [ filters: [
"priority", "priority",
"state", "state",
@ -152,7 +159,7 @@ const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
extra_options: ["sub_issue", "show_empty_groups"], extra_options: ["sub_issue", "show_empty_groups"],
display_properties: true, display_properties: true,
}, },
kanban: { [EViewLayouts.KANBAN]: {
filters: [ filters: [
"priority", "priority",
"state", "state",
@ -169,7 +176,7 @@ const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
extra_options: ["sub_issue", "show_empty_groups"], extra_options: ["sub_issue", "show_empty_groups"],
display_properties: true, display_properties: true,
}, },
calendar: { [EViewLayouts.CALENDAR]: {
filters: [ filters: [
"priority", "priority",
"state", "state",
@ -186,7 +193,7 @@ const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
extra_options: ["sub_issue"], extra_options: ["sub_issue"],
display_properties: true, display_properties: true,
}, },
spreadsheet: { [EViewLayouts.SPREADSHEET]: {
filters: [ filters: [
"priority", "priority",
"state", "state",
@ -203,8 +210,7 @@ const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
extra_options: [], extra_options: [],
display_properties: true, display_properties: true,
}, },
[EViewLayouts.GANTT]: {
gantt: {
filters: [ filters: [
"priority", "priority",
"state", "state",
@ -224,8 +230,8 @@ const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
}; };
const ARCHIVED_FILTER_PERMISSIONS: TFilterPermissions["archived"] = { const ARCHIVED_FILTER_PERMISSIONS: TFilterPermissions["archived"] = {
layouts: ["list"], layouts: [EViewLayouts.LIST],
list: { [EViewLayouts.LIST]: {
filters: [ filters: [
"priority", "priority",
"state", "state",
@ -245,8 +251,8 @@ const ARCHIVED_FILTER_PERMISSIONS: TFilterPermissions["archived"] = {
}; };
const DRAFT_FILTER_PERMISSIONS: TFilterPermissions["draft"] = { const DRAFT_FILTER_PERMISSIONS: TFilterPermissions["draft"] = {
layouts: ["list", "kanban"], layouts: [EViewLayouts.LIST, EViewLayouts.KANBAN],
list: { [EViewLayouts.LIST]: {
filters: [ filters: [
"priority", "priority",
"state", "state",
@ -263,7 +269,7 @@ const DRAFT_FILTER_PERMISSIONS: TFilterPermissions["draft"] = {
extra_options: ["sub_issue", "show_empty_groups"], extra_options: ["sub_issue", "show_empty_groups"],
display_properties: true, display_properties: true,
}, },
kanban: { [EViewLayouts.KANBAN]: {
filters: [ filters: [
"priority", "priority",
"state", "state",
@ -283,9 +289,19 @@ const DRAFT_FILTER_PERMISSIONS: TFilterPermissions["draft"] = {
}; };
export const VIEW_DEFAULT_FILTER_PARAMETERS: TFilterPermissions = { export const VIEW_DEFAULT_FILTER_PARAMETERS: TFilterPermissions = {
all: ALL_FILTER_PERMISSIONS, [EViewPageType.ALL]: ALL_FILTER_PERMISSIONS,
profile: PROFILE_FILTER_PERMISSIONS, [EViewPageType.PROFILE]: PROFILE_FILTER_PERMISSIONS,
project: PROJECT_FILTER_PERMISSIONS, [EViewPageType.PROJECT]: PROJECT_FILTER_PERMISSIONS,
archived: ARCHIVED_FILTER_PERMISSIONS, [EViewPageType.ARCHIVED]: ARCHIVED_FILTER_PERMISSIONS,
draft: DRAFT_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];

View File

@ -13,9 +13,24 @@ import {
StateGroupIcon, StateGroupIcon,
} from "@plane/ui"; } from "@plane/ui";
// types // types
import { TIssuePriorities, TStateGroups, TViewFilters } from "@plane/types"; import {
TIssuePriorities,
TStateGroups,
TViewFilters,
TViewDisplayFilters,
TViewDisplayFiltersGrouped,
TViewDisplayFiltersOrderBy,
TViewDisplayFiltersType,
} from "@plane/types";
// constants // 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 // helpers
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
import { renderFormattedDate } from "helpers/date-time.helper"; import { renderFormattedDate } from "helpers/date-time.helper";
@ -30,6 +45,11 @@ type TFilterPropertyDefaultDetails = {
label: string; label: string;
}; };
type TDisplayFilterPropertyDetails = {
icon: ReactNode;
label: string;
};
export const useViewFilter = (workspaceSlug: string, projectId: string | undefined) => { export const useViewFilter = (workspaceSlug: string, projectId: string | undefined) => {
const { projectMap, getProjectById } = useProject(); const { projectMap, getProjectById } = useProject();
const { getProjectModuleIds, getModuleById } = useModule(); 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 { return {
filterIdsWithKey, filterIdsWithKey,
propertyDefaultDetails, propertyDefaultDetails,
propertyDetails, propertyDetails,
displayFilterIdsWithKey,
displayPropertyDetails,
}; };
}; };

View File

@ -7,7 +7,7 @@ import { GlobalViewRoot } from "components/view";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants // constants
import { VIEW_TYPES } from "constants/view"; import { EViewPageType, VIEW_TYPES } from "constants/view";
const ProjectPrivateViewPage: NextPageWithLayout = () => { const ProjectPrivateViewPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
@ -38,6 +38,7 @@ const ProjectPrivateViewPage: NextPageWithLayout = () => {
projectId={projectId.toString()} projectId={projectId.toString()}
viewId={viewId.toString()} viewId={viewId.toString()}
viewType={VIEW_TYPES.PROJECT_PRIVATE_VIEWS} viewType={VIEW_TYPES.PROJECT_PRIVATE_VIEWS}
viewPageType={EViewPageType.PROJECT}
baseRoute={`/${workspaceSlug?.toString()}/projects/${projectId}/views/private`} baseRoute={`/${workspaceSlug?.toString()}/projects/${projectId}/views/private`}
workspaceViewTabOptions={workspaceViewTabOptions} workspaceViewTabOptions={workspaceViewTabOptions}
/> />

View File

@ -1,49 +1,16 @@
import { ReactElement, useMemo } from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components
import { GlobalViewRoot } from "components/view";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants
import { VIEW_TYPES } from "constants/view";
const ProjectPrivateViewPage: NextPageWithLayout = () => { const ProjectPrivateViewPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query; 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 <></>; if (!workspaceSlug || !projectId || !viewId) return <></>;
return ( return <div />;
<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>
);
}; };
ProjectPrivateViewPage.getLayout = function getLayout(page: ReactElement) { ProjectPrivateViewPage.getLayout = function getLayout(page: ReactElement) {

View File

@ -7,7 +7,7 @@ import { GlobalViewRoot } from "components/view";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants // constants
import { VIEW_TYPES } from "constants/view"; import { EViewPageType, VIEW_TYPES } from "constants/view";
const ProjectPublicViewPage: NextPageWithLayout = () => { const ProjectPublicViewPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
@ -38,6 +38,7 @@ const ProjectPublicViewPage: NextPageWithLayout = () => {
projectId={undefined} projectId={undefined}
viewId={viewId.toString()} viewId={viewId.toString()}
viewType={VIEW_TYPES.PROJECT_PUBLIC_VIEWS} viewType={VIEW_TYPES.PROJECT_PUBLIC_VIEWS}
viewPageType={EViewPageType.PROJECT}
baseRoute={`/${workspaceSlug?.toString()}/views/public`} baseRoute={`/${workspaceSlug?.toString()}/views/public`}
workspaceViewTabOptions={workspaceViewTabOptions} workspaceViewTabOptions={workspaceViewTabOptions}
/> />

View File

@ -7,7 +7,7 @@ import { GlobalViewRoot } from "components/view";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants // constants
import { VIEW_TYPES } from "constants/view"; import { EViewPageType, VIEW_TYPES } from "constants/view";
const WorkspacePrivateViewPage: NextPageWithLayout = () => { const WorkspacePrivateViewPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
@ -38,6 +38,7 @@ const WorkspacePrivateViewPage: NextPageWithLayout = () => {
projectId={undefined} projectId={undefined}
viewId={viewId.toString()} viewId={viewId.toString()}
viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS} viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS}
viewPageType={EViewPageType.ALL}
baseRoute={`/${workspaceSlug?.toString()}/views/private`} baseRoute={`/${workspaceSlug?.toString()}/views/private`}
workspaceViewTabOptions={workspaceViewTabOptions} workspaceViewTabOptions={workspaceViewTabOptions}
/> />

View File

@ -7,7 +7,7 @@ import { GlobalViewRoot } from "components/view";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants // constants
import { VIEW_TYPES } from "constants/view"; import { EViewPageType, VIEW_TYPES } from "constants/view";
const WorkspacePublicViewPage: NextPageWithLayout = () => { const WorkspacePublicViewPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
@ -38,6 +38,7 @@ const WorkspacePublicViewPage: NextPageWithLayout = () => {
projectId={undefined} projectId={undefined}
viewId={viewId.toString()} viewId={viewId.toString()}
viewType={VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS} viewType={VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS}
viewPageType={EViewPageType.ALL}
baseRoute={`/${workspaceSlug?.toString()}/views/public`} baseRoute={`/${workspaceSlug?.toString()}/views/public`}
workspaceViewTabOptions={workspaceViewTabOptions} workspaceViewTabOptions={workspaceViewTabOptions}
/> />

View File

@ -8,6 +8,8 @@ import {
TViewFilterProps, TViewFilterProps,
TViewFilterQueryParams, TViewFilterQueryParams,
} from "@plane/types"; } from "@plane/types";
// constants
import { EViewPageType, viewPageDefaultLayoutsByPageType } from "constants/view";
export class FiltersHelper { export class FiltersHelper {
// computed filters // computed filters
@ -29,21 +31,26 @@ export class FiltersHelper {
// computed display filters // computed display filters
computedDisplayFilters = ( computedDisplayFilters = (
viewPageType: EViewPageType,
displayFilters: TViewDisplayFilters, displayFilters: TViewDisplayFilters,
defaultValues?: Partial<TViewDisplayFilters> defaultValues?: Partial<TViewDisplayFilters>
): TViewDisplayFilters => ({ ): TViewDisplayFilters => {
layout: defaultValues?.layout || displayFilters?.layout || "list", const viewPageDefaultLayout = viewPageDefaultLayoutsByPageType(viewPageType)?.[0] || "list";
group_by: defaultValues?.group_by || displayFilters?.group_by || undefined,
sub_group_by: defaultValues?.sub_group_by || displayFilters?.sub_group_by || undefined, return {
order_by: defaultValues?.order_by || displayFilters?.order_by || "sort_order", layout: defaultValues?.layout || displayFilters?.layout || viewPageDefaultLayout,
type: defaultValues?.type || displayFilters?.type || undefined, group_by: defaultValues?.group_by || displayFilters?.group_by || undefined,
sub_issue: defaultValues?.sub_issue || displayFilters?.sub_issue || false, sub_group_by: defaultValues?.sub_group_by || displayFilters?.sub_group_by || undefined,
show_empty_groups: defaultValues?.show_empty_groups || displayFilters?.show_empty_groups || false, order_by: defaultValues?.order_by || displayFilters?.order_by || "sort_order",
calendar: { type: defaultValues?.type || displayFilters?.type || undefined,
show_weekends: defaultValues?.calendar?.show_weekends || displayFilters?.calendar?.show_weekends || false, sub_issue: defaultValues?.sub_issue || displayFilters?.sub_issue || false,
layout: defaultValues?.calendar?.layout || displayFilters?.calendar?.layout || "month", show_empty_groups: defaultValues?.show_empty_groups || displayFilters?.show_empty_groups || false,
}, calendar: {
}); show_weekends: defaultValues?.calendar?.show_weekends || displayFilters?.calendar?.show_weekends || false,
layout: defaultValues?.calendar?.layout || displayFilters?.calendar?.layout || "month",
},
};
};
// computed display properties // computed display properties
computedDisplayProperties = ( computedDisplayProperties = (

View File

@ -11,6 +11,8 @@ import {
} from "services/view"; } from "services/view";
// types // types
import { RootStore } from "store/root.store"; import { RootStore } from "store/root.store";
// constants
import { EViewPageType } from "constants/view";
export class GlobalViewRootStore { export class GlobalViewRootStore {
workspacePrivateViewStore: ViewRootStore; workspacePrivateViewStore: ViewRootStore;
@ -59,25 +61,29 @@ export class GlobalViewRootStore {
this.store, this.store,
workspacePrivateDefaultViews, workspacePrivateDefaultViews,
new WorkspacePrivateViewService(), new WorkspacePrivateViewService(),
new WorkspaceFiltersService() new WorkspaceFiltersService(),
EViewPageType.ALL
); );
this.workspacePublicViewStore = new ViewRootStore( this.workspacePublicViewStore = new ViewRootStore(
this.store, this.store,
workspacePublicDefaultViews, workspacePublicDefaultViews,
new WorkspacePublicViewService(), new WorkspacePublicViewService(),
new WorkspaceFiltersService() new WorkspaceFiltersService(),
EViewPageType.ALL
); );
this.projectPrivateViewStore = new ViewRootStore( this.projectPrivateViewStore = new ViewRootStore(
this.store, this.store,
undefined, undefined,
new ProjectPrivateViewService(), new ProjectPrivateViewService(),
new ProjectFiltersService() new ProjectFiltersService(),
EViewPageType.PROJECT
); );
this.projectPublicViewStore = new ViewRootStore( this.projectPublicViewStore = new ViewRootStore(
this.store, this.store,
undefined, undefined,
new ProjectPublicViewService(), new ProjectPublicViewService(),
new ProjectFiltersService() new ProjectFiltersService(),
EViewPageType.PROJECT
); );
} }
} }

View File

@ -9,6 +9,8 @@ import { ViewStore } from "./view.store";
// types // types
import { TUserViewService, TViewService } from "services/view/types"; import { TUserViewService, TViewService } from "services/view/types";
import { TView } from "@plane/types"; import { TView } from "@plane/types";
// constants
import { EViewPageType } from "constants/view";
export type TLoader = "init-loader" | "mutation-loader" | "submitting" | undefined; export type TLoader = "init-loader" | "mutation-loader" | "submitting" | undefined;
@ -37,7 +39,8 @@ export class ViewRootStore implements TViewRootStore {
private store: RootStore, private store: RootStore,
private defaultViews: TView[] = [], private defaultViews: TView[] = [],
private service: TViewService, private service: TViewService,
private userService: TUserViewService private userService: TUserViewService,
private viewPageType: EViewPageType
) { ) {
makeObservable(this, { makeObservable(this, {
// observables // observables
@ -70,7 +73,12 @@ export class ViewRootStore implements TViewRootStore {
// actions // actions
localViewCreate = async (view: TView) => { localViewCreate = async (view: TView) => {
runInAction(() => { 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) if (this.defaultViews && this.defaultViews.length > 0)
runInAction(() => { runInAction(() => {
this.defaultViews?.forEach((view) => { 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(() => { runInAction(() => {
views.forEach((view) => { 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; this.loader = undefined;
}); });
@ -105,9 +123,13 @@ export class ViewRootStore implements TViewRootStore {
const { workspaceSlug, projectId } = this.store.app.router; const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug || !viewId) return; if (!workspaceSlug || !viewId) return;
// fetching display properties and display_filters
const userView = await this.userService.fetch(workspaceSlug, projectId); const userView = await this.userService.fetch(workspaceSlug, projectId);
if (!userView) return; 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)) { if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) {
const view = { ...this.viewById(viewId) }; const view = { ...this.viewById(viewId) };
if (!view) return; if (!view) return;
@ -124,7 +146,12 @@ export class ViewRootStore implements TViewRootStore {
view?.display_properties && (view.display_properties = userView.display_properties); view?.display_properties && (view.display_properties = userView.display_properties);
runInAction(() => { 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 {} } catch {}
@ -139,7 +166,12 @@ export class ViewRootStore implements TViewRootStore {
if (!view) return; if (!view) return;
runInAction(() => { 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); if (data.id) this.remove(data.id);
@ -169,7 +201,12 @@ export class ViewRootStore implements TViewRootStore {
if (!view) return; if (!view) return;
runInAction(() => { 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 {} } catch {}
}; };

View File

@ -20,6 +20,8 @@ import {
} from "@plane/types"; } from "@plane/types";
// helpers // helpers
import { FiltersHelper } from "./helpers/filters_helpers"; import { FiltersHelper } from "./helpers/filters_helpers";
// constants
import { EViewPageType, viewDefaultFilterParametersByViewTypeAndLayout } from "constants/view";
type TLoader = "updating" | undefined; type TLoader = "updating" | undefined;
@ -79,7 +81,8 @@ export class ViewStore extends FiltersHelper implements TViewStore {
private store: RootStore, private store: RootStore,
_view: TView, _view: TView,
private service: TViewService, private service: TViewService,
private userService: TUserViewService private userService: TUserViewService,
private viewPageType: EViewPageType
) { ) {
super(); super();
this.id = _view.id; this.id = _view.id;
@ -89,7 +92,7 @@ export class ViewStore extends FiltersHelper implements TViewStore {
this.description = _view.description; this.description = _view.description;
this.query = _view.query; this.query = _view.query;
this.filters = this.computedFilters(_view.filters); 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.display_properties = this.computedDisplayProperties(_view.display_properties);
this.access = _view.access; this.access = _view.access;
this.owned_by = _view.owned_by; this.owned_by = _view.owned_by;
@ -108,7 +111,7 @@ export class ViewStore extends FiltersHelper implements TViewStore {
name: this.name, name: this.name,
description: this.description, description: this.description,
filters: this.computedFilters(_view.filters), 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), display_properties: this.computedDisplayProperties(_view.display_properties),
}; };
@ -164,7 +167,11 @@ export class ViewStore extends FiltersHelper implements TViewStore {
get appliedFilters() { get appliedFilters() {
return { return {
filters: this.computedFilters(this.filters, this.filtersToUpdate.filters), 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( display_properties: this.computedDisplayProperties(
this.display_properties, this.display_properties,
this.filtersToUpdate.display_properties this.filtersToUpdate.display_properties
@ -173,9 +180,17 @@ export class ViewStore extends FiltersHelper implements TViewStore {
} }
get appliedFiltersQueryParams() { get appliedFiltersQueryParams() {
const filters = this.appliedFilters; const appliedFilters = this.appliedFilters;
if (!filters) return undefined; if (!appliedFilters) return undefined;
return this.computeAppliedFiltersQueryParameters(filters, [])?.query || undefined;
const layout = appliedFilters?.display_filters?.layout;
const requiredFilterProperties = viewDefaultFilterParametersByViewTypeAndLayout(
this.viewPageType,
layout,
"filters"
);
return this.computeAppliedFiltersQueryParameters(appliedFilters, requiredFilterProperties)?.query || undefined;
} }
get isFiltersApplied() { get isFiltersApplied() {
@ -247,12 +262,18 @@ export class ViewStore extends FiltersHelper implements TViewStore {
set(this.filtersToUpdate, ["display_filters", _key], display_filters[_key]); 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) => { setDisplayProperties = async (displayPropertyKey: keyof TViewDisplayProperties) => {
runInAction(() => { runInAction(() => {
update(this.filtersToUpdate, ["display_properties", displayPropertyKey], (_value: boolean = true) => !_value); update(this.filtersToUpdate, ["display_properties", displayPropertyKey], (_value: boolean = true) => !_value);
}); });
// update display properties globally
}; };
setIsEditable = (is_editable: boolean) => { setIsEditable = (is_editable: boolean) => {