fix: view compoennt and store updates

This commit is contained in:
gurusainath 2024-02-05 20:09:17 +05:30
parent 6325f97c8e
commit cd81ec1002
29 changed files with 1035 additions and 337 deletions

View File

@ -7,7 +7,6 @@ import {
export type TViewTypes = export type TViewTypes =
| "WORKSPACE_YOUR_VIEWS" | "WORKSPACE_YOUR_VIEWS"
| "WORKSPACE_VIEWS" | "WORKSPACE_VIEWS"
| "WORKSPACE_PROJECT_VIEWS"
| "PROJECT_VIEWS" | "PROJECT_VIEWS"
| "PROJECT_YOUR_VIEWS"; | "PROJECT_YOUR_VIEWS";
@ -29,17 +28,20 @@ export type TView = {
name: string | undefined; name: string | undefined;
description: string | undefined; description: string | undefined;
query: string | undefined; query: string | undefined;
filters: TViewFilters | undefined; filters: TViewFilters;
display_filters: TViewDisplayFilters | undefined; display_filters: TViewDisplayFilters;
display_properties: TViewDisplayProperties | undefined; display_properties: TViewDisplayProperties;
access: TViewAccess | undefined; access: TViewAccess | undefined;
owned_by: string | undefined; owned_by: string | undefined;
sort_order: number | undefined; sort_order: number | undefined;
is_locked: boolean | undefined; is_locked: boolean;
is_pinned: boolean | undefined; is_pinned: boolean;
is_favorite: boolean | undefined; is_favorite: boolean;
created_by: string | undefined; created_by: string | undefined;
updated_by: string | undefined; updated_by: string | undefined;
created_at: Date | undefined; created_at: Date | undefined;
updated_at: Date | undefined; updated_at: Date | undefined;
// local view variables
is_local_view: boolean;
is_create: boolean;
}; };

View File

@ -5,10 +5,53 @@ export type TViewLayouts =
| "spreadsheet" | "spreadsheet"
| "gantt"; | "gantt";
export type TViewDisplayFiltersGrouped =
| "project"
| "state_detail.group"
| "state"
| "priority"
| "labels"
| "created_by"
| "assignees"
| "mentions"
| "modules"
| "cycles";
export type TViewDisplayFiltersOrderBy =
| "sort_order"
| "created_at"
| "-created_at"
| "updated_at"
| "-updated_at"
| "start_date"
| "-start_date"
| "target_date"
| "-target_date"
| "state__name"
| "-state__name"
| "priority"
| "-priority"
| "labels__name"
| "-labels__name"
| "assignees__first_name"
| "-assignees__first_name"
| "estimate_point"
| "-estimate_point"
| "link_count"
| "-link_count"
| "attachment_count"
| "-attachment_count"
| "sub_issues_count"
| "-sub_issues_count";
export type TViewDisplayFiltersType = "active" | "backlog";
export type TViewCalendarLayouts = "month" | "week"; export type TViewCalendarLayouts = "month" | "week";
export type TViewFilters = { export type TViewFilters = {
project: string[]; project: string[];
module: string[];
cycle: string[];
priority: string[]; priority: string[];
state: string[]; state: string[];
state_group: string[]; state_group: string[];
@ -23,10 +66,10 @@ export type TViewFilters = {
export type TViewDisplayFilters = { export type TViewDisplayFilters = {
layout: TViewLayouts; layout: TViewLayouts;
group_by: string | undefined; group_by: TViewDisplayFiltersGrouped | undefined;
sub_group_by: string | undefined; sub_group_by: TViewDisplayFiltersGrouped | undefined;
order_by: string; order_by: TViewDisplayFiltersOrderBy | string;
type: string | undefined; type: TViewDisplayFiltersType | undefined;
sub_issue: boolean; sub_issue: boolean;
show_empty_groups: boolean; show_empty_groups: boolean;
calendar: { calendar: {
@ -52,19 +95,21 @@ export type TViewDisplayProperties = {
}; };
export type TViewFilterProps = { export type TViewFilterProps = {
filters: TViewFilters | undefined; filters: TViewFilters;
display_filters: TViewDisplayFilters | undefined; display_filters: TViewDisplayFilters;
display_properties: TViewDisplayProperties | undefined; display_properties: TViewDisplayProperties;
}; };
export type TViewFilterPartialProps = { export type TViewFilterPartialProps = {
filters: Partial<TViewFilters> | undefined; filters: Partial<TViewFilters>;
display_filters: Partial<TViewDisplayFilters> | undefined; display_filters: Partial<TViewDisplayFilters>;
display_properties: Partial<TViewDisplayProperties> | undefined; display_properties: Partial<TViewDisplayProperties>;
}; };
export type TViewFilterQueryParams = export type TViewFilterQueryParams =
| "project" | "project"
| "module"
| "cycle"
| "priority" | "priority"
| "state" | "state"
| "state_group" | "state_group"

View File

@ -1,4 +1,4 @@
import { FC, } from "react"; import { FC, useMemo } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// components // components
import { IssuePeekOverview } from "components/issues"; import { IssuePeekOverview } from "components/issues";

View File

@ -0,0 +1,166 @@
import { FC, useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react-lite";
import { CheckCircle } from "lucide-react";
// hooks
import { useView, useViewDetail } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { ViewRoot, ViewCreateEdit, ViewFiltersRoot, ViewAppliedFiltersRoot, ViewLayoutRoot } from ".";
// ui
import { Spinner } from "@plane/ui";
// constants
import { VIEW_TYPES } from "constants/view";
// types
import { TViewOperations } from "./types";
import { TView, TViewFilters, TViewDisplayFilters, TViewDisplayProperties } from "@plane/types";
type TAllIssuesViewRoot = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string;
};
export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId } = props;
// states
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
const viewStore = useView(workspaceSlug, projectId, viewType);
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const { setToastAlert } = useToast();
const viewOperations: TViewOperations = useMemo(
() => ({
localViewCreate: (data) => viewStore?.localViewCreate(data),
clearLocalView: (viewId: string) => viewStore?.clearLocalView(viewId),
setFilters: (filters: Partial<TViewFilters>) => viewDetailStore?.setFilters(filters),
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) =>
viewDetailStore?.setDisplayFilters(display_filters),
setDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) =>
viewDetailStore?.setDisplayProperties(display_properties),
fetch: async () => await viewStore?.fetch(),
create: async (data: Partial<TView>) => {
try {
await viewStore?.create(data);
if (data.id) viewOperations.clearLocalView(data.id);
} catch {
setToastAlert({ title: "Error", message: "Error creating view", type: "error" });
}
},
}),
[viewStore, viewDetailStore, setToastAlert]
);
useEffect(() => {
if (workspaceSlug && viewId && viewType && viewStore)
viewStore?.fetch(viewStore?.viewIds.length > 0 ? "mutation-loader" : "init-loader");
}, [workspaceSlug, viewId, viewType, viewStore]);
return (
<div className="relative w-full h-full">
<div className="relative flex justify-between items-center gap-2 px-5 py-4">
<div className="relative flex items-center gap-2">
<div className="flex-shrink-0 w-6 h-6 rounded relative flex justify-center items-center bg-custom-background-80">
<CheckCircle size={12} />
</div>
<div className="font-medium">All Issues</div>
</div>
<div className="relative inline-flex items-center rounded border border-custom-border-300 bg-custom-background-80">
{workspaceViewTabOptions.map((tab) => (
<div
key={tab.key}
className={`p-4 py-1.5 rounded text-sm transition-all cursor-pointer font-medium
${
viewType === tab.key
? "text-custom-text-100 bg-custom-background-90"
: "text-custom-text-200 bg-custom-background-80 hover:text-custom-text-100"
}`}
onClick={tab.onClick}
>
{tab.title}
</div>
))}
</div>
</div>
{viewStore?.loader && viewStore?.loader === "init-loader" ? (
<div className="relative w-full h-full flex justify-center items-center">
<Spinner />
</div>
) : (
<>
<ViewRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewOperations={viewOperations}
/>
{/* <ViewFiltersRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewOperations={viewOperations}
/> */}
<div className="p-5 border-b border-custom-border-200 relative flex gap-2">
<div className="w-full">
<ViewAppliedFiltersRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewOperations={viewOperations}
/>
</div>
<div className="flex-shrink-0 h-full">
<ViewLayoutRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewOperations={viewOperations}
/>
</div>
<div className="flex-shrink-0 relative w-7 h-7 overflow-hidden border border-red-500 rounded flex justify-center items-center">
Filters
</div>
<div className="flex-shrink-0 relative w-7 h-7 overflow-hidden border border-red-500 rounded flex justify-center items-center">
Display Filters
</div>
{!viewDetailStore?.is_local_view && (
<div className="flex-shrink-0 h-full">
<ViewCreateEdit
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewOperations={viewOperations}
>
<div>Edit</div>
</ViewCreateEdit>
</div>
)}
</div>
</>
)}
</div>
);
});

View File

@ -1,17 +1,49 @@
import { FC } from "react"; import { FC } from "react";
import { User, X } from "lucide-react";
// hooks
import { useViewDetail } from "hooks/store";
// types
import { TViewFilters, TViewTypes } from "@plane/types";
type TViewFiltersItem = { type TViewAppliedFiltersItem = {
workspaceSlug: string; workspaceSlug: string;
projectId: string | undefined; projectId: string | undefined;
viewId: string | undefined; viewId: string;
viewType: TViewTypes;
filterKey: keyof TViewFilters;
filterId: string;
}; };
export const ViewFiltersItem: FC<TViewFiltersItem> = (props) => { export const ViewAppliedFiltersItem: FC<TViewAppliedFiltersItem> = (props) => {
const { workspaceSlug, projectId, viewId } = props; const { workspaceSlug, projectId, viewId, viewType, filterKey, filterId } = props;
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const removeFilterOption = () => {
const filters = viewDetailStore?.appliedFilters?.filters;
if (!filters) return;
const filterValues = filters[filterKey];
const updatedFilterValues = filterValues.filter((value) => value !== filterId);
viewDetailStore?.setFilters({ [filterKey]: updatedFilterValues });
};
return ( return (
<div> <div
<div>ViewFiltersItem</div> key={`filter_value_${filterKey}_${filterId}`}
className="border border-custom-border-200 rounded relative flex items-center gap-1 px-1 py-0.5"
>
<div className="flex-shrink-0 w-4 h-4 relative flex justify-center items-center overflow-hidden">
<User size={12} />
</div>
<div className="text-xs">
{filterKey} - {filterId}
</div>
<div
className="flex-shrink-0 w-3.5 h-3.5 relative flex justify-center items-center overflow-hidden rounded-full transition-all cursor-pointer bg-custom-background-80 hover:bg-custom-background-90 text-custom-text-300 hover:text-custom-text-200"
onClick={removeFilterOption}
>
<X size={10} />
</div>
</div> </div>
); );
}; };

View File

@ -4,46 +4,56 @@ import isEmpty from "lodash/isEmpty";
import { X } from "lucide-react"; import { X } from "lucide-react";
// hooks // hooks
import { useViewDetail } from "hooks/store"; import { useViewDetail } from "hooks/store";
// components
import { ViewAppliedFiltersItem } from "./filter-item";
// helpers // helpers
import { generateTitle } from "./helper"; import { generateTitle } from "./helper";
// types // types
import { TFilters } from "@plane/types"; import { TViewFilters, TViewTypes } from "@plane/types";
type TViewAppliedFilters = { type TViewAppliedFilters = {
workspaceSlug: string; workspaceSlug: string;
projectId: string | undefined; projectId: string | undefined;
viewId: string; viewId: string;
filterKey: keyof TFilters; viewType: TViewTypes;
filterKey: keyof TViewFilters;
}; };
export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => { export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => {
const { workspaceSlug, projectId, viewId, filterKey } = props; const { workspaceSlug, projectId, viewId, viewType, filterKey } = props;
const view = useViewDetail("WORKSPACE", workspaceSlug, projectId, viewId); const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const filterKeyValue = const filterKeyValue =
view?.appliedFilters?.filters && !isEmpty(view?.appliedFilters?.filters) viewDetailStore?.appliedFilters?.filters && !isEmpty(viewDetailStore?.appliedFilters?.filters)
? view?.appliedFilters?.filters?.[filterKey] || undefined ? viewDetailStore?.appliedFilters?.filters?.[filterKey] || undefined
: undefined; : undefined;
if (!filterKeyValue || filterKeyValue.length <= 0) return <></>; const clearFilter = () => viewDetailStore?.setFilters({ [filterKey]: [] });
return (
<div key={filterKey} className="relative flex items-center gap-2 border border-custom-border-300 rounded p-1.5">
<div className="flex-shrink-0 text-xs">{generateTitle(filterKey)}</div>
<div className="border border-red-500 relative flex items-center">
{/* <div className="relative flex items-center">
<div>Icon</div>
<div>Title</div>
<div>Close</div>
</div>
<div> if (!filterKeyValue || filterKeyValue.length <= -1) return <></>;
<div>Icon</div> return (
<div>Title</div> <div
<div>Close</div> key={filterKey}
</div> */} className="relative flex items-center gap-2 border border-custom-border-300 rounded p-1.5 py-1 min-h-[32px]"
>
<div className="flex-shrink-0 text-xs text-custom-text-200">{generateTitle(filterKey)}</div>
<div className="relative flex items-center gap-1 flex-wrap">
{["1", "2", "3", "4"].map((filterId) => (
<ViewAppliedFiltersItem
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
filterKey={filterKey}
filterId={filterId}
/>
))}
</div> </div>
<div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4 rounded-full cursor-pointer transition-all bg-custom-background-80 hover:bg-custom-background-90 text-custom-text-300 hover:text-custom-text-200"> <div
className="flex-shrink-0 relative flex justify-center items-center w-4 h-4 rounded-full cursor-pointer transition-all bg-custom-background-80 hover:bg-custom-background-90 text-custom-text-300 hover:text-custom-text-200"
onClick={clearFilter}
>
<X size={10} /> <X size={10} />
</div> </div>
</div> </div>

View File

@ -1,45 +1,66 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { X } from "lucide-react";
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
// hooks // hooks
import { useViewDetail } from "hooks/store"; import { useViewDetail } from "hooks/store";
// components // components
import { ViewAppliedFilters } from "./filter"; import { ViewAppliedFilters } from "./filter";
// types // types
import { TFilters } from "@plane/types"; import { TViewTypes, TViewFilters } from "@plane/types";
import { TViewOperations } from "../types"; import { TViewOperations } from "../types";
type TViewAppliedFiltersRoot = { type TViewAppliedFiltersRoot = {
workspaceSlug: string; workspaceSlug: string;
projectId: string | undefined; projectId: string | undefined;
viewId: string; viewId: string;
viewType: TViewTypes;
viewOperations: TViewOperations; viewOperations: TViewOperations;
}; };
export const ViewAppliedFiltersRoot: FC<TViewAppliedFiltersRoot> = observer((props) => { export const ViewAppliedFiltersRoot: FC<TViewAppliedFiltersRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId } = props; const { workspaceSlug, projectId, viewId, viewType } = props;
// hooks // hooks
const view = useViewDetail("WORKSPACE", workspaceSlug, projectId, viewId); const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const filterKeys = const filterKeys =
view?.appliedFilters && !isEmpty(view?.appliedFilters?.filters) viewDetailStore?.appliedFilters && !isEmpty(viewDetailStore?.appliedFilters?.filters)
? Object.keys(view?.appliedFilters?.filters) ? Object.keys(viewDetailStore?.appliedFilters?.filters)
: undefined; : undefined;
const clearAllFilters = () => {
const clearedFilters: Partial<Record<keyof TViewFilters, string[]>> = {};
filterKeys?.forEach((key) => {
const _key = key as keyof TViewFilters;
clearedFilters[_key] = [];
});
viewDetailStore?.setFilters(clearedFilters);
};
if (!filterKeys) return <></>; if (!filterKeys) return <></>;
return ( return (
<div className="relative flex items-center gap-2 flex-wrap border border-red-500 p-4"> <div className="relative flex items-center gap-2 flex-wrap">
{filterKeys.map((key) => { {filterKeys.map((key) => {
const filterKey = key as keyof TFilters; const filterKey = key as keyof TViewFilters;
return ( return (
<ViewAppliedFilters <ViewAppliedFilters
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
viewId={viewId} viewId={viewId}
viewType={viewType}
filterKey={filterKey} filterKey={filterKey}
/> />
); );
})} })}
<div
className="relative flex items-center gap-2 border border-custom-border-300 rounded p-1.5 py-1 cursor-pointer transition-all hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100 min-h-[32px]"
onClick={clearAllFilters}
>
<div className="text-xs">Clear All</div>
<div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4">
<X size={10} />
</div>
</div>
</div> </div>
); );
}); });

View File

@ -1,10 +1,14 @@
export * from "./root"; export * from "./all-issues-root";
// views // views
export * from "./views/root"; export * from "./views/root";
export * from "./views/view-item";
export * from "./views/create-edit"; export * from "./views/create-edit";
export * from "./views/create-edit-form"; export * from "./views/create-edit-form";
// layouts
export * from "./layout";
// view filters // view filters
export * from "./filters/root"; export * from "./filters/root";

View File

@ -0,0 +1,54 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
import { LucideIcon, List, Kanban, Calendar, Sheet, GanttChartSquare } from "lucide-react";
// hooks
import { useViewDetail } from "hooks/store";
// ui
import { Tooltip } from "@plane/ui";
// types
import { TViewTypes } from "@plane/types";
import { TViewOperations } from "./types";
type TViewLayoutRoot = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string;
viewType: TViewTypes;
viewOperations: TViewOperations;
};
const LAYOUTS_DATA: { key: string; title: string; icon: LucideIcon }[] = [
{ key: "list", title: "List Layout", icon: List },
{ key: "kanban", title: "Kanban Layout", icon: Kanban },
{ key: "calendar", title: "Calendar Layout", icon: Calendar },
{ key: "spreadsheet", title: "Spreadsheet Layout", icon: Sheet },
{ key: "gantt", title: "Gantt Chart layout", icon: GanttChartSquare },
];
export const ViewLayoutRoot: FC<TViewLayoutRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
return (
<div className="relative flex gap-0.5 items-center bg-custom-background-80 rounded-md p-1">
{LAYOUTS_DATA.map((layout) => (
<Tooltip tooltipContent={layout.title} position="bottom">
<div
key={layout.key}
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
? `bg-custom-background-100 shadow-custom-shadow-2xs`
: `hover:bg-custom-background-100`
}
`}
onClick={() => viewOperations.setDisplayFilters({ layout: layout.key })}
>
<layout.icon size={12} />
</div>
</Tooltip>
))}
</div>
);
});

View File

@ -1,77 +0,0 @@
import { FC, ReactNode, useEffect, useMemo } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useView } from "hooks/store/use-view";
// components
import { ViewRoot, ViewCreateEdit, ViewFiltersRoot, ViewAppliedFiltersRoot } from "./";
// types
import { TViewOperations } from "./types";
import { TViewTypes } from "@plane/types";
type TWorkspaceViewRoot = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string | undefined;
viewType: TViewTypes;
};
export const WorkspaceViewRoot: FC<TWorkspaceViewRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType } = props;
// hooks
const views = useView(workspaceSlug, projectId, viewType);
const viewOperations: TViewOperations = useMemo(
() => ({
create: async (data) => {
await views?.create(data);
},
fetch: async () => {
await views?.fetch();
},
}),
[views]
);
useEffect(() => {
if (workspaceSlug && viewId && viewOperations) viewOperations.fetch();
}, [workspaceSlug, viewId, viewOperations]);
console.log("views?.viewMap", Object.keys(views?.viewMap).length);
Object.keys(views?.viewMap).map((viewId) => {
console.log(views?.viewMap?.[viewId]?.access);
});
return (
<div className="relative w-full h-full border border-red-500">
<ViewCreateEdit
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewOperations={viewOperations}
/>
{/* <ViewRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewOperations={viewOperations}
/> */}
{/* <ViewFiltersRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={"61d6b507-ae5c-45d6-b169-da7162f016a0"}
viewOperations={viewOperations}
/>
<ViewAppliedFiltersRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={"61d6b507-ae5c-45d6-b169-da7162f016a0"}
viewOperations={viewOperations}
/> */}
</div>
);
});

View File

@ -1,6 +1,11 @@
import { TView } from "@plane/types"; import { TView } from "@plane/types";
export type TViewOperations = { export type TViewOperations = {
create: (data: Partial<TView>) => void; localViewCreate: (data: TView) => void;
fetch: () => void; clearLocalView: (viewId: string) => void;
setFilters: (filters: Partial<TViewFilters>) => void;
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
setDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) => void;
fetch: () => Promise<void>;
create: (data: Partial<TView>) => Promise<void>;
}; };

View File

@ -1,23 +1,40 @@
import { FC, Fragment } from "react"; import { FC, Fragment } from "react";
import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { Trash2, Plus, X } from "lucide-react"; import { Trash2, Plus, X } from "lucide-react";
// hooks
import { useViewDetail } from "hooks/store";
// components
import { ViewAppliedFiltersRoot } from "../";
// ui // ui
import { Input, Button } from "@plane/ui"; import { Input, Button } from "@plane/ui";
// types // types
import { TView, TViewTypes } from "@plane/types";
import { TViewOperations } from "../types"; import { TViewOperations } from "../types";
type TViewCreateEditForm = { type TViewCreateEditForm = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string;
viewType: TViewTypes;
viewOperations: TViewOperations;
modalToggle: boolean; modalToggle: boolean;
handleModalClose: () => void; handleModalClose: () => void;
viewOperations?: TViewOperations; onSubmit: (viewData: Partial<TView>) => void;
}; };
export const ViewCreateEditForm: FC<TViewCreateEditForm> = (props) => { export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
const { modalToggle, handleModalClose, viewOperations } = props; const { workspaceSlug, projectId, viewId, viewType, viewOperations, modalToggle, handleModalClose, onSubmit } = props;
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const createView = () => { const onContinue = async () => {
viewOperations?.create({ name: "create" }); const payload: Partial<TView> = {
id: viewDetailStore?.id,
name: viewDetailStore?.name,
filters: viewDetailStore?.filters,
};
onSubmit(payload);
}; };
return ( return (
@ -48,23 +65,24 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = (props) => {
> >
<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"> <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"> <div className="p-3 px-5 relative flex items-center gap-2">
<div className="relative rounded p-1.5 px-2 flex items-center gap-1 border border-custom-border-100 bg-custom-background-80"> {/* <div className="relative rounded p-1.5 px-2 flex items-center gap-1 border border-custom-border-100 bg-custom-background-80">
<div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4 overflow-hidden"> <div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4 overflow-hidden">
<Trash2 className="w-3.5 h-3.5" /> <Trash2 className="w-3.5 h-3.5" />
</div> </div>
<div className="text-xs uppercase">Project Identifier</div> <div className="text-xs uppercase">Project Identifier</div>
</div> </div> */}
<div className="">Create|Edit View</div> <div className="">Create View</div>
</div> </div>
<div className="p-3 px-5"> <div className="p-3 px-5">
<Input <Input
id="email" id="name"
name="email" name="name"
type="email" type="text"
// value={value} value={viewDetailStore?.name || ""}
// onChange={onChange} onChange={(e) => {
// hasError={Boolean(errors.email)} viewDetailStore?.setName(e.target.value);
}}
placeholder="What do you want to call this view?" placeholder="What do you want to call this view?"
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
autoFocus autoFocus
@ -86,13 +104,23 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = (props) => {
</div> </div>
</div> </div>
<div className="p-3 px-5 relative bg-custom-background-80">Applied Filters with each dropdown</div> <div className="p-3 px-5 relative bg-custom-background-80">
<ViewAppliedFiltersRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewOperations={viewOperations}
/>
</div>
<div className="p-3 px-5 relative flex justify-end items-center gap-2"> <div className="p-3 px-5 relative flex justify-end items-center gap-2">
<Button variant="neutral-primary" onClick={handleModalClose}> <Button variant="neutral-primary" onClick={handleModalClose}>
Cancel Cancel
</Button> </Button>
<Button variant="primary">Create View</Button> <Button variant="primary" onClick={onContinue}>
Create View
</Button>
</div> </div>
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>
@ -101,4 +129,4 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = (props) => {
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
); );
}; });

View File

@ -1,12 +1,15 @@
import { FC, useState } from "react"; import { FC, ReactNode, useState } from "react";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// components // components
import { ViewCreateEditForm } from "./create-edit-form"; import { ViewCreateEditForm } from "./create-edit-form";
// constants
import { viewLocalPayload } from "constants/view";
// types // types
import { TViewOperations } from "../types"; import { TViewOperations } from "../types";
import { TViewTypes } from "@plane/types"; import { TView, TViewFilters, TViewTypes } from "@plane/types";
type TViewCreateEdit = { type TViewCreateEdit = {
workspaceSlug: string; workspaceSlug: string;
@ -14,29 +17,72 @@ type TViewCreateEdit = {
viewId: string | undefined; viewId: string | undefined;
viewType: TViewTypes; viewType: TViewTypes;
viewOperations: TViewOperations; viewOperations: TViewOperations;
children?: ReactNode;
}; };
export const ViewCreateEdit: FC<TViewCreateEdit> = (props) => { export const ViewCreateEdit: FC<TViewCreateEdit> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewOperations } = props; const { workspaceSlug, projectId, viewId, viewType, viewOperations, children } = props;
// states // states
const [currentViewId, setCurrentViewId] = useState<string>();
const [currentFilters, setCurrentFilters] = useState<Partial<TViewFilters>>({});
const [modalToggle, setModalToggle] = useState(false); const [modalToggle, setModalToggle] = useState(false);
const handleModalOpen = () => setModalToggle(true); const handleModalOpen = () => {
const handleModalClose = () => setModalToggle(false); if (viewId === undefined) {
const viewPayload = viewLocalPayload;
setCurrentViewId(viewPayload.id);
viewOperations?.localViewCreate(viewPayload as TView);
} else {
setCurrentViewId(viewId);
}
setModalToggle(true);
};
const createView = () => { const handleModalClose = () => {
viewOperations?.create({ name: "create" }); if (viewId === undefined) {
if (currentViewId) viewOperations?.clearLocalView(currentViewId);
} else {
}
setModalToggle(false);
setCurrentViewId(undefined);
setCurrentFilters({});
};
const onSubmit = async (viewData: Partial<TView>) => {
if (!viewData?.name) return;
try {
await viewOperations.create(viewData);
handleModalClose();
} catch (error) {
console.log(error);
}
}; };
return ( return (
<> <>
<ViewCreateEditForm modalToggle={modalToggle} handleModalClose={handleModalClose} /> {currentViewId && (
<div className="border border-red-500 p-5"> <ViewCreateEditForm
<Button size="sm" className="flex justify-center items-center" onClick={createView}> workspaceSlug={workspaceSlug}
<Plus size={12} /> projectId={projectId}
<span>New View</span> viewId={currentViewId}
</Button> viewType={viewType}
viewOperations={viewOperations}
onSubmit={onSubmit}
modalToggle={modalToggle}
handleModalClose={handleModalClose}
/>
)}
<div className="inline-block" onClick={handleModalOpen}>
{children ? (
children
) : (
<Button size="sm" className="flex justify-center items-center">
<Plus size={12} />
<span>New View</span>
</Button>
)}
</div> </div>
</> </>
); );
}; });

View File

@ -1,5 +1,9 @@
import { FC } from "react"; import { FC } from "react";
import { ChevronRight } from "lucide-react"; import { observer } from "mobx-react-lite";
// hooks
import { useView } from "hooks/store";
// components
import { ViewItem, ViewCreateEdit } from "../";
// types // types
import { TViewOperations } from "../types"; import { TViewOperations } from "../types";
import { TViewTypes } from "@plane/types"; import { TViewTypes } from "@plane/types";
@ -12,25 +16,36 @@ type TViewRoot = {
viewOperations: TViewOperations; viewOperations: TViewOperations;
}; };
export const ViewRoot: FC<TViewRoot> = (props) => { export const ViewRoot: FC<TViewRoot> = observer((props) => {
const {} = props; const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
// hooks
const viewStore = useView(workspaceSlug, projectId, viewType);
return ( return (
<div className="border border-red-500 relative flex items-center gap-2"> <div className="border-b border-custom-border-100 relative flex px-5 gap-2">
{/* header */} {viewStore?.viewIds && viewStore?.viewIds.length > 0 && (
<div className="">Workspace Views</div> <div key={`views_list_${viewId}`} className="relative flex items-center w-full overflow-x-auto">
{/* divider */} {viewStore?.viewIds.map((_viewId) => (
<div className="relative w-[50px] h-[50px] flex justify-center items-center"> <ViewItem
<div className="absolute top-0 bottom-0 border border-red-500 w-[0.5px]" /> workspaceSlug={workspaceSlug}
<div className="flex-shrink-0 w-4 h-4 relative flex justify-center items-center rounded-full bg-red-500"> projectId={projectId}
<ChevronRight size={12} /> viewId={viewId}
viewType={viewType}
viewItemId={_viewId}
/>
))}
</div> </div>
</div> )}
{/* views content */}
<div className=" relative flex items-center"> <div className="flex-shrink-0 my-auto pb-1">
<div>Icon</div> <ViewCreateEdit
<div>Title</div> workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={undefined}
viewType={viewType}
viewOperations={viewOperations}
/>
</div> </div>
</div> </div>
); );
}; });

View File

@ -0,0 +1,46 @@
import { FC } from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
// hooks
import { useView } from "hooks/store";
// ui
import { PhotoFilterIcon } from "@plane/ui";
// types
import { TViewTypes } from "@plane/types";
type TViewItem = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string | undefined;
viewType: TViewTypes;
viewItemId: string;
};
export const ViewItem: FC<TViewItem> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewItemId } = props;
// hooks
const viewStore = useView(workspaceSlug, projectId, viewType);
const view = viewStore?.viewById(viewItemId);
if (!view) return <></>;
return (
<div key={viewItemId} className="space-y-0.5 relative h-full flex flex-col justify-between">
<Link
href={`/${workspaceSlug}/workspace-views/${viewItemId}`}
className={`cursor-pointer relative p-2 px-2.5 flex justify-center items-center gap-1.5 rounded transition-all hover:bg-custom-background-80
${viewItemId === viewId ? `text-custom-primary-100 bg-custom-primary-100/10` : `border-transparent`}
`}
onClick={(e) => viewItemId === viewId && e.preventDefault()}
>
<div className="flex-shrink-0 bg-custom-background-80 rounded-sm relative w-5 h-5 flex justify-center items-center overflow-hidden">
<PhotoFilterIcon className="w-3 h-3" />
</div>
<div className="w-full max-w-[80px] inline-block text-sm line-clamp-1 truncate overflow-hidden font-medium">
{view?.name}
</div>
</Link>
<div className={`border-b-2 ${viewItemId === viewId ? `border-custom-primary-100` : `border-transparent`}`} />
</div>
);
});

View File

@ -1,47 +0,0 @@
import { TViewTypes } from "@plane/types";
export const VIEW_TYPES: Record<TViewTypes, TViewTypes> = {
WORKSPACE_YOUR_VIEWS: "WORKSPACE_YOUR_VIEWS",
WORKSPACE_VIEWS: "WORKSPACE_VIEWS",
WORKSPACE_PROJECT_VIEWS: "WORKSPACE_PROJECT_VIEWS",
PROJECT_VIEWS: "PROJECT_VIEWS",
PROJECT_YOUR_VIEWS: "PROJECT_YOUR_VIEWS",
};
export const VIEW_DEFAULT_FILTER_PARAMETERS = {
filters: {
default: [
"project",
"priority",
"state",
"state_group",
"assignees",
"mentions",
"subscriber",
"created_by",
"labels",
"start_date",
"target_date",
],
},
display_filters: {
default: ["layout", "group_by", "sub_group_by", "order_by", "type", "sub_issue", "show_empty_groups", "calendar"],
},
display_properties: {
default: [
"assignee",
"start_date",
"due_date",
"labels",
"key",
"priority",
"state",
"sub_issue_count",
"link",
"attachment_count",
"estimate",
"created_on",
"updated_on",
],
},
};

View File

@ -0,0 +1,218 @@
// types
import { TViewFilters, TViewDisplayFilters, TViewLayouts } from "@plane/types";
type TViewLayoutFilterProperties = {
filters: Partial<keyof TViewFilters>[];
display_filters: Partial<keyof TViewDisplayFilters>[];
extra_options: ("sub_issue" | "show_empty_groups")[];
display_properties: boolean;
};
type TViewLayoutFilters = {
list: TViewLayoutFilterProperties;
kanban: TViewLayoutFilterProperties;
calendar: TViewLayoutFilterProperties;
spreadsheet: TViewLayoutFilterProperties;
gantt: TViewLayoutFilterProperties;
};
type TFilterPermissions = {
all: Omit<TViewLayoutFilters, "list" | "kanban" | "calendar" | "gantt"> & {
layouts: Omit<TViewLayouts, "list" | "kanban" | "calendar" | "gantt">[];
};
profile: Omit<TViewLayoutFilters, "spreadsheet" | "calendar" | "gantt"> & {
layouts: Omit<TViewLayouts, "spreadsheet" | "calendar" | "gantt">[];
};
project: TViewLayoutFilters & {
layouts: TViewLayouts[];
};
archived: Omit<TViewLayoutFilters, "kanban" | "spreadsheet" | "calendar" | "gantt"> & {
layouts: Omit<TViewLayouts, "kanban" | "spreadsheet" | "calendar" | "gantt">[];
};
draft: Omit<TViewLayoutFilters, "spreadsheet" | "calendar" | "gantt"> & {
layouts: Omit<TViewLayouts, "kanban" | "spreadsheet" | "calendar" | "gantt">[];
};
};
export const ALL_FILTER_PERMISSIONS: TFilterPermissions["all"] = {
layouts: ["spreadsheet"],
spreadsheet: {
filters: ["project", "priority", "state_group", "assignees", "created_by", "labels", "start_date", "target_date"],
display_filters: ["type"],
extra_options: [],
display_properties: true,
},
};
export const PROFILE_FILTER_PERMISSIONS: TFilterPermissions["profile"] = {
layouts: ["list", "kanban"],
list: {
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
display_filters: ["group_by", "order_by", "type"],
extra_options: [],
display_properties: true,
},
kanban: {
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
display_filters: ["group_by", "order_by", "type"],
extra_options: [],
display_properties: true,
},
};
export const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
layouts: ["list", "kanban", "spreadsheet", "calendar", "gantt"],
list: {
filters: [
"priority",
"state",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"module",
"cycle",
],
display_filters: ["group_by", "order_by", "type"],
extra_options: ["sub_issue", "show_empty_groups"],
display_properties: true,
},
kanban: {
filters: [
"priority",
"state",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"module",
"cycle",
],
display_filters: ["group_by", "sub_group_by", "order_by", "type"],
extra_options: ["sub_issue", "show_empty_groups"],
display_properties: true,
},
calendar: {
filters: [
"priority",
"state",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"module",
"cycle",
],
display_filters: ["type"],
extra_options: ["sub_issue"],
display_properties: true,
},
spreadsheet: {
filters: [
"priority",
"state",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"module",
"cycle",
],
display_filters: ["order_by", "type"],
extra_options: [],
display_properties: true,
},
gantt: {
filters: [
"priority",
"state",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"module",
"cycle",
],
display_filters: ["order_by", "type"],
extra_options: ["sub_issue"],
display_properties: false,
},
};
export const ARCHIVED_FILTER_PERMISSIONS: TFilterPermissions["archived"] = {
layouts: ["list"],
list: {
filters: [
"priority",
"state",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"module",
"cycle",
],
display_filters: ["group_by", "order_by"],
extra_options: [],
display_properties: true,
},
};
export const DRAFT_FILTER_PERMISSIONS: TFilterPermissions["draft"] = {
layouts: ["list", "kanban"],
list: {
filters: [
"priority",
"state",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"module",
"cycle",
],
display_filters: ["group_by", "order_by", "type"],
extra_options: ["sub_issue", "show_empty_groups"],
display_properties: true,
},
kanban: {
filters: [
"priority",
"state",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"module",
"cycle",
],
display_filters: ["group_by", "sub_group_by", "order_by", "type"],
extra_options: ["sub_issue", "show_empty_groups"],
display_properties: true,
},
};
export const VIEW_DEFAULT_FILTER_PARAMETERS: TFilterPermissions = {
all: ALL_FILTER_PERMISSIONS,
profile: PROFILE_FILTER_PERMISSIONS,
project: PROJECT_FILTER_PERMISSIONS,
archived: ARCHIVED_FILTER_PERMISSIONS,
draft: DRAFT_FILTER_PERMISSIONS,
};

View File

@ -0,0 +1,2 @@
export * from "./root";
export * from "./filters";

View File

@ -0,0 +1,21 @@
import { v4 as uuidV4 } from "uuid";
// types
import { TViewTypes, TView } from "@plane/types";
export const VIEW_TYPES: Record<TViewTypes, TViewTypes> = {
WORKSPACE_YOUR_VIEWS: "WORKSPACE_YOUR_VIEWS",
WORKSPACE_VIEWS: "WORKSPACE_VIEWS",
PROJECT_VIEWS: "PROJECT_VIEWS",
PROJECT_YOUR_VIEWS: "PROJECT_YOUR_VIEWS",
};
export const viewLocalPayload: Partial<TView> = {
id: uuidV4(),
name: "",
description: "",
filters: {},
display_filters: {},
display_properties: {},
is_local_view: false,
is_create: true,
};

View File

@ -15,17 +15,13 @@ export const useViewDetail = (
const context = useContext(StoreContext); const context = useContext(StoreContext);
if (context === undefined) throw new Error("useViewDetail must be used within StoreProvider"); if (context === undefined) throw new Error("useViewDetail must be used within StoreProvider");
if (!workspaceSlug) throw new Error("useViewDetail hook must require workspaceSlug"); if (!workspaceSlug || !viewId) return undefined;
if (!viewId) throw new Error("useViewDetail hook must require viewId");
switch (viewType) { switch (viewType) {
case "WORKSPACE_YOUR_VIEWS": case "WORKSPACE_YOUR_VIEWS":
return context.view.workspaceViewStore.viewById(viewId); return context.view.workspaceViewMeStore.viewById(viewId);
case "WORKSPACE_VIEWS": case "WORKSPACE_VIEWS":
return context.view.workspaceViewMeStore.viewById(viewId); return context.view.workspaceViewStore.viewById(viewId);
case "WORKSPACE_PROJECT_VIEWS":
return context.view.workspaceViewMeStore.viewById(viewId);
case "PROJECT_YOUR_VIEWS": case "PROJECT_YOUR_VIEWS":
if (!projectId) throw new Error("useView hook must require projectId"); if (!projectId) throw new Error("useView hook must require projectId");
return context.view.projectViewMeStore.viewById(viewId); return context.view.projectViewMeStore.viewById(viewId);
@ -33,6 +29,6 @@ export const useViewDetail = (
if (!projectId) throw new Error("useView hook must require projectId"); if (!projectId) throw new Error("useView hook must require projectId");
return context.view.projectViewStore.viewById(viewId); return context.view.projectViewStore.viewById(viewId);
default: default:
throw new Error("useView hook must require viewType"); return undefined;
} }
}; };

View File

@ -2,7 +2,7 @@ import { useContext } from "react";
// mobx store // mobx store
import { StoreContext } from "contexts/store-context"; import { StoreContext } from "contexts/store-context";
// types // types
import { ViewRoot } from "store/view/view-root.store"; import { ViewRootStore } from "store/view/view-root.store";
// types // types
import { TViewTypes } from "@plane/types"; import { TViewTypes } from "@plane/types";
@ -10,19 +10,17 @@ export const useView = (
workspaceSlug: string, workspaceSlug: string,
projectId: string | undefined, projectId: string | undefined,
viewType: TViewTypes | undefined viewType: TViewTypes | undefined
): ViewRoot => { ): ViewRootStore | undefined => {
const context = useContext(StoreContext); const context = useContext(StoreContext);
if (context === undefined) throw new Error("useView must be used within StoreProvider"); if (context === undefined) throw new Error("useView must be used within StoreProvider");
if (!workspaceSlug) throw new Error("useView hook must require workspaceSlug"); if (!workspaceSlug || !viewType) return undefined;
switch (viewType) { switch (viewType) {
case "WORKSPACE_YOUR_VIEWS": case "WORKSPACE_YOUR_VIEWS":
return context.view.workspaceViewStore; return context.view.workspaceViewMeStore;
case "WORKSPACE_VIEWS": case "WORKSPACE_VIEWS":
return context.view.workspaceViewMeStore; return context.view.workspaceViewStore;
case "WORKSPACE_PROJECT_VIEWS":
return context.view.workspaceViewMeStore;
case "PROJECT_YOUR_VIEWS": case "PROJECT_YOUR_VIEWS":
if (!projectId) throw new Error("useView hook must require projectId"); if (!projectId) throw new Error("useView hook must require projectId");
return context.view.projectViewMeStore; return context.view.projectViewMeStore;
@ -30,6 +28,6 @@ export const useView = (
if (!projectId) throw new Error("useView hook must require projectId"); if (!projectId) throw new Error("useView hook must require projectId");
return context.view.projectViewStore; return context.view.projectViewStore;
default: default:
throw new Error("useView hook must require viewType"); return undefined;
} }
}; };

View File

@ -56,6 +56,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
> >
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig> <SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
</PostHogProvider> </PostHogProvider>
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
</CrispWrapper> </CrispWrapper>
</StoreWrapper> </StoreWrapper>
</InstanceLayout> </InstanceLayout>

View File

@ -1,24 +1,34 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
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";
const GlobalViewIssuesPage: NextPageWithLayout = () => ( const GlobalViewIssuesPage: NextPageWithLayout = () => {
<div className="h-full overflow-hidden bg-custom-background-100"> const router = useRouter();
<div className="flex h-full w-full flex-col border-b border-custom-border-300"> const { workspaceSlug, globalViewId: viewId } = router.query;
<GlobalViewsHeader />
<AllIssueLayoutRoot /> 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()} />
{/* <GlobalViewsHeader />
<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;

View File

@ -11,7 +11,7 @@ export class WorkspaceMeViewService extends APIService implements TViewService {
} }
async fetch(workspaceSlug: string): Promise<TView[]> { async fetch(workspaceSlug: string): Promise<TView[]> {
return this.get(`/api/users/me/workspaces/${workspaceSlug}/views/?type=workspace`) return this.get(`/api/users/me/workspaces/${workspaceSlug}/views/`)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
throw error?.response; throw error?.response;

View File

@ -11,17 +11,19 @@ import {
export class FiltersHelper { export class FiltersHelper {
// computed filters // computed filters
computedFilters = (filters: TViewFilters, defaultValues?: Partial<TViewFilters>): TViewFilters => ({ computedFilters = (filters: TViewFilters, defaultValues?: Partial<TViewFilters>): TViewFilters => ({
project: filters?.project || defaultValues?.project || [], project: defaultValues?.project || filters?.project || [],
priority: filters?.priority || defaultValues?.priority || [], module: defaultValues?.module || filters?.module || [],
state: filters?.state || defaultValues?.state || [], cycle: defaultValues?.cycle || filters?.cycle || [],
state_group: filters?.state_group || defaultValues?.state_group || [], priority: defaultValues?.priority || filters?.priority || [],
assignees: filters?.assignees || defaultValues?.assignees || [], state: defaultValues?.state || filters?.state || [],
mentions: filters?.mentions || defaultValues?.mentions || [], state_group: defaultValues?.state_group || filters?.state_group || [],
subscriber: filters?.subscriber || defaultValues?.subscriber || [], assignees: defaultValues?.assignees || filters?.assignees || [],
created_by: filters?.created_by || defaultValues?.created_by || [], mentions: defaultValues?.mentions || filters?.mentions || [],
labels: filters?.labels || defaultValues?.labels || [], subscriber: defaultValues?.subscriber || filters?.subscriber || [],
start_date: filters?.start_date || defaultValues?.start_date || [], created_by: defaultValues?.created_by || filters?.created_by || [],
target_date: filters?.target_date || defaultValues?.target_date || [], labels: defaultValues?.labels || filters?.labels || [],
start_date: defaultValues?.start_date || filters?.start_date || [],
target_date: defaultValues?.target_date || filters?.target_date || [],
}); });
// computed display filters // computed display filters
@ -29,16 +31,16 @@ export class FiltersHelper {
displayFilters: TViewDisplayFilters, displayFilters: TViewDisplayFilters,
defaultValues?: Partial<TViewDisplayFilters> defaultValues?: Partial<TViewDisplayFilters>
): TViewDisplayFilters => ({ ): TViewDisplayFilters => ({
layout: displayFilters?.layout || defaultValues?.layout || "list", layout: defaultValues?.layout || displayFilters?.layout || "list",
group_by: displayFilters?.group_by || defaultValues?.group_by || "none", group_by: defaultValues?.group_by || displayFilters?.group_by || undefined,
sub_group_by: displayFilters?.sub_group_by || defaultValues?.sub_group_by || undefined, sub_group_by: defaultValues?.sub_group_by || displayFilters?.sub_group_by || undefined,
order_by: displayFilters?.order_by || defaultValues?.order_by || "sort_order", order_by: defaultValues?.order_by || displayFilters?.order_by || "sort_order",
type: displayFilters?.type || defaultValues?.type || undefined, type: defaultValues?.type || displayFilters?.type || undefined,
sub_issue: displayFilters?.sub_issue || defaultValues?.sub_issue || false, sub_issue: defaultValues?.sub_issue || displayFilters?.sub_issue || false,
show_empty_groups: displayFilters?.show_empty_groups || defaultValues?.show_empty_groups || false, show_empty_groups: defaultValues?.show_empty_groups || displayFilters?.show_empty_groups || false,
calendar: { calendar: {
show_weekends: displayFilters?.calendar?.show_weekends || defaultValues?.calendar?.show_weekends || false, show_weekends: defaultValues?.calendar?.show_weekends || displayFilters?.calendar?.show_weekends || false,
layout: displayFilters?.calendar?.layout || defaultValues?.calendar?.layout || "month", layout: defaultValues?.calendar?.layout || displayFilters?.calendar?.layout || "month",
}, },
}); });
@ -47,19 +49,19 @@ export class FiltersHelper {
displayProperties: TViewDisplayProperties, displayProperties: TViewDisplayProperties,
defaultValues?: Partial<TViewDisplayProperties> defaultValues?: Partial<TViewDisplayProperties>
): TViewDisplayProperties => ({ ): TViewDisplayProperties => ({
assignee: displayProperties?.assignee || defaultValues?.assignee || true, assignee: defaultValues?.assignee || displayProperties?.assignee || true,
start_date: displayProperties?.start_date || defaultValues?.start_date || true, start_date: defaultValues?.start_date || displayProperties?.start_date || true,
due_date: displayProperties?.due_date || defaultValues?.due_date || true, due_date: defaultValues?.due_date || displayProperties?.due_date || true,
labels: displayProperties?.labels || defaultValues?.labels || true, labels: defaultValues?.labels || displayProperties?.labels || true,
priority: displayProperties?.priority || defaultValues?.priority || true, priority: defaultValues?.priority || displayProperties?.priority || true,
state: displayProperties?.state || defaultValues?.state || true, state: defaultValues?.state || displayProperties?.state || true,
sub_issue_count: displayProperties?.sub_issue_count || defaultValues?.sub_issue_count || true, sub_issue_count: defaultValues?.sub_issue_count || displayProperties?.sub_issue_count || true,
attachment_count: displayProperties?.attachment_count || defaultValues?.attachment_count || true, attachment_count: defaultValues?.attachment_count || displayProperties?.attachment_count || true,
link: displayProperties?.link || defaultValues?.link || true, link: defaultValues?.link || displayProperties?.link || true,
estimate: displayProperties?.estimate || defaultValues?.estimate || true, estimate: defaultValues?.estimate || displayProperties?.estimate || true,
key: displayProperties?.key || defaultValues?.key || true, key: defaultValues?.key || displayProperties?.key || true,
created_on: displayProperties?.created_on || defaultValues?.created_on || true, created_on: defaultValues?.created_on || displayProperties?.created_on || true,
updated_on: displayProperties?.updated_on || defaultValues?.updated_on || true, updated_on: defaultValues?.updated_on || displayProperties?.updated_on || true,
}); });
// compute filters and display_filters issue query parameters // compute filters and display_filters issue query parameters

View File

@ -1,5 +1,4 @@
// services // services
import { import {
WorkspaceViewService, WorkspaceViewService,
WorkspaceMeViewService, WorkspaceMeViewService,
@ -21,7 +20,6 @@ export class GlobalViewRootStore {
// views root // views root
workspaceViewMeStore: ViewRootStore; workspaceViewMeStore: ViewRootStore;
workspaceViewStore: ViewRootStore; workspaceViewStore: ViewRootStore;
workspaceViewProjectStore: ViewRootStore;
projectViewStore: ViewRootStore; projectViewStore: ViewRootStore;
projectViewMeStore: ViewRootStore; projectViewMeStore: ViewRootStore;
@ -32,10 +30,41 @@ export class GlobalViewRootStore {
cycleUserViewStore?: userViewRootStore; cycleUserViewStore?: userViewRootStore;
constructor(private store: RootStore) { constructor(private store: RootStore) {
// views root const defaultViews: any[] = [
{
id: "all-issues",
name: "All Issues",
filters: {},
is_local_view: true,
},
{
id: "assigned",
name: "Assigned",
filters: {
assignees: store.user?.currentUser?.id ? [store.user?.currentUser?.id] : [],
},
is_local_view: true,
},
{
id: "created",
name: "Created",
filters: {
created_by: store.user?.currentUser?.id ? [store.user?.currentUser?.id] : [],
},
is_local_view: true,
},
{
id: "subscribed",
name: "Subscribed",
filters: {
subscriber: store.user?.currentUser?.id ? [store.user?.currentUser?.id] : [],
},
is_local_view: true,
},
];
this.workspaceViewMeStore = new ViewRootStore(this.store, new WorkspaceMeViewService()); this.workspaceViewMeStore = new ViewRootStore(this.store, new WorkspaceMeViewService());
this.workspaceViewStore = new ViewRootStore(this.store, new WorkspaceViewService()); this.workspaceViewStore = new ViewRootStore(this.store, new WorkspaceViewService(), defaultViews);
this.workspaceViewProjectStore = new ViewRootStore(this.store, new WorkspaceMeViewService());
this.projectViewStore = new ViewRootStore(this.store, new ProjectViewService()); this.projectViewStore = new ViewRootStore(this.store, new ProjectViewService());
this.projectViewMeStore = new ViewRootStore(this.store, new ProjectViewMeService()); this.projectViewMeStore = new ViewRootStore(this.store, new ProjectViewMeService());

View File

@ -1,7 +1,6 @@
import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { action, computed, makeObservable, observable, runInAction } from "mobx";
import set from "lodash/set"; import set from "lodash/set";
// stores // stores
import { RootStore } from "store/root.store";
import { UserViewStore } from "./view.store"; import { UserViewStore } from "./view.store";
// types // types
import { TUserViewService } from "services/view/types"; import { TUserViewService } from "services/view/types";

View File

@ -7,33 +7,41 @@ import { ViewStore } from "./view.store";
import { TViewService } from "services/view/types"; import { TViewService } from "services/view/types";
import { TView } from "@plane/types"; import { TView } from "@plane/types";
export type TLoader = "" | undefined; export type TLoader = "init-loader" | "mutation-loader" | "submitting" | undefined;
type TViewRootStore = { type TViewRootStore = {
// observables // observables
loader: TLoader;
viewMap: Record<string, ViewStore>; viewMap: Record<string, ViewStore>;
// computed // computed
viewIds: string[]; viewIds: string[];
// helper actions // helper actions
viewById: (viewId: string) => ViewStore | undefined; viewById: (viewId: string) => ViewStore | undefined;
// actions // actions
fetch: () => Promise<void>; fetch: (_loader?: TLoader) => Promise<void>;
localViewCreate: (view: TView) => Promise<void>;
clearLocalView: (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>;
}; };
export class ViewRootStore implements TViewRootStore { export class ViewRootStore implements TViewRootStore {
// observables
loader: TLoader = "init-loader";
viewMap: Record<string, ViewStore> = {}; viewMap: Record<string, ViewStore> = {};
constructor(private store: RootStore, private service: TViewService) { constructor(private store: RootStore, private service: TViewService, private defaultViews: TView[] = []) {
makeObservable(this, { makeObservable(this, {
// observables // observables
viewMap: observable.ref, loader: observable.ref,
viewMap: observable,
// computed // computed
viewIds: computed, viewIds: computed,
// actions // actions
fetch: action, fetch: action,
localViewCreate: action,
clearLocalView: action,
create: action, create: action,
remove: action, remove: action,
duplicate: action, duplicate: action,
@ -42,17 +50,26 @@ export class ViewRootStore implements TViewRootStore {
// computed // computed
get viewIds() { get viewIds() {
return Object.keys(this.viewMap); const views = Object.values(this.viewMap);
return views.filter((view) => !view?.is_create).map((view) => view.id) as string[];
} }
// helper actions // helper actions
viewById = (viewId: string) => this.viewMap?.[viewId] || undefined; viewById = (viewId: string) => this.viewMap?.[viewId] || undefined;
// actions // actions
fetch = async () => { fetch = async (_loader: TLoader = "init-loader") => {
const { workspaceSlug, projectId } = this.store.app.router; const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug) return; if (!workspaceSlug) return;
runInAction(() => {
if (this.defaultViews && this.defaultViews.length > 0)
this.defaultViews?.forEach((view) => {
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service));
});
});
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;
@ -60,6 +77,19 @@ export class ViewRootStore implements TViewRootStore {
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.loader = undefined;
});
};
localViewCreate = async (view: TView) => {
runInAction(() => {
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service));
});
};
clearLocalView = async (viewId: string) => {
runInAction(() => {
if (viewId) delete this.viewMap[viewId];
}); });
}; };

View File

@ -26,9 +26,10 @@ export type TViewStore = TView & {
appliedFilters: TViewFilterProps | undefined; appliedFilters: TViewFilterProps | undefined;
appliedFiltersQueryParams: string | undefined; appliedFiltersQueryParams: string | undefined;
// helper actions // helper actions
updateFilters: (filters: Partial<TViewFilters>) => void; setName: (name: string) => void;
updateDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void; setFilters: (filters: Partial<TViewFilters>) => void;
updateDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) => void; setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
setDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) => void;
resetFilterChanges: () => void; resetFilterChanges: () => void;
saveFilterChanges: () => void; saveFilterChanges: () => void;
// actions // actions
@ -46,19 +47,22 @@ export class ViewStore extends FiltersHelper implements TViewStore {
name: string | undefined; name: string | undefined;
description: string | undefined; description: string | undefined;
query: string | undefined; query: string | undefined;
filters: TViewFilters | undefined; filters: TViewFilters;
display_filters: TViewDisplayFilters | undefined; display_filters: TViewDisplayFilters;
display_properties: TViewDisplayProperties | undefined; display_properties: TViewDisplayProperties;
access: TViewAccess | undefined; access: TViewAccess | undefined;
owned_by: string | undefined; owned_by: string | undefined;
sort_order: number | undefined; sort_order: number | undefined;
is_locked: boolean | undefined; is_locked: boolean = false;
is_pinned: boolean | undefined; is_pinned: boolean = false;
is_favorite: boolean | undefined; is_favorite: boolean = false;
created_by: string | undefined; created_by: string | undefined;
updated_by: string | undefined; updated_by: string | undefined;
created_at: Date | undefined; created_at: Date | undefined;
updated_at: Date | undefined; updated_at: Date | undefined;
// local variables
is_local_view: boolean = false;
is_create: boolean = false;
loader: TLoader = undefined; loader: TLoader = undefined;
filtersToUpdate: TViewFilterPartialProps = { filtersToUpdate: TViewFilterPartialProps = {
@ -75,11 +79,9 @@ export class ViewStore extends FiltersHelper implements TViewStore {
this.name = _view.name; this.name = _view.name;
this.description = _view.description; this.description = _view.description;
this.query = _view.query; this.query = _view.query;
this.filters = _view.filters ? this.computedFilters(_view.filters) : undefined; this.filters = this.computedFilters(_view.filters);
this.display_filters = _view.display_filters ? this.computedDisplayFilters(_view.display_filters) : undefined; this.display_filters = this.computedDisplayFilters(_view.display_filters);
this.display_properties = _view.display_properties this.display_properties = this.computedDisplayProperties(_view.display_properties);
? this.computedDisplayProperties(_view.display_properties)
: undefined;
this.access = _view.access; this.access = _view.access;
this.owned_by = _view.owned_by; this.owned_by = _view.owned_by;
this.sort_order = _view.sort_order; this.sort_order = _view.sort_order;
@ -90,18 +92,42 @@ export class ViewStore extends FiltersHelper implements TViewStore {
this.updated_by = _view.updated_by; this.updated_by = _view.updated_by;
this.created_at = _view.created_at; this.created_at = _view.created_at;
this.updated_at = _view.updated_at; this.updated_at = _view.updated_at;
this.is_local_view = _view.is_local_view;
this.is_create = _view.is_create;
makeObservable(this, { makeObservable(this, {
// observables // observables
loader: observable, id: observable.ref,
filtersToUpdate: 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,
loader: observable.ref,
filtersToUpdate: observable,
// computed // computed
appliedFilters: computed, appliedFilters: computed,
appliedFiltersQueryParams: computed, appliedFiltersQueryParams: computed,
// helper actions // helper actions
updateFilters: action, setName: action,
updateDisplayFilters: action, setFilters: action,
updateDisplayProperties: action, setDisplayFilters: action,
setDisplayProperties: action,
resetFilterChanges: action, resetFilterChanges: action,
saveFilterChanges: action, saveFilterChanges: action,
// actions // actions
@ -114,13 +140,12 @@ export class ViewStore extends FiltersHelper implements TViewStore {
// computed // computed
get appliedFilters() { get appliedFilters() {
return { return {
filters: this.filters ? this.computedFilters(this.filters, this.filtersToUpdate.filters) : undefined, filters: this.computedFilters(this.filters, this.filtersToUpdate.filters),
display_filters: this.display_filters display_filters: this.computedDisplayFilters(this.display_filters, this.filtersToUpdate.display_filters),
? this.computedDisplayFilters(this.display_filters, this.filtersToUpdate.display_filters) display_properties: this.computedDisplayProperties(
: undefined, this.display_properties,
display_properties: this.display_properties this.filtersToUpdate.display_properties
? this.computedDisplayProperties(this.display_properties, this.filtersToUpdate.display_properties) ),
: undefined,
}; };
} }
@ -131,14 +156,23 @@ export class ViewStore extends FiltersHelper implements TViewStore {
} }
// helper actions // helper actions
updateFilters = (filters: Partial<TViewFilters>) => { setName = (name: string) => {
runInAction(() => { runInAction(() => {
this.loader = "submit"; this.name = name;
this.filtersToUpdate.filters = filters;
}); });
}; };
updateDisplayFilters = async (display_filters: Partial<TViewDisplayFilters>) => { 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 appliedFilters = this.appliedFilters;
const layout = appliedFilters?.display_filters?.layout; const layout = appliedFilters?.display_filters?.layout;
@ -146,7 +180,7 @@ export class ViewStore extends FiltersHelper implements TViewStore {
const group_by = appliedFilters?.display_filters?.group_by; const group_by = appliedFilters?.display_filters?.group_by;
const sub_issue = appliedFilters?.display_filters?.sub_issue; const sub_issue = appliedFilters?.display_filters?.sub_issue;
if (group_by === undefined) display_filters.sub_group_by = undefined; if (group_by === undefined && display_filters.sub_group_by) display_filters.sub_group_by = undefined;
if (layout === "kanban") { if (layout === "kanban") {
if (sub_group_by === group_by) display_filters.group_by = undefined; if (sub_group_by === group_by) display_filters.group_by = undefined;
if (group_by === null) display_filters.group_by = "state"; if (group_by === null) display_filters.group_by = "state";
@ -155,14 +189,22 @@ export class ViewStore extends FiltersHelper implements TViewStore {
runInAction(() => { runInAction(() => {
this.loader = "submit"; this.loader = "submit";
this.filtersToUpdate.display_filters = display_filters; Object.keys(display_filters).forEach((key) => {
const _key = key as keyof TViewDisplayFilters;
set(this.filtersToUpdate, ["display_filters", _key], display_filters[_key]);
});
}); });
console.log("this.filtersToUpdate", this.filtersToUpdate?.display_filters?.layout);
}; };
updateDisplayProperties = async (display_properties: Partial<TViewDisplayProperties>) => { setDisplayProperties = async (display_properties: Partial<TViewDisplayProperties>) => {
runInAction(() => { runInAction(() => {
this.loader = "submit"; this.loader = "submit";
this.filtersToUpdate.display_properties = display_properties; Object.keys(display_properties).forEach((key) => {
const _key = key as keyof TViewDisplayProperties;
set(this.filtersToUpdate, ["display_properties", _key], display_properties[_key]);
});
}); });
}; };