chore: updated view create and edit flow

This commit is contained in:
gurusainath 2024-02-17 22:14:01 +05:30
parent 4783e791ec
commit 9a60cfd44e
15 changed files with 132 additions and 62 deletions

View File

@ -45,11 +45,11 @@ export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => {
if (!propertyValues || propertyValues.length <= 0) return <></>;
return (
<div className="relative flex items-center gap-2 border border-custom-border-200 rounded p-1 px-2 min-h-[32px]">
<div className="relative flex items-center gap-2 border border-custom-border-200 rounded p-1 px-1.5">
<div className="flex-shrink-0 text-xs text-custom-text-200 capitalize">{filterKey.replaceAll("_", " ")}</div>
<div className="relative flex items-center gap-1.5 flex-wrap">
{propertyVisibleCount && propertyValues.length >= propertyVisibleCount ? (
<div className="text-xs font-medium bg-custom-primary-100/20 rounded relative flex items-center gap-1 p-1 px-2">
<div className="text-xs bg-custom-primary-100/20 rounded relative flex items-center gap-1 p-1 px-2">
<div className="flex-shrink-0 w-4-h-4">{currentDefaultFilterDetails?.icon}</div>
<div className="whitespace-nowrap">
{propertyValues.length} {currentDefaultFilterDetails?.label}

View File

@ -55,7 +55,7 @@ export const ViewFiltersEditDropdown: FC<TViewFiltersEditDropdown> = observer((p
key: "save_as_new",
label: "Save as new view",
onClick: () => {
viewOperations.localViewCreateEdit(undefined, "SAVE_AS_NEW");
viewOperations.localViewCreateEdit(viewId, "SAVE_AS_NEW");
},
},
{
@ -65,7 +65,7 @@ export const ViewFiltersEditDropdown: FC<TViewFiltersEditDropdown> = observer((p
onClick: () => viewDetailStore?.resetChanges(),
},
],
[viewOperations, viewDetailStore]
[viewId, viewOperations, viewDetailStore]
);
if (viewDetailStore?.is_local_view) return <></>;

View File

@ -1,6 +1,7 @@
import { FC, Fragment, useCallback, useEffect, useMemo, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import isEmpty from "lodash/isEmpty";
// hooks
import { useView, useViewDetail } from "hooks/store";
import useToast from "hooks/use-toast";
@ -29,7 +30,7 @@ import {
} from "constants/view";
// types
import { TViewOperations } from "./types";
import { TViewTypes } from "@plane/types";
import { TViewFilters, TViewTypes } from "@plane/types";
type TGlobalViewRoot = {
workspaceSlug: string;
@ -98,6 +99,23 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
}
},
update: async () => {
try {
await viewStore?.update();
handleViewOperationsToggle(undefined, undefined);
setToastAlert({
type: "success",
title: "Success!",
message: "View created successfully.",
});
} catch {
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong. Please try again later or contact the support team.",
});
}
},
saveChanges: async () => {
try {
await viewDetailStore?.saveChanges();
handleViewOperationsToggle(undefined, undefined);
@ -152,10 +170,12 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
[viewStore, viewDetailStore, handleViewOperationsToggle, setToastAlert, workspaceSlug, projectId]
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const applyFIltersFromRouter = () => {
const routerParams: Partial<Record<keyof TViewFilters, string[]>> = {};
if (workspaceSlug && viewId && Object.values(ELocalViews).includes(viewId as ELocalViews)) {
const routerQueryParams = { ...router.query };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { ["workspaceSlug"]: _workspaceSlug, ["viewId"]: _viewId, ...filters } = routerQueryParams;
@ -165,20 +185,19 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
"filters"
);
Object.keys(filters).forEach((key) => {
const filterKey: any = key;
const filterValue = filters[key]?.toString() || undefined;
if (filterKey && filterValue && acceptedFilters.includes(filterKey)) {
const _filterValues = filterValue.split(",");
_filterValues.forEach((element) => {
console.log("filterKey", filterKey);
console.log("element", element);
viewDetailStore?.setFilters(filterKey, element);
});
Object.keys(filters).forEach((key: string) => {
const filterKey: keyof TViewFilters | undefined = key as keyof TViewFilters;
if (filterKey) {
const filterValue = filters[key]?.toString() || undefined;
if (filterKey && filterValue && acceptedFilters.includes(filterKey)) {
const _filterValues = filterValue.split(",");
routerParams[filterKey] = _filterValues;
}
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
return isEmpty(routerParams) ? undefined : routerParams;
};
// fetch all views
@ -196,8 +215,7 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
// fetch view by id
useEffect(() => {
const fetchViewByViewId = async () => {
await viewStore?.fetchById(workspaceSlug, projectId, viewId);
// applyFIltersFromRouter();
await viewStore?.fetchById(workspaceSlug, projectId, viewId, applyFIltersFromRouter());
};
if (workspaceSlug && viewId && viewType && viewStore) {
fetchViewByViewId();
@ -205,8 +223,6 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [workspaceSlug, projectId, viewId, viewType, viewStore]);
console.log("viewStore? -->", viewStore?.viewMapCEN?.id);
return (
<div className="relative w-full h-full">
{viewStore?.loader && viewStore?.loader === "view-loader" ? (

View File

@ -8,7 +8,7 @@ export type TViewOperations = {
localViewCreateEdit: (viewId: string | undefined, status: TViewCRUD) => void;
fetch: () => Promise<void>;
create: (data: Partial<TView>) => Promise<void>;
create: () => Promise<void>;
update: () => Promise<void>;
remove: (viewId: string) => Promise<void>;
duplicate: (viewId: string) => Promise<void>;

View File

@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react";
import { Briefcase, Globe2, Plus, X } from "lucide-react";
// hooks
import { useViewDetail, useProject } from "hooks/store";
import { useViewDetail, useProject, useView } from "hooks/store";
// components
import { ViewAppliedFiltersRoot, ViewFiltersDropdown } from "../";
// ui
@ -27,11 +27,11 @@ type TViewCreateEditForm = {
export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewPageType, viewOperations, isLocalView } = props;
// hooks
const viewStore = useView(workspaceSlug, projectId, viewType);
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType, isLocalView);
const { getProjectById } = useProject();
// states
const [modalToggle, setModalToggle] = useState(false);
const [loader, setLoader] = useState(false);
const modalOpen = useCallback(() => setModalToggle(true), [setModalToggle]);
const modalClose = useCallback(() => {
@ -46,7 +46,15 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
}, [viewId, modalOpen, modalClose]);
const onContinue = async () => {
setLoader(true);
if (!viewDetailStore) return;
if (viewDetailStore?.id === "create") {
await viewOperations.create();
modalClose();
} else {
console.log("coming here...");
await viewOperations.update();
// modalClose();
}
// if (viewDetailStore?.id != "create") {
// const payload = viewDetailStore?.filtersToUpdate;
// await viewOperations.create(payload);
@ -57,7 +65,6 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
// await viewOperations.update();
// modalClose();
// }
setLoader(false);
};
const projectDetails = projectId ? getProjectById(projectId) : undefined;
@ -168,11 +175,17 @@ 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={modalClose} disabled={loader}>
<Button
variant="neutral-primary"
onClick={modalClose}
disabled={viewStore?.loader == "create-submitting"}
>
Cancel
</Button>
<Button variant="primary" onClick={onContinue} disabled={loader}>
{loader ? `Saving...` : `${viewDetailStore?.id === "create" ? `Create` : `Update`} View`}
<Button variant="primary" onClick={onContinue} disabled={viewStore?.loader == "create-submitting"}>
{viewStore?.loader == "create-submitting"
? `Saving...`
: `${viewDetailStore?.id === "create" ? `Save` : `Update`} View`}
</Button>
</div>
</Dialog.Panel>

View File

@ -31,7 +31,7 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
const handleViewTabsVisibility = () => {
const tabContainer = document.getElementById("tab-container");
const tabItemViewMore = document.getElementById("tab-item-view-more");
const itemWidth = 122;
const itemWidth = 120;
if (!tabContainer || !tabItemViewMore) return;
const containerWidth = tabContainer.clientWidth;
@ -83,7 +83,7 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
viewOperations={viewOperations}
baseRoute={baseRoute}
>
<div className="text-sm font-semibold mb-1 p-2 px-2.5 text-custom-text-200 cursor-pointer hover:bg-custom-background-80 whitespace-nowrap rounded relative flex items-center gap-1">
<div className="text-sm 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>

View File

@ -32,7 +32,7 @@ export const ViewDropdownItem: FC<TViewDropdownItem> = (props) => {
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
className={`w-full px-1 pl-2 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`}
`}
>
@ -45,7 +45,7 @@ export const ViewDropdownItem: FC<TViewDropdownItem> = (props) => {
)}
<Link
href={`${baseRoute}/${viewDetailStore?.id}`}
className={`w-full h-full overflow-hidden relative flex items-center gap-1
className={`w-full overflow-hidden relative flex items-center gap-1 py-1.5
${
currentViewId === viewDetailStore?.id
? `text-custom-text-100`
@ -53,11 +53,11 @@ export const ViewDropdownItem: FC<TViewDropdownItem> = (props) => {
}
`}
>
<div className="flex-shrink-0 w-5 h-5 relative flex justify-center items-center">
<div className="flex-shrink-0 w-4 h-4 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">
<div className="w-full line-clamp-1 truncate overflow-hidden inline-block whitespace-nowrap text-sm">
{viewDetailStore?.name}
</div>
</Link>
@ -65,7 +65,7 @@ export const ViewDropdownItem: FC<TViewDropdownItem> = (props) => {
</Tooltip>
{isEditable && (
<div className="flex-shrink-0 w-5 h-5 relative rounded flex justify-center items-center hover:bg-custom-background-100">
<div className="flex-shrink-0 w-5 h-5 relative rounded flex justify-center items-center hover:bg-custom-background-100 invisible group-hover:visible">
<MoreVertical className="h-3.5 w-3.5 flex-shrink-0" />
</div>
)}

View File

@ -105,7 +105,7 @@ export const ViewDropdown: FC<TViewDropdown> = (props) => {
<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"
className="w-full bg-transparent py-0.5 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..."
@ -133,7 +133,7 @@ export const ViewDropdown: FC<TViewDropdown> = (props) => {
</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"
className="relative flex justify-center items-center gap-1 rounded p-1 py-1 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, "CREATE")}
>
<Plus className="w-3 h-3" />

View File

@ -24,11 +24,11 @@ export const ViewItem: FC<TViewItem> = observer((props) => {
if (!viewDetailStore) return <></>;
return (
<div className="space-y-0.5 relative h-full flex flex-col justify-between">
<div className="space-y-0.5 relative h-full flex flex-col justify-between ">
<Tooltip tooltipContent={viewDetailStore?.name} position="top">
<Link
href={`${baseRoute}/${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
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()}
@ -36,7 +36,7 @@ export const ViewItem: FC<TViewItem> = observer((props) => {
<div className={`flex-shrink-0 rounded-sm relative w-3 h-3 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">
<div className="w-full max-w-[80px] inline-block text-sm line-clamp-1 truncate overflow-hidden">
{viewDetailStore?.name}
</div>
</Link>

View File

@ -25,15 +25,15 @@ export const useViewDetail = (
if (isEditable) return context.view.workspacePrivateViewStore.viewMapCEN;
return context.view.workspacePrivateViewStore.viewById(viewId);
case VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS:
if (isEditable) return context.view.workspacePrivateViewStore.viewMapCEN;
if (isEditable) return context.view.workspacePublicViewStore.viewMapCEN;
return context.view.workspacePublicViewStore.viewById(viewId);
case VIEW_TYPES.PROJECT_PRIVATE_VIEWS:
if (!projectId) return undefined;
if (isEditable) return context.view.workspacePrivateViewStore.viewMapCEN;
if (isEditable) return context.view.projectPrivateViewStore.viewMapCEN;
return context.view.projectPrivateViewStore.viewById(viewId);
case VIEW_TYPES.PROJECT_PUBLIC_VIEWS:
if (!projectId) return undefined;
if (isEditable) return context.view.workspacePrivateViewStore.viewMapCEN;
if (isEditable) return context.view.projectPublicViewStore.viewMapCEN;
return context.view.projectPublicViewStore.viewById(viewId);
default:
return undefined;

View File

@ -1,9 +0,0 @@
type TUseViewsProps = {};
export const useViews = (issueId: string | undefined): TUseViewsProps => {
console.log("issueId", issueId);
return {
issueId,
};
};

View File

@ -38,7 +38,6 @@ const ProjectPublicViewPage: NextPageWithLayout = observer(() => {
async () => {
if (workspaceSlug && projectId) {
await viewStore?.fetch(workspaceSlug?.toString(), projectId?.toString());
console.log("viewStore", viewStore?.viewIds);
}
}
);

View File

@ -38,7 +38,6 @@ const ProjectPublicViewPage: NextPageWithLayout = observer(() => {
async () => {
if (workspaceSlug && projectId) {
await viewStore?.fetch(workspaceSlug.toString(), projectId.toString());
console.log("viewStore", viewStore?.viewIds);
}
}
);

View File

@ -9,7 +9,7 @@ import { RootStore } from "store/root.store";
import { ViewStore } from "./view.store";
// types
import { TUserViewService, TViewService } from "services/view/types";
import { TView, TViewTypes } from "@plane/types";
import { TView, TViewFilters, TViewTypes } from "@plane/types";
// constants
import { EViewPageType, TViewCRUD, generateViewStoreKey, viewLocalPayload } from "constants/view";
@ -18,6 +18,7 @@ export type TLoader =
| "view-mutation-loader"
| "view-detail-loader"
| "create-submitting"
| "edit-submitting"
| "delete-submitting"
| "duplicate-submitting"
| undefined;
@ -33,10 +34,16 @@ type TViewRootStore = {
localView: () => ViewStore | undefined;
// actions
fetch: (workspaceSlug: string, projectId: string | undefined, _loader?: TLoader) => Promise<void>;
fetchById: (workspaceSlug: string, projectId: string | undefined, viewId: string) => Promise<void>;
fetchById: (
workspaceSlug: string,
projectId: string | undefined,
viewId: string,
defaultFilters?: Partial<Record<keyof TViewFilters, string[]>> | undefined
) => Promise<void>;
remove: (viewId: string) => Promise<void>;
localViewHandler: (viewId: string | undefined, status: TViewCRUD) => void;
create: () => Promise<void>;
update: () => Promise<void>;
duplicate: (viewId: string) => Promise<void>;
};
@ -66,6 +73,7 @@ export class ViewRootStore implements TViewRootStore {
fetch: action,
fetchById: action,
create: action,
update: action,
remove: action,
duplicate: action,
});
@ -83,7 +91,7 @@ export class ViewRootStore implements TViewRootStore {
let apiViews = views.filter((view) => !view.is_local_view);
apiViews = reverse(sortBy(apiViews, "sort_order"));
const _viewIds = [...localViews.map((view) => view.id), ...apiViews.map((view) => view.id)];
return _viewIds as string[];
return _viewIds.filter((view) => view !== undefined) as string[];
}
viewById = computedFn((viewId: string) => {
@ -136,7 +144,12 @@ export class ViewRootStore implements TViewRootStore {
}
};
fetchById = async (workspaceSlug: string, projectId: string | undefined, viewId: string) => {
fetchById = async (
workspaceSlug: string,
projectId: string | undefined,
viewId: string,
defaultFilters: Partial<Record<keyof TViewFilters, string[]>> | undefined = undefined
) => {
try {
runInAction(() => (this.loader = "view-detail-loader"));
@ -154,6 +167,7 @@ export class ViewRootStore implements TViewRootStore {
const currentView = { ...this.viewById(viewId) } as TView;
if (!currentView) return;
view = currentView;
defaultFilters && (view.filters = defaultFilters as TViewFilters);
} else {
const currentView = await this.service.fetchById(workspaceSlug, viewId, projectId);
if (!currentView) return;
@ -221,7 +235,14 @@ export class ViewRootStore implements TViewRootStore {
_view = cloneDeep(this.viewMap?.[viewRootSlug]?.[viewId]);
} else if (status === "SAVE_AS_NEW") {
if (!viewId) return;
_view = cloneDeep(this.viewMap?.[viewRootSlug]?.[viewId]);
const clonedView = cloneDeep(this.viewMap?.[viewRootSlug]?.[viewId]);
_view = {
id: "create",
name: clonedView?.name,
filters: clonedView?.filtersToUpdate?.filters,
display_filters: clonedView?.filtersToUpdate?.display_filters,
display_properties: clonedView?.filtersToUpdate?.display_properties,
};
} else return;
runInAction(() => {
@ -243,7 +264,39 @@ export class ViewRootStore implements TViewRootStore {
const viewRootSlug = generateViewStoreKey(workspaceSlug, projectId, this.viewType);
const view = await this.service.create(workspaceSlug, this.viewMapCEN, projectId);
const view = await this.service.create(workspaceSlug, this.viewMapCEN.filtersToUpdate, projectId);
if (!view) return;
runInAction(() => {
if (view.id)
set(
this.viewMap,
[viewRootSlug, view.id],
new ViewStore(this.store, view, this.service, this.userService, this.viewPageType)
);
this.viewMapCEN = undefined;
this.loader = undefined;
});
} catch {
runInAction(() => (this.loader = undefined));
}
};
update = async () => {
try {
const { workspaceSlug, projectId } = this.store.view;
if (!workspaceSlug || !this.viewMapCEN || !this.viewMapCEN.id) return;
runInAction(() => (this.loader = "edit-submitting"));
const viewRootSlug = generateViewStoreKey(workspaceSlug, projectId, this.viewType);
const view = await this.service.update(
workspaceSlug,
this.viewMapCEN.id,
this.viewMapCEN.filtersToUpdate,
projectId
);
if (!view) return;
runInAction(() => {

View File

@ -302,7 +302,6 @@ export class ViewStore extends FiltersHelper implements TViewStore {
saveChanges = async () => {
try {
if (!this.id) return;
console.log("coming here");
await this.update(this.filtersToUpdate);
} catch {
Object.keys(this.filtersToUpdate).forEach((key) => {