chore: implemented view create and edit operations and updated the store

This commit is contained in:
gurusainath 2024-02-07 11:30:06 +05:30
parent bc694bb742
commit f05b8de91d
21 changed files with 874 additions and 436 deletions

View File

@ -44,4 +44,5 @@ export type TView = {
// local view variables // local view variables
is_local_view: boolean; is_local_view: boolean;
is_create: boolean; is_create: boolean;
is_editable: boolean;
}; };

View File

@ -48,7 +48,9 @@ export const ProfileSidebar = observer(() => {
]; ];
return ( return (
<div className="w-full flex-shrink-0 overflow-y-auto shadow-custom-shadow-sm md:h-full md:w-80 border-l border-custom-border-100"> <div
className={`w-full flex-shrink-0 overflow-y-auto shadow-custom-shadow-sm md:h-full md:w-80 border-l border-custom-border-100`}
>
{userProjectsData ? ( {userProjectsData ? (
<> <>
<div className="relative h-32"> <div className="relative h-32">

View File

@ -5,11 +5,11 @@ import { CheckCircle } from "lucide-react";
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, ViewCreateEdit, ViewFiltersRoot, ViewAppliedFiltersRoot, ViewLayoutRoot } from "."; import { ViewRoot, ViewCreateEditForm, ViewAppliedFiltersRoot, ViewLayoutRoot } from ".";
// ui // ui
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// constants // constants
import { VIEW_TYPES } 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 } from "@plane/types";
@ -23,6 +23,7 @@ type TAllIssuesViewRoot = {
export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => { export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId } = props; const { workspaceSlug, projectId, viewId } = props;
// states // states
const [currentCreateEditViewId, setCurrentCreateEditViewId] = useState<string | undefined>(undefined);
const [viewType, setViewType] = useState(VIEW_TYPES.WORKSPACE_VIEWS); const [viewType, setViewType] = useState(VIEW_TYPES.WORKSPACE_VIEWS);
const workspaceViewTabOptions = [ const workspaceViewTabOptions = [
{ {
@ -43,18 +44,45 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
const viewOperations: TViewOperations = useMemo( const viewOperations: TViewOperations = useMemo(
() => ({ () => ({
localViewCreate: (data) => viewStore?.localViewCreate(data), setName: (name: string) => viewDetailStore?.setName(name),
clearLocalView: (viewId: string) => viewStore?.clearLocalView(viewId), setDescription: (name: string) => viewDetailStore?.setDescription(name),
setFilters: (filters: Partial<TViewFilters>) => viewDetailStore?.setFilters(filters), setFilters: (filters: Partial<TViewFilters>) => viewDetailStore?.setFilters(filters),
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) =>
viewDetailStore?.setDisplayFilters(display_filters), viewDetailStore?.setDisplayFilters(display_filters),
setDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) => setDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) =>
viewDetailStore?.setDisplayProperties(display_properties), viewDetailStore?.setDisplayProperties(display_properties),
localViewCreateEdit: (viewId: string | undefined) => {
if (viewId === undefined) {
const viewPayload = viewLocalPayload;
setCurrentCreateEditViewId(viewPayload.id);
viewStore?.localViewCreate(viewPayload as TView);
} else setCurrentCreateEditViewId(viewId);
},
localViewCreateEditClear: async (viewId: string | undefined) => {
console.log("viewId", viewId);
if (viewId) viewStore?.remove(viewId);
setCurrentCreateEditViewId(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);
if (data.id) viewOperations.clearLocalView(data.id); setCurrentCreateEditViewId(undefined);
} catch {
setToastAlert({ title: "Error", message: "Error creating view", type: "error" });
}
},
update: async () => {
try {
await viewDetailStore?.saveChanges();
setCurrentCreateEditViewId(undefined);
} catch {
setToastAlert({ title: "Error", message: "Error creating view", type: "error" });
}
},
remove: async (viewId: string) => {
try {
await viewStore?.remove(viewId);
} catch { } catch {
setToastAlert({ title: "Error", message: "Error creating view", type: "error" }); setToastAlert({ title: "Error", message: "Error creating view", type: "error" });
} }
@ -77,15 +105,15 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
</div> </div>
<div className="font-medium">All Issues</div> <div className="font-medium">All Issues</div>
</div> </div>
<div className="relative inline-flex items-center rounded border border-custom-border-300 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 <div
key={tab.key} key={tab.key}
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-90" ? "text-custom-text-100 bg-custom-background-100"
: "text-custom-text-200 bg-custom-background-80 hover:text-custom-text-100" : "text-custom-text-300 bg-custom-background-80 hover:text-custom-text-100"
}`} }`}
onClick={tab.onClick} onClick={tab.onClick}
> >
@ -101,23 +129,18 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
</div> </div>
) : ( ) : (
<> <>
<ViewRoot <div className="border-b border-custom-border-200">
workspaceSlug={workspaceSlug} <ViewRoot
projectId={projectId} workspaceSlug={workspaceSlug}
viewId={viewId} projectId={projectId}
viewType={viewType} viewId={viewId}
viewOperations={viewOperations} viewType={viewType}
/> viewOperations={viewOperations}
/>
{/* <ViewFiltersRoot </div>
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewOperations={viewOperations}
/> */}
<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
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
@ -125,9 +148,9 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
viewType={viewType} viewType={viewType}
viewOperations={viewOperations} viewOperations={viewOperations}
/> />
</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}
@ -135,32 +158,34 @@ 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 relative w-7 h-7 overflow-hidden border border-red-500 rounded flex justify-center items-center">
Filters Filters
</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 relative w-7 h-7 overflow-hidden border border-red-500 rounded flex justify-center items-center">
Display Filters Display Filters
</div> </div> */}
{!viewDetailStore?.is_local_view && ( {/* {!viewDetailStore?.is_local_view && (
<div className="flex-shrink-0 h-full"> <div className="flex-shrink-0 h-full">
<ViewCreateEdit <div>Edit</div>
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewOperations={viewOperations}
>
<div>Edit</div>
</ViewCreateEdit>
</div> </div>
)} )} */}
</div> </div>
</> </>
)} )}
{currentCreateEditViewId != undefined && (
<ViewCreateEditForm
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={currentCreateEditViewId}
viewType={viewType}
viewOperations={viewOperations}
/>
)}
</div> </div>
); );
}); });

View File

@ -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 isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
import { X } from "lucide-react"; import { X } from "lucide-react";
@ -33,21 +33,20 @@ export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => {
if (!filterKeyValue || filterKeyValue.length <= -1) return <></>; if (!filterKeyValue || filterKeyValue.length <= -1) return <></>;
return ( return (
<div <div className="relative flex items-center gap-2 border border-custom-border-300 rounded p-1.5 py-1 min-h-[32px]">
key={filterKey}
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="flex-shrink-0 text-xs text-custom-text-200">{generateTitle(filterKey)}</div>
<div className="relative flex items-center gap-1 flex-wrap"> <div className="relative flex items-center gap-1 flex-wrap">
{["1", "2", "3", "4"].map((filterId) => ( {["1", "2", "3", "4"].map((filterId) => (
<ViewAppliedFiltersItem <Fragment key={filterId}>
workspaceSlug={workspaceSlug} <ViewAppliedFiltersItem
projectId={projectId} workspaceSlug={workspaceSlug}
viewId={viewId} projectId={projectId}
viewType={viewType} viewId={viewId}
filterKey={filterKey} viewType={viewType}
filterId={filterId} filterKey={filterKey}
/> filterId={filterId}
/>
</Fragment>
))} ))}
</div> </div>
<div <div

View File

@ -1,7 +1,7 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
// types // types
import { TFilters } from "@plane/types"; import { TViewFilters } from "@plane/types";
type TComputedAppliedFilters = { type TComputedAppliedFilters = {
key: string; key: string;
@ -10,7 +10,7 @@ type TComputedAppliedFilters = {
dropdownOptions?: { id: string; icon: ""; title: ""; component: ReactNode }[]; dropdownOptions?: { id: string; icon: ""; title: ""; component: ReactNode }[];
}[]; }[];
export const filterOptions = (key: keyof TFilters, selectedFilters: string[]) => { export const filterOptions = (key: keyof TViewFilters, selectedFilters: string[]) => {
switch (key) { switch (key) {
case "project": case "project":
return []; return [];
@ -45,12 +45,12 @@ export const generateTitle = (title: string) =>
.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" "); .join(" ");
export const constructAppliedFilters = (filters: TFilters): TComputedAppliedFilters => { export const constructAppliedFilters = (filters: TViewFilters): TComputedAppliedFilters => {
const appliedFilters: TComputedAppliedFilters = []; const appliedFilters: TComputedAppliedFilters = [];
if (filters && !isEmpty(filters)) { if (filters && !isEmpty(filters)) {
Object.keys(filters).forEach((_filterKey) => { Object.keys(filters).forEach((_filterKey) => {
const _key = _filterKey as keyof TFilters; const _key = _filterKey as keyof TViewFilters;
const _value = filters[_key]; const _value = filters[_key];
if (_value && !isEmpty(_value)) { if (_value && !isEmpty(_value)) {

View File

@ -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 { X } from "lucide-react"; import { X } from "lucide-react";
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
@ -43,13 +43,15 @@ export const ViewAppliedFiltersRoot: FC<TViewAppliedFiltersRoot> = observer((pro
{filterKeys.map((key) => { {filterKeys.map((key) => {
const filterKey = key as keyof TViewFilters; const filterKey = key as keyof TViewFilters;
return ( return (
<ViewAppliedFilters <Fragment key={filterKey}>
workspaceSlug={workspaceSlug} <ViewAppliedFilters
projectId={projectId} workspaceSlug={workspaceSlug}
viewId={viewId} projectId={projectId}
viewType={viewType} viewId={viewId}
filterKey={filterKey} viewType={viewType}
/> filterKey={filterKey}
/>
</Fragment>
); );
})} })}
<div <div

View File

@ -3,7 +3,10 @@ export * from "./all-issues-root";
// views // views
export * from "./views/root"; export * from "./views/root";
export * from "./views/view-item"; export * from "./views/view-item";
export * from "./views/create-edit";
export * from "./views/dropdown/root";
export * from "./views/dropdown/dropdown-item";
export * from "./views/create-edit-form"; export * from "./views/create-edit-form";
// layouts // layouts

View File

@ -1,11 +1,17 @@
import { TView } from "@plane/types"; import { TView } from "@plane/types";
export type TViewOperations = { export type TViewOperations = {
localViewCreate: (data: TView) => void; setName: (name: string) => void;
clearLocalView: (viewId: string) => void; setDescription: (description: string) => void;
setFilters: (filters: Partial<TViewFilters>) => void; setFilters: (filters: Partial<TViewFilters>) => void;
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void; setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
setDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) => void; setDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) => void;
localViewCreateEdit: (viewId: string | undefined) => void;
localViewCreateEditClear: (viewId: string | undefined) => Promise<void>;
fetch: () => Promise<void>; fetch: () => Promise<void>;
create: (data: Partial<TView>) => Promise<void>; create: (data: Partial<TView>) => Promise<void>;
update: () => Promise<void>;
remove: (viewId: string) => Promise<void>;
}; };

View File

@ -1,15 +1,15 @@
import { FC, Fragment } from "react"; import { FC, Fragment, useCallback, useEffect, useState } from "react";
import { observer } from "mobx-react-lite"; 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 { Briefcase, Globe2, Plus, X } from "lucide-react";
// hooks // hooks
import { useViewDetail } from "hooks/store"; import { useViewDetail, useProject } from "hooks/store";
// components // components
import { ViewAppliedFiltersRoot } from "../"; 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 { TViewTypes } from "@plane/types";
import { TViewOperations } from "../types"; import { TViewOperations } from "../types";
type TViewCreateEditForm = { type TViewCreateEditForm = {
@ -18,28 +18,50 @@ type TViewCreateEditForm = {
viewId: string; viewId: string;
viewType: TViewTypes; viewType: TViewTypes;
viewOperations: TViewOperations; viewOperations: TViewOperations;
modalToggle: boolean;
handleModalClose: () => void;
onSubmit: (viewData: Partial<TView>) => void;
}; };
export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => { export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewOperations, modalToggle, handleModalClose, onSubmit } = props; const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
// hooks // hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const { getProjectById } = useProject();
// states
const [modalToggle, setModalToggle] = useState(false);
const [loader, setLoader] = useState(false);
const modalOpen = useCallback(() => setModalToggle(true), [setModalToggle]);
const modalClose = useCallback(() => {
setModalToggle(false);
setTimeout(() => {
viewOperations.localViewCreateEditClear(viewId);
}, 200);
}, [viewId, setModalToggle, viewOperations]);
useEffect(() => {
if (viewId) modalOpen();
}, [viewId, modalOpen, modalClose]);
const onContinue = async () => { const onContinue = async () => {
const payload: Partial<TView> = { setLoader(true);
id: viewDetailStore?.id, if (viewDetailStore?.is_create) {
name: viewDetailStore?.name, const payload = viewDetailStore?.filtersToUpdate;
filters: viewDetailStore?.filters, await viewOperations.create(payload);
}; modalClose();
onSubmit(payload); } else {
const payload = viewDetailStore?.filtersToUpdate;
if (!payload) return;
await viewOperations.update();
modalClose();
}
setLoader(false);
}; };
const projectDetails = projectId ? getProjectById(projectId) : undefined;
if (!viewDetailStore?.id) return <></>;
return ( return (
<Transition.Root show={modalToggle} as={Fragment}> <Transition.Root show={modalToggle} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleModalClose}> <Dialog as="div" className="relative z-20" onClose={modalClose}>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
@ -65,13 +87,22 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((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"> {projectId && projectDetails ? (
<div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4 overflow-hidden"> <div className="relative rounded p-1.5 px-2 flex items-center gap-1 border border-custom-border-100 bg-custom-background-80">
<Trash2 className="w-3.5 h-3.5" /> <div className="flex-shrink-0 relative flex justify-center items-center w-5 h-5 overflow-hidden">
<Briefcase className="w-3.5 h-3.5" />
</div>
<div className="text-xs uppercase font-medium">{projectDetails?.identifier || "Project"}</div>
</div> </div>
<div className="text-xs uppercase">Project Identifier</div> ) : (
</div> */} <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="">Create View</div> <div className="flex-shrink-0 relative flex justify-center items-center w-5 h-5 overflow-hidden">
<Globe2 className="w-3.5 h-3.5" />
</div>
<div className="text-xs uppercase font-medium">Workspace</div>
</div>
)}
<div className="font-medium text-lg">Save View</div>
</div> </div>
<div className="p-3 px-5"> <div className="p-3 px-5">
@ -79,7 +110,7 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
id="name" id="name"
name="name" name="name"
type="text" type="text"
value={viewDetailStore?.name || ""} value={viewDetailStore?.filtersToUpdate?.name || ""}
onChange={(e) => { onChange={(e) => {
viewDetailStore?.setName(e.target.value); viewDetailStore?.setName(e.target.value);
}} }}
@ -90,13 +121,13 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
</div> </div>
<div className="p-3 px-5 relative flex justify-between items-center gap-2"> <div className="p-3 px-5 relative flex justify-between 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="cursor-pointer relative rounded p-1.5 px-2 flex items-center gap-1 border border-custom-border-100 bg-custom-background-80">
<div className="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">
<Plus className="w-3 h-3" /> <Plus className="w-3 h-3" />
</div> </div>
<div className="text-xs">Filters</div> <div className="text-xs">Filters</div>
</div> </div>
<div className="relative rounded p-1.5 px-2 flex items-center gap-1 border border-dashed border-custom-border-100 bg-custom-background-80"> <div className="cursor-pointer relative rounded p-1.5 px-2 flex items-center gap-1 border border-dashed border-custom-border-100 bg-custom-background-80">
<div className="text-xs">Clear all filters</div> <div className="text-xs">Clear all filters</div>
<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">
<X className="w-3 h-3" /> <X className="w-3 h-3" />
@ -115,11 +146,11 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
</div> </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={modalClose} disabled={loader}>
Cancel Cancel
</Button> </Button>
<Button variant="primary" onClick={onContinue}> <Button variant="primary" onClick={onContinue} disabled={loader}>
Create View {loader ? `Saving...` : `${viewDetailStore?.is_create ? `Create` : `Update`} View`}
</Button> </Button>
</div> </div>
</Dialog.Panel> </Dialog.Panel>

View File

@ -1,88 +0,0 @@
import { FC, ReactNode, useState } from "react";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// ui
import { Button } from "@plane/ui";
// components
import { ViewCreateEditForm } from "./create-edit-form";
// constants
import { viewLocalPayload } from "constants/view";
// types
import { TViewOperations } from "../types";
import { TView, TViewFilters, TViewTypes } from "@plane/types";
type TViewCreateEdit = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string | undefined;
viewType: TViewTypes;
viewOperations: TViewOperations;
children?: ReactNode;
};
export const ViewCreateEdit: FC<TViewCreateEdit> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewOperations, children } = props;
// states
const [currentViewId, setCurrentViewId] = useState<string>();
const [currentFilters, setCurrentFilters] = useState<Partial<TViewFilters>>({});
const [modalToggle, setModalToggle] = useState(false);
const handleModalOpen = () => {
if (viewId === undefined) {
const viewPayload = viewLocalPayload;
setCurrentViewId(viewPayload.id);
viewOperations?.localViewCreate(viewPayload as TView);
} else {
setCurrentViewId(viewId);
}
setModalToggle(true);
};
const handleModalClose = () => {
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 (
<>
{currentViewId && (
<ViewCreateEditForm
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={currentViewId}
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>
</>
);
});

View File

@ -0,0 +1,74 @@
import { FC } from "react";
import Link from "next/link";
import { Combobox } from "@headlessui/react";
import { GripVertical, MoreVertical } from "lucide-react";
// hooks
import { useViewDetail } from "hooks/store";
// ui
import { PhotoFilterIcon, Tooltip } from "@plane/ui";
// types
import { TViewTypes } from "@plane/types";
type TViewDropdownItem = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string;
viewType: TViewTypes;
currentViewId: string | undefined;
searchQuery: string;
};
export const ViewDropdownItem: FC<TViewDropdownItem> = (props) => {
const { workspaceSlug, projectId, viewId, viewType, currentViewId, searchQuery } = props;
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const isDragEnabled = false;
const isEditable = !viewDetailStore?.is_local_view || false;
if (!viewDetailStore) return <></>;
if (!searchQuery || (searchQuery && viewDetailStore?.name?.toLowerCase().includes(searchQuery.toLowerCase())))
return (
<Combobox.Option
value={undefined}
className={`w-full px-1 pl-2 py-1.5 truncate flex items-center justify-between gap-1 rounded cursor-pointer select-none group
${currentViewId === viewDetailStore?.id ? `bg-custom-primary-100/10` : `hover:bg-custom-background-80`}
`}
>
<Tooltip tooltipContent={viewDetailStore?.name} position="left">
<div className="relative w-full flex items-center gap-1 overflow-hidden">
{isDragEnabled && (
<div className="flex-shrink-0 w-5 h-5 relative rounded flex justify-center items-center hover:bg-custom-background-100">
<GripVertical className="w-3.5 h-3.5 text-custom-text-200 group-hover:text-custom-text-100" />
</div>
)}
<Link
href={`/${workspaceSlug}/workspace-views/${viewDetailStore?.id}`}
className={`w-full h-full overflow-hidden relative flex items-center gap-1
${
currentViewId === viewDetailStore?.id
? `text-custom-text-100`
: `text-custom-text-200 group-hover:text-custom-text-100`
}
`}
>
<div className="flex-shrink-0 w-5 h-5 relative flex justify-center items-center">
<PhotoFilterIcon className="w-3 h-3 " />
</div>
<div className="w-full line-clamp-1 truncate overflow-hidden inline-block whitespace-nowrap text-sm font-medium">
{viewDetailStore?.name}
</div>
</Link>
</div>
</Tooltip>
{isEditable && (
<div className="flex-shrink-0 w-5 h-5 relative rounded flex justify-center items-center hover:bg-custom-background-100">
<MoreVertical className="h-3.5 w-3.5 flex-shrink-0" />
</div>
)}
</Combobox.Option>
);
return <></>;
};

View File

@ -0,0 +1,125 @@
import { FC, Fragment, ReactNode, useRef, useState } from "react";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Plus, Search } from "lucide-react";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { useView } from "hooks/store";
// components
import { ViewDropdownItem } from "../../";
// types
import { TViewTypes } from "@plane/types";
import { TViewOperations } from "../../types";
type TViewDropdown = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string | undefined;
viewType: TViewTypes;
viewOperations: TViewOperations;
children?: ReactNode;
};
export const ViewDropdown: FC<TViewDropdown> = (props) => {
const { workspaceSlug, projectId, viewId: currentViewId, viewType, viewOperations, children } = props;
// hooks
const viewStore = useView(workspaceSlug, projectId, viewType);
// states
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>
) : (
<span className="whitespace-nowrap">More...</span>
)}
</button>
</Combobox.Button>
{dropdownToggle && (
<Combobox.Options className="fixed z-10" static>
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
className="my-1 w-64 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-60 space-y-0.5 overflow-y-scroll">
{viewStore?.viewIds &&
viewStore?.viewIds.length > 0 &&
viewStore?.viewIds.map((viewId) => (
<Fragment key={viewId}>
<ViewDropdownItem
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
currentViewId={currentViewId}
searchQuery={query}
/>
</Fragment>
))}
</div>
<div
className="relative flex justify-center items-center gap-1 rounded p-1 py-1.5 transition-all border border-custom-border-200 bg-custom-background-90 hover:bg-custom-background-80 text-custom-text-300 hover:text-custom-text-200 cursor-pointer"
onClick={() => viewOperations?.localViewCreateEdit(undefined)}
>
<Plus className="w-3 h-3" />
<div className="text-sm">New view</div>
</div>
</div>
</Combobox.Options>
)}
</Combobox>
);
};

View File

@ -1,9 +1,12 @@
import { FC } from "react"; import { FC, Fragment, useEffect, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// hooks // hooks
import { useView } from "hooks/store"; import { useView } from "hooks/store";
// components // components
import { ViewItem, ViewCreateEdit } from "../"; import { ViewItem, ViewDropdown } from "../";
// ui
import { Button } from "@plane/ui";
// types // types
import { TViewOperations } from "../types"; import { TViewOperations } from "../types";
import { TViewTypes } from "@plane/types"; import { TViewTypes } from "@plane/types";
@ -20,31 +23,84 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props; const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
// hooks // hooks
const viewStore = useView(workspaceSlug, projectId, viewType); const viewStore = useView(workspaceSlug, projectId, viewType);
// state
const [itemsToRenderViewsCount, setItemsToRenderViewCount] = useState<number>(0);
useEffect(() => {
const handleViewTabsVisibility = () => {
const tabContainer = document.getElementById("tab-container");
const tabItemViewMore = document.getElementById("tab-item-view-more");
const itemWidth = 124;
if (!tabContainer || !tabItemViewMore) return;
const containerWidth = tabContainer.clientWidth;
const itemViewMoreLeftOffset = tabItemViewMore.offsetLeft;
const itemViewMoreRightOffset = containerWidth - itemViewMoreLeftOffset;
if (itemViewMoreLeftOffset + (tabItemViewMore.clientWidth + 10) > containerWidth) {
const itemsToRender = Math.floor(containerWidth / itemWidth);
setItemsToRenderViewCount(itemsToRender);
}
if (itemViewMoreRightOffset > itemWidth + 10) {
const itemsToRenderLeft = Math.floor(itemViewMoreLeftOffset / itemWidth) || 0;
const itemsToRenderRight = Math.floor(itemViewMoreRightOffset / itemWidth) || 0;
setItemsToRenderViewCount(itemsToRenderLeft + itemsToRenderRight);
}
};
window.addEventListener("resize", () => handleViewTabsVisibility());
handleViewTabsVisibility();
return () => window.removeEventListener("resize", () => handleViewTabsVisibility());
}, [viewStore?.viewIds]);
return ( return (
<div className="border-b border-custom-border-100 relative flex 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 && (
<div key={`views_list_${viewId}`} className="relative flex items-center w-full overflow-x-auto"> <div
{viewStore?.viewIds.map((_viewId) => ( key={`views_list_${viewId}`}
<ViewItem id="tab-container"
workspaceSlug={workspaceSlug} className="relative flex items-center w-full overflow-hidden"
projectId={projectId} >
viewId={viewId} {viewStore?.viewIds?.slice(0, itemsToRenderViewsCount || viewStore?.viewIds.length).map((_viewId) => (
viewType={viewType} <Fragment key={_viewId}>
viewItemId={_viewId} <ViewItem
/> workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewItemId={_viewId}
/>
</Fragment>
))} ))}
<div id="tab-item-view-more" className="min-w-[90px]">
{viewStore?.viewIds.length <= (itemsToRenderViewsCount || viewStore?.viewIds.length) ? null : (
<ViewDropdown
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewOperations={viewOperations}
>
<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>
<Plus size={12} />
</span>
<span>
{viewStore?.viewIds.length - (itemsToRenderViewsCount || viewStore?.viewIds.length)} More...
</span>
</div>
</ViewDropdown>
)}
</div>
</div> </div>
)} )}
<div className="flex-shrink-0 my-auto pb-1"> <div className="flex-shrink-0 my-auto pb-1">
<ViewCreateEdit <Button size="sm" prependIcon={<Plus />} onClick={() => viewOperations?.localViewCreateEdit(undefined)}>
workspaceSlug={workspaceSlug} New View
projectId={projectId} </Button>
viewId={undefined}
viewType={viewType}
viewOperations={viewOperations}
/>
</div> </div>
</div> </div>
); );

View File

@ -1,10 +1,10 @@
import { FC } from "react"; 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 } from "hooks/store"; import { useView, useViewDetail } from "hooks/store";
// ui // ui
import { PhotoFilterIcon } from "@plane/ui"; import { PhotoFilterIcon, Tooltip } from "@plane/ui";
// types // types
import { TViewTypes } from "@plane/types"; import { TViewTypes } from "@plane/types";
@ -19,27 +19,31 @@ type TViewItem = {
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 } = props;
// hooks // hooks
const viewStore = useView(workspaceSlug, projectId, viewType); const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewItemId, viewType);
const view = viewStore?.viewById(viewItemId); if (!viewDetailStore) return <></>;
if (!view) return <></>;
return ( return (
<div key={viewItemId} 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">
<Link <Tooltip tooltipContent={viewDetailStore?.name} position="top">
href={`/${workspaceSlug}/workspace-views/${viewItemId}`} <Link
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 href={`/${workspaceSlug}/workspace-views/${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
${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`}
`} `}
onClick={(e) => viewItemId === viewId && e.preventDefault()} 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"> <div
<PhotoFilterIcon className="w-3 h-3" /> className={`flex-shrink-0 rounded-sm relative w-5 h-5 flex justify-center items-center overflow-hidden
</div> ${viewItemId === viewId ? `bg-transparent` : `bg-custom-background-80`}
<div className="w-full max-w-[80px] inline-block text-sm line-clamp-1 truncate overflow-hidden font-medium"> `}
{view?.name} >
</div> <PhotoFilterIcon className="w-3 h-3" />
</Link> </div>
<div className="w-full max-w-[80px] inline-block text-sm line-clamp-1 truncate overflow-hidden font-medium">
{viewDetailStore?.name}
</div>
</Link>
</Tooltip>
<div className={`border-b-2 ${viewItemId === viewId ? `border-custom-primary-100` : `border-transparent`}`} /> <div className={`border-b-2 ${viewItemId === viewId ? `border-custom-primary-100` : `border-transparent`}`} />
</div> </div>
); );

View File

@ -13,9 +13,9 @@ export const viewLocalPayload: Partial<TView> = {
id: uuidV4(), id: uuidV4(),
name: "", name: "",
description: "", description: "",
filters: {}, filters: undefined,
display_filters: {}, display_filters: undefined,
display_properties: {}, display_properties: undefined,
is_local_view: false, is_local_view: false,
is_create: true, is_create: true,
}; };

View File

@ -47,7 +47,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
<InstanceLayout> <InstanceLayout>
<StoreWrapper> <StoreWrapper>
<CrispWrapper user={currentUser}> <CrispWrapper user={currentUser}>
<PostHogProvider {/* <PostHogProvider
user={currentUser} user={currentUser}
workspaceRole={currentWorkspaceRole} workspaceRole={currentWorkspaceRole}
projectRole={currentProjectRole} projectRole={currentProjectRole}
@ -55,7 +55,8 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
posthogHost={envConfig?.posthog_host || null} posthogHost={envConfig?.posthog_host || null}
> >
<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

@ -30,13 +30,7 @@ export class GlobalViewRootStore {
cycleUserViewStore?: userViewRootStore; cycleUserViewStore?: userViewRootStore;
constructor(private store: RootStore) { constructor(private store: RootStore) {
const defaultViews: any[] = [ const workspaceViewMeStoreDefaultViews: any[] = [
{
id: "all-issues",
name: "All Issues",
filters: {},
is_local_view: true,
},
{ {
id: "assigned", id: "assigned",
name: "Assigned", name: "Assigned",
@ -62,9 +56,21 @@ export class GlobalViewRootStore {
is_local_view: true, is_local_view: true,
}, },
]; ];
const workspaceViewStoreDefaultViews: any[] = [
{
id: "all-issues",
name: "All Issues",
filters: {},
is_local_view: true,
},
];
this.workspaceViewMeStore = new ViewRootStore(this.store, new WorkspaceMeViewService()); this.workspaceViewMeStore = new ViewRootStore(
this.workspaceViewStore = new ViewRootStore(this.store, new WorkspaceViewService(), defaultViews); this.store,
new WorkspaceMeViewService(),
workspaceViewMeStoreDefaultViews
);
this.workspaceViewStore = new ViewRootStore(this.store, new WorkspaceViewService(), workspaceViewStoreDefaultViews);
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

@ -51,13 +51,13 @@ export class userViewRootStore implements TUserViewRootStore {
const view = await this.service.fetch(this.workspaceSlug, this.projectId, this.featureId); const view = await this.service.fetch(this.workspaceSlug, this.projectId, this.featureId);
if (!view) return; if (!view) return;
runInAction(() => { // runInAction(() => {
if (this.workspaceSlug && view.id) // if (this.workspaceSlug && view.id)
set( // set(
this.viewMap, // this.viewMap,
[view.id], // [view.id],
new UserViewStore(view, this.service, this.workspaceSlug, this.projectId, this.featureId) // new UserViewStore(view, this.service, this.workspaceSlug, this.projectId, this.featureId)
); // );
}); // });
}; };
} }

View File

@ -1,109 +1,155 @@
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";
// store
import { RootStore } from "store/root.store";
// types // types
import { TUserViewService } from "services/view/types"; import { TViewService } from "services/view/types";
import { import {
TUserView, TView,
TViewFilters, TViewFilters,
TViewDisplayFilters, TViewDisplayFilters,
TViewDisplayProperties, TViewDisplayProperties,
TViewFilterProps, TViewFilterProps,
TViewFilterPartialProps, TViewAccess,
} from "@plane/types"; } from "@plane/types";
// helpers // helpers
import { FiltersHelper } from "../helpers/filters_helpers"; import { FiltersHelper } from "../helpers/filters_helpers";
type TLoader = "submitting" | "submit" | undefined; type TLoader = "submitting" | "submit" | undefined;
export type TUserViewStore = TUserView & { export type TUserViewStore = TView & {
// observables // observables
loader: TLoader; loader: TLoader;
filtersToUpdate: TViewFilterPartialProps; filtersToUpdate: Partial<TView>;
// computed // computed
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; setDescription: (description: string) => void;
updateDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) => void; setFilters: (filters: Partial<TViewFilters>) => void;
resetFilterChanges: () => void; setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
saveFilterChanges: () => void; setDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) => void;
resetChanges: () => void;
saveChanges: () => Promise<void>;
// actions // actions
update: (viewData: Partial<TUserView>) => Promise<void>; 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 { export class UserViewStore extends FiltersHelper implements TUserViewStore {
id: string | undefined; id: string | undefined;
workspace: string | undefined; workspace: string | undefined;
project: string | undefined; project: string | undefined;
module: string | undefined; name: string | undefined;
cycle: string | undefined; description: string | undefined;
filters: TViewFilters | undefined; query: string | undefined;
display_filters: TViewDisplayFilters | undefined; filters: TViewFilters;
display_properties: TViewDisplayProperties | undefined; display_filters: TViewDisplayFilters;
user: string | undefined; 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; 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;
is_local_view: boolean = false;
is_create: boolean = false;
is_editable: boolean = false;
loader: TLoader = undefined; loader: TLoader = undefined;
filtersToUpdate: TViewFilterPartialProps = { filtersToUpdate: Partial<TView> = {
filters: {}, name: "",
display_filters: {}, description: "",
display_properties: {}, filters: undefined,
display_filters: undefined,
display_properties: undefined,
}; };
constructor( constructor(private store: RootStore, _view: TView, private service: TViewService) {
_view: TUserView,
private service: TUserViewService,
private workspaceSlug: string,
private projectId: string | undefined,
private featureId: string | undefined // moduleId/cycleId
) {
super(); super();
this.id = _view.id; this.id = _view.id;
this.workspace = _view.workspace; this.workspace = _view.workspace;
this.project = _view.project; this.project = _view.project;
this.filters = _view.filters ? this.computedFilters(_view.filters) : undefined; this.name = _view.name;
this.display_filters = _view.display_filters ? this.computedDisplayFilters(_view.display_filters) : undefined; this.description = _view.description;
this.display_properties = _view.display_properties this.query = _view.query;
? this.computedDisplayProperties(_view.display_properties) this.filters = this.computedFilters(_view.filters);
: undefined; this.display_filters = this.computedDisplayFilters(_view.display_filters);
this.user = _view.user; 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.created_by = _view.created_by;
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;
this.is_editable = _view.is_editable;
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,
is_editable: 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,
resetFilterChanges: action, setDisplayProperties: action,
saveFilterChanges: action, resetChanges: action,
saveChanges: action,
// actions // actions
update: action, update: action,
lockView: action,
unlockView: action,
}); });
} }
// 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,
}; };
} }
@ -114,14 +160,29 @@ export class UserViewStore extends FiltersHelper implements TUserViewStore {
} }
// helper actions // helper actions
updateFilters = (filters: Partial<TViewFilters>) => { setName = (name: string) => {
runInAction(() => { runInAction(() => {
this.loader = "submit"; this.filtersToUpdate.name = name;
this.filtersToUpdate.filters = filters;
}); });
}; };
updateDisplayFilters = async (display_filters: Partial<TViewDisplayFilters>) => { 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 appliedFilters = this.appliedFilters;
const layout = appliedFilters?.display_filters?.layout; const layout = appliedFilters?.display_filters?.layout;
@ -129,7 +190,7 @@ export class UserViewStore extends FiltersHelper implements TUserViewStore {
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";
@ -137,47 +198,130 @@ export class UserViewStore extends FiltersHelper implements TUserViewStore {
if (layout === "spreadsheet" && sub_issue === true) display_filters.sub_issue = false; if (layout === "spreadsheet" && sub_issue === true) display_filters.sub_issue = false;
runInAction(() => { runInAction(() => {
this.loader = "submit"; Object.keys(display_filters).forEach((key) => {
this.filtersToUpdate.display_filters = display_filters; const _key = key as keyof TViewDisplayFilters;
set(this.filtersToUpdate, ["display_filters", _key], display_filters[_key]);
});
}); });
}; };
updateDisplayProperties = async (display_properties: Partial<TViewDisplayProperties>) => { setDisplayProperties = async (display_properties: Partial<TViewDisplayProperties>) => {
runInAction(() => { runInAction(() => {
this.loader = "submit"; Object.keys(display_properties).forEach((key) => {
this.filtersToUpdate.display_properties = display_properties; const _key = key as keyof TViewDisplayProperties;
set(this.filtersToUpdate, ["display_properties", _key], display_properties[_key]);
});
}); });
}; };
resetFilterChanges = () => { resetChanges = () => {
runInAction(() => { runInAction(() => {
this.loader = undefined; this.loader = undefined;
this.filtersToUpdate = { this.filtersToUpdate = {
filters: {}, name: this.name,
display_filters: {}, description: this.description,
display_properties: {}, filters: this.filters,
display_filters: this.display_filters,
display_properties: this.display_properties,
}; };
}); });
}; };
saveFilterChanges = async () => { saveChanges = async () => {
this.loader = "submitting"; try {
if (this.appliedFilters) await this.update(this.appliedFilters); this.loader = "submitting";
this.loader = undefined; 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 // actions
update = async (viewData: Partial<TViewFilterProps>) => { lockView = async () => {
if (!this.workspaceSlug || !this.id) return; try {
const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug || !this.id || !this.service.lock) return;
const view = await this.service.update(this.workspaceSlug, viewData, this.projectId, this.featureId); const view = await this.service.lock(workspaceSlug, this.id, projectId);
if (!view) return; if (!view) return;
runInAction(() => { runInAction(() => {
Object.keys(viewData).forEach((key) => { this.is_locked = view.is_locked;
const _key = key as keyof TViewFilterProps;
set(this, _key, viewData[_key]);
}); });
}); } 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();
}
}; };
} }

View File

@ -1,5 +1,7 @@
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";
import sortBy from "lodash/sortBy";
import reverse from "lodash/reverse";
// stores // stores
import { RootStore } from "store/root.store"; import { RootStore } from "store/root.store";
import { ViewStore } from "./view.store"; import { ViewStore } from "./view.store";
@ -18,9 +20,8 @@ type TViewRootStore = {
// helper actions // helper actions
viewById: (viewId: string) => ViewStore | undefined; viewById: (viewId: string) => ViewStore | undefined;
// actions // actions
fetch: (_loader?: TLoader) => Promise<void>;
localViewCreate: (view: TView) => Promise<void>; localViewCreate: (view: TView) => Promise<void>;
clearLocalView: (viewId: string) => Promise<void>; fetch: (_loader?: TLoader) => 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>;
@ -39,9 +40,8 @@ export class ViewRootStore implements TViewRootStore {
// computed // computed
viewIds: computed, viewIds: computed,
// actions // actions
fetch: action,
localViewCreate: action, localViewCreate: action,
clearLocalView: action, fetch: action,
create: action, create: action,
remove: action, remove: action,
duplicate: action, duplicate: action,
@ -51,80 +51,91 @@ export class ViewRootStore implements TViewRootStore {
// computed // computed
get viewIds() { get viewIds() {
const views = Object.values(this.viewMap); const views = Object.values(this.viewMap);
return views.filter((view) => !view?.is_create).map((view) => view.id) as string[]; const localViews = views.filter((view) => view.is_local_view);
let apiViews = views.filter((view) => !view.is_local_view && !view.is_create);
apiViews = reverse(sortBy(apiViews, "sort_order"));
const _viewIds = [...localViews.map((view) => view.id), ...apiViews.map((view) => view.id)];
return _viewIds as string[];
} }
// helper actions // helper actions
viewById = (viewId: string) => this.viewMap?.[viewId] || undefined; viewById = (viewId: string) => this.viewMap?.[viewId] || undefined;
// actions // actions
fetch = async (_loader: TLoader = "init-loader") => {
const { workspaceSlug, projectId } = this.store.app.router;
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);
if (!views) return;
runInAction(() => {
views.forEach((view) => {
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service));
});
this.loader = undefined;
});
};
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));
}); });
}; };
clearLocalView = async (viewId: string) => { fetch = async (_loader: TLoader = "init-loader") => {
runInAction(() => { try {
if (viewId) delete this.viewMap[viewId]; const { workspaceSlug, projectId } = this.store.app.router;
}); if (!workspaceSlug) return;
if (this.defaultViews && this.defaultViews.length > 0)
runInAction(() => {
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);
if (!views) return;
runInAction(() => {
views.forEach((view) => {
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service));
});
this.loader = undefined;
});
} catch {}
}; };
create = async (data: Partial<TView>) => { create = async (data: Partial<TView>) => {
const { workspaceSlug, projectId } = this.store.app.router; try {
if (!workspaceSlug) return; const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug) return;
const view = await this.service.create(workspaceSlug, data, projectId); const view = await this.service.create(workspaceSlug, data, projectId);
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));
}); });
if (data.id) this.remove(data.id);
} catch {}
}; };
remove = async (viewId: string) => { remove = async (viewId: string) => {
const { workspaceSlug, projectId } = this.store.app.router; try {
if (!workspaceSlug || !viewId) return; const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug || !viewId) return;
await this.service.remove?.(workspaceSlug, viewId, projectId); if (this.viewMap?.[viewId] != undefined && !this.viewMap?.[viewId]?.is_create)
await this.service.remove?.(workspaceSlug, viewId, projectId);
runInAction(() => { runInAction(() => {
delete this.viewMap[viewId]; delete this.viewMap[viewId];
}); });
} catch {}
}; };
duplicate = async (viewId: string) => { duplicate = async (viewId: string) => {
const { workspaceSlug, projectId } = this.store.app.router; try {
if (!workspaceSlug || !this.service.duplicate) return; const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug || !this.service.duplicate) return;
const view = await this.service.duplicate(workspaceSlug, viewId, projectId); const view = await this.service.duplicate(workspaceSlug, viewId, projectId);
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));
}); });
} catch {}
}; };
} }

View File

@ -10,7 +10,6 @@ import {
TViewDisplayFilters, TViewDisplayFilters,
TViewDisplayProperties, TViewDisplayProperties,
TViewFilterProps, TViewFilterProps,
TViewFilterPartialProps,
TViewAccess, TViewAccess,
} from "@plane/types"; } from "@plane/types";
// helpers // helpers
@ -21,17 +20,18 @@ type TLoader = "submitting" | "submit" | undefined;
export type TViewStore = TView & { export type TViewStore = TView & {
// observables // observables
loader: TLoader; loader: TLoader;
filtersToUpdate: TViewFilterPartialProps; filtersToUpdate: Partial<TView>;
// computed // computed
appliedFilters: TViewFilterProps | undefined; appliedFilters: TViewFilterProps | undefined;
appliedFiltersQueryParams: string | undefined; appliedFiltersQueryParams: string | undefined;
// helper actions // helper actions
setName: (name: string) => void; setName: (name: string) => void;
setDescription: (description: string) => void;
setFilters: (filters: Partial<TViewFilters>) => void; setFilters: (filters: Partial<TViewFilters>) => void;
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void; setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
setDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) => void; setDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) => void;
resetFilterChanges: () => void; resetChanges: () => void;
saveFilterChanges: () => void; saveChanges: () => Promise<void>;
// actions // actions
lockView: () => Promise<void>; lockView: () => Promise<void>;
unlockView: () => Promise<void>; unlockView: () => Promise<void>;
@ -60,15 +60,16 @@ export class ViewStore extends FiltersHelper implements TViewStore {
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_local_view: boolean = false;
is_create: boolean = false; is_create: boolean = false;
is_editable: boolean = false;
loader: TLoader = undefined; loader: TLoader = undefined;
filtersToUpdate: TViewFilterPartialProps = { filtersToUpdate: Partial<TView> = {
filters: {}, name: "",
display_filters: {}, description: "",
display_properties: {}, filters: undefined,
display_filters: undefined,
display_properties: undefined,
}; };
constructor(private store: RootStore, _view: TView, private service: TViewService) { constructor(private store: RootStore, _view: TView, private service: TViewService) {
@ -92,8 +93,10 @@ 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_local_view = _view.is_local_view;
this.is_create = _view.is_create; this.is_create = _view.is_create;
this.is_editable = _view.is_editable;
makeObservable(this, { makeObservable(this, {
// observables // observables
@ -118,6 +121,7 @@ export class ViewStore extends FiltersHelper implements TViewStore {
updated_at: observable.ref, updated_at: observable.ref,
is_local_view: observable.ref, is_local_view: observable.ref,
is_create: observable.ref, is_create: observable.ref,
is_editable: observable.ref,
loader: observable.ref, loader: observable.ref,
filtersToUpdate: observable, filtersToUpdate: observable,
// computed // computed
@ -128,8 +132,8 @@ export class ViewStore extends FiltersHelper implements TViewStore {
setFilters: action, setFilters: action,
setDisplayFilters: action, setDisplayFilters: action,
setDisplayProperties: action, setDisplayProperties: action,
resetFilterChanges: action, resetChanges: action,
saveFilterChanges: action, saveChanges: action,
// actions // actions
update: action, update: action,
lockView: action, lockView: action,
@ -158,7 +162,13 @@ export class ViewStore extends FiltersHelper implements TViewStore {
// helper actions // helper actions
setName = (name: string) => { setName = (name: string) => {
runInAction(() => { runInAction(() => {
this.name = name; this.filtersToUpdate.name = name;
});
};
setDescription = (description: string) => {
runInAction(() => {
this.filtersToUpdate.description = description;
}); });
}; };
@ -188,19 +198,15 @@ export class ViewStore extends FiltersHelper implements TViewStore {
if (layout === "spreadsheet" && sub_issue === true) display_filters.sub_issue = false; if (layout === "spreadsheet" && sub_issue === true) display_filters.sub_issue = false;
runInAction(() => { runInAction(() => {
this.loader = "submit";
Object.keys(display_filters).forEach((key) => { Object.keys(display_filters).forEach((key) => {
const _key = key as keyof TViewDisplayFilters; const _key = key as keyof TViewDisplayFilters;
set(this.filtersToUpdate, ["display_filters", _key], display_filters[_key]); set(this.filtersToUpdate, ["display_filters", _key], display_filters[_key]);
}); });
}); });
console.log("this.filtersToUpdate", this.filtersToUpdate?.display_filters?.layout);
}; };
setDisplayProperties = async (display_properties: Partial<TViewDisplayProperties>) => { setDisplayProperties = async (display_properties: Partial<TViewDisplayProperties>) => {
runInAction(() => { runInAction(() => {
this.loader = "submit";
Object.keys(display_properties).forEach((key) => { Object.keys(display_properties).forEach((key) => {
const _key = key as keyof TViewDisplayProperties; const _key = key as keyof TViewDisplayProperties;
set(this.filtersToUpdate, ["display_properties", _key], display_properties[_key]); set(this.filtersToUpdate, ["display_properties", _key], display_properties[_key]);
@ -208,84 +214,114 @@ export class ViewStore extends FiltersHelper implements TViewStore {
}); });
}; };
resetFilterChanges = () => { resetChanges = () => {
runInAction(() => { runInAction(() => {
this.loader = undefined; this.loader = undefined;
this.filtersToUpdate = { this.filtersToUpdate = {
filters: {}, name: this.name,
display_filters: {}, description: this.description,
display_properties: {}, filters: this.filters,
display_filters: this.display_filters,
display_properties: this.display_properties,
}; };
}); });
}; };
saveFilterChanges = async () => { saveChanges = async () => {
this.loader = "submitting"; try {
if (this.appliedFilters) await this.update(this.appliedFilters); this.loader = "submitting";
this.loader = undefined; 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 // actions
lockView = async () => { lockView = async () => {
const { workspaceSlug, projectId } = this.store.app.router; try {
if (!workspaceSlug || !this.id || !this.service.lock) return; 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); const view = await this.service.lock(workspaceSlug, this.id, projectId);
if (!view) return; if (!view) return;
runInAction(() => { runInAction(() => {
this.is_locked = view.is_locked; this.is_locked = view.is_locked;
}); });
} catch {
this.is_locked = this.is_locked;
}
}; };
unlockView = async () => { unlockView = async () => {
const { workspaceSlug, projectId } = this.store.app.router; try {
if (!workspaceSlug || !this.id || !this.service.unlock) return; 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); const view = await this.service.unlock(workspaceSlug, this.id, projectId);
if (!view) return; if (!view) return;
runInAction(() => { runInAction(() => {
this.is_locked = view.is_locked; this.is_locked = view.is_locked;
}); });
} catch {
this.is_locked = this.is_locked;
}
}; };
makeFavorite = async () => { makeFavorite = async () => {
const { workspaceSlug, projectId } = this.store.app.router; try {
if (!workspaceSlug || !this.id || !this.service.makeFavorite) return; 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); const view = await this.service.makeFavorite(workspaceSlug, this.id, projectId);
if (!view) return; if (!view) return;
runInAction(() => { runInAction(() => {
this.is_favorite = view.is_locked; this.is_favorite = view.is_locked;
}); });
} catch {
this.is_favorite = this.is_favorite;
}
}; };
removeFavorite = async () => { removeFavorite = async () => {
const { workspaceSlug, projectId } = this.store.app.router; try {
if (!workspaceSlug || !this.id || !this.service.removeFavorite) return; 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); const view = await this.service.removeFavorite(workspaceSlug, this.id, projectId);
if (!view) return; if (!view) return;
runInAction(() => { runInAction(() => {
this.is_favorite = view.is_locked; this.is_favorite = view.is_locked;
}); });
} catch {
this.is_favorite = this.is_favorite;
}
}; };
update = async (viewData: Partial<TView>) => { update = async (viewData: Partial<TView>) => {
const { workspaceSlug, projectId } = this.store.app.router; try {
if (!workspaceSlug || !this.id) return; const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug || !this.id) return;
const view = await this.service.update(workspaceSlug, this.id, viewData, projectId); const view = await this.service.update(workspaceSlug, this.id, viewData, projectId);
if (!view) return; if (!view) return;
runInAction(() => { runInAction(() => {
Object.keys(viewData).forEach((key) => { Object.keys(viewData).forEach((key) => {
const _key = key as keyof TView; const _key = key as keyof TView;
set(this, _key, viewData[_key]); set(this, _key, viewData[_key]);
});
}); });
}); } catch {
this.resetChanges();
}
}; };
} }