chore: updated filters and filter components

This commit is contained in:
gurusainath 2024-02-12 12:30:59 +05:30
parent 8ae1e3cff2
commit 46a6a599a1
15 changed files with 420 additions and 109 deletions

View File

@ -46,3 +46,11 @@ export type TView = {
is_create: boolean; is_create: boolean;
is_editable: boolean; is_editable: boolean;
}; };
export type TUpdateView = {
name: string | undefined;
description: string | undefined;
filters: TViewFilters;
display_filters: TViewDisplayFilters;
display_properties: TViewDisplayProperties;
};

View File

@ -30,7 +30,7 @@ export const ViewAppliedFiltersItem: FC<TViewAppliedFiltersItem> = (props) => {
return ( return (
<div <div
key={`filter_value_${filterKey}_${propertyId}`} key={`filter_value_${filterKey}_${propertyId}`}
className="bg-custom-background-80 rounded relative flex items-center gap-1.5 p-1.5 py-1" className="bg-custom-background-80 rounded relative flex items-center gap-1.5 p-1"
> >
<div className="flex-shrink-0 w-4 h-4 relative flex justify-center items-center overflow-hidden"> <div className="flex-shrink-0 w-4 h-4 relative flex justify-center items-center overflow-hidden">
{propertyDetail?.icon || <ImagePlus size={14} />} {propertyDetail?.icon || <ImagePlus size={14} />}

View File

@ -37,7 +37,7 @@ export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => {
if (!propertyValues || propertyValues.length <= 0) return <></>; if (!propertyValues || propertyValues.length <= 0) return <></>;
return ( return (
<div className="relative flex items-center gap-2 border border-custom-border-300 rounded p-1.5 px-2 min-h-[32px]"> <div className="relative flex items-center gap-2 border border-custom-border-300 rounded p-1 px-2 min-h-[32px]">
<div className="flex-shrink-0 text-xs text-custom-text-200 capitalize">{filterKey.replaceAll("_", " ")}</div> <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"> <div className="relative flex items-center gap-1.5 flex-wrap">
{propertyVisibleCount && propertyValues.length >= propertyVisibleCount ? ( {propertyVisibleCount && propertyValues.length >= propertyVisibleCount ? (

View File

@ -87,7 +87,7 @@ export const ViewDisplayFiltersDropdown: FC<TViewDisplayFiltersDropdown> = obser
) : ( ) : (
<Tooltip tooltipContent={"Display"} position="bottom"> <Tooltip tooltipContent={"Display"} position="bottom">
<div <div
className={`relative flex items-center gap-1 h-7 rounded px-2 transition-all className={`relative flex items-center gap-1 h-8 rounded px-2 transition-all
${ ${
displayDropdownText displayDropdownText
? `border border-custom-border-300 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80` ? `border border-custom-border-300 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80`

View File

@ -89,7 +89,7 @@ export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) =>
) : ( ) : (
<Tooltip tooltipContent={"Filters"} position="bottom"> <Tooltip tooltipContent={"Filters"} position="bottom">
<div <div
className={`relative flex items-center gap-1 h-7 rounded px-2 transition-all className={`relative flex items-center gap-1 h-8 rounded px-2 transition-all
${ ${
displayDropdownText displayDropdownText
? `border border-custom-border-300 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80` ? `border border-custom-border-300 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80`

View File

@ -0,0 +1,124 @@
import { FC, Fragment, useMemo, useRef, useState } from "react";
import { ChevronDown, ChevronUp, RotateCcw } from "lucide-react";
import { observer } from "mobx-react-lite";
import { usePopper } from "react-popper";
import { Menu, Transition } from "@headlessui/react";
// hooks
import { useViewDetail } from "hooks/store";
// ui
import { PhotoFilterIcon } from "@plane/ui";
// types
import { TViewTypes } from "@plane/types";
import { TViewFilterEditDropdownOptions, TViewOperations } from "../types";
type TViewFiltersEditDropdown = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string;
viewType: TViewTypes;
viewOperations: TViewOperations;
};
export const ViewFiltersEditDropdown: FC<TViewFiltersEditDropdown> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
// 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-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 (
<Menu as="div" className="relative flex-shrink-0" ref={dropdownRef}>
<div className=" relative flex items-center rounded h-8 transition-all cursor-pointer bg-custom-primary-100/20 text-custom-primary-100">
<button
className="text-sm px-3 font-medium h-full border-r border-white/50 flex justify-center items-center rounded-l transition-all hover:bg-custom-primary-100/30"
disabled={viewDetailStore?.loader === "updating"}
onClick={() => viewOperations.update()}
>
{viewDetailStore?.loader === "updating" ? "updating..." : "Update"}
</button>
<Menu.Button
as="div"
className="flex-shrink-0 px-1.5 hover:bg-custom-primary-100/30 h-full flex justify-center items-center rounded-r transition-all outline-none"
ref={setReferenceElement}
>
{({ open }) => (!open ? <ChevronDown size={16} /> : <ChevronUp size={16} />)}
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="absolute right-0 z-20 mt-1.5 flex w-52 flex-col rounded border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 text-xs shadow-lg outline-none p-1"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
{dropdownOptions &&
dropdownOptions.length > 0 &&
dropdownOptions.map((option) => (
<Menu.Item
key={option.key}
as="button"
type="button"
className="relative flex items-center gap-2 p-1 py-1.5 rounded transition-all hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100 cursor-pointer"
onClick={option.onClick}
>
<div className="flex-shrink-0 w-4 h-4 relative flex justify-center items-center">
<option.icon size={12} className="w-3 h-3" />
</div>
<div className="text-xs whitespace-nowrap">{option.label}</div>
</Menu.Item>
))}
</Menu.Items>
</Transition>
</Menu>
);
});

View File

@ -17,6 +17,7 @@ export * from "./filters/root";
export * from "./filters/filter-item-root"; export * from "./filters/filter-item-root";
export * from "./filters/filter-item"; export * from "./filters/filter-item";
export * from "./filters/filter-selection"; export * from "./filters/filter-selection";
export * from "./filters/edit-dropdown";
// view display filters // view display filters
export * from "./display-filters/dropdown"; export * from "./display-filters/dropdown";

View File

@ -31,12 +31,12 @@ export const ViewLayoutRoot: FC<TViewLayoutRoot> = observer((props) => {
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
return ( return (
<div className="relative flex gap-0.5 items-center bg-custom-background-80 rounded p-1 h-7 shadow-custom-shadow-2xs"> <div className="relative flex gap-0.5 items-center bg-custom-background-80 rounded p-1 shadow-custom-shadow-2xs">
{LAYOUTS_DATA.map((layout) => ( {LAYOUTS_DATA.map((layout) => (
<Fragment key={layout.key}> <Fragment key={layout.key}>
<Tooltip tooltipContent={layout.title} position="bottom"> <Tooltip tooltipContent={layout.title} position="bottom">
<div <div
className={`relative h-[24px] w-7 flex justify-center items-center overflow-hidden rounded transition-all cursor-pointer className={`relative h-6 w-7 flex justify-center items-center overflow-hidden rounded transition-all cursor-pointer
${ ${
viewDetailStore?.filtersToUpdate?.display_filters?.layout === layout.key viewDetailStore?.filtersToUpdate?.display_filters?.layout === layout.key
? `bg-custom-background-100 shadow-custom-shadow-2xs` ? `bg-custom-background-100 shadow-custom-shadow-2xs`

View File

@ -9,13 +9,14 @@ import useToast from "hooks/use-toast";
import { import {
ViewRoot, ViewRoot,
ViewCreateEditForm, ViewCreateEditForm,
ViewEditDropdown,
ViewLayoutRoot, ViewLayoutRoot,
ViewFiltersDropdown, ViewFiltersDropdown,
ViewFiltersEditDropdown,
ViewDisplayFiltersDropdown, ViewDisplayFiltersDropdown,
ViewAppliedFiltersRoot, ViewAppliedFiltersRoot,
ViewDuplicateConfirmationModal, ViewDuplicateConfirmationModal,
ViewDeleteConfirmationModal, ViewDeleteConfirmationModal,
ViewEditDropdown,
} from "."; } from ".";
// ui // ui
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
@ -53,7 +54,7 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
const handleViewOperationsToggle = (type: TViewOperationsToggle["type"], viewId: string | undefined) => const handleViewOperationsToggle = (type: TViewOperationsToggle["type"], viewId: string | undefined) =>
setViewOperationsToggle({ type, viewId }); setViewOperationsToggle({ type, viewId });
const viewDetailCreateStore = useViewDetail( const viewDetailCreateEditStore = useViewDetail(
workspaceSlug, workspaceSlug,
projectId, projectId,
viewOperationsToggle?.viewId || viewId, viewOperationsToggle?.viewId || viewId,
@ -62,55 +63,112 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
const viewOperations: TViewOperations = useMemo( 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<TViewDisplayFilters>) =>
viewDetailStore?.setDisplayFilters(display_filters),
setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) =>
viewDetailStore?.setDisplayProperties(displayPropertyKey),
localViewCreateEdit: (viewId: string | undefined) => { localViewCreateEdit: (viewId: string | undefined) => {
if (viewId === undefined) { if (viewId === undefined) {
const viewPayload = viewLocalPayload; const viewPayload = viewLocalPayload;
handleViewOperationsToggle("CREATE", viewPayload.id); handleViewOperationsToggle("CREATE", viewPayload.id);
viewStore?.localViewCreate(viewPayload as TView); viewStore?.localViewCreate(viewPayload as TView);
} else handleViewOperationsToggle("EDIT", viewId); } else {
handleViewOperationsToggle("EDIT", viewId);
viewDetailCreateEditStore?.setIsEditable(true);
}
}, },
localViewCreateEditClear: async (viewId: string | undefined) => { localViewCreateEditClear: async (viewId: string | undefined) => {
if (viewId) viewStore?.remove(viewId); if (viewDetailCreateEditStore?.is_create && viewId) viewStore?.remove(viewId);
handleViewOperationsToggle(undefined, undefined); 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<TViewDisplayFilters>) => {
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<TView>) => { create: async (data: Partial<TView>) => {
try { try {
await viewStore?.create(data); await viewStore?.create(data);
handleViewOperationsToggle(undefined, undefined); handleViewOperationsToggle(undefined, undefined);
setToastAlert({
type: "success",
title: "Success!",
message: "View created successfully.",
});
} catch { } 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) => { remove: async (viewId: string) => {
try { try {
await viewStore?.remove(viewId); await viewStore?.remove(viewId);
handleViewOperationsToggle(undefined, undefined); handleViewOperationsToggle(undefined, undefined);
setToastAlert({
type: "success",
title: "Success!",
message: "View removed successfully.",
});
} catch { } 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 () => { update: async () => {
try { try {
await viewDetailStore?.saveChanges(); await viewDetailStore?.saveChanges();
handleViewOperationsToggle(undefined, undefined); handleViewOperationsToggle(undefined, undefined);
setToastAlert({
type: "success",
title: "Success!",
message: "View updated successfully.",
});
} catch { } 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 // fetch all views
@ -222,13 +280,9 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
/> />
</div> </div>
<div className="relative flex items-center gap-1 rounded px-2 h-7 transition-all hover:bg-custom-background-80 cursor-pointer"> <ViewEditDropdown viewId={viewId} viewOperations={viewOperations} />
<div className="w-4 h-4 relative flex justify-center items-center overflow-hidden">
<Pencil size={12} />
</div>
</div>
<ViewEditDropdown <ViewFiltersEditDropdown
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
viewId={viewId} viewId={viewId}

View File

@ -1,17 +1,36 @@
import { LucideIcon } from "lucide-react";
// types
import { TView, TViewFilters, TViewDisplayFilters, TViewDisplayProperties } from "@plane/types"; import { TView, TViewFilters, TViewDisplayFilters, TViewDisplayProperties } from "@plane/types";
export type TViewOperations = { export type TViewOperations = {
localViewCreateEdit: (viewId: string | undefined) => void;
localViewCreateEditClear: (viewId: string | undefined) => Promise<void>;
resetChanges: () => void;
setName: (name: string) => void; setName: (name: string) => void;
setDescription: (description: string) => void; setDescription: (description: string) => void;
setFilters: (filterKey: keyof TViewFilters | undefined, filterValue: "clear_all" | string) => void; setFilters: (filterKey: keyof TViewFilters | undefined, filterValue: "clear_all" | string) => void;
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void; setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) => void; setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) => void;
localViewCreateEdit: (viewId: string | undefined) => void;
localViewCreateEditClear: (viewId: string | undefined) => Promise<void>;
fetch: () => Promise<void>; fetch: () => Promise<void>;
create: (data: Partial<TView>) => Promise<void>; create: (data: Partial<TView>) => Promise<void>;
update: () => Promise<void>; update: () => Promise<void>;
remove: (viewId: string) => Promise<void>; remove: (viewId: string) => Promise<void>;
}; };
// 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;
};

View File

@ -23,7 +23,7 @@ type TViewCreateEditForm = {
export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => { export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props; const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
// hooks // hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); const currentViewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const { getProjectById } = useProject(); const { getProjectById } = useProject();
// states // states
const [modalToggle, setModalToggle] = useState(false); const [modalToggle, setModalToggle] = useState(false);
@ -43,12 +43,12 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
const onContinue = async () => { const onContinue = async () => {
setLoader(true); setLoader(true);
if (viewDetailStore?.is_create) { if (currentViewDetailStore?.is_create) {
const payload = viewDetailStore?.filtersToUpdate; const payload = currentViewDetailStore?.filtersToUpdate;
await viewOperations.create(payload); await viewOperations.create(payload);
modalClose(); modalClose();
} else { } else {
const payload = viewDetailStore?.filtersToUpdate; const payload = currentViewDetailStore?.filtersToUpdate;
if (!payload) return; if (!payload) return;
await viewOperations.update(); await viewOperations.update();
modalClose(); modalClose();
@ -58,7 +58,7 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
const projectDetails = projectId ? getProjectById(projectId) : undefined; const projectDetails = projectId ? getProjectById(projectId) : undefined;
if (!viewDetailStore?.id) return <></>; if (!currentViewDetailStore?.id) return <></>;
return ( return (
<Transition.Root show={modalToggle} as={Fragment}> <Transition.Root show={modalToggle} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={modalClose}> <Dialog as="div" className="relative z-20" onClose={modalClose}>
@ -110,9 +110,9 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
id="name" id="name"
name="name" name="name"
type="text" type="text"
value={viewDetailStore?.filtersToUpdate?.name || ""} value={currentViewDetailStore?.filtersToUpdate?.name || ""}
onChange={(e) => { onChange={(e) => {
viewDetailStore?.setName(e.target.value); viewOperations?.setName(e.target.value);
}} }}
placeholder="What do you want to call this view?" placeholder="What do you want to call this view?"
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
@ -165,7 +165,7 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
Cancel Cancel
</Button> </Button>
<Button variant="primary" onClick={onContinue} disabled={loader}> <Button variant="primary" onClick={onContinue} disabled={loader}>
{loader ? `Saving...` : `${viewDetailStore?.is_create ? `Create` : `Update`} View`} {loader ? `Saving...` : `${currentViewDetailStore?.is_create ? `Create` : `Update`} View`}
</Button> </Button>
</div> </div>
</Dialog.Panel> </Dialog.Panel>

View File

@ -1,41 +1,143 @@
import { FC } from "react"; import { FC, Fragment, useMemo, useRef, useState } from "react";
import { ChevronDown } from "lucide-react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks import { Menu, Transition } from "@headlessui/react";
import { useViewDetail } from "hooks/store"; import { usePopper } from "react-popper";
// components import { Copy, Eye, Globe2, Link2, Pencil, Trash } from "lucide-react";
// types // types
import { TViewTypes } from "@plane/types"; import { TViewEditDropdownOptions, TViewOperations } from "../types";
import { TViewOperations } from "../types";
type TViewEditDropdown = { type TViewEditDropdown = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string; viewId: string;
viewType: TViewTypes;
viewOperations: TViewOperations; viewOperations: TViewOperations;
}; };
export const ViewEditDropdown: FC<TViewEditDropdown> = observer((props) => { export const ViewEditDropdown: FC<TViewEditDropdown> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props; const { viewId, viewOperations } = props;
// hooks // refs
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); 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-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 ( return (
<> <Menu as="div" className="relative flex-shrink-0" ref={dropdownRef}>
<div className=" relative flex items-center rounded h-7 transition-all cursor-pointer bg-custom-primary-100/20 text-custom-primary-100"> <Menu.Button
<button className="relative flex items-center gap-1 rounded px-2 h-8 transition-all hover:bg-custom-background-80 cursor-pointer outline-none"
className="text-sm px-3 font-medium h-full border-r border-white/50 flex justify-center items-center rounded-l transition-all hover:bg-custom-primary-100/30" ref={setReferenceElement}
disabled={viewDetailStore?.loader === "filters_submitting"} >
onClick={() => viewOperations.update()} <div className="w-4 h-4 relative flex justify-center items-center overflow-hidden">
> <Pencil size={12} />
Update
</button>
<div className="flex-shrink-0 px-1.5 hover:bg-custom-primary-100/30 h-full flex justify-center items-center rounded-r transition-all">
<ChevronDown size={16} />
</div> </div>
</div> </Menu.Button>
</>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="absolute right-0 z-20 mt-1.5 flex w-52 flex-col rounded border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 text-xs shadow-lg outline-none p-1"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
{dropdownOptions &&
dropdownOptions.length > 0 &&
dropdownOptions.map((option) => (
<Menu.Item
key={option.key}
as="button"
type="button"
className="relative w-full flex items-center gap-2 p-1 py-1.5 rounded transition-all hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100 cursor-pointer"
onClick={option.onClick}
>
<div className="flex-shrink-0 w-4 h-4 relative flex justify-center items-center">
<option.icon size={12} />
</div>
<div className="text-xs whitespace-nowrap">{option.label}</div>
</Menu.Item>
))}
</Menu.Items>
</Transition>
</Menu>
); );
}); });

View File

@ -31,22 +31,16 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
const handleViewTabsVisibility = () => { const handleViewTabsVisibility = () => {
const tabContainer = document.getElementById("tab-container"); const tabContainer = document.getElementById("tab-container");
const tabItemViewMore = document.getElementById("tab-item-view-more"); const tabItemViewMore = document.getElementById("tab-item-view-more");
const itemWidth = 128; const itemWidth = 116;
if (!tabContainer || !tabItemViewMore) return; if (!tabContainer || !tabItemViewMore) return;
const containerWidth = tabContainer.clientWidth; const containerWidth = tabContainer.clientWidth;
const itemViewMoreLeftOffset = tabItemViewMore.offsetLeft + (tabItemViewMore.clientWidth + 10); const itemViewMoreLeftOffset = tabItemViewMore.offsetLeft + (tabItemViewMore.clientWidth + 10);
const itemViewMoreRightOffset = containerWidth - itemViewMoreLeftOffset; const itemViewMoreRightOffset = containerWidth - itemViewMoreLeftOffset;
if (itemViewMoreLeftOffset > containerWidth) { const itemsToRenderLeft = Math.floor(itemViewMoreLeftOffset / itemWidth) || 0;
const itemsToRender = Math.floor(containerWidth / itemWidth); const itemsToRenderRight = Math.floor(itemViewMoreRightOffset / itemWidth) || 0;
setItemsToRenderViewCount(itemsToRender); setItemsToRenderViewCount(itemsToRenderLeft + itemsToRenderRight);
}
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()); window.addEventListener("resize", () => handleViewTabsVisibility());

View File

@ -28,16 +28,12 @@ export const ViewItem: FC<TViewItem> = observer((props) => {
<Tooltip tooltipContent={viewDetailStore?.name} position="top"> <Tooltip tooltipContent={viewDetailStore?.name} position="top">
<Link <Link
href={`${baseRoute}/${viewItemId}`} href={`${baseRoute}/${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 className={`cursor-pointer relative p-2 px-2.5 flex justify-center items-center gap-1.5 rounded transition-all hover:bg-custom-background-80
${viewItemId === viewId ? `text-custom-primary-100 bg-custom-primary-100/10` : `border-transparent`} ${viewItemId === viewId ? `text-custom-primary-100 bg-custom-primary-100/10` : `border-transparent`}
`} `}
onClick={(e) => viewItemId === viewId && e.preventDefault()} onClick={(e) => viewItemId === viewId && e.preventDefault()}
> >
<div <div className={`flex-shrink-0 rounded-sm relative w-3 h-3 flex justify-center items-center overflow-hidden`}>
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" /> <PhotoFilterIcon className="w-3 h-3" />
</div> </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 font-medium">

View File

@ -4,12 +4,14 @@ import update from "lodash/update";
import concat from "lodash/concat"; import concat from "lodash/concat";
import pull from "lodash/pull"; import pull from "lodash/pull";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import cloneDeep from "lodash/cloneDeep";
// store // store
import { RootStore } from "store/root.store"; import { RootStore } from "store/root.store";
// types // types
import { TUserViewService, TViewService } from "services/view/types"; import { TUserViewService, TViewService } from "services/view/types";
import { import {
TView, TView,
TUpdateView,
TViewFilters, TViewFilters,
TViewDisplayFilters, TViewDisplayFilters,
TViewDisplayProperties, TViewDisplayProperties,
@ -19,12 +21,12 @@ import {
// helpers // helpers
import { FiltersHelper } from "./helpers/filters_helpers"; import { FiltersHelper } from "./helpers/filters_helpers";
type TLoader = "filters_submit" | "filters_submitting" | "update" | "updating" | undefined; type TLoader = "updating" | undefined;
export type TViewStore = TView & { export type TViewStore = TView & {
// observables // observables
loader: TLoader; loader: TLoader;
filtersToUpdate: Partial<TView>; filtersToUpdate: TUpdateView;
// computed // computed
appliedFilters: TViewFilterProps | undefined; appliedFilters: TViewFilterProps | undefined;
appliedFiltersQueryParams: string | undefined; appliedFiltersQueryParams: string | undefined;
@ -36,10 +38,11 @@ export type TViewStore = TView & {
setFilters: (filterKey: keyof TViewFilters | undefined, filterValue: "clear_all" | string) => void; setFilters: (filterKey: keyof TViewFilters | undefined, filterValue: "clear_all" | string) => void;
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void; setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) => void; setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) => void;
setIsEditable: (id_editable: boolean) => void;
resetChanges: () => void; resetChanges: () => void;
saveChanges: () => Promise<void>; saveChanges: () => Promise<void>;
// actions // actions
update: (viewData: Partial<TView>) => Promise<void>; update: (viewData: TUpdateView) => Promise<void>;
lockView: () => Promise<void>; lockView: () => Promise<void>;
unlockView: () => Promise<void>; unlockView: () => Promise<void>;
makeFavorite: () => Promise<void>; makeFavorite: () => Promise<void>;
@ -70,13 +73,7 @@ export class ViewStore extends FiltersHelper implements TViewStore {
is_create: boolean = false; is_create: boolean = false;
is_editable: boolean = false; is_editable: boolean = false;
loader: TLoader = undefined; loader: TLoader = undefined;
filtersToUpdate: Partial<TView> = { filtersToUpdate: TUpdateView;
name: "",
description: "",
filters: undefined,
display_filters: undefined,
display_properties: undefined,
};
constructor( constructor(
private store: RootStore, private store: RootStore,
@ -108,6 +105,8 @@ export class ViewStore extends FiltersHelper implements TViewStore {
this.is_create = _view.is_create; this.is_create = _view.is_create;
this.is_editable = _view.is_editable; this.is_editable = _view.is_editable;
this.filtersToUpdate = { this.filtersToUpdate = {
name: this.name,
description: this.description,
filters: this.computedFilters(_view.filters), filters: this.computedFilters(_view.filters),
display_filters: this.computedDisplayFilters(_view.display_filters), display_filters: this.computedDisplayFilters(_view.display_filters),
display_properties: this.computedDisplayProperties(_view.display_properties), display_properties: this.computedDisplayProperties(_view.display_properties),
@ -149,6 +148,7 @@ export class ViewStore extends FiltersHelper implements TViewStore {
setFilters: action, setFilters: action,
setDisplayFilters: action, setDisplayFilters: action,
setDisplayProperties: action, setDisplayProperties: action,
setIsEditable: action,
resetChanges: action, resetChanges: action,
saveChanges: action, saveChanges: action,
// actions // actions
@ -189,12 +189,13 @@ export class ViewStore extends FiltersHelper implements TViewStore {
} }
get isFiltersUpdateEnabled() { get isFiltersUpdateEnabled() {
const filters = this.filters; const _filters = this.filters;
const appliedFilters = this.appliedFilters?.filters; const _appliedFilters = this.appliedFilters?.filters;
let isFiltersUpdateEnabled = false; let isFiltersUpdateEnabled = false;
Object.keys(appliedFilters).forEach((key) => { Object.keys(_appliedFilters).forEach((key) => {
const _key = key as keyof TViewFilters; 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; return isFiltersUpdateEnabled;
} }
@ -254,14 +255,21 @@ export class ViewStore extends FiltersHelper implements TViewStore {
}); });
}; };
setIsEditable = (is_editable: boolean) => {
runInAction(() => {
this.is_editable = is_editable;
});
};
resetChanges = () => { resetChanges = () => {
runInAction(() => { runInAction(() => {
const _view = cloneDeep(this);
this.filtersToUpdate = { this.filtersToUpdate = {
name: this.name, name: _view.name,
description: this.description, description: _view.description,
filters: this.filters, filters: _view.filters,
display_filters: this.display_filters, display_filters: _view.display_filters,
display_properties: this.display_properties, display_properties: _view.display_properties,
}; };
}); });
}; };
@ -271,15 +279,19 @@ export class ViewStore extends FiltersHelper implements TViewStore {
if (this.filtersToUpdate) await this.update(this.filtersToUpdate); if (this.filtersToUpdate) await this.update(this.filtersToUpdate);
} catch { } catch {
Object.keys(this.filtersToUpdate).forEach((key) => { Object.keys(this.filtersToUpdate).forEach((key) => {
const _key = key as keyof TView; const _key = key as keyof TUpdateView;
set(this, _key, this.filtersToUpdate[_key]); set(this, _key, this.filtersToUpdate[_key]);
}); });
} }
}; };
// actions // actions
update = async (viewData: Partial<TView>) => { update = async (viewData: TUpdateView) => {
try { try {
runInAction(() => {
this.loader = "updating";
});
const { workspaceSlug, projectId } = this.store.app.router; const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug || !this.id) return; if (!workspaceSlug || !this.id) return;
@ -287,10 +299,11 @@ export class ViewStore extends FiltersHelper implements TViewStore {
if (!view) return; if (!view) return;
runInAction(() => { runInAction(() => {
Object.keys(viewData).forEach((key) => { Object.keys(view).forEach((key) => {
const _key = key as keyof TView; const _key = key as keyof TView;
set(this, _key, viewData[_key]); set(this, _key, view[_key]);
}); });
this.loader = undefined;
}); });
} catch { } catch {
this.resetChanges(); this.resetChanges();