mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: ui fixes on filter and implemented default data
This commit is contained in:
parent
84d3d34e14
commit
1ce7f20c2d
@ -1516,11 +1516,9 @@ class WorkspaceUserPropertiesEndpoint(BaseAPIView):
|
|||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
(
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
workspace_properties,
|
workspace_properties, _ = WorkspaceUserProperties.objects.get_or_create(
|
||||||
_,
|
user=request.user, workspace=workspace
|
||||||
) = WorkspaceUserProperties.objects.get_or_create(
|
|
||||||
user=request.user, workspace__slug=slug
|
|
||||||
)
|
)
|
||||||
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
|
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
8
packages/types/src/view/base.d.ts
vendored
8
packages/types/src/view/base.d.ts
vendored
@ -5,10 +5,10 @@ import {
|
|||||||
} from "./filter";
|
} from "./filter";
|
||||||
|
|
||||||
export type TViewTypes =
|
export type TViewTypes =
|
||||||
| "WORKSPACE_YOUR_VIEWS"
|
| "WORKSPACE_PRIVATE_VIEWS"
|
||||||
| "WORKSPACE_VIEWS"
|
| "WORKSPACE_PUBLIC_VIEWS"
|
||||||
| "PROJECT_VIEWS"
|
| "PROJECT_PRIVATE_VIEWS"
|
||||||
| "PROJECT_YOUR_VIEWS";
|
| "PROJECT_PUBLIC_VIEWS";
|
||||||
|
|
||||||
declare enum EViewAccess {
|
declare enum EViewAccess {
|
||||||
"public" = 0,
|
"public" = 0,
|
||||||
|
9
packages/types/src/view/user-base.d.ts
vendored
9
packages/types/src/view/user-base.d.ts
vendored
@ -7,13 +7,10 @@ import {
|
|||||||
export type TUserView = {
|
export type TUserView = {
|
||||||
id: string | undefined;
|
id: string | undefined;
|
||||||
workspace: string | undefined;
|
workspace: string | undefined;
|
||||||
project: string | undefined;
|
|
||||||
module: string | undefined;
|
|
||||||
cycle: string | undefined;
|
|
||||||
filters: TViewFilters | undefined;
|
|
||||||
display_filters: TViewDisplayFilters | undefined;
|
|
||||||
display_properties: TViewDisplayProperties | undefined;
|
|
||||||
user: string | undefined;
|
user: string | undefined;
|
||||||
|
filters: TViewFilters;
|
||||||
|
display_filters: TViewDisplayFilters;
|
||||||
|
display_properties: TViewDisplayProperties;
|
||||||
created_by: string | undefined;
|
created_by: string | undefined;
|
||||||
updated_by: string | undefined;
|
updated_by: string | undefined;
|
||||||
created_at: Date | undefined;
|
created_at: Date | undefined;
|
||||||
|
@ -23,14 +23,13 @@ import { ANALYTICS } from "constants/fetch-keys";
|
|||||||
type Props = {
|
type Props = {
|
||||||
analytics: IAnalyticsResponse | undefined;
|
analytics: IAnalyticsResponse | undefined;
|
||||||
params: IAnalyticsParams;
|
params: IAnalyticsParams;
|
||||||
fullScreen: boolean;
|
|
||||||
isProjectLevel: boolean;
|
isProjectLevel: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const analyticsService = new AnalyticsService();
|
const analyticsService = new AnalyticsService();
|
||||||
|
|
||||||
export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||||
const { analytics, params, fullScreen, isProjectLevel = false } = props;
|
const { analytics, params, isProjectLevel = false } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
@ -139,13 +138,7 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const selectedProjects = params.project && params.project.length > 0 ? params.project : workspaceProjectIds;
|
const selectedProjects = params.project && params.project.length > 0 ? params.project : workspaceProjectIds;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`relative w-full h-full flex flex-col space-y-4 px-5 py-4`}>
|
||||||
className={`flex items-center justify-between space-y-2 px-5 py-2.5 ${
|
|
||||||
fullScreen
|
|
||||||
? "overflow-hidden border-l border-custom-border-200 md:h-full md:flex-col md:items-start md:space-y-4 md:border-l md:border-custom-border-200 md:py-5"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
||||||
<LayersIcon height={14} width={14} />
|
<LayersIcon height={14} width={14} />
|
||||||
@ -164,16 +157,16 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-full w-full overflow-hidden">
|
<div className="h-full w-full overflow-hidden">
|
||||||
{fullScreen ? (
|
<>
|
||||||
<>
|
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
||||||
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
<CustomAnalyticsSidebarProjectsList projectIds={selectedProjects} />
|
||||||
<CustomAnalyticsSidebarProjectsList projectIds={selectedProjects} />
|
)}
|
||||||
)}
|
<CustomAnalyticsSidebarHeader />
|
||||||
<CustomAnalyticsSidebarHeader />
|
</>
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2 justify-self-end">
|
<div className="flex flex-wrap items-center gap-2 justify-self-end">
|
||||||
<Button
|
<Button
|
||||||
variant="neutral-primary"
|
variant="neutral-primary"
|
||||||
|
@ -1,46 +1,73 @@
|
|||||||
import { FC, useEffect, useMemo, useState } from "react";
|
import { FC, Fragment, useEffect, useMemo, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { CheckCircle } from "lucide-react";
|
import { CheckCircle, Pencil } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useView, useViewDetail } from "hooks/store";
|
import { useView, useViewDetail } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { ViewRoot, ViewCreateEditForm, ViewAppliedFiltersRoot, ViewLayoutRoot } from ".";
|
import {
|
||||||
|
ViewRoot,
|
||||||
|
ViewCreateEditForm,
|
||||||
|
ViewLayoutRoot,
|
||||||
|
ViewFiltersRoot,
|
||||||
|
ViewFiltersDropdown,
|
||||||
|
ViewDisplayFiltersDropdown,
|
||||||
|
ViewDisplayPropertiesRoot,
|
||||||
|
ViewAppliedFiltersRoot,
|
||||||
|
ViewDuplicateConfirmationModal,
|
||||||
|
ViewDeleteConfirmationModal,
|
||||||
|
} from ".";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { VIEW_TYPES, viewLocalPayload } from "constants/view";
|
import { VIEW_TYPES, viewLocalPayload } from "constants/view";
|
||||||
// types
|
// types
|
||||||
import { TViewOperations } from "./types";
|
import { TViewOperations } from "./types";
|
||||||
import { TView, TViewFilters, TViewDisplayFilters, TViewDisplayProperties } from "@plane/types";
|
import { TView, TViewFilters, TViewDisplayFilters, TViewDisplayProperties, TViewTypes } from "@plane/types";
|
||||||
|
|
||||||
type TAllIssuesViewRoot = {
|
type TAllIssuesViewRoot = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string | undefined;
|
projectId: string | undefined;
|
||||||
viewId: string;
|
viewId: string;
|
||||||
|
viewType: TViewTypes;
|
||||||
|
baseRoute: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TViewOperationsToggle = {
|
||||||
|
type: "CREATE" | "EDIT" | "DUPLICATE" | "DELETE" | undefined;
|
||||||
|
viewId: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, viewId } = props;
|
const { workspaceSlug, projectId, viewId, viewType, baseRoute } = props;
|
||||||
// states
|
|
||||||
const [currentCreateEditViewId, setCurrentCreateEditViewId] = useState<string | undefined>(undefined);
|
|
||||||
const [viewType, setViewType] = useState(VIEW_TYPES.WORKSPACE_VIEWS);
|
|
||||||
const workspaceViewTabOptions = [
|
|
||||||
{
|
|
||||||
key: VIEW_TYPES.WORKSPACE_YOUR_VIEWS,
|
|
||||||
title: "Your views",
|
|
||||||
onClick: () => VIEW_TYPES.WORKSPACE_YOUR_VIEWS != viewType && setViewType(VIEW_TYPES.WORKSPACE_YOUR_VIEWS),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: VIEW_TYPES.WORKSPACE_VIEWS,
|
|
||||||
title: "Workspace Views",
|
|
||||||
onClick: () => VIEW_TYPES.WORKSPACE_VIEWS != viewType && setViewType(VIEW_TYPES.WORKSPACE_VIEWS),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
// 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);
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
// states
|
||||||
|
const [viewOperationsToggle, setViewOperationsToggle] = useState<TViewOperationsToggle>({
|
||||||
|
type: undefined,
|
||||||
|
viewId: undefined,
|
||||||
|
});
|
||||||
|
const handleViewOperationsToggle = (type: TViewOperationsToggle["type"], viewId: string | undefined) =>
|
||||||
|
setViewOperationsToggle({ type, viewId });
|
||||||
|
|
||||||
|
const workspaceViewTabOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS,
|
||||||
|
title: "Private",
|
||||||
|
href: `/${workspaceSlug}/views/private/assigned`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS,
|
||||||
|
title: "Public",
|
||||||
|
href: `/${workspaceSlug}/views/public/all-issues`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
const viewOperations: TViewOperations = useMemo(
|
const viewOperations: TViewOperations = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -54,28 +81,19 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
|||||||
localViewCreateEdit: (viewId: string | undefined) => {
|
localViewCreateEdit: (viewId: string | undefined) => {
|
||||||
if (viewId === undefined) {
|
if (viewId === undefined) {
|
||||||
const viewPayload = viewLocalPayload;
|
const viewPayload = viewLocalPayload;
|
||||||
setCurrentCreateEditViewId(viewPayload.id);
|
handleViewOperationsToggle("CREATE", viewPayload.id);
|
||||||
viewStore?.localViewCreate(viewPayload as TView);
|
viewStore?.localViewCreate(viewPayload as TView);
|
||||||
} else setCurrentCreateEditViewId(viewId);
|
} else handleViewOperationsToggle("EDIT", viewId);
|
||||||
},
|
},
|
||||||
localViewCreateEditClear: async (viewId: string | undefined) => {
|
localViewCreateEditClear: async (viewId: string | undefined) => {
|
||||||
console.log("viewId", viewId);
|
|
||||||
if (viewId) viewStore?.remove(viewId);
|
if (viewId) viewStore?.remove(viewId);
|
||||||
setCurrentCreateEditViewId(undefined);
|
handleViewOperationsToggle(undefined, undefined);
|
||||||
},
|
},
|
||||||
fetch: async () => await viewStore?.fetch(),
|
fetch: async () => await viewStore?.fetch(),
|
||||||
create: async (data: Partial<TView>) => {
|
create: async (data: Partial<TView>) => {
|
||||||
try {
|
try {
|
||||||
await viewStore?.create(data);
|
await viewStore?.create(data);
|
||||||
setCurrentCreateEditViewId(undefined);
|
handleViewOperationsToggle(undefined, undefined);
|
||||||
} catch {
|
|
||||||
setToastAlert({ title: "Error", message: "Error creating view", type: "error" });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update: async () => {
|
|
||||||
try {
|
|
||||||
await viewDetailStore?.saveChanges();
|
|
||||||
setCurrentCreateEditViewId(undefined);
|
|
||||||
} catch {
|
} catch {
|
||||||
setToastAlert({ title: "Error", message: "Error creating view", type: "error" });
|
setToastAlert({ title: "Error", message: "Error creating view", type: "error" });
|
||||||
}
|
}
|
||||||
@ -83,8 +101,17 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
|||||||
remove: async (viewId: string) => {
|
remove: async (viewId: string) => {
|
||||||
try {
|
try {
|
||||||
await viewStore?.remove(viewId);
|
await viewStore?.remove(viewId);
|
||||||
|
handleViewOperationsToggle(undefined, undefined);
|
||||||
} catch {
|
} catch {
|
||||||
setToastAlert({ title: "Error", message: "Error creating view", type: "error" });
|
setToastAlert({ title: "Error", message: "Error removing view", type: "error" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: async () => {
|
||||||
|
try {
|
||||||
|
await viewDetailStore?.saveChanges();
|
||||||
|
handleViewOperationsToggle(undefined, undefined);
|
||||||
|
} catch {
|
||||||
|
setToastAlert({ title: "Error", message: "Error updating view", type: "error" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@ -92,8 +119,12 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (workspaceSlug && viewId && viewType && viewStore)
|
const fetchViews = async () => {
|
||||||
viewStore?.fetch(viewStore?.viewIds.length > 0 ? "mutation-loader" : "init-loader");
|
await viewStore?.fetch(viewStore?.viewIds.length > 0 ? "mutation-loader" : "init-loader");
|
||||||
|
viewId && (await viewStore?.fetchById(viewId));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (workspaceSlug && viewId && viewType && viewStore) fetchViews();
|
||||||
}, [workspaceSlug, viewId, viewType, viewStore]);
|
}, [workspaceSlug, viewId, viewType, viewStore]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -107,18 +138,18 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="relative inline-flex items-center rounded border border-custom-border-200 bg-custom-background-80">
|
<div className="relative inline-flex items-center rounded border border-custom-border-200 bg-custom-background-80">
|
||||||
{workspaceViewTabOptions.map((tab) => (
|
{workspaceViewTabOptions.map((tab) => (
|
||||||
<div
|
<Link
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
|
href={tab.href}
|
||||||
className={`p-4 py-1.5 rounded text-sm transition-all cursor-pointer font-medium
|
className={`p-4 py-1.5 rounded text-sm transition-all cursor-pointer font-medium
|
||||||
${
|
${
|
||||||
viewType === tab.key
|
viewType === tab.key
|
||||||
? "text-custom-text-100 bg-custom-background-100"
|
? "text-custom-text-100 bg-custom-background-100"
|
||||||
: "text-custom-text-300 bg-custom-background-80 hover:text-custom-text-100"
|
: "text-custom-text-200 bg-custom-background-80 hover:text-custom-text-100"
|
||||||
}`}
|
}`}
|
||||||
onClick={tab.onClick}
|
|
||||||
>
|
>
|
||||||
{tab.title}
|
{tab.title}
|
||||||
</div>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -136,9 +167,31 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
|||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
viewOperations={viewOperations}
|
viewOperations={viewOperations}
|
||||||
|
baseRoute={baseRoute}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="p-5 border-b border-custom-border-300">
|
||||||
|
<ViewDisplayPropertiesRoot
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
viewId={viewId}
|
||||||
|
viewType={viewType}
|
||||||
|
viewOperations={viewOperations}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className="m-5 px-2 rounded border border-custom-border-300 w-[400px] max-h-[500px] overflow-hidden overflow-y-auto mx-auto bg-custom-background-100">
|
||||||
|
<ViewFiltersRoot
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
viewId={viewId}
|
||||||
|
viewType={viewType}
|
||||||
|
viewOperations={viewOperations}
|
||||||
|
baseRoute={baseRoute}
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
<div className="p-5 border-b border-custom-border-200 relative flex gap-2">
|
<div className="p-5 border-b border-custom-border-200 relative flex gap-2">
|
||||||
{/* <div className="w-full">
|
{/* <div className="w-full">
|
||||||
<ViewAppliedFiltersRoot
|
<ViewAppliedFiltersRoot
|
||||||
@ -150,7 +203,7 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
{/* <div className="flex-shrink-0 h-full">
|
<div className="flex-shrink-0 h-full">
|
||||||
<ViewLayoutRoot
|
<ViewLayoutRoot
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
@ -158,33 +211,62 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
|||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
viewOperations={viewOperations}
|
viewOperations={viewOperations}
|
||||||
/>
|
/>
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
{/* <div className="flex-shrink-0 relative w-7 h-7 overflow-hidden border border-red-500 rounded flex justify-center items-center">
|
<div className="flex-shrink-0">
|
||||||
Filters
|
<ViewFiltersDropdown
|
||||||
</div> */}
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
viewId={viewId}
|
||||||
|
viewType={viewType}
|
||||||
|
viewOperations={viewOperations}
|
||||||
|
baseRoute={baseRoute}
|
||||||
|
displayDropdownText={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* <div className="flex-shrink-0 relative w-7 h-7 overflow-hidden border border-red-500 rounded flex justify-center items-center">
|
<div className="flex-shrink-0">
|
||||||
Display Filters
|
<ViewDisplayFiltersDropdown
|
||||||
</div> */}
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
viewId={viewId}
|
||||||
|
viewType={viewType}
|
||||||
|
viewOperations={viewOperations}
|
||||||
|
baseRoute={baseRoute}
|
||||||
|
displayDropdownText={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* {!viewDetailStore?.is_local_view && (
|
<div className="border border-custom-border-300 relative flex items-center gap-1 h-8 rounded px-2 transition-all text-custom-text-200 hover:text-custom-text-100 bg-custom-background-100 hover:bg-custom-background-80 cursor-pointer shadow-custom-shadow-2xs">
|
||||||
<div className="flex-shrink-0 h-full">
|
<div className="w-4 h-4 relative flex justify-center items-center overflow-hidden">
|
||||||
<div>Edit</div>
|
<Pencil size={12} />
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentCreateEditViewId != undefined && (
|
{/* create edit modal */}
|
||||||
<ViewCreateEditForm
|
{viewOperationsToggle.type && viewOperationsToggle.viewId && (
|
||||||
workspaceSlug={workspaceSlug}
|
<Fragment>
|
||||||
projectId={projectId}
|
{["CREATE", "EDIT"].includes(viewOperationsToggle.type) && (
|
||||||
viewId={currentCreateEditViewId}
|
<ViewCreateEditForm
|
||||||
viewType={viewType}
|
workspaceSlug={workspaceSlug}
|
||||||
viewOperations={viewOperations}
|
projectId={projectId}
|
||||||
/>
|
viewId={viewOperationsToggle.viewId}
|
||||||
|
viewType={viewType}
|
||||||
|
viewOperations={viewOperations}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{["DUPLICATE"].includes(viewOperationsToggle.type) && (
|
||||||
|
<ViewDuplicateConfirmationModal viewId={viewOperationsToggle.viewId} viewOperations={viewOperations} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{["DELETE"].includes(viewOperationsToggle.type) && (
|
||||||
|
<ViewDeleteConfirmationModal viewId={viewOperationsToggle.viewId} viewOperations={viewOperations} />
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
77
web/components/view/confirmation-modals/delete.tsx
Normal file
77
web/components/view/confirmation-modals/delete.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { FC, Fragment, useCallback, useEffect, useState } from "react";
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { Button } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { TViewOperations } from "../types";
|
||||||
|
|
||||||
|
type TViewDeleteConfirmationModal = {
|
||||||
|
viewId: string;
|
||||||
|
viewOperations: TViewOperations;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewDeleteConfirmationModal: FC<TViewDeleteConfirmationModal> = (props) => {
|
||||||
|
const { viewId, viewOperations } = props;
|
||||||
|
// state
|
||||||
|
const [modalToggle, setModalToggle] = useState(false);
|
||||||
|
const [loader, setLoader] = useState(false);
|
||||||
|
|
||||||
|
const modalOpen = useCallback(() => setModalToggle(true), [setModalToggle]);
|
||||||
|
const modalClose = useCallback(() => {
|
||||||
|
setModalToggle(false);
|
||||||
|
}, [setModalToggle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewId) modalOpen();
|
||||||
|
}, [viewId, modalOpen, modalClose]);
|
||||||
|
|
||||||
|
const onContinue = async () => {
|
||||||
|
setLoader(true);
|
||||||
|
setLoader(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={modalToggle} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={modalClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem] py-5 border-[0.1px] border-custom-border-100">
|
||||||
|
<div className="p-3 px-5 relative flex items-center gap-2">Content</div>
|
||||||
|
|
||||||
|
<div className="p-3 px-5 relative flex justify-end items-center gap-2">
|
||||||
|
<Button variant="neutral-primary" onClick={modalClose} disabled={loader}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={onContinue} disabled={loader}>
|
||||||
|
{loader ? `Duplicating` : `Duplicate View`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
77
web/components/view/confirmation-modals/duplicate.tsx
Normal file
77
web/components/view/confirmation-modals/duplicate.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { FC, Fragment, useCallback, useEffect, useState } from "react";
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { Button } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { TViewOperations } from "../types";
|
||||||
|
|
||||||
|
type TViewDuplicateConfirmationModal = {
|
||||||
|
viewId: string;
|
||||||
|
viewOperations: TViewOperations;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewDuplicateConfirmationModal: FC<TViewDuplicateConfirmationModal> = (props) => {
|
||||||
|
const { viewId, viewOperations } = props;
|
||||||
|
// state
|
||||||
|
const [modalToggle, setModalToggle] = useState(false);
|
||||||
|
const [loader, setLoader] = useState(false);
|
||||||
|
|
||||||
|
const modalOpen = useCallback(() => setModalToggle(true), [setModalToggle]);
|
||||||
|
const modalClose = useCallback(() => {
|
||||||
|
setModalToggle(false);
|
||||||
|
}, [setModalToggle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewId) modalOpen();
|
||||||
|
}, [viewId, modalOpen, modalClose]);
|
||||||
|
|
||||||
|
const onContinue = async () => {
|
||||||
|
setLoader(true);
|
||||||
|
setLoader(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={modalToggle} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={modalClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem] py-5 border-[0.1px] border-custom-border-100">
|
||||||
|
<div className="p-3 px-5 relative flex items-center gap-2">Content</div>
|
||||||
|
|
||||||
|
<div className="p-3 px-5 relative flex justify-end items-center gap-2">
|
||||||
|
<Button variant="neutral-primary" onClick={modalClose} disabled={loader}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={onContinue} disabled={loader}>
|
||||||
|
{loader ? `Duplicating` : `Duplicate View`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
124
web/components/view/display-filters/dropdown.tsx
Normal file
124
web/components/view/display-filters/dropdown.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { FC, Fragment, ReactNode, useRef, useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Combobox } from "@headlessui/react";
|
||||||
|
import { usePopper } from "react-popper";
|
||||||
|
import { MonitorDot } from "lucide-react";
|
||||||
|
// hooks
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
// components
|
||||||
|
import { ViewDisplayPropertiesRoot } from "../";
|
||||||
|
// types
|
||||||
|
import { TViewOperations } from "../types";
|
||||||
|
import { TViewTypes } from "@plane/types";
|
||||||
|
|
||||||
|
type TViewDisplayFiltersDropdown = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string | undefined;
|
||||||
|
viewId: string;
|
||||||
|
viewType: TViewTypes;
|
||||||
|
viewOperations: TViewOperations;
|
||||||
|
baseRoute: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
displayDropdownText?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewDisplayFiltersDropdown: FC<TViewDisplayFiltersDropdown> = observer((props) => {
|
||||||
|
const {
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
viewId,
|
||||||
|
viewType,
|
||||||
|
viewOperations,
|
||||||
|
baseRoute,
|
||||||
|
children,
|
||||||
|
displayDropdownText = true,
|
||||||
|
} = props;
|
||||||
|
// state
|
||||||
|
const [dropdownToggle, setDropdownToggle] = useState(false);
|
||||||
|
// refs
|
||||||
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
// popper-js refs
|
||||||
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
// popper-js init
|
||||||
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
|
placement: "bottom-start",
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: "preventOverflow",
|
||||||
|
options: {
|
||||||
|
padding: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDropdownOpen = () => setDropdownToggle(true);
|
||||||
|
const handleDropdownClose = () => setDropdownToggle(false);
|
||||||
|
const handleDropdownToggle = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!dropdownToggle) handleDropdownOpen();
|
||||||
|
else handleDropdownClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
useOutsideClickDetector(dropdownRef, handleDropdownClose);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox as="div" ref={dropdownRef}>
|
||||||
|
<Combobox.Button as={Fragment}>
|
||||||
|
<button
|
||||||
|
ref={setReferenceElement}
|
||||||
|
type="button"
|
||||||
|
className={"block h-full w-full outline-none"}
|
||||||
|
onClick={handleDropdownToggle}
|
||||||
|
>
|
||||||
|
{children ? (
|
||||||
|
<span className="relative inline-block">{children}</span>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={`relative flex items-center gap-1 h-8 rounded px-2 transition-all
|
||||||
|
${
|
||||||
|
displayDropdownText
|
||||||
|
? `border border-custom-border-300 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80`
|
||||||
|
: `hover:bg-custom-background-80`
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="w-4 h-4 relative flex justify-center items-center overflow-hidden">
|
||||||
|
<MonitorDot size={14} />
|
||||||
|
</div>
|
||||||
|
{displayDropdownText && <div className="text-sm whitespace-nowrap">Display</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Combobox.Button>
|
||||||
|
|
||||||
|
{dropdownToggle && (
|
||||||
|
<Combobox.Options className="fixed z-10" static>
|
||||||
|
<div
|
||||||
|
ref={setPopperElement}
|
||||||
|
style={styles.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"
|
||||||
|
>
|
||||||
|
<div className="max-h-96 space-y-1 overflow-y-scroll">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium text-custom-text-200">Properties</div>
|
||||||
|
<ViewDisplayPropertiesRoot
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
viewId={viewId}
|
||||||
|
viewType={viewType}
|
||||||
|
viewOperations={viewOperations}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-red-500">Content</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Combobox.Options>
|
||||||
|
)}
|
||||||
|
</Combobox>
|
||||||
|
);
|
||||||
|
});
|
@ -3,7 +3,7 @@ import { FC } from "react";
|
|||||||
type TViewDisplayFiltersRoot = {
|
type TViewDisplayFiltersRoot = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string | undefined;
|
projectId: string | undefined;
|
||||||
viewId: string | undefined;
|
viewId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = (props) => {
|
export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = (props) => {
|
||||||
|
@ -1,17 +1,50 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
|
// types
|
||||||
|
import { TViewDisplayProperties, TViewTypes } from "@plane/types";
|
||||||
|
import { TViewOperations } from "../types";
|
||||||
|
|
||||||
type TViewDisplayPropertiesRoot = {
|
type TViewDisplayPropertiesRoot = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string | undefined;
|
projectId: string | undefined;
|
||||||
viewId: string | undefined;
|
viewId: string;
|
||||||
|
viewType: TViewTypes;
|
||||||
|
viewOperations: TViewOperations;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewDisplayPropertiesRoot: FC<TViewDisplayPropertiesRoot> = (props) => {
|
export const ViewDisplayPropertiesRoot: FC<TViewDisplayPropertiesRoot> = (props) => {
|
||||||
const { workspaceSlug, projectId, viewId } = props;
|
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
|
||||||
|
|
||||||
|
const displayProperties: Partial<keyof TViewDisplayProperties>[] = [
|
||||||
|
"key",
|
||||||
|
"state",
|
||||||
|
"labels",
|
||||||
|
"priority",
|
||||||
|
"assignee",
|
||||||
|
"start_date",
|
||||||
|
"due_date",
|
||||||
|
"sub_issue_count",
|
||||||
|
"attachment_count",
|
||||||
|
"estimate",
|
||||||
|
"link",
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="relative flex items-center flex-wrap gap-2">
|
||||||
<div>ViewDisplayPropertiesRoot</div>
|
{displayProperties.map((property) => (
|
||||||
|
<div
|
||||||
|
key={property}
|
||||||
|
className={`relative flex items-center gap-1 text-xs rounded p-0.5 px-2 border transition-all capitalize cursor-pointer
|
||||||
|
${
|
||||||
|
false
|
||||||
|
? `border-custom-primary-100 bg-custom-primary-100`
|
||||||
|
: `border-custom-border-300 hover:bg-custom-background-80`
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
{["key"].includes(property) ? "ID" : property.replaceAll("_", " ")}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
133
web/components/view/filters/dropdown.tsx
Normal file
133
web/components/view/filters/dropdown.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { FC, Fragment, ReactNode, useRef, useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Combobox } from "@headlessui/react";
|
||||||
|
import { usePopper } from "react-popper";
|
||||||
|
import { ListFilter, Search } from "lucide-react";
|
||||||
|
// hooks
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
// components
|
||||||
|
import { ViewFiltersRoot } from "../";
|
||||||
|
// types
|
||||||
|
import { TViewOperations } from "../types";
|
||||||
|
import { TViewTypes } from "@plane/types";
|
||||||
|
|
||||||
|
type TViewFiltersDropdown = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string | undefined;
|
||||||
|
viewId: string;
|
||||||
|
viewType: TViewTypes;
|
||||||
|
viewOperations: TViewOperations;
|
||||||
|
baseRoute: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
displayDropdownText?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) => {
|
||||||
|
const {
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
viewId,
|
||||||
|
viewType,
|
||||||
|
viewOperations,
|
||||||
|
baseRoute,
|
||||||
|
children,
|
||||||
|
displayDropdownText = true,
|
||||||
|
} = props;
|
||||||
|
// state
|
||||||
|
const [dropdownToggle, setDropdownToggle] = useState(false);
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
// refs
|
||||||
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
// popper-js refs
|
||||||
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
// popper-js init
|
||||||
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
|
placement: "bottom-start",
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: "preventOverflow",
|
||||||
|
options: {
|
||||||
|
padding: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDropdownOpen = () => setDropdownToggle(true);
|
||||||
|
const handleDropdownClose = () => setDropdownToggle(false);
|
||||||
|
const handleDropdownToggle = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!dropdownToggle) handleDropdownOpen();
|
||||||
|
else handleDropdownClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
useOutsideClickDetector(dropdownRef, handleDropdownClose);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox as="div" ref={dropdownRef}>
|
||||||
|
<Combobox.Button as={Fragment}>
|
||||||
|
<button
|
||||||
|
ref={setReferenceElement}
|
||||||
|
type="button"
|
||||||
|
className={"block h-full w-full outline-none"}
|
||||||
|
onClick={handleDropdownToggle}
|
||||||
|
>
|
||||||
|
{children ? (
|
||||||
|
<span className="relative inline-block">{children}</span>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={`relative flex items-center gap-1 h-8 rounded px-2 transition-all
|
||||||
|
${
|
||||||
|
displayDropdownText
|
||||||
|
? `border border-custom-border-300 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80`
|
||||||
|
: `hover:bg-custom-background-80`
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="w-4 h-4 relative flex justify-center items-center overflow-hidden">
|
||||||
|
<ListFilter size={14} />
|
||||||
|
</div>
|
||||||
|
{displayDropdownText && <div className="text-sm whitespace-nowrap">Filters</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Combobox.Button>
|
||||||
|
|
||||||
|
{dropdownToggle && (
|
||||||
|
<Combobox.Options className="fixed z-10" static>
|
||||||
|
<div
|
||||||
|
ref={setPopperElement}
|
||||||
|
style={styles.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"
|
||||||
|
>
|
||||||
|
<div className="relative p-0.5 px-2 text-sm flex items-center gap-2 rounded border border-custom-border-100 bg-custom-background-90">
|
||||||
|
<Search className="h-3 w-3 text-custom-text-300" strokeWidth={1.5} />
|
||||||
|
<Combobox.Input
|
||||||
|
className="w-full bg-transparent py-1 text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Search for a view..."
|
||||||
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-h-[460px] space-y-0.5 overflow-y-scroll mb-2">
|
||||||
|
<ViewFiltersRoot
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
viewId={viewId}
|
||||||
|
viewType={viewType}
|
||||||
|
viewOperations={viewOperations}
|
||||||
|
baseRoute={baseRoute}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Combobox.Options>
|
||||||
|
)}
|
||||||
|
</Combobox>
|
||||||
|
);
|
||||||
|
});
|
87
web/components/view/filters/filter-item-root.tsx
Normal file
87
web/components/view/filters/filter-item-root.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { FC, useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import concat from "lodash/concat";
|
||||||
|
import pull from "lodash/pull";
|
||||||
|
import uniq from "lodash/uniq";
|
||||||
|
// hooks
|
||||||
|
import { useViewFilter, useViewDetail } from "hooks/store";
|
||||||
|
// components
|
||||||
|
import { ViewFiltersItem, ViewFilterSelection } from "../";
|
||||||
|
// types
|
||||||
|
import { TViewOperations } from "../types";
|
||||||
|
import { TViewFilters, TViewTypes } from "@plane/types";
|
||||||
|
|
||||||
|
type TViewFiltersItemRoot = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string | undefined;
|
||||||
|
viewId: string;
|
||||||
|
viewType: TViewTypes;
|
||||||
|
viewOperations: TViewOperations;
|
||||||
|
baseRoute: string;
|
||||||
|
filterKey: keyof TViewFilters;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewFiltersItemRoot: FC<TViewFiltersItemRoot> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId, viewId, viewType, viewOperations, baseRoute, filterKey } = props;
|
||||||
|
// hooks
|
||||||
|
const viewFilterHelper = useViewFilter(workspaceSlug, projectId);
|
||||||
|
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||||
|
// state
|
||||||
|
const [viewAll, setViewAll] = useState(false);
|
||||||
|
|
||||||
|
const propertyIds = viewFilterHelper?.filterIdsWithKey(filterKey) || [];
|
||||||
|
|
||||||
|
const filterPropertyIds = propertyIds.length > 5 ? (viewAll ? propertyIds : propertyIds.slice(0, 5)) : propertyIds;
|
||||||
|
|
||||||
|
const handlePropertySelection = (_propertyId: string) => {
|
||||||
|
const _propertyIds = viewDetailStore?.appliedFilters?.filters?.[filterKey] || [];
|
||||||
|
const isSelected = _propertyIds?.includes(_propertyId) || false;
|
||||||
|
viewOperations?.setFilters({
|
||||||
|
[filterKey]: isSelected ? pull(_propertyIds, _propertyId) : uniq(concat(_propertyIds, [_propertyId])),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (propertyIds.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) => (
|
||||||
|
<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)}
|
||||||
|
>
|
||||||
|
<ViewFilterSelection
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
viewId={viewId}
|
||||||
|
viewType={viewType}
|
||||||
|
viewOperations={viewOperations}
|
||||||
|
baseRoute={baseRoute}
|
||||||
|
filterKey={filterKey}
|
||||||
|
propertyId={propertyId}
|
||||||
|
/>
|
||||||
|
<ViewFiltersItem
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
viewId={viewId}
|
||||||
|
viewType={viewType}
|
||||||
|
viewOperations={viewOperations}
|
||||||
|
baseRoute={baseRoute}
|
||||||
|
filterKey={filterKey}
|
||||||
|
propertyId={propertyId}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{propertyIds.length > 5 && (
|
||||||
|
<div
|
||||||
|
className="text-xs transition-all text-custom-primary-100/90 hover:text-custom-primary-100 font-medium pl-8 cursor-pointer py-1"
|
||||||
|
onClick={() => setViewAll((prevData) => !prevData)}
|
||||||
|
>
|
||||||
|
{viewAll ? "View less" : "View all"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
40
web/components/view/filters/filter-item.tsx
Normal file
40
web/components/view/filters/filter-item.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { FC, Fragment } from "react";
|
||||||
|
import { CheckSquare } from "lucide-react";
|
||||||
|
// hooks
|
||||||
|
import { useViewFilter } from "hooks/store";
|
||||||
|
// types
|
||||||
|
import { TViewFilters, TViewTypes } from "@plane/types";
|
||||||
|
import { TViewOperations } from "../types";
|
||||||
|
// helpers
|
||||||
|
// import { filterPropertyItemByFilterKeyAndId } from "../helpers/filters";
|
||||||
|
|
||||||
|
type TViewFiltersItem = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string | undefined;
|
||||||
|
viewId: string;
|
||||||
|
viewType: TViewTypes;
|
||||||
|
viewOperations: TViewOperations;
|
||||||
|
baseRoute: string;
|
||||||
|
filterKey: keyof TViewFilters;
|
||||||
|
propertyId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewFiltersItem: FC<TViewFiltersItem> = (props) => {
|
||||||
|
const { workspaceSlug, projectId, viewId, viewType, viewOperations, baseRoute, filterKey, propertyId } = props;
|
||||||
|
// hooks
|
||||||
|
const viewFilterHelper = useViewFilter(workspaceSlug, projectId);
|
||||||
|
|
||||||
|
const propertyDetail = viewFilterHelper?.propertyDetails(filterKey, propertyId) || undefined;
|
||||||
|
|
||||||
|
if (!propertyDetail) return <></>;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className="flex-shrink-0 w-4 h-4 flex justify-center items-center">
|
||||||
|
{propertyDetail?.icon || <CheckSquare size={14} />}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs block truncate line-clamp-1 text-custom-text-200 group-hover:text-custom-text-100">
|
||||||
|
{propertyDetail?.label || propertyId}
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
40
web/components/view/filters/filter-selection.tsx
Normal file
40
web/components/view/filters/filter-selection.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// hooks
|
||||||
|
import { useViewDetail } from "hooks/store";
|
||||||
|
// types
|
||||||
|
import { TViewFilters, TViewTypes } from "@plane/types";
|
||||||
|
import { TViewOperations } from "../types";
|
||||||
|
|
||||||
|
type TViewFilterSelection = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string | undefined;
|
||||||
|
viewId: string;
|
||||||
|
viewType: TViewTypes;
|
||||||
|
viewOperations: TViewOperations;
|
||||||
|
baseRoute: string;
|
||||||
|
filterKey: keyof TViewFilters;
|
||||||
|
propertyId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewFilterSelection: FC<TViewFilterSelection> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId, viewId, viewType, viewOperations, baseRoute, filterKey, propertyId } = props;
|
||||||
|
|
||||||
|
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||||
|
|
||||||
|
const propertyIds = viewDetailStore?.appliedFilters?.filters?.[filterKey] || [];
|
||||||
|
const isSelected = propertyIds?.includes(propertyId) || false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex-shrink-0 w-3 h-3 flex justify-center items-center border rounded text-bold ${
|
||||||
|
isSelected
|
||||||
|
? "border-custom-primary-100 bg-custom-primary-100"
|
||||||
|
: "border-custom-border-400 bg-custom-background-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isSelected && <Check size={14} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -1,34 +1,73 @@
|
|||||||
import { FC } from "react";
|
import { FC, useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { ChevronDown, ChevronUp, Search } from "lucide-react";
|
||||||
|
import concat from "lodash/concat";
|
||||||
|
import uniq from "lodash/uniq";
|
||||||
|
import filter from "lodash/filter";
|
||||||
|
// hooks
|
||||||
|
import { useViewDetail } from "hooks/store";
|
||||||
|
// components
|
||||||
|
import { ViewFiltersItemRoot } from "../";
|
||||||
// types
|
// types
|
||||||
import { TViewOperations } from "../types";
|
import { TViewOperations } from "../types";
|
||||||
|
import { TViewFilters, TViewTypes } from "@plane/types";
|
||||||
|
import { VIEW_DEFAULT_FILTER_PARAMETERS } from "constants/view";
|
||||||
|
|
||||||
type TViewFiltersRoot = {
|
type TViewFiltersRoot = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string | undefined;
|
projectId: string | undefined;
|
||||||
viewId: string | undefined;
|
viewId: string;
|
||||||
|
viewType: TViewTypes;
|
||||||
viewOperations: TViewOperations;
|
viewOperations: TViewOperations;
|
||||||
|
baseRoute: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewFiltersRoot: FC<TViewFiltersRoot> = (props) => {
|
export const ViewFiltersRoot: FC<TViewFiltersRoot> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, viewId, viewOperations } = props;
|
const { workspaceSlug, projectId, viewId, viewType, viewOperations, baseRoute } = props;
|
||||||
|
// hooks
|
||||||
const filters = {
|
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||||
project: ["1", "2", "3", "4", "5", "6"],
|
// state
|
||||||
priority: ["1", "2", "3", "4", "5", "6"],
|
const [filterVisibility, setFilterVisibility] = useState<Partial<keyof TViewFilters>[]>([]);
|
||||||
state: ["1", "2", "3", "4", "5", "6"],
|
const handleFilterVisibility = (key: keyof TViewFilters) => {
|
||||||
state_group: ["1", "2", "3", "4", "5", "6"],
|
setFilterVisibility((prevData = []) => {
|
||||||
assignees: ["1", "2", "3", "4", "5", "6"],
|
if (prevData.includes(key)) return filter(prevData, (item) => item !== key);
|
||||||
mentions: ["1", "2", "3", "4", "5", "6"],
|
return uniq(concat(prevData, [key]));
|
||||||
subscriber: ["1", "2", "3", "4", "5", "6"],
|
});
|
||||||
created_by: ["1", "2", "3", "4", "5", "6"],
|
|
||||||
labels: ["1", "2", "3", "4", "5", "6"],
|
|
||||||
start_date: ["1", "2", "3", "4", "5", "6"],
|
|
||||||
target_date: ["1", "2", "3", "4", "5", "6"],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const layout = viewDetailStore?.appliedFilters?.display_filters?.layout || "spreadsheet";
|
||||||
|
|
||||||
|
const filtersProperties = VIEW_DEFAULT_FILTER_PARAMETERS?.["all"]?.["spreadsheet"]?.filters || [];
|
||||||
|
|
||||||
|
if (!layout || filtersProperties.length <= 0) return <></>;
|
||||||
return (
|
return (
|
||||||
<div className="border border-red-500">
|
<div className="space-y-1 divide-y divide-custom-border-300">
|
||||||
<div>ViewFiltersRoot</div>
|
{filtersProperties.map((filterKey) => (
|
||||||
|
<div key={filterKey} className="relative py-1 first:pt-0 last:pb-0">
|
||||||
|
<div className="sticky top-0 z-20 flex justify-between items-center gap-2 bg-custom-background-100 select-none">
|
||||||
|
<div className="font-medium text-xs text-custom-text-300 capitalize py-1">
|
||||||
|
{filterKey.replace("_", " ")}
|
||||||
|
</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) && (
|
||||||
|
<ViewFiltersItemRoot
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
viewId={viewId}
|
||||||
|
viewType={viewType}
|
||||||
|
viewOperations={viewOperations}
|
||||||
|
baseRoute={baseRoute}
|
||||||
|
filterKey={filterKey}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
49
web/components/view/helpers/filters.tsx
Normal file
49
web/components/view/helpers/filters.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// hooks
|
||||||
|
import { useProject, useProjectState, useMember } from "hooks/store";
|
||||||
|
// types
|
||||||
|
import { TViewFilters } from "@plane/types";
|
||||||
|
|
||||||
|
type TFilterPropertyItemByFilterKeyAndId = {
|
||||||
|
key: keyof TViewFilters;
|
||||||
|
id: string;
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterPropertyItemByFilterKeyAndId = (
|
||||||
|
key: keyof TViewFilters,
|
||||||
|
id: string
|
||||||
|
): TFilterPropertyItemByFilterKeyAndId | undefined => {
|
||||||
|
if (!key || id) return undefined;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "project":
|
||||||
|
return undefined; // store
|
||||||
|
case "module":
|
||||||
|
return undefined; // store
|
||||||
|
case "cycle":
|
||||||
|
return undefined; // store
|
||||||
|
case "priority":
|
||||||
|
return undefined; // constant
|
||||||
|
case "state":
|
||||||
|
return undefined; // store
|
||||||
|
case "state_group":
|
||||||
|
return undefined; // constant
|
||||||
|
case "assignees":
|
||||||
|
return undefined; // store -> workspace and project level
|
||||||
|
case "mentions":
|
||||||
|
return undefined; // store -> workspace and project level
|
||||||
|
case "subscriber":
|
||||||
|
return undefined; // store -> workspace and project level
|
||||||
|
case "created_by":
|
||||||
|
return undefined; // store -> workspace and project level
|
||||||
|
case "labels":
|
||||||
|
return undefined; // store -> workspace and project level
|
||||||
|
case "start_date":
|
||||||
|
return undefined; // constants
|
||||||
|
case "target_date":
|
||||||
|
return undefined; // constants
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
@ -13,9 +13,14 @@ export * from "./views/create-edit-form";
|
|||||||
export * from "./layout";
|
export * from "./layout";
|
||||||
|
|
||||||
// view filters
|
// view filters
|
||||||
|
export * from "./filters/dropdown";
|
||||||
export * from "./filters/root";
|
export * from "./filters/root";
|
||||||
|
export * from "./filters/filter-item-root";
|
||||||
|
export * from "./filters/filter-item";
|
||||||
|
export * from "./filters/filter-selection";
|
||||||
|
|
||||||
// view display filters
|
// view display filters
|
||||||
|
export * from "./display-filters/dropdown";
|
||||||
export * from "./display-filters/root";
|
export * from "./display-filters/root";
|
||||||
|
|
||||||
// view display properties
|
// view display properties
|
||||||
@ -23,3 +28,7 @@ export * from "./display-properties/root";
|
|||||||
|
|
||||||
// view applied filters
|
// view applied filters
|
||||||
export * from "./applied-filters/root";
|
export * from "./applied-filters/root";
|
||||||
|
|
||||||
|
// confirmation modals
|
||||||
|
export * from "./confirmation-modals/duplicate";
|
||||||
|
export * from "./confirmation-modals/delete";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC } from "react";
|
import { FC, Fragment } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { LucideIcon, List, Kanban, Calendar, Sheet, GanttChartSquare } from "lucide-react";
|
import { LucideIcon, List, Kanban, Calendar, Sheet, GanttChartSquare } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
@ -31,23 +31,24 @@ export const ViewLayoutRoot: FC<TViewLayoutRoot> = observer((props) => {
|
|||||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex gap-0.5 items-center bg-custom-background-80 rounded-md p-1">
|
<div className="relative flex gap-0.5 items-center bg-custom-background-80 rounded p-1 h-8 shadow-custom-shadow-2xs">
|
||||||
{LAYOUTS_DATA.map((layout) => (
|
{LAYOUTS_DATA.map((layout) => (
|
||||||
<Tooltip tooltipContent={layout.title} position="bottom">
|
<Fragment key={layout.key}>
|
||||||
<div
|
<Tooltip tooltipContent={layout.title} position="bottom">
|
||||||
key={layout.key}
|
<div
|
||||||
className={`relative h-[24px] w-7 flex justify-center items-center overflow-hidden rounded transition-all cursor-pointer
|
className={`relative h-[24px] 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={() => viewOperations.setDisplayFilters({ layout: layout.key })}
|
onClick={() => viewOperations.setDisplayFilters({ layout: layout.key })}
|
||||||
>
|
>
|
||||||
<layout.icon size={12} />
|
<layout.icon size={12} />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -14,12 +14,13 @@ type TViewDropdownItem = {
|
|||||||
projectId: string | undefined;
|
projectId: string | undefined;
|
||||||
viewId: string;
|
viewId: string;
|
||||||
viewType: TViewTypes;
|
viewType: TViewTypes;
|
||||||
currentViewId: string | undefined;
|
currentViewId: string;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
|
baseRoute: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewDropdownItem: FC<TViewDropdownItem> = (props) => {
|
export const ViewDropdownItem: FC<TViewDropdownItem> = (props) => {
|
||||||
const { workspaceSlug, projectId, viewId, viewType, currentViewId, searchQuery } = props;
|
const { workspaceSlug, projectId, viewId, viewType, currentViewId, searchQuery, baseRoute } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ export const ViewDropdownItem: FC<TViewDropdownItem> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
href={`/${workspaceSlug}/workspace-views/${viewDetailStore?.id}`}
|
href={`${baseRoute}/${viewDetailStore?.id}`}
|
||||||
className={`w-full h-full overflow-hidden relative flex items-center gap-1
|
className={`w-full h-full overflow-hidden relative flex items-center gap-1
|
||||||
${
|
${
|
||||||
currentViewId === viewDetailStore?.id
|
currentViewId === viewDetailStore?.id
|
||||||
|
@ -14,14 +14,15 @@ import { TViewOperations } from "../../types";
|
|||||||
type TViewDropdown = {
|
type TViewDropdown = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string | undefined;
|
projectId: string | undefined;
|
||||||
viewId: string | undefined;
|
viewId: string;
|
||||||
viewType: TViewTypes;
|
viewType: TViewTypes;
|
||||||
viewOperations: TViewOperations;
|
viewOperations: TViewOperations;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
baseRoute: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewDropdown: FC<TViewDropdown> = (props) => {
|
export const ViewDropdown: FC<TViewDropdown> = (props) => {
|
||||||
const { workspaceSlug, projectId, viewId: currentViewId, viewType, viewOperations, children } = props;
|
const { workspaceSlug, projectId, viewId: currentViewId, viewType, viewOperations, children, baseRoute } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const viewStore = useView(workspaceSlug, projectId, viewType);
|
const viewStore = useView(workspaceSlug, projectId, viewType);
|
||||||
// states
|
// states
|
||||||
@ -105,6 +106,7 @@ export const ViewDropdown: FC<TViewDropdown> = (props) => {
|
|||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
currentViewId={currentViewId}
|
currentViewId={currentViewId}
|
||||||
searchQuery={query}
|
searchQuery={query}
|
||||||
|
baseRoute={baseRoute}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
|
@ -14,13 +14,14 @@ import { TViewTypes } from "@plane/types";
|
|||||||
type TViewRoot = {
|
type TViewRoot = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string | undefined;
|
projectId: string | undefined;
|
||||||
viewId: string | undefined;
|
viewId: string;
|
||||||
viewType: TViewTypes;
|
viewType: TViewTypes;
|
||||||
viewOperations: TViewOperations;
|
viewOperations: TViewOperations;
|
||||||
|
baseRoute: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewRoot: FC<TViewRoot> = observer((props) => {
|
export const ViewRoot: FC<TViewRoot> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
|
const { workspaceSlug, projectId, viewId, viewType, viewOperations, baseRoute } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const viewStore = useView(workspaceSlug, projectId, viewType);
|
const viewStore = useView(workspaceSlug, projectId, viewType);
|
||||||
// state
|
// state
|
||||||
@ -30,14 +31,14 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
|
|||||||
const handleViewTabsVisibility = () => {
|
const handleViewTabsVisibility = () => {
|
||||||
const tabContainer = document.getElementById("tab-container");
|
const tabContainer = document.getElementById("tab-container");
|
||||||
const tabItemViewMore = document.getElementById("tab-item-view-more");
|
const tabItemViewMore = document.getElementById("tab-item-view-more");
|
||||||
const itemWidth = 124;
|
const itemWidth = 128;
|
||||||
if (!tabContainer || !tabItemViewMore) return;
|
if (!tabContainer || !tabItemViewMore) return;
|
||||||
|
|
||||||
const containerWidth = tabContainer.clientWidth;
|
const containerWidth = tabContainer.clientWidth;
|
||||||
const itemViewMoreLeftOffset = tabItemViewMore.offsetLeft;
|
const itemViewMoreLeftOffset = tabItemViewMore.offsetLeft + (tabItemViewMore.clientWidth + 10);
|
||||||
const itemViewMoreRightOffset = containerWidth - itemViewMoreLeftOffset;
|
const itemViewMoreRightOffset = containerWidth - itemViewMoreLeftOffset;
|
||||||
|
|
||||||
if (itemViewMoreLeftOffset + (tabItemViewMore.clientWidth + 10) > containerWidth) {
|
if (itemViewMoreLeftOffset > containerWidth) {
|
||||||
const itemsToRender = Math.floor(containerWidth / itemWidth);
|
const itemsToRender = Math.floor(containerWidth / itemWidth);
|
||||||
setItemsToRenderViewCount(itemsToRender);
|
setItemsToRenderViewCount(itemsToRender);
|
||||||
}
|
}
|
||||||
@ -54,6 +55,13 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
|
|||||||
return () => window.removeEventListener("resize", () => handleViewTabsVisibility());
|
return () => window.removeEventListener("resize", () => handleViewTabsVisibility());
|
||||||
}, [viewStore?.viewIds]);
|
}, [viewStore?.viewIds]);
|
||||||
|
|
||||||
|
const viewIds = viewStore?.viewIds?.slice(0, itemsToRenderViewsCount || viewStore?.viewIds.length) || [];
|
||||||
|
|
||||||
|
if (!viewIds.includes(viewId)) {
|
||||||
|
viewIds.pop();
|
||||||
|
viewIds.push(viewId);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex justify-between px-5 gap-2">
|
<div className="relative flex justify-between px-5 gap-2">
|
||||||
{viewStore?.viewIds && viewStore?.viewIds.length > 0 && (
|
{viewStore?.viewIds && viewStore?.viewIds.length > 0 && (
|
||||||
@ -62,7 +70,7 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
|
|||||||
id="tab-container"
|
id="tab-container"
|
||||||
className="relative flex items-center w-full overflow-hidden"
|
className="relative flex items-center w-full overflow-hidden"
|
||||||
>
|
>
|
||||||
{viewStore?.viewIds?.slice(0, itemsToRenderViewsCount || viewStore?.viewIds.length).map((_viewId) => (
|
{viewIds.map((_viewId) => (
|
||||||
<Fragment key={_viewId}>
|
<Fragment key={_viewId}>
|
||||||
<ViewItem
|
<ViewItem
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
@ -70,6 +78,7 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
|
|||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
viewItemId={_viewId}
|
viewItemId={_viewId}
|
||||||
|
baseRoute={baseRoute}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
@ -82,6 +91,7 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
|
|||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
viewOperations={viewOperations}
|
viewOperations={viewOperations}
|
||||||
|
baseRoute={baseRoute}
|
||||||
>
|
>
|
||||||
<div className="text-sm font-semibold mb-1 p-2 px-2.5 text-custom-text-200 cursor-pointer hover:bg-custom-background-80 whitespace-nowrap rounded relative flex items-center gap-1">
|
<div className="text-sm font-semibold mb-1 p-2 px-2.5 text-custom-text-200 cursor-pointer hover:bg-custom-background-80 whitespace-nowrap rounded relative flex items-center gap-1">
|
||||||
<span>
|
<span>
|
||||||
|
@ -2,7 +2,7 @@ import { FC, Fragment } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useView, useViewDetail } from "hooks/store";
|
import { useViewDetail } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { PhotoFilterIcon, Tooltip } from "@plane/ui";
|
import { PhotoFilterIcon, Tooltip } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
@ -11,13 +11,14 @@ import { TViewTypes } from "@plane/types";
|
|||||||
type TViewItem = {
|
type TViewItem = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string | undefined;
|
projectId: string | undefined;
|
||||||
viewId: string | undefined;
|
viewId: string;
|
||||||
viewType: TViewTypes;
|
viewType: TViewTypes;
|
||||||
viewItemId: string;
|
viewItemId: string;
|
||||||
|
baseRoute: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewItem: FC<TViewItem> = observer((props) => {
|
export const ViewItem: FC<TViewItem> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, viewId, viewType, viewItemId } = props;
|
const { workspaceSlug, projectId, viewId, viewType, viewItemId, baseRoute } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewItemId, viewType);
|
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewItemId, viewType);
|
||||||
|
|
||||||
@ -26,7 +27,7 @@ export const ViewItem: FC<TViewItem> = observer((props) => {
|
|||||||
<div className="space-y-0.5 relative h-full flex flex-col justify-between">
|
<div className="space-y-0.5 relative h-full flex flex-col justify-between">
|
||||||
<Tooltip tooltipContent={viewDetailStore?.name} position="top">
|
<Tooltip tooltipContent={viewDetailStore?.name} position="top">
|
||||||
<Link
|
<Link
|
||||||
href={`/${workspaceSlug}/workspace-views/${viewItemId}`}
|
href={`${baseRoute}/${viewItemId}`}
|
||||||
className={`cursor-pointer relative p-2 px-2.5 flex justify-center items-center gap-1 rounded transition-all hover:bg-custom-background-80
|
className={`cursor-pointer relative p-2 px-2.5 flex justify-center items-center gap-1 rounded transition-all hover:bg-custom-background-80
|
||||||
${viewItemId === viewId ? `text-custom-primary-100 bg-custom-primary-100/10` : `border-transparent`}
|
${viewItemId === viewId ? `text-custom-primary-100 bg-custom-primary-100/10` : `border-transparent`}
|
||||||
`}
|
`}
|
||||||
@ -44,7 +45,11 @@ export const ViewItem: FC<TViewItem> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div className={`border-b-2 ${viewItemId === viewId ? `border-custom-primary-100` : `border-transparent`}`} />
|
<div
|
||||||
|
className={`border-b-2 rounded-t-sm ${
|
||||||
|
viewItemId === viewId ? `border-custom-primary-100` : `border-transparent`
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -270,7 +270,7 @@ export const SIDEBAR_MENU_ITEMS: {
|
|||||||
{
|
{
|
||||||
key: "all-issues",
|
key: "all-issues",
|
||||||
label: "All Issues",
|
label: "All Issues",
|
||||||
href: `/workspace-views/all-issues`,
|
href: `/views/public/all-issues`,
|
||||||
access: EUserWorkspaceRoles.GUEST,
|
access: EUserWorkspaceRoles.GUEST,
|
||||||
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views`),
|
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views`),
|
||||||
Icon: CheckCircle,
|
Icon: CheckCircle,
|
||||||
|
@ -1,8 +1,65 @@
|
|||||||
// types
|
// types
|
||||||
import { TViewFilters, TViewDisplayFilters, TViewLayouts } from "@plane/types";
|
import { TStateGroups, TIssuePriorities, TViewFilters, TViewDisplayFilters, TViewLayouts } from "@plane/types";
|
||||||
|
|
||||||
|
// filters constants
|
||||||
|
export const STATE_GROUP_PROPERTY: {
|
||||||
|
[key in TStateGroups]: {
|
||||||
|
label: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
backlog: {
|
||||||
|
label: "Backlog",
|
||||||
|
color: "#d9d9d9",
|
||||||
|
},
|
||||||
|
unstarted: {
|
||||||
|
label: "Unstarted",
|
||||||
|
color: "#3f76ff",
|
||||||
|
},
|
||||||
|
started: {
|
||||||
|
label: "Started",
|
||||||
|
color: "#f59e0b",
|
||||||
|
},
|
||||||
|
completed: {
|
||||||
|
label: "Completed",
|
||||||
|
color: "#16a34a",
|
||||||
|
},
|
||||||
|
cancelled: {
|
||||||
|
label: "Canceled",
|
||||||
|
color: "#dc2626",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PRIORITIES_PROPERTY: {
|
||||||
|
[key in TIssuePriorities]: {
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
urgent: { label: "Urgent" },
|
||||||
|
high: { label: "High" },
|
||||||
|
medium: { label: "Medium" },
|
||||||
|
low: { label: "Low" },
|
||||||
|
none: { label: "None" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DATE_PROPERTY: {
|
||||||
|
[key in string]: {
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
last_week: { label: "Last Week" },
|
||||||
|
"2_weeks_from_now": { label: "2 weeks from now" },
|
||||||
|
"1_month_from_now": { label: "1 month from now" },
|
||||||
|
"2_months_from_now": { label: "2 months from now" },
|
||||||
|
custom: { label: "Custom" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// display filter constants
|
||||||
|
|
||||||
|
// layout, filter, display filter and display properties permissions for views
|
||||||
type TViewLayoutFilterProperties = {
|
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;
|
||||||
@ -34,17 +91,32 @@ type TFilterPermissions = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ALL_FILTER_PERMISSIONS: TFilterPermissions["all"] = {
|
const ALL_FILTER_PERMISSIONS: TFilterPermissions["all"] = {
|
||||||
layouts: ["spreadsheet"],
|
layouts: ["spreadsheet"],
|
||||||
spreadsheet: {
|
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: [
|
||||||
|
"project",
|
||||||
|
"module",
|
||||||
|
"cycle",
|
||||||
|
"priority",
|
||||||
|
"state",
|
||||||
|
"state_group",
|
||||||
|
"assignees",
|
||||||
|
"mentions",
|
||||||
|
"subscriber",
|
||||||
|
"created_by",
|
||||||
|
"labels",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
],
|
||||||
display_filters: ["type"],
|
display_filters: ["type"],
|
||||||
extra_options: [],
|
extra_options: [],
|
||||||
display_properties: true,
|
display_properties: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PROFILE_FILTER_PERMISSIONS: TFilterPermissions["profile"] = {
|
const PROFILE_FILTER_PERMISSIONS: TFilterPermissions["profile"] = {
|
||||||
layouts: ["list", "kanban"],
|
layouts: ["list", "kanban"],
|
||||||
list: {
|
list: {
|
||||||
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
|
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
|
||||||
@ -60,7 +132,7 @@ export const PROFILE_FILTER_PERMISSIONS: TFilterPermissions["profile"] = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
|
const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
|
||||||
layouts: ["list", "kanban", "spreadsheet", "calendar", "gantt"],
|
layouts: ["list", "kanban", "spreadsheet", "calendar", "gantt"],
|
||||||
list: {
|
list: {
|
||||||
filters: [
|
filters: [
|
||||||
@ -150,7 +222,7 @@ export const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ARCHIVED_FILTER_PERMISSIONS: TFilterPermissions["archived"] = {
|
const ARCHIVED_FILTER_PERMISSIONS: TFilterPermissions["archived"] = {
|
||||||
layouts: ["list"],
|
layouts: ["list"],
|
||||||
list: {
|
list: {
|
||||||
filters: [
|
filters: [
|
||||||
@ -171,7 +243,7 @@ export const ARCHIVED_FILTER_PERMISSIONS: TFilterPermissions["archived"] = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DRAFT_FILTER_PERMISSIONS: TFilterPermissions["draft"] = {
|
const DRAFT_FILTER_PERMISSIONS: TFilterPermissions["draft"] = {
|
||||||
layouts: ["list", "kanban"],
|
layouts: ["list", "kanban"],
|
||||||
list: {
|
list: {
|
||||||
filters: [
|
filters: [
|
||||||
|
@ -3,10 +3,10 @@ import { v4 as uuidV4 } from "uuid";
|
|||||||
import { TViewTypes, TView } from "@plane/types";
|
import { TViewTypes, TView } from "@plane/types";
|
||||||
|
|
||||||
export const VIEW_TYPES: Record<TViewTypes, TViewTypes> = {
|
export const VIEW_TYPES: Record<TViewTypes, TViewTypes> = {
|
||||||
WORKSPACE_YOUR_VIEWS: "WORKSPACE_YOUR_VIEWS",
|
WORKSPACE_PRIVATE_VIEWS: "WORKSPACE_PRIVATE_VIEWS",
|
||||||
WORKSPACE_VIEWS: "WORKSPACE_VIEWS",
|
WORKSPACE_PUBLIC_VIEWS: "WORKSPACE_PUBLIC_VIEWS",
|
||||||
PROJECT_VIEWS: "PROJECT_VIEWS",
|
PROJECT_PRIVATE_VIEWS: "PROJECT_PRIVATE_VIEWS",
|
||||||
PROJECT_YOUR_VIEWS: "PROJECT_YOUR_VIEWS",
|
PROJECT_PUBLIC_VIEWS: "PROJECT_PUBLIC_VIEWS",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const viewLocalPayload: Partial<TView> = {
|
export const viewLocalPayload: Partial<TView> = {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export * from "./use-application";
|
export * from "./use-application";
|
||||||
export * from "./use-event-tracker"
|
export * from "./use-event-tracker";
|
||||||
export * from "./use-calendar-view";
|
export * from "./use-calendar-view";
|
||||||
export * from "./use-cycle";
|
export * from "./use-cycle";
|
||||||
export * from "./use-dashboard";
|
export * from "./use-dashboard";
|
||||||
@ -24,5 +24,6 @@ export * from "./use-inbox";
|
|||||||
export * from "./use-inbox-issues";
|
export * from "./use-inbox-issues";
|
||||||
|
|
||||||
// new store
|
// new store
|
||||||
export * from "./use-view";
|
export * from "./views/use-view";
|
||||||
export * from "./use-view-detail";
|
export * from "./views/use-view-detail";
|
||||||
|
export * from "./views/use-view-filters";
|
||||||
|
@ -5,6 +5,8 @@ import { StoreContext } from "contexts/store-context";
|
|||||||
import { TViewStore } from "store/view/view.store";
|
import { TViewStore } from "store/view/view.store";
|
||||||
// types
|
// types
|
||||||
import { TViewTypes } from "@plane/types";
|
import { TViewTypes } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { VIEW_TYPES } from "constants/view";
|
||||||
|
|
||||||
export const useViewDetail = (
|
export const useViewDetail = (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
@ -18,16 +20,16 @@ export const useViewDetail = (
|
|||||||
if (!workspaceSlug || !viewId) return undefined;
|
if (!workspaceSlug || !viewId) return undefined;
|
||||||
|
|
||||||
switch (viewType) {
|
switch (viewType) {
|
||||||
case "WORKSPACE_YOUR_VIEWS":
|
case VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS:
|
||||||
return context.view.workspaceViewMeStore.viewById(viewId);
|
return context.view.workspacePrivateViewStore.viewById(viewId);
|
||||||
case "WORKSPACE_VIEWS":
|
case VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS:
|
||||||
return context.view.workspaceViewStore.viewById(viewId);
|
return context.view.workspacePublicViewStore.viewById(viewId);
|
||||||
case "PROJECT_YOUR_VIEWS":
|
case VIEW_TYPES.PROJECT_PRIVATE_VIEWS:
|
||||||
if (!projectId) throw new Error("useView hook must require projectId");
|
if (!projectId) return undefined;
|
||||||
return context.view.projectViewMeStore.viewById(viewId);
|
return context.view.projectPrivateViewStore.viewById(viewId);
|
||||||
case "PROJECT_VIEWS":
|
case VIEW_TYPES.PROJECT_PUBLIC_VIEWS:
|
||||||
if (!projectId) throw new Error("useView hook must require projectId");
|
if (!projectId) return undefined;
|
||||||
return context.view.projectViewStore.viewById(viewId);
|
return context.view.projectPublicViewStore.viewById(viewId);
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
225
web/hooks/store/views/use-view-filters.tsx
Normal file
225
web/hooks/store/views/use-view-filters.tsx
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
// hooks
|
||||||
|
import { useProject, useModule, useCycle, useProjectState, useMember, useLabel } from "hooks/store";
|
||||||
|
// ui
|
||||||
|
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { TIssuePriorities, TStateGroups, TViewFilters } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { STATE_GROUP_PROPERTY, PRIORITIES_PROPERTY, DATE_PROPERTY } from "constants/view/filters";
|
||||||
|
import { Briefcase, CalendarDays } from "lucide-react";
|
||||||
|
// helpers
|
||||||
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
|
|
||||||
|
type TFilterPropertyDetails = {
|
||||||
|
icon: ReactNode;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useViewFilter = (workspaceSlug: string, projectId: string | undefined) => {
|
||||||
|
const { projectMap, getProjectById } = useProject();
|
||||||
|
const { getProjectModuleIds, getModuleById } = useModule();
|
||||||
|
const { getProjectCycleIds, getCycleById } = useCycle();
|
||||||
|
const { getProjectStates, getStateById } = useProjectState();
|
||||||
|
const {
|
||||||
|
getUserDetails,
|
||||||
|
workspace: { workspaceMemberIds },
|
||||||
|
project: { getProjectMemberIds },
|
||||||
|
} = useMember();
|
||||||
|
const { workspaceLabels, getProjectLabels, getLabelById } = useLabel();
|
||||||
|
|
||||||
|
if (!workspaceSlug) return undefined;
|
||||||
|
|
||||||
|
const filterIdsWithKey = (filterKey: keyof TViewFilters): string[] | undefined => {
|
||||||
|
if (!filterKey) return undefined;
|
||||||
|
|
||||||
|
switch (filterKey) {
|
||||||
|
case "project":
|
||||||
|
return Object.keys(projectMap) || undefined;
|
||||||
|
case "module":
|
||||||
|
if (!projectId) return undefined;
|
||||||
|
return getProjectModuleIds(projectId) || undefined;
|
||||||
|
case "cycle":
|
||||||
|
if (!projectId) return undefined;
|
||||||
|
return getProjectCycleIds(projectId) || undefined;
|
||||||
|
case "priority":
|
||||||
|
return Object.keys(PRIORITIES_PROPERTY) || undefined;
|
||||||
|
case "state":
|
||||||
|
if (!projectId) return undefined;
|
||||||
|
return getProjectStates(projectId)?.map((state) => state.id) || undefined;
|
||||||
|
case "state_group":
|
||||||
|
return Object.keys(STATE_GROUP_PROPERTY) || undefined;
|
||||||
|
case "assignees":
|
||||||
|
if (projectId) return getProjectMemberIds(projectId) || undefined;
|
||||||
|
return workspaceMemberIds || undefined;
|
||||||
|
case "mentions":
|
||||||
|
if (projectId) return getProjectMemberIds(projectId) || undefined;
|
||||||
|
return workspaceMemberIds || undefined;
|
||||||
|
case "subscriber":
|
||||||
|
if (projectId) return getProjectMemberIds(projectId) || undefined;
|
||||||
|
return workspaceMemberIds || undefined;
|
||||||
|
case "created_by":
|
||||||
|
if (projectId) return getProjectMemberIds(projectId) || undefined;
|
||||||
|
return workspaceMemberIds || undefined;
|
||||||
|
case "labels":
|
||||||
|
if (projectId) return getProjectLabels(projectId)?.map((label) => label.id) || undefined;
|
||||||
|
return workspaceLabels?.map((label) => label.id) || undefined;
|
||||||
|
case "start_date":
|
||||||
|
return Object.keys(DATE_PROPERTY) || undefined;
|
||||||
|
case "target_date":
|
||||||
|
return Object.keys(DATE_PROPERTY) || undefined;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const propertyDetails = (filterKey: keyof TViewFilters, propertyId: string): TFilterPropertyDetails | undefined => {
|
||||||
|
if (!filterKey || !propertyId) return undefined;
|
||||||
|
|
||||||
|
switch (filterKey) {
|
||||||
|
case "project":
|
||||||
|
const projectPropertyDetail = getProjectById(propertyId);
|
||||||
|
if (!projectPropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: (
|
||||||
|
<>
|
||||||
|
{projectPropertyDetail.emoji ? (
|
||||||
|
<div className="text-xs">{renderEmoji(projectPropertyDetail.emoji)}</div>
|
||||||
|
) : projectPropertyDetail.icon_prop ? (
|
||||||
|
<div className="text-xs">{renderEmoji(projectPropertyDetail.icon_prop)}</div>
|
||||||
|
) : (
|
||||||
|
<Briefcase size={12} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
label: projectPropertyDetail.name,
|
||||||
|
};
|
||||||
|
case "module":
|
||||||
|
const modulePropertyDetail = getModuleById(propertyId);
|
||||||
|
if (!modulePropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: <DiceIcon className="w-3 h-3" />,
|
||||||
|
label: modulePropertyDetail.name,
|
||||||
|
};
|
||||||
|
case "cycle":
|
||||||
|
const cyclePropertyDetail = getCycleById(propertyId);
|
||||||
|
if (!cyclePropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: <CycleGroupIcon cycleGroup={cyclePropertyDetail.status} height="14px" width="14px" />,
|
||||||
|
label: cyclePropertyDetail.name,
|
||||||
|
};
|
||||||
|
case "priority":
|
||||||
|
const priorityPropertyDetail = PRIORITIES_PROPERTY?.[propertyId as TIssuePriorities];
|
||||||
|
if (!priorityPropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: <PriorityIcon priority={propertyId as TIssuePriorities} size={10} withContainer />,
|
||||||
|
label: priorityPropertyDetail.label,
|
||||||
|
};
|
||||||
|
case "state":
|
||||||
|
const statePropertyDetail = getStateById(propertyId);
|
||||||
|
if (!statePropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: <StateGroupIcon stateGroup={statePropertyDetail.group} />,
|
||||||
|
label: statePropertyDetail.name,
|
||||||
|
};
|
||||||
|
case "state_group":
|
||||||
|
const stateGroupPropertyDetail = STATE_GROUP_PROPERTY?.[propertyId as TStateGroups];
|
||||||
|
if (!stateGroupPropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: <StateGroupIcon stateGroup={propertyId as TStateGroups} />,
|
||||||
|
label: stateGroupPropertyDetail.label,
|
||||||
|
};
|
||||||
|
case "assignees":
|
||||||
|
const assigneePropertyDetail = getUserDetails(propertyId);
|
||||||
|
if (!assigneePropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: (
|
||||||
|
<Avatar
|
||||||
|
name={assigneePropertyDetail.display_name}
|
||||||
|
src={assigneePropertyDetail.avatar}
|
||||||
|
size={"sm"}
|
||||||
|
showTooltip={false}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
label: assigneePropertyDetail.display_name,
|
||||||
|
};
|
||||||
|
case "mentions":
|
||||||
|
const mentionPropertyDetail = getUserDetails(propertyId);
|
||||||
|
if (!mentionPropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: (
|
||||||
|
<Avatar
|
||||||
|
name={mentionPropertyDetail.display_name}
|
||||||
|
src={mentionPropertyDetail.avatar}
|
||||||
|
size={"sm"}
|
||||||
|
showTooltip={false}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
label: mentionPropertyDetail.display_name,
|
||||||
|
};
|
||||||
|
case "subscriber":
|
||||||
|
const subscribedPropertyDetail = getUserDetails(propertyId);
|
||||||
|
if (!subscribedPropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: (
|
||||||
|
<Avatar
|
||||||
|
name={subscribedPropertyDetail.display_name}
|
||||||
|
src={subscribedPropertyDetail.avatar}
|
||||||
|
size={"sm"}
|
||||||
|
showTooltip={false}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
label: subscribedPropertyDetail.display_name,
|
||||||
|
};
|
||||||
|
case "created_by":
|
||||||
|
const createdByPropertyDetail = getUserDetails(propertyId);
|
||||||
|
if (!createdByPropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: (
|
||||||
|
<Avatar
|
||||||
|
name={createdByPropertyDetail.display_name}
|
||||||
|
src={createdByPropertyDetail.avatar}
|
||||||
|
size={"sm"}
|
||||||
|
showTooltip={false}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
label: createdByPropertyDetail.display_name,
|
||||||
|
};
|
||||||
|
case "labels":
|
||||||
|
const labelPropertyDetail = getLabelById(propertyId);
|
||||||
|
if (!labelPropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: (
|
||||||
|
<div
|
||||||
|
className="w-2.5 h-2.5 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: labelPropertyDetail.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
label: labelPropertyDetail.name,
|
||||||
|
};
|
||||||
|
case "start_date":
|
||||||
|
const startDatePropertyDetail = DATE_PROPERTY?.[propertyId];
|
||||||
|
if (!startDatePropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: <CalendarDays size={12} />,
|
||||||
|
label: startDatePropertyDetail.label,
|
||||||
|
};
|
||||||
|
case "target_date":
|
||||||
|
const targetDatePropertyDetail = DATE_PROPERTY?.[propertyId];
|
||||||
|
if (!targetDatePropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: <CalendarDays size={12} />,
|
||||||
|
label: targetDatePropertyDetail.label,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
filterIdsWithKey,
|
||||||
|
propertyDetails,
|
||||||
|
};
|
||||||
|
};
|
@ -3,8 +3,9 @@ import { useContext } from "react";
|
|||||||
import { StoreContext } from "contexts/store-context";
|
import { StoreContext } from "contexts/store-context";
|
||||||
// types
|
// types
|
||||||
import { ViewRootStore } from "store/view/view-root.store";
|
import { ViewRootStore } from "store/view/view-root.store";
|
||||||
// types
|
|
||||||
import { TViewTypes } from "@plane/types";
|
import { TViewTypes } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { VIEW_TYPES } from "constants/view";
|
||||||
|
|
||||||
export const useView = (
|
export const useView = (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
@ -17,16 +18,16 @@ export const useView = (
|
|||||||
if (!workspaceSlug || !viewType) return undefined;
|
if (!workspaceSlug || !viewType) return undefined;
|
||||||
|
|
||||||
switch (viewType) {
|
switch (viewType) {
|
||||||
case "WORKSPACE_YOUR_VIEWS":
|
case VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS:
|
||||||
return context.view.workspaceViewMeStore;
|
return context.view.workspacePrivateViewStore;
|
||||||
case "WORKSPACE_VIEWS":
|
case VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS:
|
||||||
return context.view.workspaceViewStore;
|
return context.view.workspacePublicViewStore;
|
||||||
case "PROJECT_YOUR_VIEWS":
|
case VIEW_TYPES.PROJECT_PRIVATE_VIEWS:
|
||||||
if (!projectId) throw new Error("useView hook must require projectId");
|
if (!projectId) return undefined;
|
||||||
return context.view.projectViewMeStore;
|
return context.view.projectPrivateViewStore;
|
||||||
case "PROJECT_VIEWS":
|
case VIEW_TYPES.PROJECT_PUBLIC_VIEWS:
|
||||||
if (!projectId) throw new Error("useView hook must require projectId");
|
if (!projectId) return undefined;
|
||||||
return context.view.projectViewStore;
|
return context.view.projectPublicViewStore;
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
@ -20,6 +20,7 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
|||||||
const {
|
const {
|
||||||
workspace: { fetchWorkspaceMembers },
|
workspace: { fetchWorkspaceMembers },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
|
const { fetchWorkspaceLabels } = useLabel();
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
@ -38,6 +39,11 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
|||||||
workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null,
|
workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null,
|
||||||
workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null
|
workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null
|
||||||
);
|
);
|
||||||
|
// fetch workspace labels
|
||||||
|
useSWR(
|
||||||
|
workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null,
|
||||||
|
workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null
|
||||||
|
);
|
||||||
// fetch workspace user projects role
|
// fetch workspace user projects role
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null,
|
workspaceSlug ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null,
|
||||||
|
36
web/pages/[workspaceSlug]/views/private/[viewId].tsx
Normal file
36
web/pages/[workspaceSlug]/views/private/[viewId].tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
// layouts
|
||||||
|
import { AppLayout } from "layouts/app-layout";
|
||||||
|
// components
|
||||||
|
import { AllIssuesViewRoot } from "components/view";
|
||||||
|
// types
|
||||||
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
// constants
|
||||||
|
import { VIEW_TYPES } from "constants/view";
|
||||||
|
|
||||||
|
const GlobalViewIssuesPage: NextPageWithLayout = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, viewId } = router.query;
|
||||||
|
|
||||||
|
if (!workspaceSlug || !viewId) return <></>;
|
||||||
|
return (
|
||||||
|
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||||
|
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||||
|
<AllIssuesViewRoot
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={undefined}
|
||||||
|
viewId={viewId.toString()}
|
||||||
|
viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS}
|
||||||
|
baseRoute={`/${workspaceSlug?.toString()}/views/private`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <AppLayout header={<></>}>{page}</AppLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GlobalViewIssuesPage;
|
36
web/pages/[workspaceSlug]/views/public/[viewId].tsx
Normal file
36
web/pages/[workspaceSlug]/views/public/[viewId].tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
// layouts
|
||||||
|
import { AppLayout } from "layouts/app-layout";
|
||||||
|
// components
|
||||||
|
import { AllIssuesViewRoot } from "components/view";
|
||||||
|
// types
|
||||||
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
// constants
|
||||||
|
import { VIEW_TYPES } from "constants/view";
|
||||||
|
|
||||||
|
const GlobalViewIssuesPage: NextPageWithLayout = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, viewId } = router.query;
|
||||||
|
|
||||||
|
if (!workspaceSlug || !viewId) return <></>;
|
||||||
|
return (
|
||||||
|
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||||
|
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||||
|
<AllIssuesViewRoot
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={undefined}
|
||||||
|
viewId={viewId.toString()}
|
||||||
|
viewType={VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS}
|
||||||
|
baseRoute={`/${workspaceSlug?.toString()}/views/public`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <AppLayout header={<></>}>{page}</AppLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GlobalViewIssuesPage;
|
@ -3,10 +3,9 @@ import { useRouter } from "next/router";
|
|||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
// import { GlobalViewsHeader } from "components/workspace";
|
import { GlobalViewsHeader } from "components/workspace";
|
||||||
// import { AllIssueLayoutRoot } from "components/issues";
|
import { AllIssueLayoutRoot } from "components/issues";
|
||||||
// import { GlobalIssuesHeader } from "components/headers";
|
import { GlobalIssuesHeader } from "components/headers";
|
||||||
import { AllIssuesViewRoot } from "components/view";
|
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
|
||||||
@ -18,17 +17,15 @@ const GlobalViewIssuesPage: NextPageWithLayout = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="h-full overflow-hidden bg-custom-background-100">
|
<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">
|
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||||
<AllIssuesViewRoot workspaceSlug={workspaceSlug.toString()} projectId={undefined} viewId={viewId.toString()} />
|
<GlobalViewsHeader />
|
||||||
{/* <GlobalViewsHeader />
|
<AllIssueLayoutRoot />
|
||||||
<AllIssueLayoutRoot /> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
// return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
|
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
|
||||||
return <AppLayout header={<></>}>{page}</AppLayout>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GlobalViewIssuesPage;
|
export default GlobalViewIssuesPage;
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
// view services
|
// view services
|
||||||
export * from "./workspace_me.service";
|
export * from "./workspace_private.service";
|
||||||
export * from "./workspace.service";
|
export * from "./workspace_public.service";
|
||||||
export * from "./project_me.service";
|
export * from "./project_private.service";
|
||||||
export * from "./project.service";
|
export * from "./project_public.service";
|
||||||
|
|
||||||
// user view services
|
// user view services
|
||||||
export * from "./user/workspace.service";
|
export * from "./user/workspace.service";
|
||||||
export * from "./user/project.service";
|
export * from "./user/project.service";
|
||||||
export * from "./user/module.service";
|
export * from "./user/module.service";
|
||||||
export * from "./user/cycle.service";
|
export * from "./user/cycle.service";
|
||||||
|
|
||||||
// views that are being stored in the local-store
|
|
||||||
// export * from "./user/local_storage.service";
|
|
||||||
|
@ -5,7 +5,7 @@ import { TViewService } from "./types";
|
|||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
export class ProjectViewMeService extends APIService implements TViewService {
|
export class ProjectPrivateViewService extends APIService implements TViewService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
}
|
}
|
@ -5,7 +5,7 @@ import { TViewService } from "./types";
|
|||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
export class ProjectViewService extends APIService implements TViewService {
|
export class ProjectPublicViewService extends APIService implements TViewService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
}
|
}
|
12
web/services/view/types.d.ts
vendored
12
web/services/view/types.d.ts
vendored
@ -21,10 +21,10 @@ export type TViewService = {
|
|||||||
data: Partial<TView>,
|
data: Partial<TView>,
|
||||||
projectId?: string
|
projectId?: string
|
||||||
) => Promise<TView | undefined>;
|
) => Promise<TView | undefined>;
|
||||||
remove?: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<void> | undefined;
|
remove: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<void> | undefined;
|
||||||
lock?: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
lock: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
||||||
unlock?: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
unlock: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
||||||
duplicate?: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
duplicate: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
||||||
makeFavorite?: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
makeFavorite: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
||||||
removeFavorite?: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
removeFavorite: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@ import { TViewService } from "./types";
|
|||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
export class WorkspaceMeViewService extends APIService implements TViewService {
|
export class WorkspacePrivateViewService extends APIService implements TViewService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
}
|
}
|
@ -5,7 +5,7 @@ import { TViewService } from "./types";
|
|||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
export class WorkspaceViewService extends APIService implements TViewService {
|
export class WorkspacePublicViewService extends APIService implements TViewService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
}
|
}
|
@ -24,8 +24,6 @@ import { GlobalViewRootStore } from "./view/root.store";
|
|||||||
enableStaticRendering(typeof window === "undefined");
|
enableStaticRendering(typeof window === "undefined");
|
||||||
|
|
||||||
export class RootStore {
|
export class RootStore {
|
||||||
view: GlobalViewRootStore;
|
|
||||||
// old store structure
|
|
||||||
app: IAppRootStore;
|
app: IAppRootStore;
|
||||||
eventTracker: IEventTrackerStore;
|
eventTracker: IEventTrackerStore;
|
||||||
user: IUserRootStore;
|
user: IUserRootStore;
|
||||||
@ -44,9 +42,9 @@ export class RootStore {
|
|||||||
mention: IMentionStore;
|
mention: IMentionStore;
|
||||||
dashboard: IDashboardStore;
|
dashboard: IDashboardStore;
|
||||||
projectPages: IProjectPageStore;
|
projectPages: IProjectPageStore;
|
||||||
|
view: GlobalViewRootStore;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.view = new GlobalViewRootStore(this);
|
|
||||||
// old store structure
|
// old store structure
|
||||||
this.app = new AppRootStore(this);
|
this.app = new AppRootStore(this);
|
||||||
this.eventTracker = new EventTrackerStore(this);
|
this.eventTracker = new EventTrackerStore(this);
|
||||||
@ -67,6 +65,7 @@ export class RootStore {
|
|||||||
this.mention = new MentionStore(this);
|
this.mention = new MentionStore(this);
|
||||||
this.projectPages = new ProjectPageStore(this);
|
this.projectPages = new ProjectPageStore(this);
|
||||||
this.dashboard = new DashboardStore(this);
|
this.dashboard = new DashboardStore(this);
|
||||||
|
this.view = new GlobalViewRootStore(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
resetOnSignout() {
|
resetOnSignout() {
|
||||||
@ -86,5 +85,6 @@ export class RootStore {
|
|||||||
this.mention = new MentionStore(this);
|
this.mention = new MentionStore(this);
|
||||||
this.projectPages = new ProjectPageStore(this);
|
this.projectPages = new ProjectPageStore(this);
|
||||||
this.dashboard = new DashboardStore(this);
|
this.dashboard = new DashboardStore(this);
|
||||||
|
this.view = new GlobalViewRootStore(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,41 +1,31 @@
|
|||||||
|
// stores
|
||||||
|
import { autorun, makeObservable, observable } from "mobx";
|
||||||
|
import { ViewRootStore } from "./view-root.store";
|
||||||
// services
|
// services
|
||||||
import {
|
import {
|
||||||
WorkspaceViewService,
|
WorkspacePrivateViewService,
|
||||||
WorkspaceMeViewService,
|
WorkspacePublicViewService,
|
||||||
ProjectViewService,
|
ProjectPublicViewService,
|
||||||
ProjectViewMeService,
|
ProjectPrivateViewService,
|
||||||
WorkspaceFiltersService,
|
WorkspaceFiltersService,
|
||||||
ProjectFiltersService,
|
ProjectFiltersService,
|
||||||
ModuleFiltersService,
|
|
||||||
CycleFiltersService,
|
|
||||||
// LocalStorageFiltersService,
|
|
||||||
} from "services/view";
|
} from "services/view";
|
||||||
// stores
|
|
||||||
import { ViewRootStore } from "./view-root.store";
|
|
||||||
import { userViewRootStore } from "./user/view-root.store";
|
|
||||||
// types
|
// types
|
||||||
import { RootStore } from "store/root.store";
|
import { RootStore } from "store/root.store";
|
||||||
|
|
||||||
export class GlobalViewRootStore {
|
export class GlobalViewRootStore {
|
||||||
// views root
|
workspacePrivateViewStore: ViewRootStore;
|
||||||
workspaceViewMeStore: ViewRootStore;
|
workspacePublicViewStore: ViewRootStore;
|
||||||
workspaceViewStore: ViewRootStore;
|
projectPrivateViewStore: ViewRootStore;
|
||||||
projectViewStore: ViewRootStore;
|
projectPublicViewStore: ViewRootStore;
|
||||||
projectViewMeStore: ViewRootStore;
|
|
||||||
|
|
||||||
// user views root
|
|
||||||
workspaceUserViewStore?: userViewRootStore;
|
|
||||||
projectUserViewStore?: userViewRootStore;
|
|
||||||
moduleUserViewStore?: userViewRootStore;
|
|
||||||
cycleUserViewStore?: userViewRootStore;
|
|
||||||
|
|
||||||
constructor(private store: RootStore) {
|
constructor(private store: RootStore) {
|
||||||
const workspaceViewMeStoreDefaultViews: any[] = [
|
const workspacePrivateDefaultViews: any[] = [
|
||||||
{
|
{
|
||||||
id: "assigned",
|
id: "assigned",
|
||||||
name: "Assigned",
|
name: "Assigned",
|
||||||
filters: {
|
filters: {
|
||||||
assignees: store.user?.currentUser?.id ? [store.user?.currentUser?.id] : [],
|
assignees: store?.user?.currentUser?.id ? [store?.user?.currentUser?.id] : [],
|
||||||
},
|
},
|
||||||
is_local_view: true,
|
is_local_view: true,
|
||||||
},
|
},
|
||||||
@ -43,7 +33,7 @@ export class GlobalViewRootStore {
|
|||||||
id: "created",
|
id: "created",
|
||||||
name: "Created",
|
name: "Created",
|
||||||
filters: {
|
filters: {
|
||||||
created_by: store.user?.currentUser?.id ? [store.user?.currentUser?.id] : [],
|
created_by: store?.user?.currentUser?.id ? [store?.user?.currentUser?.id] : [],
|
||||||
},
|
},
|
||||||
is_local_view: true,
|
is_local_view: true,
|
||||||
},
|
},
|
||||||
@ -51,12 +41,13 @@ export class GlobalViewRootStore {
|
|||||||
id: "subscribed",
|
id: "subscribed",
|
||||||
name: "Subscribed",
|
name: "Subscribed",
|
||||||
filters: {
|
filters: {
|
||||||
subscriber: store.user?.currentUser?.id ? [store.user?.currentUser?.id] : [],
|
subscriber: store?.user?.currentUser?.id ? [store?.user?.currentUser?.id] : [],
|
||||||
},
|
},
|
||||||
is_local_view: true,
|
is_local_view: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const workspaceViewStoreDefaultViews: any[] = [
|
|
||||||
|
const workspacePublicDefaultViews: any[] = [
|
||||||
{
|
{
|
||||||
id: "all-issues",
|
id: "all-issues",
|
||||||
name: "All Issues",
|
name: "All Issues",
|
||||||
@ -65,41 +56,29 @@ export class GlobalViewRootStore {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
this.workspaceViewMeStore = new ViewRootStore(
|
this.workspacePrivateViewStore = new ViewRootStore(
|
||||||
this.store,
|
this.store,
|
||||||
new WorkspaceMeViewService(),
|
workspacePrivateDefaultViews,
|
||||||
workspaceViewMeStoreDefaultViews
|
new WorkspacePrivateViewService(),
|
||||||
|
new WorkspaceFiltersService()
|
||||||
);
|
);
|
||||||
this.workspaceViewStore = new ViewRootStore(this.store, new WorkspaceViewService(), workspaceViewStoreDefaultViews);
|
this.workspacePublicViewStore = new ViewRootStore(
|
||||||
this.projectViewStore = new ViewRootStore(this.store, new ProjectViewService());
|
this.store,
|
||||||
this.projectViewMeStore = new ViewRootStore(this.store, new ProjectViewMeService());
|
workspacePublicDefaultViews,
|
||||||
|
new WorkspacePublicViewService(),
|
||||||
// user views root
|
new WorkspaceFiltersService()
|
||||||
this.workspaceUserViewStore = new userViewRootStore(
|
);
|
||||||
new WorkspaceFiltersService(),
|
this.projectPrivateViewStore = new ViewRootStore(
|
||||||
store.app?.router?.workspaceSlug,
|
this.store,
|
||||||
undefined,
|
undefined,
|
||||||
undefined
|
new ProjectPrivateViewService(),
|
||||||
|
new ProjectFiltersService()
|
||||||
);
|
);
|
||||||
this.projectUserViewStore = new userViewRootStore(
|
this.projectPublicViewStore = new ViewRootStore(
|
||||||
new ProjectFiltersService(),
|
this.store,
|
||||||
store.app?.router?.workspaceSlug,
|
undefined,
|
||||||
store.app?.router?.projectId,
|
new ProjectPublicViewService(),
|
||||||
undefined
|
new ProjectFiltersService()
|
||||||
);
|
);
|
||||||
this.moduleUserViewStore = new userViewRootStore(
|
|
||||||
new ModuleFiltersService(),
|
|
||||||
store.app?.router?.workspaceSlug,
|
|
||||||
store.app?.router?.projectId,
|
|
||||||
store.app?.router?.moduleId
|
|
||||||
);
|
|
||||||
this.cycleUserViewStore = new userViewRootStore(
|
|
||||||
new CycleFiltersService(),
|
|
||||||
store.app?.router?.workspaceSlug,
|
|
||||||
store.app?.router?.projectId,
|
|
||||||
store.app?.router?.cycleId
|
|
||||||
);
|
|
||||||
// this.archivedUserViewStore = new userViewRootStore( new LocalStorageFiltersService());
|
|
||||||
// this.draftUserViewStore = new userViewRootStore( new LocalStorageFiltersService());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
|
||||||
import set from "lodash/set";
|
|
||||||
// stores
|
|
||||||
import { UserViewStore } from "./view.store";
|
|
||||||
// types
|
|
||||||
import { TUserViewService } from "services/view/types";
|
|
||||||
|
|
||||||
type TUserViewRootStore = {
|
|
||||||
// observables
|
|
||||||
viewMap: Record<string, UserViewStore>;
|
|
||||||
// computed
|
|
||||||
viewIds: string[];
|
|
||||||
// helper actions
|
|
||||||
viewById: (viewId: string) => UserViewStore | undefined;
|
|
||||||
// actions
|
|
||||||
fetch: () => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class userViewRootStore implements TUserViewRootStore {
|
|
||||||
// observables
|
|
||||||
viewMap: Record<string, UserViewStore> = {};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private service: TUserViewService,
|
|
||||||
private workspaceSlug: string | undefined,
|
|
||||||
private projectId: string | undefined,
|
|
||||||
private featureId: string | undefined // moduleId/cycleId
|
|
||||||
) {
|
|
||||||
makeObservable(this, {
|
|
||||||
// observables
|
|
||||||
viewMap: observable.ref,
|
|
||||||
// computed
|
|
||||||
viewIds: computed,
|
|
||||||
// actions
|
|
||||||
fetch: action,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// computed
|
|
||||||
get viewIds() {
|
|
||||||
return Object.keys(this.viewMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper actions
|
|
||||||
viewById = (viewId: string) => this.viewMap?.[viewId] || undefined;
|
|
||||||
|
|
||||||
// actions
|
|
||||||
fetch = async () => {
|
|
||||||
if (!this.workspaceSlug) return;
|
|
||||||
|
|
||||||
const view = await this.service.fetch(this.workspaceSlug, this.projectId, this.featureId);
|
|
||||||
if (!view) return;
|
|
||||||
|
|
||||||
// runInAction(() => {
|
|
||||||
// if (this.workspaceSlug && view.id)
|
|
||||||
// set(
|
|
||||||
// this.viewMap,
|
|
||||||
// [view.id],
|
|
||||||
// new UserViewStore(view, this.service, this.workspaceSlug, this.projectId, this.featureId)
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,327 +0,0 @@
|
|||||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
|
||||||
import set from "lodash/set";
|
|
||||||
// store
|
|
||||||
import { RootStore } from "store/root.store";
|
|
||||||
// types
|
|
||||||
import { TViewService } from "services/view/types";
|
|
||||||
import {
|
|
||||||
TView,
|
|
||||||
TViewFilters,
|
|
||||||
TViewDisplayFilters,
|
|
||||||
TViewDisplayProperties,
|
|
||||||
TViewFilterProps,
|
|
||||||
TViewAccess,
|
|
||||||
} from "@plane/types";
|
|
||||||
// helpers
|
|
||||||
import { FiltersHelper } from "../helpers/filters_helpers";
|
|
||||||
|
|
||||||
type TLoader = "submitting" | "submit" | undefined;
|
|
||||||
|
|
||||||
export type TUserViewStore = TView & {
|
|
||||||
// observables
|
|
||||||
loader: TLoader;
|
|
||||||
filtersToUpdate: Partial<TView>;
|
|
||||||
// computed
|
|
||||||
appliedFilters: TViewFilterProps | undefined;
|
|
||||||
appliedFiltersQueryParams: string | undefined;
|
|
||||||
// helper actions
|
|
||||||
setName: (name: string) => void;
|
|
||||||
setDescription: (description: string) => void;
|
|
||||||
setFilters: (filters: Partial<TViewFilters>) => void;
|
|
||||||
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
|
|
||||||
setDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) => void;
|
|
||||||
resetChanges: () => void;
|
|
||||||
saveChanges: () => Promise<void>;
|
|
||||||
// actions
|
|
||||||
lockView: () => Promise<void>;
|
|
||||||
unlockView: () => Promise<void>;
|
|
||||||
makeFavorite: () => Promise<void>;
|
|
||||||
removeFavorite: () => Promise<void>;
|
|
||||||
update: (viewData: Partial<TView>) => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class UserViewStore extends FiltersHelper implements TUserViewStore {
|
|
||||||
id: string | undefined;
|
|
||||||
workspace: string | undefined;
|
|
||||||
project: string | undefined;
|
|
||||||
name: string | undefined;
|
|
||||||
description: string | undefined;
|
|
||||||
query: string | undefined;
|
|
||||||
filters: TViewFilters;
|
|
||||||
display_filters: TViewDisplayFilters;
|
|
||||||
display_properties: TViewDisplayProperties;
|
|
||||||
access: TViewAccess | undefined;
|
|
||||||
owned_by: string | undefined;
|
|
||||||
sort_order: number | undefined;
|
|
||||||
is_locked: boolean = false;
|
|
||||||
is_pinned: boolean = false;
|
|
||||||
is_favorite: boolean = false;
|
|
||||||
created_by: string | undefined;
|
|
||||||
updated_by: string | undefined;
|
|
||||||
created_at: Date | undefined;
|
|
||||||
updated_at: Date | undefined;
|
|
||||||
is_local_view: boolean = false;
|
|
||||||
is_create: boolean = false;
|
|
||||||
is_editable: boolean = false;
|
|
||||||
loader: TLoader = undefined;
|
|
||||||
filtersToUpdate: Partial<TView> = {
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
filters: undefined,
|
|
||||||
display_filters: undefined,
|
|
||||||
display_properties: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(private store: RootStore, _view: TView, private service: TViewService) {
|
|
||||||
super();
|
|
||||||
this.id = _view.id;
|
|
||||||
this.workspace = _view.workspace;
|
|
||||||
this.project = _view.project;
|
|
||||||
this.name = _view.name;
|
|
||||||
this.description = _view.description;
|
|
||||||
this.query = _view.query;
|
|
||||||
this.filters = this.computedFilters(_view.filters);
|
|
||||||
this.display_filters = this.computedDisplayFilters(_view.display_filters);
|
|
||||||
this.display_properties = this.computedDisplayProperties(_view.display_properties);
|
|
||||||
this.access = _view.access;
|
|
||||||
this.owned_by = _view.owned_by;
|
|
||||||
this.sort_order = _view.sort_order;
|
|
||||||
this.is_locked = _view.is_locked;
|
|
||||||
this.is_pinned = _view.is_pinned;
|
|
||||||
this.is_favorite = _view.is_favorite;
|
|
||||||
this.created_by = _view.created_by;
|
|
||||||
this.updated_by = _view.updated_by;
|
|
||||||
this.created_at = _view.created_at;
|
|
||||||
this.updated_at = _view.updated_at;
|
|
||||||
|
|
||||||
this.is_local_view = _view.is_local_view;
|
|
||||||
this.is_create = _view.is_create;
|
|
||||||
this.is_editable = _view.is_editable;
|
|
||||||
|
|
||||||
makeObservable(this, {
|
|
||||||
// observables
|
|
||||||
id: observable.ref,
|
|
||||||
workspace: observable.ref,
|
|
||||||
project: observable.ref,
|
|
||||||
name: observable.ref,
|
|
||||||
description: observable.ref,
|
|
||||||
query: observable.ref,
|
|
||||||
filters: observable,
|
|
||||||
display_filters: observable,
|
|
||||||
display_properties: observable,
|
|
||||||
access: observable.ref,
|
|
||||||
owned_by: observable.ref,
|
|
||||||
sort_order: observable.ref,
|
|
||||||
is_locked: observable.ref,
|
|
||||||
is_pinned: observable.ref,
|
|
||||||
is_favorite: observable.ref,
|
|
||||||
created_by: observable.ref,
|
|
||||||
updated_by: observable.ref,
|
|
||||||
created_at: observable.ref,
|
|
||||||
updated_at: observable.ref,
|
|
||||||
is_local_view: observable.ref,
|
|
||||||
is_create: observable.ref,
|
|
||||||
is_editable: observable.ref,
|
|
||||||
loader: observable.ref,
|
|
||||||
filtersToUpdate: observable,
|
|
||||||
// computed
|
|
||||||
appliedFilters: computed,
|
|
||||||
appliedFiltersQueryParams: computed,
|
|
||||||
// helper actions
|
|
||||||
setName: action,
|
|
||||||
setFilters: action,
|
|
||||||
setDisplayFilters: action,
|
|
||||||
setDisplayProperties: action,
|
|
||||||
resetChanges: action,
|
|
||||||
saveChanges: action,
|
|
||||||
// actions
|
|
||||||
update: action,
|
|
||||||
lockView: action,
|
|
||||||
unlockView: action,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// computed
|
|
||||||
get appliedFilters() {
|
|
||||||
return {
|
|
||||||
filters: this.computedFilters(this.filters, this.filtersToUpdate.filters),
|
|
||||||
display_filters: this.computedDisplayFilters(this.display_filters, this.filtersToUpdate.display_filters),
|
|
||||||
display_properties: this.computedDisplayProperties(
|
|
||||||
this.display_properties,
|
|
||||||
this.filtersToUpdate.display_properties
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get appliedFiltersQueryParams() {
|
|
||||||
const filters = this.appliedFilters;
|
|
||||||
if (!filters) return undefined;
|
|
||||||
return this.computeAppliedFiltersQueryParameters(filters, [])?.query || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper actions
|
|
||||||
setName = (name: string) => {
|
|
||||||
runInAction(() => {
|
|
||||||
this.filtersToUpdate.name = name;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
setDescription = (description: string) => {
|
|
||||||
runInAction(() => {
|
|
||||||
this.filtersToUpdate.description = description;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
setFilters = (filters: Partial<TViewFilters>) => {
|
|
||||||
runInAction(() => {
|
|
||||||
this.loader = "submit";
|
|
||||||
Object.keys(filters).forEach((key) => {
|
|
||||||
const _key = key as keyof TViewFilters;
|
|
||||||
set(this.filtersToUpdate, ["filters", _key], filters[_key]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
setDisplayFilters = async (display_filters: Partial<TViewDisplayFilters>) => {
|
|
||||||
const appliedFilters = this.appliedFilters;
|
|
||||||
|
|
||||||
const layout = appliedFilters?.display_filters?.layout;
|
|
||||||
const sub_group_by = appliedFilters?.display_filters?.sub_group_by;
|
|
||||||
const group_by = appliedFilters?.display_filters?.group_by;
|
|
||||||
const sub_issue = appliedFilters?.display_filters?.sub_issue;
|
|
||||||
|
|
||||||
if (group_by === undefined && display_filters.sub_group_by) display_filters.sub_group_by = undefined;
|
|
||||||
if (layout === "kanban") {
|
|
||||||
if (sub_group_by === group_by) display_filters.group_by = undefined;
|
|
||||||
if (group_by === null) display_filters.group_by = "state";
|
|
||||||
}
|
|
||||||
if (layout === "spreadsheet" && sub_issue === true) display_filters.sub_issue = false;
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
Object.keys(display_filters).forEach((key) => {
|
|
||||||
const _key = key as keyof TViewDisplayFilters;
|
|
||||||
set(this.filtersToUpdate, ["display_filters", _key], display_filters[_key]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
setDisplayProperties = async (display_properties: Partial<TViewDisplayProperties>) => {
|
|
||||||
runInAction(() => {
|
|
||||||
Object.keys(display_properties).forEach((key) => {
|
|
||||||
const _key = key as keyof TViewDisplayProperties;
|
|
||||||
set(this.filtersToUpdate, ["display_properties", _key], display_properties[_key]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
resetChanges = () => {
|
|
||||||
runInAction(() => {
|
|
||||||
this.loader = undefined;
|
|
||||||
this.filtersToUpdate = {
|
|
||||||
name: this.name,
|
|
||||||
description: this.description,
|
|
||||||
filters: this.filters,
|
|
||||||
display_filters: this.display_filters,
|
|
||||||
display_properties: this.display_properties,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
saveChanges = async () => {
|
|
||||||
try {
|
|
||||||
this.loader = "submitting";
|
|
||||||
if (this.filtersToUpdate) await this.update(this.filtersToUpdate);
|
|
||||||
this.loader = undefined;
|
|
||||||
} catch {
|
|
||||||
this.loader = undefined;
|
|
||||||
Object.keys(this.filtersToUpdate).forEach((key) => {
|
|
||||||
const _key = key as keyof TView;
|
|
||||||
set(this, _key, this.filtersToUpdate[_key]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// actions
|
|
||||||
lockView = async () => {
|
|
||||||
try {
|
|
||||||
const { workspaceSlug, projectId } = this.store.app.router;
|
|
||||||
if (!workspaceSlug || !this.id || !this.service.lock) return;
|
|
||||||
|
|
||||||
const view = await this.service.lock(workspaceSlug, this.id, projectId);
|
|
||||||
if (!view) return;
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.is_locked = view.is_locked;
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
this.is_locked = this.is_locked;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
unlockView = async () => {
|
|
||||||
try {
|
|
||||||
const { workspaceSlug, projectId } = this.store.app.router;
|
|
||||||
if (!workspaceSlug || !this.id || !this.service.unlock) return;
|
|
||||||
|
|
||||||
const view = await this.service.unlock(workspaceSlug, this.id, projectId);
|
|
||||||
if (!view) return;
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.is_locked = view.is_locked;
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
this.is_locked = this.is_locked;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
makeFavorite = async () => {
|
|
||||||
try {
|
|
||||||
const { workspaceSlug, projectId } = this.store.app.router;
|
|
||||||
if (!workspaceSlug || !this.id || !this.service.makeFavorite) return;
|
|
||||||
|
|
||||||
const view = await this.service.makeFavorite(workspaceSlug, this.id, projectId);
|
|
||||||
if (!view) return;
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.is_favorite = view.is_locked;
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
this.is_favorite = this.is_favorite;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
removeFavorite = async () => {
|
|
||||||
try {
|
|
||||||
const { workspaceSlug, projectId } = this.store.app.router;
|
|
||||||
if (!workspaceSlug || !this.id || !this.service.removeFavorite) return;
|
|
||||||
|
|
||||||
const view = await this.service.removeFavorite(workspaceSlug, this.id, projectId);
|
|
||||||
if (!view) return;
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.is_favorite = view.is_locked;
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
this.is_favorite = this.is_favorite;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
update = async (viewData: Partial<TView>) => {
|
|
||||||
try {
|
|
||||||
const { workspaceSlug, projectId } = this.store.app.router;
|
|
||||||
if (!workspaceSlug || !this.id) return;
|
|
||||||
|
|
||||||
const view = await this.service.update(workspaceSlug, this.id, viewData, projectId);
|
|
||||||
if (!view) return;
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
Object.keys(viewData).forEach((key) => {
|
|
||||||
const _key = key as keyof TView;
|
|
||||||
set(this, _key, viewData[_key]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
this.resetChanges();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,4 +1,5 @@
|
|||||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||||
|
import { computedFn } from "mobx-utils";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
import sortBy from "lodash/sortBy";
|
import sortBy from "lodash/sortBy";
|
||||||
import reverse from "lodash/reverse";
|
import reverse from "lodash/reverse";
|
||||||
@ -6,7 +7,7 @@ import reverse from "lodash/reverse";
|
|||||||
import { RootStore } from "store/root.store";
|
import { RootStore } from "store/root.store";
|
||||||
import { ViewStore } from "./view.store";
|
import { ViewStore } from "./view.store";
|
||||||
// types
|
// types
|
||||||
import { TViewService } from "services/view/types";
|
import { TUserViewService, TViewService } from "services/view/types";
|
||||||
import { TView } from "@plane/types";
|
import { TView } from "@plane/types";
|
||||||
|
|
||||||
export type TLoader = "init-loader" | "mutation-loader" | "submitting" | undefined;
|
export type TLoader = "init-loader" | "mutation-loader" | "submitting" | undefined;
|
||||||
@ -17,11 +18,11 @@ type TViewRootStore = {
|
|||||||
viewMap: Record<string, ViewStore>;
|
viewMap: Record<string, ViewStore>;
|
||||||
// computed
|
// computed
|
||||||
viewIds: string[];
|
viewIds: string[];
|
||||||
// helper actions
|
|
||||||
viewById: (viewId: string) => ViewStore | undefined;
|
viewById: (viewId: string) => ViewStore | undefined;
|
||||||
// actions
|
// actions
|
||||||
localViewCreate: (view: TView) => Promise<void>;
|
localViewCreate: (view: TView) => Promise<void>;
|
||||||
fetch: (_loader?: TLoader) => Promise<void>;
|
fetch: (_loader?: TLoader) => Promise<void>;
|
||||||
|
fetchById: (viewId: string) => Promise<void>;
|
||||||
create: (view: Partial<TView>) => Promise<void>;
|
create: (view: Partial<TView>) => Promise<void>;
|
||||||
remove: (viewId: string) => Promise<void>;
|
remove: (viewId: string) => Promise<void>;
|
||||||
duplicate: (viewId: string) => Promise<void>;
|
duplicate: (viewId: string) => Promise<void>;
|
||||||
@ -32,7 +33,12 @@ export class ViewRootStore implements TViewRootStore {
|
|||||||
loader: TLoader = "init-loader";
|
loader: TLoader = "init-loader";
|
||||||
viewMap: Record<string, ViewStore> = {};
|
viewMap: Record<string, ViewStore> = {};
|
||||||
|
|
||||||
constructor(private store: RootStore, private service: TViewService, private defaultViews: TView[] = []) {
|
constructor(
|
||||||
|
private store: RootStore,
|
||||||
|
private defaultViews: TView[] = [],
|
||||||
|
private service: TViewService,
|
||||||
|
private userService: TUserViewService
|
||||||
|
) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// observables
|
// observables
|
||||||
loader: observable.ref,
|
loader: observable.ref,
|
||||||
@ -42,6 +48,7 @@ export class ViewRootStore implements TViewRootStore {
|
|||||||
// actions
|
// actions
|
||||||
localViewCreate: action,
|
localViewCreate: action,
|
||||||
fetch: action,
|
fetch: action,
|
||||||
|
fetchById: action,
|
||||||
create: action,
|
create: action,
|
||||||
remove: action,
|
remove: action,
|
||||||
duplicate: action,
|
duplicate: action,
|
||||||
@ -54,19 +61,16 @@ export class ViewRootStore implements TViewRootStore {
|
|||||||
const localViews = views.filter((view) => view.is_local_view);
|
const localViews = views.filter((view) => view.is_local_view);
|
||||||
let apiViews = views.filter((view) => !view.is_local_view && !view.is_create);
|
let apiViews = views.filter((view) => !view.is_local_view && !view.is_create);
|
||||||
apiViews = reverse(sortBy(apiViews, "sort_order"));
|
apiViews = reverse(sortBy(apiViews, "sort_order"));
|
||||||
|
|
||||||
const _viewIds = [...localViews.map((view) => view.id), ...apiViews.map((view) => view.id)];
|
const _viewIds = [...localViews.map((view) => view.id), ...apiViews.map((view) => view.id)];
|
||||||
|
|
||||||
return _viewIds as string[];
|
return _viewIds as string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper actions
|
viewById = computedFn((viewId: string) => this.viewMap?.[viewId] || undefined);
|
||||||
viewById = (viewId: string) => this.viewMap?.[viewId] || undefined;
|
|
||||||
|
|
||||||
// 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));
|
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -75,26 +79,57 @@ export class ViewRootStore implements TViewRootStore {
|
|||||||
const { workspaceSlug, projectId } = this.store.app.router;
|
const { workspaceSlug, projectId } = this.store.app.router;
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
|
this.loader = _loader;
|
||||||
|
|
||||||
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));
|
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.loader = _loader;
|
|
||||||
const views = await this.service.fetch(workspaceSlug, projectId);
|
const views = await this.service.fetch(workspaceSlug, projectId);
|
||||||
if (!views) return;
|
if (!views) return;
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
views.forEach((view) => {
|
views.forEach((view) => {
|
||||||
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service));
|
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||||
});
|
});
|
||||||
this.loader = undefined;
|
this.loader = undefined;
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fetchById = async (viewId: string) => {
|
||||||
|
try {
|
||||||
|
const { workspaceSlug, projectId } = this.store.app.router;
|
||||||
|
if (!workspaceSlug || !viewId) return;
|
||||||
|
|
||||||
|
const userView = await this.userService.fetch(workspaceSlug, projectId);
|
||||||
|
if (!userView) return;
|
||||||
|
|
||||||
|
if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) {
|
||||||
|
const view = { ...this.viewById(viewId) };
|
||||||
|
if (!view) return;
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
view.display_filters = userView.display_filters;
|
||||||
|
view.display_properties = userView.display_properties;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const view = await this.service.fetchById(workspaceSlug, viewId, projectId);
|
||||||
|
if (!view) return;
|
||||||
|
|
||||||
|
view?.display_filters && (view.display_filters = userView.display_filters);
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
create = async (data: Partial<TView>) => {
|
create = async (data: Partial<TView>) => {
|
||||||
try {
|
try {
|
||||||
const { workspaceSlug, projectId } = this.store.app.router;
|
const { workspaceSlug, projectId } = this.store.app.router;
|
||||||
@ -104,7 +139,7 @@ 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));
|
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.id) this.remove(data.id);
|
if (data.id) this.remove(data.id);
|
||||||
@ -134,7 +169,7 @@ 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));
|
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import set from "lodash/set";
|
|||||||
// store
|
// store
|
||||||
import { RootStore } from "store/root.store";
|
import { RootStore } from "store/root.store";
|
||||||
// types
|
// types
|
||||||
import { TViewService } from "services/view/types";
|
import { TUserViewService, TViewService } from "services/view/types";
|
||||||
import {
|
import {
|
||||||
TView,
|
TView,
|
||||||
TViewFilters,
|
TViewFilters,
|
||||||
@ -72,7 +72,12 @@ export class ViewStore extends FiltersHelper implements TViewStore {
|
|||||||
display_properties: undefined,
|
display_properties: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(private store: RootStore, _view: TView, private service: TViewService) {
|
constructor(
|
||||||
|
private store: RootStore,
|
||||||
|
_view: TView,
|
||||||
|
private service: TViewService,
|
||||||
|
private userService: TUserViewService
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
this.id = _view.id;
|
this.id = _view.id;
|
||||||
this.workspace = _view.workspace;
|
this.workspace = _view.workspace;
|
||||||
|
Loading…
Reference in New Issue
Block a user