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
is_local_view: boolean;
is_create: boolean;
is_editable: boolean;
};

View File

@ -48,7 +48,9 @@ export const ProfileSidebar = observer(() => {
];
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 ? (
<>
<div className="relative h-32">

View File

@ -5,11 +5,11 @@ import { CheckCircle } from "lucide-react";
import { useView, useViewDetail } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { ViewRoot, ViewCreateEdit, ViewFiltersRoot, ViewAppliedFiltersRoot, ViewLayoutRoot } from ".";
import { ViewRoot, ViewCreateEditForm, ViewAppliedFiltersRoot, ViewLayoutRoot } from ".";
// ui
import { Spinner } from "@plane/ui";
// constants
import { VIEW_TYPES } from "constants/view";
import { VIEW_TYPES, viewLocalPayload } from "constants/view";
// types
import { TViewOperations } from "./types";
import { TView, TViewFilters, TViewDisplayFilters, TViewDisplayProperties } from "@plane/types";
@ -23,6 +23,7 @@ type TAllIssuesViewRoot = {
export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId } = props;
// states
const [currentCreateEditViewId, setCurrentCreateEditViewId] = useState<string | undefined>(undefined);
const [viewType, setViewType] = useState(VIEW_TYPES.WORKSPACE_VIEWS);
const workspaceViewTabOptions = [
{
@ -43,18 +44,45 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
const viewOperations: TViewOperations = useMemo(
() => ({
localViewCreate: (data) => viewStore?.localViewCreate(data),
clearLocalView: (viewId: string) => viewStore?.clearLocalView(viewId),
setName: (name: string) => viewDetailStore?.setName(name),
setDescription: (name: string) => viewDetailStore?.setDescription(name),
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),
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(),
create: async (data: Partial<TView>) => {
try {
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 {
setToastAlert({ title: "Error", message: "Error creating view", type: "error" });
}
@ -77,15 +105,15 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
</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">
<div className="relative inline-flex items-center rounded border border-custom-border-200 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"
? "text-custom-text-100 bg-custom-background-100"
: "text-custom-text-300 bg-custom-background-80 hover:text-custom-text-100"
}`}
onClick={tab.onClick}
>
@ -101,23 +129,18 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
</div>
) : (
<>
<ViewRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewOperations={viewOperations}
/>
{/* <ViewFiltersRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewOperations={viewOperations}
/> */}
<div className="border-b border-custom-border-200">
<ViewRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewOperations={viewOperations}
/>
</div>
<div className="p-5 border-b border-custom-border-200 relative flex gap-2">
<div className="w-full">
{/* <div className="w-full">
<ViewAppliedFiltersRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
@ -125,9 +148,9 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
viewType={viewType}
viewOperations={viewOperations}
/>
</div>
</div> */}
<div className="flex-shrink-0 h-full">
{/* <div className="flex-shrink-0 h-full">
<ViewLayoutRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
@ -135,32 +158,34 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
viewType={viewType}
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
</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
</div>
</div> */}
{!viewDetailStore?.is_local_view && (
{/* {!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>Edit</div>
</div>
)}
)} */}
</div>
</>
)}
{currentCreateEditViewId != undefined && (
<ViewCreateEditForm
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={currentCreateEditViewId}
viewType={viewType}
viewOperations={viewOperations}
/>
)}
</div>
);
});

View File

@ -1,4 +1,4 @@
import { FC } from "react";
import { FC, Fragment } from "react";
import { observer } from "mobx-react-lite";
import isEmpty from "lodash/isEmpty";
import { X } from "lucide-react";
@ -33,21 +33,20 @@ export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => {
if (!filterKeyValue || filterKeyValue.length <= -1) return <></>;
return (
<div
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="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}
/>
<Fragment key={filterId}>
<ViewAppliedFiltersItem
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
filterKey={filterKey}
filterId={filterId}
/>
</Fragment>
))}
</div>
<div

View File

@ -1,7 +1,7 @@
import { ReactNode } from "react";
import isEmpty from "lodash/isEmpty";
// types
import { TFilters } from "@plane/types";
import { TViewFilters } from "@plane/types";
type TComputedAppliedFilters = {
key: string;
@ -10,7 +10,7 @@ type TComputedAppliedFilters = {
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) {
case "project":
return [];
@ -45,12 +45,12 @@ export const generateTitle = (title: string) =>
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
export const constructAppliedFilters = (filters: TFilters): TComputedAppliedFilters => {
export const constructAppliedFilters = (filters: TViewFilters): TComputedAppliedFilters => {
const appliedFilters: TComputedAppliedFilters = [];
if (filters && !isEmpty(filters)) {
Object.keys(filters).forEach((_filterKey) => {
const _key = _filterKey as keyof TFilters;
const _key = _filterKey as keyof TViewFilters;
const _value = filters[_key];
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 { X } from "lucide-react";
import isEmpty from "lodash/isEmpty";
@ -43,13 +43,15 @@ export const ViewAppliedFiltersRoot: FC<TViewAppliedFiltersRoot> = observer((pro
{filterKeys.map((key) => {
const filterKey = key as keyof TViewFilters;
return (
<ViewAppliedFilters
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
filterKey={filterKey}
/>
<Fragment key={filterKey}>
<ViewAppliedFilters
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
filterKey={filterKey}
/>
</Fragment>
);
})}
<div

View File

@ -3,7 +3,10 @@ export * from "./all-issues-root";
// views
export * from "./views/root";
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";
// layouts

View File

@ -1,11 +1,17 @@
import { TView } from "@plane/types";
export type TViewOperations = {
localViewCreate: (data: TView) => void;
clearLocalView: (viewId: string) => void;
setName: (name: string) => void;
setDescription: (description: string) => void;
setFilters: (filters: Partial<TViewFilters>) => void;
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
setDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) => void;
localViewCreateEdit: (viewId: string | undefined) => void;
localViewCreateEditClear: (viewId: string | undefined) => Promise<void>;
fetch: () => 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 { Dialog, Transition } from "@headlessui/react";
import { Trash2, Plus, X } from "lucide-react";
import { Briefcase, Globe2, Plus, X } from "lucide-react";
// hooks
import { useViewDetail } from "hooks/store";
import { useViewDetail, useProject } from "hooks/store";
// components
import { ViewAppliedFiltersRoot } from "../";
// ui
import { Input, Button } from "@plane/ui";
// types
import { TView, TViewTypes } from "@plane/types";
import { TViewTypes } from "@plane/types";
import { TViewOperations } from "../types";
type TViewCreateEditForm = {
@ -18,28 +18,50 @@ type TViewCreateEditForm = {
viewId: string;
viewType: TViewTypes;
viewOperations: TViewOperations;
modalToggle: boolean;
handleModalClose: () => void;
onSubmit: (viewData: Partial<TView>) => void;
};
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
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 payload: Partial<TView> = {
id: viewDetailStore?.id,
name: viewDetailStore?.name,
filters: viewDetailStore?.filters,
};
onSubmit(payload);
setLoader(true);
if (viewDetailStore?.is_create) {
const payload = viewDetailStore?.filtersToUpdate;
await viewOperations.create(payload);
modalClose();
} 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 (
<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
as={Fragment}
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">
<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="flex-shrink-0 relative flex justify-center items-center w-4 h-4 overflow-hidden">
<Trash2 className="w-3.5 h-3.5" />
{projectId && projectDetails ? (
<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-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 className="text-xs uppercase">Project Identifier</div>
</div> */}
<div className="">Create View</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="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 className="p-3 px-5">
@ -79,7 +110,7 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
id="name"
name="name"
type="text"
value={viewDetailStore?.name || ""}
value={viewDetailStore?.filtersToUpdate?.name || ""}
onChange={(e) => {
viewDetailStore?.setName(e.target.value);
}}
@ -90,13 +121,13 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
</div>
<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">
<Plus className="w-3 h-3" />
</div>
<div className="text-xs">Filters</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="flex-shrink-0 relative flex justify-center items-center w-4 h-4 overflow-hidden">
<X className="w-3 h-3" />
@ -115,11 +146,11 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
</div>
<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
</Button>
<Button variant="primary" onClick={onContinue}>
Create View
<Button variant="primary" onClick={onContinue} disabled={loader}>
{loader ? `Saving...` : `${viewDetailStore?.is_create ? `Create` : `Update`} View`}
</Button>
</div>
</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 { Plus } from "lucide-react";
// hooks
import { useView } from "hooks/store";
// components
import { ViewItem, ViewCreateEdit } from "../";
import { ViewItem, ViewDropdown } from "../";
// ui
import { Button } from "@plane/ui";
// types
import { TViewOperations } from "../types";
import { TViewTypes } from "@plane/types";
@ -20,31 +23,84 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
// hooks
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 (
<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 && (
<div key={`views_list_${viewId}`} className="relative flex items-center w-full overflow-x-auto">
{viewStore?.viewIds.map((_viewId) => (
<ViewItem
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
viewItemId={_viewId}
/>
<div
key={`views_list_${viewId}`}
id="tab-container"
className="relative flex items-center w-full overflow-hidden"
>
{viewStore?.viewIds?.slice(0, itemsToRenderViewsCount || viewStore?.viewIds.length).map((_viewId) => (
<Fragment key={_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 className="flex-shrink-0 my-auto pb-1">
<ViewCreateEdit
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={undefined}
viewType={viewType}
viewOperations={viewOperations}
/>
<Button size="sm" prependIcon={<Plus />} onClick={() => viewOperations?.localViewCreateEdit(undefined)}>
New View
</Button>
</div>
</div>
);

View File

@ -1,10 +1,10 @@
import { FC } from "react";
import { FC, Fragment } from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
// hooks
import { useView } from "hooks/store";
import { useView, useViewDetail } from "hooks/store";
// ui
import { PhotoFilterIcon } from "@plane/ui";
import { PhotoFilterIcon, Tooltip } from "@plane/ui";
// types
import { TViewTypes } from "@plane/types";
@ -19,27 +19,31 @@ type TViewItem = {
export const ViewItem: FC<TViewItem> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewItemId } = props;
// hooks
const viewStore = useView(workspaceSlug, projectId, viewType);
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewItemId, viewType);
const view = viewStore?.viewById(viewItemId);
if (!view) return <></>;
if (!viewDetailStore) 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
<div className="space-y-0.5 relative h-full flex flex-col justify-between">
<Tooltip tooltipContent={viewDetailStore?.name} position="top">
<Link
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`}
`}
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>
onClick={(e) => viewItemId === viewId && e.preventDefault()}
>
<div
className={`flex-shrink-0 rounded-sm relative w-5 h-5 flex justify-center items-center overflow-hidden
${viewItemId === viewId ? `bg-transparent` : `bg-custom-background-80`}
`}
>
<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">
{viewDetailStore?.name}
</div>
</Link>
</Tooltip>
<div className={`border-b-2 ${viewItemId === viewId ? `border-custom-primary-100` : `border-transparent`}`} />
</div>
);

View File

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

View File

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

View File

@ -30,13 +30,7 @@ export class GlobalViewRootStore {
cycleUserViewStore?: userViewRootStore;
constructor(private store: RootStore) {
const defaultViews: any[] = [
{
id: "all-issues",
name: "All Issues",
filters: {},
is_local_view: true,
},
const workspaceViewMeStoreDefaultViews: any[] = [
{
id: "assigned",
name: "Assigned",
@ -62,9 +56,21 @@ export class GlobalViewRootStore {
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.workspaceViewStore = new ViewRootStore(this.store, new WorkspaceViewService(), defaultViews);
this.workspaceViewMeStore = new ViewRootStore(
this.store,
new WorkspaceMeViewService(),
workspaceViewMeStoreDefaultViews
);
this.workspaceViewStore = new ViewRootStore(this.store, new WorkspaceViewService(), workspaceViewStoreDefaultViews);
this.projectViewStore = new ViewRootStore(this.store, new ProjectViewService());
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);
if (!view) return;
runInAction(() => {
if (this.workspaceSlug && view.id)
set(
this.viewMap,
[view.id],
new UserViewStore(view, this.service, this.workspaceSlug, this.projectId, this.featureId)
);
});
// runInAction(() => {
// if (this.workspaceSlug && view.id)
// set(
// this.viewMap,
// [view.id],
// 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 set from "lodash/set";
// store
import { RootStore } from "store/root.store";
// types
import { TUserViewService } from "services/view/types";
import { TViewService } from "services/view/types";
import {
TUserView,
TView,
TViewFilters,
TViewDisplayFilters,
TViewDisplayProperties,
TViewFilterProps,
TViewFilterPartialProps,
TViewAccess,
} from "@plane/types";
// helpers
import { FiltersHelper } from "../helpers/filters_helpers";
type TLoader = "submitting" | "submit" | undefined;
export type TUserViewStore = TUserView & {
export type TUserViewStore = TView & {
// observables
loader: TLoader;
filtersToUpdate: TViewFilterPartialProps;
filtersToUpdate: Partial<TView>;
// computed
appliedFilters: TViewFilterProps | undefined;
appliedFiltersQueryParams: string | undefined;
// helper actions
updateFilters: (filters: Partial<TViewFilters>) => void;
updateDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
updateDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) => void;
resetFilterChanges: () => void;
saveFilterChanges: () => void;
setName: (name: string) => void;
setDescription: (description: string) => void;
setFilters: (filters: Partial<TViewFilters>) => void;
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
setDisplayProperties: (display_properties: Partial<TViewDisplayProperties>) => void;
resetChanges: () => void;
saveChanges: () => Promise<void>;
// actions
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 {
id: string | undefined;
workspace: string | undefined;
project: string | undefined;
module: string | undefined;
cycle: string | undefined;
filters: TViewFilters | undefined;
display_filters: TViewDisplayFilters | undefined;
display_properties: TViewDisplayProperties | undefined;
user: string | undefined;
name: string | undefined;
description: string | undefined;
query: string | undefined;
filters: TViewFilters;
display_filters: TViewDisplayFilters;
display_properties: TViewDisplayProperties;
access: TViewAccess | undefined;
owned_by: string | undefined;
sort_order: number | undefined;
is_locked: boolean = false;
is_pinned: boolean = false;
is_favorite: boolean = false;
created_by: string | undefined;
updated_by: string | undefined;
created_at: Date | undefined;
updated_at: Date | undefined;
is_local_view: boolean = false;
is_create: boolean = false;
is_editable: boolean = false;
loader: TLoader = undefined;
filtersToUpdate: TViewFilterPartialProps = {
filters: {},
display_filters: {},
display_properties: {},
filtersToUpdate: Partial<TView> = {
name: "",
description: "",
filters: undefined,
display_filters: undefined,
display_properties: undefined,
};
constructor(
_view: TUserView,
private service: TUserViewService,
private workspaceSlug: string,
private projectId: string | undefined,
private featureId: string | undefined // moduleId/cycleId
) {
constructor(private store: RootStore, _view: TView, private service: TViewService) {
super();
this.id = _view.id;
this.workspace = _view.workspace;
this.project = _view.project;
this.filters = _view.filters ? this.computedFilters(_view.filters) : undefined;
this.display_filters = _view.display_filters ? this.computedDisplayFilters(_view.display_filters) : undefined;
this.display_properties = _view.display_properties
? this.computedDisplayProperties(_view.display_properties)
: undefined;
this.user = _view.user;
this.name = _view.name;
this.description = _view.description;
this.query = _view.query;
this.filters = this.computedFilters(_view.filters);
this.display_filters = this.computedDisplayFilters(_view.display_filters);
this.display_properties = this.computedDisplayProperties(_view.display_properties);
this.access = _view.access;
this.owned_by = _view.owned_by;
this.sort_order = _view.sort_order;
this.is_locked = _view.is_locked;
this.is_pinned = _view.is_pinned;
this.is_favorite = _view.is_favorite;
this.created_by = _view.created_by;
this.updated_by = _view.updated_by;
this.created_at = _view.created_at;
this.updated_at = _view.updated_at;
this.is_local_view = _view.is_local_view;
this.is_create = _view.is_create;
this.is_editable = _view.is_editable;
makeObservable(this, {
// observables
loader: observable,
filtersToUpdate: observable.ref,
id: observable.ref,
workspace: observable.ref,
project: observable.ref,
name: observable.ref,
description: observable.ref,
query: observable.ref,
filters: observable,
display_filters: observable,
display_properties: observable,
access: observable.ref,
owned_by: observable.ref,
sort_order: observable.ref,
is_locked: observable.ref,
is_pinned: observable.ref,
is_favorite: observable.ref,
created_by: observable.ref,
updated_by: observable.ref,
created_at: observable.ref,
updated_at: observable.ref,
is_local_view: observable.ref,
is_create: observable.ref,
is_editable: observable.ref,
loader: observable.ref,
filtersToUpdate: observable,
// computed
appliedFilters: computed,
appliedFiltersQueryParams: computed,
// helper actions
updateFilters: action,
updateDisplayFilters: action,
updateDisplayProperties: action,
resetFilterChanges: action,
saveFilterChanges: action,
setName: action,
setFilters: action,
setDisplayFilters: action,
setDisplayProperties: action,
resetChanges: action,
saveChanges: action,
// actions
update: action,
lockView: action,
unlockView: action,
});
}
// computed
get appliedFilters() {
return {
filters: this.filters ? this.computedFilters(this.filters, this.filtersToUpdate.filters) : undefined,
display_filters: this.display_filters
? this.computedDisplayFilters(this.display_filters, this.filtersToUpdate.display_filters)
: undefined,
display_properties: this.display_properties
? this.computedDisplayProperties(this.display_properties, this.filtersToUpdate.display_properties)
: undefined,
filters: this.computedFilters(this.filters, this.filtersToUpdate.filters),
display_filters: this.computedDisplayFilters(this.display_filters, this.filtersToUpdate.display_filters),
display_properties: this.computedDisplayProperties(
this.display_properties,
this.filtersToUpdate.display_properties
),
};
}
@ -114,14 +160,29 @@ export class UserViewStore extends FiltersHelper implements TUserViewStore {
}
// helper actions
updateFilters = (filters: Partial<TViewFilters>) => {
setName = (name: string) => {
runInAction(() => {
this.loader = "submit";
this.filtersToUpdate.filters = filters;
this.filtersToUpdate.name = name;
});
};
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 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 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 (sub_group_by === group_by) display_filters.group_by = undefined;
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;
runInAction(() => {
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]);
});
});
};
updateDisplayProperties = async (display_properties: Partial<TViewDisplayProperties>) => {
setDisplayProperties = async (display_properties: Partial<TViewDisplayProperties>) => {
runInAction(() => {
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]);
});
});
};
resetFilterChanges = () => {
resetChanges = () => {
runInAction(() => {
this.loader = undefined;
this.filtersToUpdate = {
filters: {},
display_filters: {},
display_properties: {},
name: this.name,
description: this.description,
filters: this.filters,
display_filters: this.display_filters,
display_properties: this.display_properties,
};
});
};
saveFilterChanges = async () => {
this.loader = "submitting";
if (this.appliedFilters) await this.update(this.appliedFilters);
this.loader = undefined;
saveChanges = async () => {
try {
this.loader = "submitting";
if (this.filtersToUpdate) await this.update(this.filtersToUpdate);
this.loader = undefined;
} catch {
this.loader = undefined;
Object.keys(this.filtersToUpdate).forEach((key) => {
const _key = key as keyof TView;
set(this, _key, this.filtersToUpdate[_key]);
});
}
};
// actions
update = async (viewData: Partial<TViewFilterProps>) => {
if (!this.workspaceSlug || !this.id) return;
lockView = async () => {
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);
if (!view) return;
const view = await this.service.lock(workspaceSlug, this.id, projectId);
if (!view) return;
runInAction(() => {
Object.keys(viewData).forEach((key) => {
const _key = key as keyof TViewFilterProps;
set(this, _key, viewData[_key]);
runInAction(() => {
this.is_locked = view.is_locked;
});
});
} catch {
this.is_locked = this.is_locked;
}
};
unlockView = async () => {
try {
const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug || !this.id || !this.service.unlock) return;
const view = await this.service.unlock(workspaceSlug, this.id, projectId);
if (!view) return;
runInAction(() => {
this.is_locked = view.is_locked;
});
} catch {
this.is_locked = this.is_locked;
}
};
makeFavorite = async () => {
try {
const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug || !this.id || !this.service.makeFavorite) return;
const view = await this.service.makeFavorite(workspaceSlug, this.id, projectId);
if (!view) return;
runInAction(() => {
this.is_favorite = view.is_locked;
});
} catch {
this.is_favorite = this.is_favorite;
}
};
removeFavorite = async () => {
try {
const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug || !this.id || !this.service.removeFavorite) return;
const view = await this.service.removeFavorite(workspaceSlug, this.id, projectId);
if (!view) return;
runInAction(() => {
this.is_favorite = view.is_locked;
});
} catch {
this.is_favorite = this.is_favorite;
}
};
update = async (viewData: Partial<TView>) => {
try {
const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug || !this.id) return;
const view = await this.service.update(workspaceSlug, this.id, viewData, projectId);
if (!view) return;
runInAction(() => {
Object.keys(viewData).forEach((key) => {
const _key = key as keyof TView;
set(this, _key, viewData[_key]);
});
});
} catch {
this.resetChanges();
}
};
}

View File

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

View File

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