mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: updated empty states
This commit is contained in:
parent
c35d650de0
commit
430d9de722
@ -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,8 +147,9 @@ 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 ${
|
||||||
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
|
snapshot?.isDragging ? "opacity-60" : ""
|
||||||
|
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
|
||||||
>
|
>
|
||||||
{provided && (
|
{provided && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -157,9 +158,11 @@ 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}
|
||||||
>
|
>
|
||||||
<MoreVertical className="h-3.5" />
|
<MoreVertical className="h-3.5" />
|
||||||
@ -170,12 +173,14 @@ 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 ? (
|
||||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
||||||
@ -195,8 +200,9 @@ 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" : ""} ${
|
||||||
} mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`}
|
isMenuActive ? "!block" : ""
|
||||||
|
} mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
@ -320,10 +326,11 @@ 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 ${
|
||||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
router.asPath.includes(item.href)
|
||||||
: "text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||||
} ${isCollapsed ? "justify-center" : ""}`}
|
: "text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||||
|
} ${isCollapsed ? "justify-center" : ""}`}
|
||||||
>
|
>
|
||||||
<item.Icon className="h-4 w-4 stroke-[1.5]" />
|
<item.Icon className="h-4 w-4 stroke-[1.5]" />
|
||||||
{!isCollapsed && item.name}
|
{!isCollapsed && item.name}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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"}
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
@ -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]
|
||||||
|
7
web/services/view/types.d.ts
vendored
7
web/services/view/types.d.ts
vendored
@ -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 = {
|
||||||
|
@ -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.display_filters = userView.display_filters;
|
view.filters = userView.filters;
|
||||||
view.display_properties = userView.display_properties;
|
view.display_filters = userView.display_filters;
|
||||||
});
|
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);
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
if (view.id)
|
|
||||||
set(
|
|
||||||
this.viewMap,
|
|
||||||
[view.id],
|
|
||||||
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!view) return;
|
||||||
|
runInAction(() => {
|
||||||
|
if (view?.id)
|
||||||
|
set(
|
||||||
|
this.viewMap,
|
||||||
|
[view.id],
|
||||||
|
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
|
||||||
|
);
|
||||||
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user