chore: updated empty states

This commit is contained in:
gurusainath 2024-02-14 19:14:18 +05:30
parent c35d650de0
commit 430d9de722
12 changed files with 396 additions and 94 deletions

View File

@ -54,7 +54,7 @@ const navigation = (workspaceSlug: string, projectId: string) => [
}, },
{ {
name: "Views", name: "Views",
href: `/${workspaceSlug}/projects/${projectId}/views`, href: `/${workspaceSlug}/projects/${projectId}/views/public`,
Icon: PhotoFilterIcon, Icon: PhotoFilterIcon,
}, },
{ {
@ -147,7 +147,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
{({ open }) => ( {({ open }) => (
<> <>
<div <div
className={`group relative flex w-full items-center rounded-md px-2 py-1 text-custom-sidebar-text-10 hover:bg-custom-sidebar-background-80 ${snapshot?.isDragging ? "opacity-60" : "" className={`group relative flex w-full items-center rounded-md px-2 py-1 text-custom-sidebar-text-10 hover:bg-custom-sidebar-background-80 ${
snapshot?.isDragging ? "opacity-60" : ""
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`} } ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
> >
{provided && ( {provided && (
@ -157,8 +158,10 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
> >
<button <button
type="button" type="button"
className={`absolute -left-2.5 top-1/2 hidden -translate-y-1/2 rounded p-0.5 text-custom-sidebar-text-400 ${isCollapsed ? "" : "group-hover:!flex" className={`absolute -left-2.5 top-1/2 hidden -translate-y-1/2 rounded p-0.5 text-custom-sidebar-text-400 ${
} ${project.sort_order === null ? "cursor-not-allowed opacity-60" : ""} ${isMenuActive ? "!flex" : "" isCollapsed ? "" : "group-hover:!flex"
} ${project.sort_order === null ? "cursor-not-allowed opacity-60" : ""} ${
isMenuActive ? "!flex" : ""
}`} }`}
{...provided?.dragHandleProps} {...provided?.dragHandleProps}
> >
@ -170,11 +173,13 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
<Tooltip tooltipContent={`${project.name}`} position="right" className="ml-2" disabled={!isCollapsed}> <Tooltip tooltipContent={`${project.name}`} position="right" className="ml-2" disabled={!isCollapsed}>
<Disclosure.Button <Disclosure.Button
as="div" as="div"
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${isCollapsed ? "justify-center" : `justify-between` className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${
isCollapsed ? "justify-center" : `justify-between`
}`} }`}
> >
<div <div
className={`flex w-full flex-grow items-center gap-x-2 truncate ${isCollapsed ? "justify-center" : "" className={`flex w-full flex-grow items-center gap-x-2 truncate ${
isCollapsed ? "justify-center" : ""
}`} }`}
> >
{project.emoji ? ( {project.emoji ? (
@ -195,7 +200,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
</div> </div>
{!isCollapsed && ( {!isCollapsed && (
<ChevronDown <ChevronDown
className={`hidden h-4 w-4 flex-shrink-0 ${open ? "rotate-180" : ""} ${isMenuActive ? "!block" : "" className={`hidden h-4 w-4 flex-shrink-0 ${open ? "rotate-180" : ""} ${
isMenuActive ? "!block" : ""
} mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`} } mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`}
/> />
)} )}
@ -320,7 +326,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
disabled={!isCollapsed} disabled={!isCollapsed}
> >
<div <div
className={`group flex items-center gap-2.5 rounded-md px-2 py-1.5 text-xs font-medium outline-none ${router.asPath.includes(item.href) className={`group flex items-center gap-2.5 rounded-md px-2 py-1.5 text-xs font-medium outline-none ${
router.asPath.includes(item.href)
? "bg-custom-primary-100/10 text-custom-primary-100" ? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80" : "text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${isCollapsed ? "justify-center" : ""}`} } ${isCollapsed ? "justify-center" : ""}`}

View File

@ -47,8 +47,8 @@ export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = observer((pro
}, [viewDetailStore, viewPageType]); }, [viewDetailStore, viewPageType]);
return ( return (
<div className="space-y-1 divide-y divide-custom-border-300"> <div className="space-y-1 divide-y divide-custom-border-300 [&>div]:first:pt-0 [&>div]:last:pb-0">
<div className="relative py-1 first:pt-0"> <div className="relative py-1">
<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">Properties</div> <div className="font-medium text-xs text-custom-text-300 capitalize py-1">Properties</div>
<div <div
@ -95,8 +95,8 @@ export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = observer((pro
</div> </div>
))} ))}
<div className="pt-1 pb-0">
{filtersExtraProperties.map((option) => ( {filtersExtraProperties.map((option) => (
<div className="py-1">
<DisplayFilterExtraOptions <DisplayFilterExtraOptions
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
@ -104,8 +104,8 @@ export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = observer((pro
viewType={viewType} viewType={viewType}
filterKey={option as TViewDisplayFiltersExtraOptions} filterKey={option as TViewDisplayFiltersExtraOptions}
/> />
))}
</div> </div>
))}
</div> </div>
); );
}); });

View File

@ -5,6 +5,7 @@ import { Briefcase, CheckCircle, ChevronRight } from "lucide-react";
import { useProject } from "hooks/store"; import { useProject } from "hooks/store";
// types // types
import { TViewTypes } from "@plane/types"; import { TViewTypes } from "@plane/types";
import { renderEmoji } from "helpers/emoji.helper";
type TViewHeader = { type TViewHeader = {
projectId: string | undefined; projectId: string | undefined;
@ -29,8 +30,14 @@ export const ViewHeader: FC<TViewHeader> = (props) => {
{projectDetails && ( {projectDetails && (
<Fragment> <Fragment>
<div className="relative flex items-center gap-2 overflow-hidden"> <div className="relative flex items-center gap-2 overflow-hidden">
<div className="flex-shrink-0 w-6 h-6 rounded relative flex justify-center items-center bg-custom-background-80"> <div className="flex-shrink-0 w-6 h-6 rounded relative flex justify-center items-center bg-custom-background-80 text-sm">
{projectDetails?.icon_prop ? projectDetails?.icon_prop.toString() : <Briefcase size={12} />} {projectDetails?.emoji ? (
renderEmoji(projectDetails?.emoji)
) : projectDetails?.icon_prop ? (
renderEmoji(projectDetails?.icon_prop)
) : (
<Briefcase size={12} />
)}
</div> </div>
<div className="font-medium inline-block whitespace-nowrap overflow-hidden truncate line-clamp-1 text-sm"> <div className="font-medium inline-block whitespace-nowrap overflow-hidden truncate line-clamp-1 text-sm">
{projectDetails?.name ? projectDetails?.name : "Project Issues"} {projectDetails?.name ? projectDetails?.name : "Project Issues"}

View File

@ -10,6 +10,37 @@ import {
TViewDisplayFiltersExtraOptions, TViewDisplayFiltersExtraOptions,
} from "@plane/types"; } from "@plane/types";
// global variables
export enum EViewPageType {
ALL = "all",
PROFILE = "profile",
PROJECT = "project",
ARCHIVED = "archived",
DRAFT = "draft",
}
export enum ELocalViews {
ALL_ISSUES = "all-issues",
ASSIGNED = "assigned",
CREATED = "created",
SUBSCRIBED = "subscribed",
}
export enum EFilterTypes {
FILTERS = "filters",
DISPLAY_FILTERS = "display_filters",
DISPLAY_PROPERTIES = "display_properties",
KANBAN_FILTERS = "kanban_filters",
}
export enum EViewLayouts {
LIST = "list",
KANBAN = "kanban",
CALENDAR = "calendar",
SPREADSHEET = "spreadsheet",
GANTT = "gantt",
}
// filters constants // filters constants
export const STATE_GROUP_PROPERTY: Record<TStateGroups, { label: string; color: string }> = { export const STATE_GROUP_PROPERTY: Record<TStateGroups, { label: string; color: string }> = {
backlog: { label: "Backlog", color: "#d9d9d9" }, backlog: { label: "Backlog", color: "#d9d9d9" },
@ -67,26 +98,10 @@ export const EXTRA_OPTIONS_PROPERTY: Record<TViewDisplayFiltersExtraOptions, { l
show_empty_groups: { label: "Show Empty Groups" }, 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 = { export type TViewLayoutFilterProperties = {
filters: Partial<keyof TViewFilters>[]; filters: Partial<keyof TViewFilters>[];
display_filters: Partial<keyof TViewDisplayFilters>[]; display_filters: Partial<keyof TViewDisplayFilters>[];
extra_options: ("sub_issue" | "show_empty_groups")[]; extra_options: TViewDisplayFiltersExtraOptions[];
display_properties: boolean; display_properties: boolean;
readonlyFilters?: Partial<keyof TViewFilters>[]; readonlyFilters?: Partial<keyof TViewFilters>[];
}; };
@ -112,10 +127,8 @@ const ALL_FILTER_PERMISSIONS: TFilterPermissions["all"] = {
layouts: [EViewLayouts.SPREADSHEET], layouts: [EViewLayouts.SPREADSHEET],
[EViewLayouts.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"],
// display_filters: ["type"], display_filters: ["type"],
// extra_options: [], extra_options: [],
display_filters: ["group_by", "sub_group_by", "order_by", "type"],
extra_options: ["sub_issue", "show_empty_groups"],
display_properties: true, display_properties: true,
}, },
}; };

View File

@ -40,7 +40,6 @@ const ProjectPrivateViewPage: NextPageWithLayout = () => {
viewType={VIEW_TYPES.PROJECT_PRIVATE_VIEWS} viewType={VIEW_TYPES.PROJECT_PRIVATE_VIEWS}
viewPageType={EViewPageType.PROJECT} viewPageType={EViewPageType.PROJECT}
baseRoute={`/${workspaceSlug?.toString()}/projects/${projectId}/views/private`} baseRoute={`/${workspaceSlug?.toString()}/projects/${projectId}/views/private`}
workspaceViewTabOptions={workspaceViewTabOptions}
/> />
</div> </div>
</div> </div>

View File

@ -1,20 +1,114 @@
import { ReactElement } from "react"; import { Fragment, ReactElement, useEffect, useMemo } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { useTheme } from "next-themes";
import { CheckCircle } from "lucide-react";
// hooks
import { useUser, useView } from "hooks/store";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
import { ViewHeader } from "components/view";
// ui
import { Spinner } from "@plane/ui";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants
import { VIEW_TYPES } from "constants/view";
import { VIEW_EMPTY_STATE_DETAILS } from "constants/empty-state";
const ProjectPrivateViewPage: NextPageWithLayout = () => { const ProjectPublicViewPage: NextPageWithLayout = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query; const { workspaceSlug, projectId } = router.query;
// hooks
const viewStore = useView(workspaceSlug?.toString(), projectId?.toString(), VIEW_TYPES.PROJECT_PUBLIC_VIEWS);
const { currentUser } = useUser();
// theme
const { resolvedTheme } = useTheme();
if (!workspaceSlug || !projectId || !viewId) return <></>; const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
return <div />; const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "views", isLightMode);
};
ProjectPrivateViewPage.getLayout = function getLayout(page: ReactElement) { useSWR(
workspaceSlug && projectId
? `PROJECT_VIEWS_${VIEW_TYPES.PROJECT_PUBLIC_VIEWS}_${workspaceSlug.toString()}_${projectId.toString()}`
: null,
async () => {
await viewStore?.fetch();
console.log("viewStore", viewStore?.viewIds);
}
);
useEffect(() => {
if (workspaceSlug && projectId && viewStore?.viewIds && viewStore?.viewIds.length > 0) {
router.push(`/${workspaceSlug}/projects/${projectId}/views/public/${viewStore?.viewIds[0]}`);
}
}, [workspaceSlug, projectId, viewStore?.viewIds, router]);
const workspaceViewTabOptions = useMemo(
() => [
{
key: VIEW_TYPES.PROJECT_PRIVATE_VIEWS,
title: "Private",
href: `/${workspaceSlug}/projects/${projectId}/views/private`,
},
{
key: VIEW_TYPES.PROJECT_PUBLIC_VIEWS,
title: "Public",
href: `/${workspaceSlug}/projects/${projectId}/views/public`,
},
],
[workspaceSlug, projectId]
);
if (!workspaceSlug || !projectId) return <></>;
return (
<div className="relative w-full h-full flex flex-col overflow-hidden">
{viewStore?.loader === "init-loader" ? (
<div className="relative w-full h-full flex justify-center items-center">
<Spinner />
</div>
) : (
<>
{viewStore?.viewIds && viewStore?.viewIds?.length <= 0 && (
<Fragment>
<div className="flex-shrink-0 px-5 pt-4 pb-4 border-b border-custom-border-200">
<ViewHeader
projectId={projectId.toString()}
viewType={VIEW_TYPES.PROJECT_PRIVATE_VIEWS}
titleIcon={<CheckCircle size={12} />}
title="Views"
workspaceViewTabOptions={workspaceViewTabOptions}
/>
</div>
<div className="relative w-full h-full flex justify-center items-center overflow-hidden overflow-y-auto">
<EmptyState
title={VIEW_EMPTY_STATE_DETAILS["project-views"].title}
description={VIEW_EMPTY_STATE_DETAILS["project-views"].description}
image={EmptyStateImagePath}
comicBox={{
title: VIEW_EMPTY_STATE_DETAILS["project-views"].comicBox.title,
description: VIEW_EMPTY_STATE_DETAILS["project-views"].comicBox.description,
}}
primaryButton={{
text: VIEW_EMPTY_STATE_DETAILS["project-views"].primaryButton.text,
onClick: () => {},
}}
size="lg"
/>
</div>
</Fragment>
)}
</>
)}
</div>
);
});
ProjectPublicViewPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<></>}>{page}</AppLayout>; return <AppLayout header={<></>}>{page}</AppLayout>;
}; };
export default ProjectPrivateViewPage; export default ProjectPublicViewPage;

View File

@ -40,7 +40,6 @@ const ProjectPublicViewPage: NextPageWithLayout = () => {
viewType={VIEW_TYPES.PROJECT_PUBLIC_VIEWS} viewType={VIEW_TYPES.PROJECT_PUBLIC_VIEWS}
viewPageType={EViewPageType.PROJECT} viewPageType={EViewPageType.PROJECT}
baseRoute={`/${workspaceSlug?.toString()}/views/public`} baseRoute={`/${workspaceSlug?.toString()}/views/public`}
workspaceViewTabOptions={workspaceViewTabOptions}
/> />
</div> </div>
<div className="w-full h-full overflow-hidden">Issues render</div> <div className="w-full h-full overflow-hidden">Issues render</div>

View File

@ -0,0 +1,114 @@
import { Fragment, ReactElement, useEffect, useMemo } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { useTheme } from "next-themes";
import { CheckCircle } from "lucide-react";
// hooks
import { useUser, useView } from "hooks/store";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
import { ViewHeader } from "components/view";
// ui
import { Spinner } from "@plane/ui";
// types
import { NextPageWithLayout } from "lib/types";
// constants
import { VIEW_TYPES } from "constants/view";
import { VIEW_EMPTY_STATE_DETAILS } from "constants/empty-state";
const ProjectPublicViewPage: NextPageWithLayout = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// hooks
const viewStore = useView(workspaceSlug?.toString(), projectId?.toString(), VIEW_TYPES.PROJECT_PUBLIC_VIEWS);
const { currentUser } = useUser();
// theme
const { resolvedTheme } = useTheme();
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "views", isLightMode);
useSWR(
workspaceSlug && projectId
? `PROJECT_VIEWS_${VIEW_TYPES.PROJECT_PUBLIC_VIEWS}_${workspaceSlug.toString()}_${projectId.toString()}`
: null,
async () => {
await viewStore?.fetch();
console.log("viewStore", viewStore?.viewIds);
}
);
useEffect(() => {
if (workspaceSlug && projectId && viewStore?.viewIds && viewStore?.viewIds.length > 0) {
router.push(`/${workspaceSlug}/projects/${projectId}/views/public/${viewStore?.viewIds[0]}`);
}
}, [workspaceSlug, projectId, viewStore?.viewIds, router]);
const workspaceViewTabOptions = useMemo(
() => [
{
key: VIEW_TYPES.PROJECT_PRIVATE_VIEWS,
title: "Private",
href: `/${workspaceSlug}/projects/${projectId}/views/private`,
},
{
key: VIEW_TYPES.PROJECT_PUBLIC_VIEWS,
title: "Public",
href: `/${workspaceSlug}/projects/${projectId}/views/public`,
},
],
[workspaceSlug, projectId]
);
if (!workspaceSlug || !projectId) return <></>;
return (
<div className="relative w-full h-full flex flex-col overflow-hidden">
{viewStore?.loader === "init-loader" ? (
<div className="relative w-full h-full flex justify-center items-center">
<Spinner />
</div>
) : (
<>
{viewStore?.viewIds && viewStore?.viewIds?.length <= 0 && (
<Fragment>
<div className="flex-shrink-0 px-5 pt-4 pb-4 border-b border-custom-border-200">
<ViewHeader
projectId={projectId.toString()}
viewType={VIEW_TYPES.PROJECT_PUBLIC_VIEWS}
titleIcon={<CheckCircle size={12} />}
title="Views"
workspaceViewTabOptions={workspaceViewTabOptions}
/>
</div>
<div className="relative w-full h-full flex justify-center items-center overflow-hidden overflow-y-auto">
<EmptyState
title={VIEW_EMPTY_STATE_DETAILS["project-views"].title}
description={VIEW_EMPTY_STATE_DETAILS["project-views"].description}
image={EmptyStateImagePath}
comicBox={{
title: VIEW_EMPTY_STATE_DETAILS["project-views"].comicBox.title,
description: VIEW_EMPTY_STATE_DETAILS["project-views"].comicBox.description,
}}
primaryButton={{
text: VIEW_EMPTY_STATE_DETAILS["project-views"].primaryButton.text,
onClick: () => {},
}}
size="lg"
/>
</div>
</Fragment>
)}
</>
)}
</div>
);
});
ProjectPublicViewPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<></>}>{page}</AppLayout>;
};
export default ProjectPublicViewPage;

View File

@ -8,7 +8,7 @@ import { GlobalViewRoot, ViewHeader } from "components/view";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants // constants
import { EViewPageType, VIEW_TYPES } from "constants/view"; import { ELocalViews, EViewPageType, VIEW_TYPES } from "constants/view";
const WorkspacePublicViewPage: NextPageWithLayout = () => { const WorkspacePublicViewPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
@ -19,12 +19,12 @@ const WorkspacePublicViewPage: NextPageWithLayout = () => {
{ {
key: VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS, key: VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS,
title: "Private", title: "Private",
href: `/${workspaceSlug}/views/private/assigned`, href: `/${workspaceSlug}/views/private/${ELocalViews.ASSIGNED}`,
}, },
{ {
key: VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS, key: VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS,
title: "Public", title: "Public",
href: `/${workspaceSlug}/views/public/all-issues`, href: `/${workspaceSlug}/views/public/${ELocalViews.ALL_ISSUES}`,
}, },
], ],
[workspaceSlug] [workspaceSlug]

View File

@ -3,12 +3,7 @@ import { TView, TUserView } from "@plane/types";
export type TUserViewService = { export type TUserViewService = {
// featureId represents moduleId/cycleId // featureId represents moduleId/cycleId
fetch: (workspaceSlug: string, projectId?: string, featureId?: string) => Promise<TUserView | undefined>; fetch: (workspaceSlug: string, projectId?: string, featureId?: string) => Promise<TUserView | undefined>;
update: ( update: (workspaceSlug: string, data: any, projectId?: string, featureId?: string) => Promise<TUserView | undefined>;
workspaceSlug: string,
data: Partial<TView>,
projectId?: string,
featureId?: string
) => Promise<TUserView | undefined>;
}; };
export type TViewService = { export type TViewService = {

View File

@ -123,37 +123,37 @@ 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 let view: TView | undefined = undefined;
// 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 currentView = { ...this.viewById(viewId) };
if (!view) return; if (!currentView) return;
runInAction(() => { view = currentView;
view.filters = userView.filters;
view.display_filters = userView.display_filters; view.display_filters = userView.display_filters;
view.display_properties = userView.display_properties; view.display_properties = userView.display_properties;
});
} else { } else {
const view = await this.service.fetchById(workspaceSlug, viewId, projectId); const currentView = await this.service.fetchById(workspaceSlug, viewId, projectId);
if (!view) return; if (!currentView) return;
view = currentView;
view?.display_filters && (view.display_filters = userView.display_filters); view?.display_filters && (view.display_filters = userView.display_filters);
view?.display_properties && (view.display_properties = userView.display_properties); view?.display_properties && (view.display_properties = userView.display_properties);
}
if (!view) return;
runInAction(() => { runInAction(() => {
if (view.id) if (view?.id)
set( set(
this.viewMap, this.viewMap,
[view.id], [view.id],
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType) new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
); );
}); });
}
} catch {} } catch {}
}; };

View File

@ -21,7 +21,12 @@ import {
// helpers // helpers
import { FiltersHelper } from "./helpers/filters_helpers"; import { FiltersHelper } from "./helpers/filters_helpers";
// constants // constants
import { EViewLayouts, EViewPageType, viewDefaultFilterParametersByViewTypeAndLayout } from "constants/view"; import {
EViewLayouts,
EViewPageType,
EFilterTypes,
viewDefaultFilterParametersByViewTypeAndLayout,
} from "constants/view";
type TLoader = "updating" | undefined; type TLoader = "updating" | undefined;
@ -160,6 +165,9 @@ export class ViewStore extends FiltersHelper implements TViewStore {
unlockView: action, unlockView: action,
makeFavorite: action, makeFavorite: action,
removeFavorite: action, removeFavorite: action,
updateUserFilters: action,
updateUserDisplayFilters: action,
updateUserDisplayProperties: action,
}); });
} }
@ -187,7 +195,7 @@ export class ViewStore extends FiltersHelper implements TViewStore {
const requiredFilterProperties = viewDefaultFilterParametersByViewTypeAndLayout( const requiredFilterProperties = viewDefaultFilterParametersByViewTypeAndLayout(
this.viewPageType, this.viewPageType,
layout, layout,
"filters" EFilterTypes.FILTERS
); );
return this.computeAppliedFiltersQueryParameters(appliedFilters, requiredFilterProperties)?.query || undefined; return this.computeAppliedFiltersQueryParameters(appliedFilters, requiredFilterProperties)?.query || undefined;
@ -231,9 +239,9 @@ export class ViewStore extends FiltersHelper implements TViewStore {
setFilters = (filterKey: keyof TViewFilters | undefined = undefined, filterValue: "clear_all" | string) => { setFilters = (filterKey: keyof TViewFilters | undefined = undefined, filterValue: "clear_all" | string) => {
runInAction(() => { runInAction(() => {
if (filterKey === undefined) { if (filterKey === undefined) {
if (filterValue === "clear_all") set(this.filtersToUpdate, ["filters"], {}); if (filterValue === "clear_all") set(this.filtersToUpdate, [EFilterTypes.FILTERS], {});
} else } else
update(this.filtersToUpdate, ["filters", filterKey], (_values = []) => { update(this.filtersToUpdate, [EFilterTypes.FILTERS, filterKey], (_values = []) => {
if (filterValue === "clear_all") return []; if (filterValue === "clear_all") return [];
if (_values.includes(filterValue)) return pull(_values, filterValue); if (_values.includes(filterValue)) return pull(_values, filterValue);
return concat(_values, filterValue); return concat(_values, filterValue);
@ -259,21 +267,25 @@ export class ViewStore extends FiltersHelper implements TViewStore {
runInAction(() => { runInAction(() => {
Object.keys(display_filters).forEach((key) => { Object.keys(display_filters).forEach((key) => {
const _key = key as keyof TViewDisplayFilters; const _key = key as keyof TViewDisplayFilters;
set(this.filtersToUpdate, ["display_filters", _key], display_filters[_key]); set(this.filtersToUpdate, [EFilterTypes.DISPLAY_FILTERS, _key], display_filters[_key]);
}); });
}); });
// update display properties globally // update display filters globally
this.updateUserDisplayFilters({ [EFilterTypes.DISPLAY_FILTERS]: this.filtersToUpdate.display_filters });
// 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,
[EFilterTypes.DISPLAY_PROPERTIES, displayPropertyKey],
(_value: boolean = true) => !_value
);
}); });
// update display properties globally // update display properties globally
this.updateUserDisplayProperties({ [EFilterTypes.DISPLAY_PROPERTIES]: this.filtersToUpdate.display_properties });
}; };
setIsEditable = (is_editable: boolean) => { setIsEditable = (is_editable: boolean) => {
@ -297,7 +309,12 @@ export class ViewStore extends FiltersHelper implements TViewStore {
saveChanges = async () => { saveChanges = async () => {
try { try {
if (this.filtersToUpdate) await this.update(this.filtersToUpdate); if (!this.id) return;
if (["all-issues", "assigned", "created", "subscribed"].includes(this.id)) {
const payload = this.filtersToUpdate.filters;
await this.updateUserFilters({ [EFilterTypes.FILTERS]: payload });
} else await this.update(this.filtersToUpdate);
} catch { } catch {
Object.keys(this.filtersToUpdate).forEach((key) => { Object.keys(this.filtersToUpdate).forEach((key) => {
const _key = key as keyof TUpdateView; const _key = key as keyof TUpdateView;
@ -394,4 +411,61 @@ export class ViewStore extends FiltersHelper implements TViewStore {
this.is_favorite = this.is_favorite; this.is_favorite = this.is_favorite;
} }
}; };
// updating the user specific filters, display filters, and display properties
updateUserFilters = async (filters: { [EFilterTypes.FILTERS]: TViewFilters }) => {
try {
const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug) return;
const userView = await this.userService.update(workspaceSlug, filters, projectId);
if (!userView) return;
runInAction(() => {
this.filters = userView.filters;
});
} catch {
runInAction(() => {
this.filters = this.filters;
});
}
};
updateUserDisplayFilters = async (display_filters: { [EFilterTypes.DISPLAY_FILTERS]: TViewDisplayFilters }) => {
try {
const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug) return;
const userView = await this.userService.update(workspaceSlug, display_filters, projectId);
if (!userView) return;
runInAction(() => {
this.display_filters = userView.display_filters;
});
} catch {
runInAction(() => {
this.display_filters = this.display_filters;
});
}
};
updateUserDisplayProperties = async (display_properties: {
[EFilterTypes.DISPLAY_PROPERTIES]: TViewDisplayProperties;
}) => {
try {
const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug) return;
const userView = await this.userService.update(workspaceSlug, display_properties, projectId);
if (!userView) return;
runInAction(() => {
this.display_properties = userView.display_properties;
});
} catch {
runInAction(() => {
this.display_properties = this.display_properties;
});
}
};
} }