From 46a6a599a11e158b3d853e9eb7cd5c9a63821e29 Mon Sep 17 00:00:00 2001 From: gurusainath Date: Mon, 12 Feb 2024 12:30:59 +0530 Subject: [PATCH] chore: updated filters and filter components --- packages/types/src/view/base.d.ts | 8 + .../view/applied-filters/filter-item.tsx | 2 +- .../view/applied-filters/filter.tsx | 2 +- .../view/display-filters/dropdown.tsx | 2 +- web/components/view/filters/dropdown.tsx | 2 +- web/components/view/filters/edit-dropdown.tsx | 124 ++++++++++++++ web/components/view/index.ts | 1 + web/components/view/layout.tsx | 4 +- web/components/view/root.tsx | 106 +++++++++--- web/components/view/types.d.ts | 25 ++- .../view/views/create-edit-form.tsx | 16 +- web/components/view/views/edit-dropdown.tsx | 156 +++++++++++++++--- web/components/view/views/root.tsx | 14 +- web/components/view/views/view-item.tsx | 8 +- web/store/view/view.store.ts | 59 ++++--- 15 files changed, 420 insertions(+), 109 deletions(-) create mode 100644 web/components/view/filters/edit-dropdown.tsx diff --git a/packages/types/src/view/base.d.ts b/packages/types/src/view/base.d.ts index 35363714c..0f017fa2d 100644 --- a/packages/types/src/view/base.d.ts +++ b/packages/types/src/view/base.d.ts @@ -46,3 +46,11 @@ export type TView = { is_create: boolean; is_editable: boolean; }; + +export type TUpdateView = { + name: string | undefined; + description: string | undefined; + filters: TViewFilters; + display_filters: TViewDisplayFilters; + display_properties: TViewDisplayProperties; +}; diff --git a/web/components/view/applied-filters/filter-item.tsx b/web/components/view/applied-filters/filter-item.tsx index 0c7b0c3cc..90765dd55 100644 --- a/web/components/view/applied-filters/filter-item.tsx +++ b/web/components/view/applied-filters/filter-item.tsx @@ -30,7 +30,7 @@ export const ViewAppliedFiltersItem: FC = (props) => { return (
{propertyDetail?.icon || } diff --git a/web/components/view/applied-filters/filter.tsx b/web/components/view/applied-filters/filter.tsx index 7333fee2e..bb3cbec54 100644 --- a/web/components/view/applied-filters/filter.tsx +++ b/web/components/view/applied-filters/filter.tsx @@ -37,7 +37,7 @@ export const ViewAppliedFilters: FC = observer((props) => { if (!propertyValues || propertyValues.length <= 0) return <>; return ( -
+
{filterKey.replaceAll("_", " ")}
{propertyVisibleCount && propertyValues.length >= propertyVisibleCount ? ( diff --git a/web/components/view/display-filters/dropdown.tsx b/web/components/view/display-filters/dropdown.tsx index facbf29d1..8e25c2585 100644 --- a/web/components/view/display-filters/dropdown.tsx +++ b/web/components/view/display-filters/dropdown.tsx @@ -87,7 +87,7 @@ export const ViewDisplayFiltersDropdown: FC = obser ) : (
= observer((props) => ) : (
= observer((props) => { + const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props; + // hooks + const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); + // refs + const dropdownRef = useRef(null); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "bottom-end", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + { + name: "offset", + options: { + offset: [0, 10], + }, + }, + ], + }); + + // dropdown options + const dropdownOptions: TViewFilterEditDropdownOptions[] = useMemo( + () => [ + { + icon: PhotoFilterIcon, + key: "save_as_new", + label: "Save as new view", + onClick: () => viewOperations.update(), + }, + { + icon: RotateCcw, + key: "reset_changes", + label: "Reset changes", + onClick: () => viewOperations.resetChanges(), + }, + ], + [viewOperations] + ); + + if (!viewDetailStore?.isFiltersUpdateEnabled) return <>; + return ( + +
+ + + {({ open }) => (!open ? : )} + +
+ + + + {dropdownOptions && + dropdownOptions.length > 0 && + dropdownOptions.map((option) => ( + +
+ +
+
{option.label}
+
+ ))} +
+
+
+ ); +}); diff --git a/web/components/view/index.ts b/web/components/view/index.ts index 32e82b957..701a15709 100644 --- a/web/components/view/index.ts +++ b/web/components/view/index.ts @@ -17,6 +17,7 @@ export * from "./filters/root"; export * from "./filters/filter-item-root"; export * from "./filters/filter-item"; export * from "./filters/filter-selection"; +export * from "./filters/edit-dropdown"; // view display filters export * from "./display-filters/dropdown"; diff --git a/web/components/view/layout.tsx b/web/components/view/layout.tsx index d2e75abb2..0daa7e32a 100644 --- a/web/components/view/layout.tsx +++ b/web/components/view/layout.tsx @@ -31,12 +31,12 @@ export const ViewLayoutRoot: FC = observer((props) => { const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); return ( -
+
{LAYOUTS_DATA.map((layout) => (
= observer((props) => { const handleViewOperationsToggle = (type: TViewOperationsToggle["type"], viewId: string | undefined) => setViewOperationsToggle({ type, viewId }); - const viewDetailCreateStore = useViewDetail( + const viewDetailCreateEditStore = useViewDetail( workspaceSlug, projectId, viewOperationsToggle?.viewId || viewId, @@ -62,55 +63,112 @@ export const GlobalViewRoot: FC = observer((props) => { const viewOperations: TViewOperations = useMemo( () => ({ - setName: (name: string) => viewDetailStore?.setName(name), - setDescription: (name: string) => viewDetailStore?.setDescription(name), - setFilters: (filterKey: keyof TViewFilters | undefined, filterValue: "clear_all" | string) => { - if (viewOperationsToggle.type && ["CREATE", "EDIT"].includes(viewOperationsToggle.type)) - viewDetailCreateStore?.setFilters(filterKey, filterValue); - else viewDetailStore?.setFilters(filterKey, filterValue); - }, - setDisplayFilters: (display_filters: Partial) => - viewDetailStore?.setDisplayFilters(display_filters), - setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) => - viewDetailStore?.setDisplayProperties(displayPropertyKey), localViewCreateEdit: (viewId: string | undefined) => { if (viewId === undefined) { const viewPayload = viewLocalPayload; handleViewOperationsToggle("CREATE", viewPayload.id); viewStore?.localViewCreate(viewPayload as TView); - } else handleViewOperationsToggle("EDIT", viewId); + } else { + handleViewOperationsToggle("EDIT", viewId); + viewDetailCreateEditStore?.setIsEditable(true); + } }, localViewCreateEditClear: async (viewId: string | undefined) => { - if (viewId) viewStore?.remove(viewId); + if (viewDetailCreateEditStore?.is_create && viewId) viewStore?.remove(viewId); handleViewOperationsToggle(undefined, undefined); }, - fetch: async () => await viewStore?.fetch(), + resetChanges: () => viewDetailStore?.resetChanges(), + + setName: (name: string) => { + if (viewOperationsToggle.type && ["CREATE", "EDIT"].includes(viewOperationsToggle.type)) + viewDetailCreateEditStore?.setName(name); + else viewDetailStore?.setName(name); + }, + setDescription: (name: string) => { + if (viewOperationsToggle.type && ["CREATE", "EDIT"].includes(viewOperationsToggle.type)) + viewDetailCreateEditStore?.setDescription(name); + else viewDetailStore?.setDescription(name); + }, + setFilters: (filterKey: keyof TViewFilters | undefined, filterValue: "clear_all" | string) => { + if (viewOperationsToggle.type && ["CREATE", "EDIT"].includes(viewOperationsToggle.type)) + viewDetailCreateEditStore?.setFilters(filterKey, filterValue); + else viewDetailStore?.setFilters(filterKey, filterValue); + }, + setDisplayFilters: (display_filters: Partial) => { + if (viewOperationsToggle.type && ["CREATE", "EDIT"].includes(viewOperationsToggle.type)) + viewDetailCreateEditStore?.setDisplayFilters(display_filters); + else viewDetailStore?.setDisplayFilters(display_filters); + }, + setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) => { + if (viewOperationsToggle.type && ["CREATE", "EDIT"].includes(viewOperationsToggle.type)) + viewDetailCreateEditStore?.setDisplayProperties(displayPropertyKey); + else viewDetailStore?.setDisplayProperties(displayPropertyKey); + }, + + fetch: async () => { + try { + await viewStore?.fetch(); + } catch { + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong. Please try again later or contact the support team.", + }); + } + }, create: async (data: Partial) => { try { await viewStore?.create(data); handleViewOperationsToggle(undefined, undefined); + setToastAlert({ + type: "success", + title: "Success!", + message: "View created successfully.", + }); } catch { - setToastAlert({ title: "Error", message: "Error creating view", type: "error" }); + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong. Please try again later or contact the support team.", + }); } }, remove: async (viewId: string) => { try { await viewStore?.remove(viewId); handleViewOperationsToggle(undefined, undefined); + setToastAlert({ + type: "success", + title: "Success!", + message: "View removed successfully.", + }); } catch { - setToastAlert({ title: "Error", message: "Error removing view", type: "error" }); + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong. Please try again later or contact the support team.", + }); } }, update: async () => { try { await viewDetailStore?.saveChanges(); handleViewOperationsToggle(undefined, undefined); + setToastAlert({ + type: "success", + title: "Success!", + message: "View updated successfully.", + }); } catch { - setToastAlert({ title: "Error", message: "Error updating view", type: "error" }); + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong. Please try again later or contact the support team.", + }); } }, }), - [viewStore, viewDetailStore, setToastAlert, viewOperationsToggle, viewDetailCreateStore] + [viewStore, viewDetailStore, setToastAlert, viewOperationsToggle, viewDetailCreateEditStore] ); // fetch all views @@ -222,13 +280,9 @@ export const GlobalViewRoot: FC = observer((props) => { />
-
-
- -
-
+ - void; + localViewCreateEditClear: (viewId: string | undefined) => Promise; + resetChanges: () => void; + setName: (name: string) => void; setDescription: (description: string) => void; setFilters: (filterKey: keyof TViewFilters | undefined, filterValue: "clear_all" | string) => void; setDisplayFilters: (display_filters: Partial) => void; setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) => void; - localViewCreateEdit: (viewId: string | undefined) => void; - localViewCreateEditClear: (viewId: string | undefined) => Promise; - fetch: () => Promise; create: (data: Partial) => Promise; update: () => Promise; remove: (viewId: string) => Promise; }; + +// view and view filter edit dropdowns +export type TViewEditDropdownOptions = { + icon: LucideIcon; + key: string; + label: string; + onClick: () => void; + children: TViewEditDropdownOptions[] | undefined; +}; + +export type TViewFilterEditDropdownOptions = { + icon: LucideIcon | any; + key: string; + label: string; + onClick: () => void; +}; diff --git a/web/components/view/views/create-edit-form.tsx b/web/components/view/views/create-edit-form.tsx index 63dec16e8..a6ea3d4f1 100644 --- a/web/components/view/views/create-edit-form.tsx +++ b/web/components/view/views/create-edit-form.tsx @@ -23,7 +23,7 @@ type TViewCreateEditForm = { export const ViewCreateEditForm: FC = observer((props) => { const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props; // hooks - const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); + const currentViewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); const { getProjectById } = useProject(); // states const [modalToggle, setModalToggle] = useState(false); @@ -43,12 +43,12 @@ export const ViewCreateEditForm: FC = observer((props) => { const onContinue = async () => { setLoader(true); - if (viewDetailStore?.is_create) { - const payload = viewDetailStore?.filtersToUpdate; + if (currentViewDetailStore?.is_create) { + const payload = currentViewDetailStore?.filtersToUpdate; await viewOperations.create(payload); modalClose(); } else { - const payload = viewDetailStore?.filtersToUpdate; + const payload = currentViewDetailStore?.filtersToUpdate; if (!payload) return; await viewOperations.update(); modalClose(); @@ -58,7 +58,7 @@ export const ViewCreateEditForm: FC = observer((props) => { const projectDetails = projectId ? getProjectById(projectId) : undefined; - if (!viewDetailStore?.id) return <>; + if (!currentViewDetailStore?.id) return <>; return ( @@ -110,9 +110,9 @@ export const ViewCreateEditForm: FC = observer((props) => { id="name" name="name" type="text" - value={viewDetailStore?.filtersToUpdate?.name || ""} + value={currentViewDetailStore?.filtersToUpdate?.name || ""} onChange={(e) => { - viewDetailStore?.setName(e.target.value); + viewOperations?.setName(e.target.value); }} placeholder="What do you want to call this view?" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" @@ -165,7 +165,7 @@ export const ViewCreateEditForm: FC = observer((props) => { Cancel
diff --git a/web/components/view/views/edit-dropdown.tsx b/web/components/view/views/edit-dropdown.tsx index dba74f8d2..10dfa878b 100644 --- a/web/components/view/views/edit-dropdown.tsx +++ b/web/components/view/views/edit-dropdown.tsx @@ -1,41 +1,143 @@ -import { FC } from "react"; -import { ChevronDown } from "lucide-react"; +import { FC, Fragment, useMemo, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; -// hooks -import { useViewDetail } from "hooks/store"; -// components +import { Menu, Transition } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Copy, Eye, Globe2, Link2, Pencil, Trash } from "lucide-react"; // types -import { TViewTypes } from "@plane/types"; -import { TViewOperations } from "../types"; +import { TViewEditDropdownOptions, TViewOperations } from "../types"; type TViewEditDropdown = { - workspaceSlug: string; - projectId: string | undefined; viewId: string; - viewType: TViewTypes; viewOperations: TViewOperations; }; export const ViewEditDropdown: FC = observer((props) => { - const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props; - // hooks - const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); + const { viewId, viewOperations } = props; + // refs + const dropdownRef = useRef(null); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "bottom-end", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + { + name: "offset", + options: { + offset: [0, 10], + }, + }, + ], + }); + + // dropdown options + const dropdownOptions: TViewEditDropdownOptions[] = useMemo( + () => [ + { + icon: Pencil, + key: "rename", + label: "Rename", + onClick: () => viewOperations.localViewCreateEdit(viewId), + children: undefined, + }, + { + icon: Eye, + key: "accessability", + label: "Change Accessability", + onClick: () => {}, + children: [ + { + icon: Eye, + key: "private", + label: "Private", + onClick: () => viewOperations.create({}), + children: undefined, + }, + { + icon: Globe2, + key: "public", + label: "Public", + onClick: () => viewOperations.create({}), + children: undefined, + }, + ], + }, + { + icon: Copy, + key: "duplicate", + label: "Duplicate view", + onClick: () => viewOperations.remove(viewId), + children: undefined, + }, + { + icon: Link2, + key: "copy_link", + label: "Copy view link", + onClick: () => viewOperations.remove(viewId), + children: undefined, + }, + { + icon: Trash, + key: "delete", + label: "Delete view", + onClick: () => viewOperations.remove(viewId), + children: undefined, + }, + ], + [viewOperations, viewId] + ); - if (!viewDetailStore?.isFiltersUpdateEnabled) return <>; return ( - <> -
- -
- + + +
+
-
- + + + + + {dropdownOptions && + dropdownOptions.length > 0 && + dropdownOptions.map((option) => ( + +
+ +
+
{option.label}
+
+ ))} +
+
+ ); }); diff --git a/web/components/view/views/root.tsx b/web/components/view/views/root.tsx index 68881c616..c548e6d2f 100644 --- a/web/components/view/views/root.tsx +++ b/web/components/view/views/root.tsx @@ -31,22 +31,16 @@ export const ViewRoot: FC = observer((props) => { const handleViewTabsVisibility = () => { const tabContainer = document.getElementById("tab-container"); const tabItemViewMore = document.getElementById("tab-item-view-more"); - const itemWidth = 128; + const itemWidth = 116; if (!tabContainer || !tabItemViewMore) return; const containerWidth = tabContainer.clientWidth; const itemViewMoreLeftOffset = tabItemViewMore.offsetLeft + (tabItemViewMore.clientWidth + 10); const itemViewMoreRightOffset = containerWidth - itemViewMoreLeftOffset; - if (itemViewMoreLeftOffset > 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); - } + const itemsToRenderLeft = Math.floor(itemViewMoreLeftOffset / itemWidth) || 0; + const itemsToRenderRight = Math.floor(itemViewMoreRightOffset / itemWidth) || 0; + setItemsToRenderViewCount(itemsToRenderLeft + itemsToRenderRight); }; window.addEventListener("resize", () => handleViewTabsVisibility()); diff --git a/web/components/view/views/view-item.tsx b/web/components/view/views/view-item.tsx index dffc31063..00da5e7b4 100644 --- a/web/components/view/views/view-item.tsx +++ b/web/components/view/views/view-item.tsx @@ -28,16 +28,12 @@ export const ViewItem: FC = observer((props) => { viewItemId === viewId && e.preventDefault()} > -
+
diff --git a/web/store/view/view.store.ts b/web/store/view/view.store.ts index 39e888a5c..155504ca2 100644 --- a/web/store/view/view.store.ts +++ b/web/store/view/view.store.ts @@ -4,12 +4,14 @@ import update from "lodash/update"; import concat from "lodash/concat"; import pull from "lodash/pull"; import isEqual from "lodash/isEqual"; +import cloneDeep from "lodash/cloneDeep"; // store import { RootStore } from "store/root.store"; // types import { TUserViewService, TViewService } from "services/view/types"; import { TView, + TUpdateView, TViewFilters, TViewDisplayFilters, TViewDisplayProperties, @@ -19,12 +21,12 @@ import { // helpers import { FiltersHelper } from "./helpers/filters_helpers"; -type TLoader = "filters_submit" | "filters_submitting" | "update" | "updating" | undefined; +type TLoader = "updating" | undefined; export type TViewStore = TView & { // observables loader: TLoader; - filtersToUpdate: Partial; + filtersToUpdate: TUpdateView; // computed appliedFilters: TViewFilterProps | undefined; appliedFiltersQueryParams: string | undefined; @@ -36,10 +38,11 @@ export type TViewStore = TView & { setFilters: (filterKey: keyof TViewFilters | undefined, filterValue: "clear_all" | string) => void; setDisplayFilters: (display_filters: Partial) => void; setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) => void; + setIsEditable: (id_editable: boolean) => void; resetChanges: () => void; saveChanges: () => Promise; // actions - update: (viewData: Partial) => Promise; + update: (viewData: TUpdateView) => Promise; lockView: () => Promise; unlockView: () => Promise; makeFavorite: () => Promise; @@ -70,13 +73,7 @@ export class ViewStore extends FiltersHelper implements TViewStore { is_create: boolean = false; is_editable: boolean = false; loader: TLoader = undefined; - filtersToUpdate: Partial = { - name: "", - description: "", - filters: undefined, - display_filters: undefined, - display_properties: undefined, - }; + filtersToUpdate: TUpdateView; constructor( private store: RootStore, @@ -108,6 +105,8 @@ export class ViewStore extends FiltersHelper implements TViewStore { this.is_create = _view.is_create; this.is_editable = _view.is_editable; this.filtersToUpdate = { + name: this.name, + description: this.description, filters: this.computedFilters(_view.filters), display_filters: this.computedDisplayFilters(_view.display_filters), display_properties: this.computedDisplayProperties(_view.display_properties), @@ -149,6 +148,7 @@ export class ViewStore extends FiltersHelper implements TViewStore { setFilters: action, setDisplayFilters: action, setDisplayProperties: action, + setIsEditable: action, resetChanges: action, saveChanges: action, // actions @@ -189,12 +189,13 @@ export class ViewStore extends FiltersHelper implements TViewStore { } get isFiltersUpdateEnabled() { - const filters = this.filters; - const appliedFilters = this.appliedFilters?.filters; + const _filters = this.filters; + const _appliedFilters = this.appliedFilters?.filters; + let isFiltersUpdateEnabled = false; - Object.keys(appliedFilters).forEach((key) => { + Object.keys(_appliedFilters).forEach((key) => { const _key = key as keyof TViewFilters; - if (!isEqual(appliedFilters[_key].slice().sort(), filters[_key].slice().sort())) isFiltersUpdateEnabled = true; + if (!isEqual(_appliedFilters[_key].slice().sort(), _filters[_key].slice().sort())) isFiltersUpdateEnabled = true; }); return isFiltersUpdateEnabled; } @@ -254,14 +255,21 @@ export class ViewStore extends FiltersHelper implements TViewStore { }); }; + setIsEditable = (is_editable: boolean) => { + runInAction(() => { + this.is_editable = is_editable; + }); + }; + resetChanges = () => { runInAction(() => { + const _view = cloneDeep(this); this.filtersToUpdate = { - name: this.name, - description: this.description, - filters: this.filters, - display_filters: this.display_filters, - display_properties: this.display_properties, + name: _view.name, + description: _view.description, + filters: _view.filters, + display_filters: _view.display_filters, + display_properties: _view.display_properties, }; }); }; @@ -271,15 +279,19 @@ export class ViewStore extends FiltersHelper implements TViewStore { if (this.filtersToUpdate) await this.update(this.filtersToUpdate); } catch { Object.keys(this.filtersToUpdate).forEach((key) => { - const _key = key as keyof TView; + const _key = key as keyof TUpdateView; set(this, _key, this.filtersToUpdate[_key]); }); } }; // actions - update = async (viewData: Partial) => { + update = async (viewData: TUpdateView) => { try { + runInAction(() => { + this.loader = "updating"; + }); + const { workspaceSlug, projectId } = this.store.app.router; if (!workspaceSlug || !this.id) return; @@ -287,10 +299,11 @@ export class ViewStore extends FiltersHelper implements TViewStore { if (!view) return; runInAction(() => { - Object.keys(viewData).forEach((key) => { + Object.keys(view).forEach((key) => { const _key = key as keyof TView; - set(this, _key, viewData[_key]); + set(this, _key, view[_key]); }); + this.loader = undefined; }); } catch { this.resetChanges();