chore: implemented extra options in the display filter properties dropdown and hanlded the arrow functions with useCallback

This commit is contained in:
gurusainath 2024-02-14 16:01:19 +05:30
parent 6bde956166
commit c35d650de0
23 changed files with 371 additions and 168 deletions

View File

@ -52,6 +52,8 @@ export type TViewDisplayFiltersOrderBy =
| "sub_issues_count"
| "-sub_issues_count";
export type TViewDisplayFiltersExtraOptions = "sub_issue" | "show_empty_groups";
export type TViewDisplayFiltersType = "active" | "backlog";
export type TViewCalendarLayouts = "month" | "week";

View File

@ -1,4 +1,4 @@
import { FC } from "react";
import { FC, useCallback } from "react";
import { ImagePlus, X } from "lucide-react";
// hooks
import { useViewDetail, useViewFilter } from "hooks/store";
@ -22,9 +22,10 @@ export const ViewAppliedFiltersItem: FC<TViewAppliedFiltersItem> = (props) => {
const propertyDetail = viewFilterHelper?.propertyDetails(filterKey, propertyId) || undefined;
const removeFilterOption = () => {
viewDetailStore?.setFilters(filterKey, propertyId);
};
const removeFilterOption = useCallback(
() => viewDetailStore?.setFilters(filterKey, propertyId),
[viewDetailStore, filterKey, propertyId]
);
return (
<div

View File

@ -1,4 +1,4 @@
import { FC, Fragment } from "react";
import { FC, Fragment, useCallback, useMemo } from "react";
import { observer } from "mobx-react-lite";
import isEmpty from "lodash/isEmpty";
import { X } from "lucide-react";
@ -24,14 +24,23 @@ export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => {
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const viewFilterStore = useViewFilter(workspaceSlug, projectId);
const currentDefaultFilterDetails = viewFilterStore?.propertyDefaultDetails(filterKey);
const currentDefaultFilterDetails = useMemo(
() => viewFilterStore?.propertyDefaultDetails(filterKey),
[viewFilterStore, filterKey]
);
const propertyValues =
viewDetailStore?.appliedFilters?.filters && !isEmpty(viewDetailStore?.appliedFilters?.filters)
? viewDetailStore?.appliedFilters?.filters?.[filterKey] || undefined
: undefined;
const propertyValues = useMemo(
() =>
viewDetailStore?.appliedFilters?.filters && !isEmpty(viewDetailStore?.appliedFilters?.filters)
? viewDetailStore?.appliedFilters?.filters?.[filterKey] || undefined
: undefined,
[filterKey, viewDetailStore?.appliedFilters?.filters]
);
const clearPropertyFilter = () => viewDetailStore?.setFilters(filterKey, "clear_all");
const clearPropertyFilter = useCallback(
() => viewDetailStore?.setFilters(filterKey, "clear_all"),
[viewDetailStore, filterKey]
);
if (!propertyValues || propertyValues.length <= 0) return <></>;
return (

View File

@ -1,4 +1,4 @@
import { FC, Fragment } from "react";
import { FC, Fragment, useCallback, useMemo } from "react";
import { observer } from "mobx-react-lite";
import { X } from "lucide-react";
import isEmpty from "lodash/isEmpty";
@ -23,12 +23,15 @@ export const ViewAppliedFiltersRoot: FC<TViewAppliedFiltersRoot> = observer((pro
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const filterKeys =
viewDetailStore?.filtersToUpdate && !isEmpty(viewDetailStore?.filtersToUpdate?.filters)
? Object.keys(viewDetailStore?.filtersToUpdate?.filters)
: undefined;
const filterKeys = useMemo(
() =>
viewDetailStore?.filtersToUpdate && !isEmpty(viewDetailStore?.filtersToUpdate?.filters)
? Object.keys(viewDetailStore?.filtersToUpdate?.filters)
: undefined,
[viewDetailStore?.filtersToUpdate]
);
const clearAllFilters = () => viewDetailStore?.setFilters(undefined, "clear_all");
const clearAllFilters = useCallback(() => viewDetailStore?.setFilters(undefined, "clear_all"), [viewDetailStore]);
if (!filterKeys || !viewDetailStore?.isFiltersApplied)
return (

View File

@ -0,0 +1,54 @@
import { FC, Fragment, useCallback, useMemo } from "react";
import { observer } from "mobx-react-lite";
import { Check } from "lucide-react";
// hooks
import { useViewDetail } from "hooks/store";
// types
import { TViewDisplayFiltersExtraOptions, TViewTypes } from "@plane/types";
// constants
import { EXTRA_OPTIONS_PROPERTY } from "constants/view";
type TDisplayFilterExtraOptions = {
workspaceSlug: string;
projectId: string | undefined;
viewId: string;
viewType: TViewTypes;
filterKey: TViewDisplayFiltersExtraOptions;
};
export const DisplayFilterExtraOptions: FC<TDisplayFilterExtraOptions> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, filterKey } = props;
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const optionTitle = useMemo(() => EXTRA_OPTIONS_PROPERTY[filterKey].label, [filterKey]);
const isSelected = viewDetailStore?.appliedFilters?.display_filters?.[filterKey] ? true : false;
const handlePropertySelection = useCallback(
() => viewDetailStore?.setDisplayFilters({ [filterKey]: !isSelected }),
[viewDetailStore, filterKey, isSelected]
);
return (
<Fragment>
<div
className="relative w-full flex items-center overflow-hidden gap-2.5 cursor-pointer p-1 py-1.5 rounded hover:bg-custom-background-80 transition-all group"
onClick={handlePropertySelection}
>
<div
className={`flex-shrink-0 w-3 h-3 flex justify-center items-center border rounded text-bold ${
isSelected
? "border-custom-primary-100 bg-custom-primary-100"
: "border-custom-border-400 bg-custom-background-100"
}`}
>
{isSelected && <Check size={14} />}
</div>
<div className="text-xs block truncate line-clamp-1 text-custom-text-200 group-hover:text-custom-text-100">
{optionTitle || "Extra Option"}
</div>
</div>
</Fragment>
);
});

View File

@ -1,4 +1,4 @@
import { FC, useState } from "react";
import { FC, useCallback, useMemo, useState } from "react";
import { observer } from "mobx-react-lite";
import { ChevronUp, ChevronDown } from "lucide-react";
import filter from "lodash/filter";
@ -7,9 +7,9 @@ import uniq from "lodash/uniq";
// hooks
import { useViewDetail } from "hooks/store";
// components
import { ViewDisplayPropertiesRoot, ViewDisplayFiltersItemRoot } from "../";
import { ViewDisplayPropertiesRoot, ViewDisplayFiltersItemRoot, DisplayFilterExtraOptions } from "../";
// types
import { TViewDisplayFilters, TViewTypes } from "@plane/types";
import { TViewDisplayFilters, TViewDisplayFiltersExtraOptions, TViewTypes } from "@plane/types";
// constants
import { EViewPageType, viewDefaultFilterParametersByViewTypeAndLayout } from "constants/view";
@ -29,18 +29,22 @@ export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = observer((pro
const [filterVisibility, setFilterVisibility] = useState<(Partial<keyof TViewDisplayFilters> | "display_property")[]>(
[]
);
const handleFilterVisibility = (key: keyof TViewDisplayFilters | "display_property") => {
const handleFilterVisibility = useCallback((key: keyof TViewDisplayFilters | "display_property") => {
setFilterVisibility((prevData = []) => {
if (prevData.includes(key)) return filter(prevData, (item) => item !== key);
return uniq(concat(prevData, [key]));
});
};
}, []);
const layout = viewDetailStore?.appliedFilters?.display_filters?.layout;
const filtersProperties = useMemo(() => {
const layout = viewDetailStore?.appliedFilters?.display_filters?.layout;
return layout ? viewDefaultFilterParametersByViewTypeAndLayout(viewPageType, layout, "display_filters") : [];
}, [viewDetailStore, viewPageType]);
const filtersProperties = layout
? viewDefaultFilterParametersByViewTypeAndLayout(viewPageType, layout, "display_filters")
: [];
const filtersExtraProperties = useMemo(() => {
const layout = viewDetailStore?.appliedFilters?.display_filters?.layout;
return layout ? viewDefaultFilterParametersByViewTypeAndLayout(viewPageType, layout, "extra_options") : [];
}, [viewDetailStore, viewPageType]);
return (
<div className="space-y-1 divide-y divide-custom-border-300">
@ -67,7 +71,7 @@ export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = observer((pro
</div>
{filtersProperties.map((filterKey) => (
<div key={filterKey} className="relative py-1 last:pb-0">
<div key={filterKey} className="relative py-1">
<div className="sticky top-0 z-20 flex justify-between items-center gap-2 bg-custom-background-100 select-none">
<div className="font-medium text-xs text-custom-text-300 capitalize py-1">
{filterKey.replaceAll("_", " ")}
@ -91,10 +95,16 @@ export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = observer((pro
</div>
))}
{/* extra options */}
<div>
<div>Show sub issues</div>
<div>Show Empty groups</div>
<div className="pt-1 pb-0">
{filtersExtraProperties.map((option) => (
<DisplayFilterExtraOptions
workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
filterKey={option as TViewDisplayFiltersExtraOptions}
/>
))}
</div>
</div>
);

View File

@ -1,4 +1,4 @@
import { FC } from "react";
import { FC, useCallback } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useViewDetail } from "hooks/store";
@ -18,9 +18,12 @@ export const ViewDisplayPropertySelection: FC<TViewDisplayPropertySelection> = o
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const propertyIsSelected = viewDetailStore?.appliedFilters?.display_properties?.[property];
const handlePropertySelection = useCallback(
() => viewDetailStore?.setDisplayProperties(property),
[viewDetailStore, property]
);
const handlePropertySelection = () => viewDetailStore?.setDisplayProperties(property);
const propertyIsSelected = viewDetailStore?.appliedFilters?.display_properties?.[property];
return (
<div

View File

@ -1,4 +1,4 @@
import { FC, Fragment, ReactNode, useRef, useState } from "react";
import { FC, Fragment, ReactNode, useCallback, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
@ -64,14 +64,17 @@ export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) =>
],
});
const handleDropdownOpen = () => setDropdownToggle(true);
const handleDropdownClose = () => setDropdownToggle(false);
const handleDropdownToggle = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
event.stopPropagation();
if (!dropdownToggle) handleDropdownOpen();
else handleDropdownClose();
};
const handleDropdownOpen = useCallback(() => setDropdownToggle(true), []);
const handleDropdownClose = useCallback(() => setDropdownToggle(false), []);
const handleDropdownToggle = useCallback(
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
event.stopPropagation();
if (!dropdownToggle) handleDropdownOpen();
else handleDropdownClose();
},
[dropdownToggle, handleDropdownOpen, handleDropdownClose]
);
useOutsideClickDetector(dropdownRef, () => dateCustomFilterToggle === undefined && handleDropdownClose());

View File

@ -1,4 +1,4 @@
import { FC, useState } from "react";
import { FC, useCallback, useMemo, useState } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useViewDetail, useViewFilter } from "hooks/store";
@ -27,28 +27,37 @@ export const ViewFiltersItemRoot: FC<TViewFiltersItemRoot> = observer((props) =>
// state
const [viewAll, setViewAll] = useState(false);
const propertyIds = viewFilterHelper?.filterIdsWithKey(filterKey) || [];
const propertyIds = useMemo(() => viewFilterHelper?.filterIdsWithKey(filterKey) || [], [viewFilterHelper, filterKey]);
const filterPropertyIds = propertyIds.length > 5 ? (viewAll ? propertyIds : propertyIds.slice(0, 5)) : propertyIds;
const filterPropertyIds = useMemo(
() => (propertyIds.length > 5 ? (viewAll ? propertyIds : propertyIds.slice(0, 5)) : propertyIds),
[propertyIds, viewAll]
);
const handlePropertySelection = (_propertyId: string) => {
if (["start_date", "target_date"].includes(filterKey)) {
if (_propertyId === "custom") {
const _propertyIds = viewDetailStore?.appliedFilters?.filters?.[filterKey] || [];
const selectedDates = _propertyIds.filter((id) => id.includes("-"));
if (selectedDates.length > 0)
selectedDates.forEach((date: string) => viewDetailStore?.setFilters(filterKey, date));
else setDateCustomFilterToggle(filterKey);
const handlePropertySelection = useCallback(
(_propertyId: string) => {
if (["start_date", "target_date"].includes(filterKey)) {
if (_propertyId === "custom") {
const _propertyIds = viewDetailStore?.appliedFilters?.filters?.[filterKey] || [];
const selectedDates = _propertyIds.filter((id) => id.includes("-"));
if (selectedDates.length > 0)
selectedDates.forEach((date: string) => viewDetailStore?.setFilters(filterKey, date));
else setDateCustomFilterToggle(filterKey);
} else viewDetailStore?.setFilters(filterKey, _propertyId);
} else viewDetailStore?.setFilters(filterKey, _propertyId);
} else viewDetailStore?.setFilters(filterKey, _propertyId);
};
},
[filterKey, viewDetailStore, setDateCustomFilterToggle]
);
const handleCustomDateSelection = (selectedDates: string[]) => {
selectedDates.forEach((date: string) => {
viewDetailStore?.setFilters(filterKey, date);
setDateCustomFilterToggle(undefined);
});
};
const handleCustomDateSelection = useCallback(
(selectedDates: string[]) => {
selectedDates.forEach((date: string) => {
viewDetailStore?.setFilters(filterKey, date);
setDateCustomFilterToggle(undefined);
});
},
[filterKey, viewDetailStore, setDateCustomFilterToggle]
);
if (propertyIds.length <= 0)
return <div className="text-xs italic py-1 text-custom-text-300">No items are available.</div>;

View File

@ -1,4 +1,4 @@
import { FC } from "react";
import { FC, useMemo } from "react";
import { Check } from "lucide-react";
import { observer } from "mobx-react-lite";
// hooks
@ -20,7 +20,10 @@ export const ViewFilterSelection: FC<TViewFilterSelection> = observer((props) =>
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const propertyIds = viewDetailStore?.appliedFilters?.filters?.[filterKey] || [];
const propertyIds = useMemo(
() => viewDetailStore?.appliedFilters?.filters?.[filterKey] || [],
[viewDetailStore?.appliedFilters?.filters, filterKey]
);
const isSelected = ["start_date", "target_date"].includes(filterKey)
? propertyId === "custom"
@ -30,6 +33,18 @@ export const ViewFilterSelection: FC<TViewFilterSelection> = observer((props) =>
: propertyIds?.includes(propertyId)
: propertyIds?.includes(propertyId) || false;
// const isSelected = useMemo(
// () =>
// ["start_date", "target_date"].includes(filterKey)
// ? propertyId === "custom"
// ? propertyIds.filter((id) => id.includes("-")).length > 0
// ? true
// : false
// : propertyIds?.includes(propertyId)
// : propertyIds?.includes(propertyId) || false,
// [filterKey, propertyId, propertyIds]
// );
return (
<div
className={`flex-shrink-0 w-3 h-3 flex justify-center items-center border rounded text-bold ${

View File

@ -1,4 +1,4 @@
import { FC, useState } from "react";
import { FC, useCallback, useMemo, useState } from "react";
import { observer } from "mobx-react-lite";
import { ChevronDown, ChevronUp } from "lucide-react";
import concat from "lodash/concat";
@ -36,20 +36,19 @@ export const ViewFiltersRoot: FC<TViewFiltersRoot> = observer((props) => {
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
// state
const [filterVisibility, setFilterVisibility] = useState<Partial<keyof TViewFilters>[]>([]);
const handleFilterVisibility = (key: keyof TViewFilters) => {
const handleFilterVisibility = useCallback((key: keyof TViewFilters) => {
setFilterVisibility((prevData = []) => {
if (prevData.includes(key)) return filter(prevData, (item) => item !== key);
return uniq(concat(prevData, [key]));
});
};
}, []);
const layout = viewDetailStore?.appliedFilters?.display_filters?.layout;
const filtersProperties = useMemo(() => {
const layout = viewDetailStore?.appliedFilters?.display_filters?.layout;
return layout ? viewDefaultFilterParametersByViewTypeAndLayout(viewPageType, layout, "filters") : [];
}, [viewDetailStore?.appliedFilters?.display_filters?.layout, viewPageType]);
const filtersProperties = layout
? viewDefaultFilterParametersByViewTypeAndLayout(viewPageType, layout, "filters")
: [];
if (!layout || filtersProperties.length <= 0) return <></>;
if (filtersProperties.length <= 0) return <></>;
return (
<div className="space-y-1 divide-y divide-custom-border-300">
{filtersProperties.map((filterKey) => (

View File

@ -0,0 +1,74 @@
import { FC, Fragment, ReactNode, useMemo } from "react";
import Link from "next/link";
import { Briefcase, CheckCircle, ChevronRight } from "lucide-react";
// hooks
import { useProject } from "hooks/store";
// types
import { TViewTypes } from "@plane/types";
type TViewHeader = {
projectId: string | undefined;
viewType: TViewTypes;
titleIcon: ReactNode;
title: string;
workspaceViewTabOptions: { key: TViewTypes; title: string; href: string }[];
};
export const ViewHeader: FC<TViewHeader> = (props) => {
const { projectId, viewType, titleIcon, title, workspaceViewTabOptions } = props;
// hooks
const { getProjectById } = useProject();
const projectDetails = useMemo(
() => (projectId ? getProjectById(projectId) : undefined),
[projectId, getProjectById]
);
return (
<div className="relative flex items-center gap-2">
{projectDetails && (
<Fragment>
<div className="relative flex items-center gap-2 overflow-hidden">
<div className="flex-shrink-0 w-6 h-6 rounded relative flex justify-center items-center bg-custom-background-80">
{projectDetails?.icon_prop ? projectDetails?.icon_prop.toString() : <Briefcase size={12} />}
</div>
<div className="font-medium inline-block whitespace-nowrap overflow-hidden truncate line-clamp-1 text-sm">
{projectDetails?.name ? projectDetails?.name : "Project Issues"}
</div>
</div>
<div className="text-custom-text-200">
<ChevronRight size={12} />
</div>
</Fragment>
)}
<div className="relative flex items-center gap-2 overflow-hidden">
<div className="flex-shrink-0 w-6 h-6 rounded relative flex justify-center items-center bg-custom-background-80">
{titleIcon ? titleIcon : <CheckCircle size={12} />}
</div>
<div className="font-medium inline-block whitespace-nowrap overflow-hidden truncate line-clamp-1 text-sm">
{title ? title : "All Issues"}
</div>
</div>
<div className="ml-auto relative flex items-center gap-3">
<div className="relative flex items-center rounded border border-custom-border-200 bg-custom-background-80">
{workspaceViewTabOptions.map((tab) => (
<Link
key={tab.key}
href={tab.href}
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-100"
: "text-custom-text-200 bg-custom-background-80 hover:text-custom-text-100"
}`}
>
{tab.title}
</Link>
))}
</div>
</div>
</div>
);
};

View File

@ -1,5 +1,7 @@
export * from "./root";
export * from "./header-tabs";
// views
export * from "./views/root";
export * from "./views/view-item";
@ -25,6 +27,7 @@ export * from "./display-filters/root";
export * from "./display-filters/display-filter-item-root";
export * from "./display-filters/display-filter-item";
export * from "./display-filters/display-filter-selection";
export * from "./display-filters/extra-options";
// view display properties
export * from "./display-properties/root";

View File

@ -1,4 +1,4 @@
import { FC, Fragment } from "react";
import { FC, Fragment, useMemo } from "react";
import { observer } from "mobx-react-lite";
import { LucideIcon, List, Kanban, Calendar, Sheet, GanttChartSquare } from "lucide-react";
// hooks
@ -18,20 +18,23 @@ type TViewLayoutRoot = {
viewPageType: EViewPageType;
};
const LAYOUTS_DATA: { key: EViewLayouts; title: string; icon: LucideIcon }[] = [
{ key: EViewLayouts.LIST, title: "List Layout", icon: List },
{ key: EViewLayouts.KANBAN, title: "Kanban Layout", icon: Kanban },
{ key: EViewLayouts.CALENDAR, title: "Calendar Layout", icon: Calendar },
{ key: EViewLayouts.SPREADSHEET, title: "Spreadsheet Layout", icon: Sheet },
{ key: EViewLayouts.GANTT, title: "Gantt Chart layout", icon: GanttChartSquare },
];
export const ViewLayoutRoot: FC<TViewLayoutRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewPageType } = props;
// hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const validLayouts = viewPageDefaultLayoutsByPageType(viewPageType);
const LAYOUTS_DATA: { key: EViewLayouts; title: string; icon: LucideIcon }[] = useMemo(
() => [
{ key: EViewLayouts.LIST, title: "List Layout", icon: List },
{ key: EViewLayouts.KANBAN, title: "Kanban Layout", icon: Kanban },
{ key: EViewLayouts.CALENDAR, title: "Calendar Layout", icon: Calendar },
{ key: EViewLayouts.SPREADSHEET, title: "Spreadsheet Layout", icon: Sheet },
{ key: EViewLayouts.GANTT, title: "Gantt Chart layout", icon: GanttChartSquare },
],
[]
);
const validLayouts = useMemo(() => viewPageDefaultLayoutsByPageType(viewPageType), [viewPageType]);
if (!viewDetailStore || validLayouts.length <= 1) return <></>;
return (

View File

@ -1,7 +1,5 @@
import { FC, Fragment, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { FC, Fragment, useCallback, useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react-lite";
import { CheckCircle } from "lucide-react";
import { v4 as uuidV4 } from "uuid";
import cloneDeep from "lodash/cloneDeep";
// hooks
@ -35,7 +33,6 @@ type TGlobalViewRoot = {
viewType: TViewTypes;
viewPageType: EViewPageType;
baseRoute: string;
workspaceViewTabOptions: { key: TViewTypes; title: string; href: string }[];
};
type TViewOperationsToggle = {
@ -44,7 +41,7 @@ type TViewOperationsToggle = {
};
export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewPageType, baseRoute, workspaceViewTabOptions } = props;
const { workspaceSlug, projectId, viewId, viewType, viewPageType, baseRoute } = props;
// hooks
const viewStore = useView(workspaceSlug, projectId, viewType);
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
@ -54,9 +51,11 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
type: undefined,
viewId: undefined,
});
const handleViewOperationsToggle = (type: TViewOperationsToggle["type"], viewId: string | undefined) =>
setViewOperationsToggle({ type, viewId });
const handleViewOperationsToggle = useCallback(
(type: TViewOperationsToggle["type"], viewId: string | undefined) => setViewOperationsToggle({ type, viewId }),
[]
);
// hooks
const viewDetailCreateEditStore = useViewDetail(
workspaceSlug,
projectId,
@ -153,7 +152,7 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
}
},
}),
[viewStore, viewDetailStore, setToastAlert, viewDetailCreateEditStore]
[viewStore, viewDetailStore, setToastAlert, viewDetailCreateEditStore, handleViewOperationsToggle]
);
// fetch all views
@ -174,43 +173,13 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
return (
<div className="relative w-full h-full">
<div className="relative flex items-center gap-2 px-5 py-4">
<div className="relative flex items-center gap-2 overflow-hidden">
<div className="flex-shrink-0 w-6 h-6 rounded relative flex justify-center items-center bg-custom-background-80">
<CheckCircle size={12} />
</div>
<div className="font-medium inline-block whitespace-nowrap overflow-hidden truncate line-clamp-1">
All Issues
</div>
</div>
<div className="ml-auto relative flex items-center gap-3">
<div className="relative flex items-center rounded border border-custom-border-200 bg-custom-background-80">
{workspaceViewTabOptions.map((tab) => (
<Link
key={tab.key}
href={tab.href}
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-100"
: "text-custom-text-200 bg-custom-background-80 hover:text-custom-text-100"
}`}
>
{tab.title}
</Link>
))}
</div>
</div>
</div>
{viewStore?.loader && viewStore?.loader === "init-loader" ? (
<div className="relative w-full h-full flex justify-center items-center">
<Spinner />
</div>
) : (
<>
<div className="border-b border-custom-border-200">
<div className="border-b border-custom-border-200 pt-2">
<ViewRoot
workspaceSlug={workspaceSlug}
projectId={projectId}

View File

@ -1,4 +1,4 @@
import { FC, Fragment, useEffect, useState } from "react";
import { FC, Fragment, useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// hooks
@ -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 = 116;
const itemWidth = 124;
if (!tabContainer || !tabItemViewMore) return;
const containerWidth = tabContainer.clientWidth;
@ -49,12 +49,14 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
return () => window.removeEventListener("resize", () => handleViewTabsVisibility());
}, [viewStore?.viewIds]);
const viewIds = viewStore?.viewIds?.slice(0, itemsToRenderViewsCount || viewStore?.viewIds.length) || [];
if (!viewIds.includes(viewId)) {
viewIds.pop();
viewIds.push(viewId);
}
const viewIds = useMemo(() => {
const ids = viewStore?.viewIds?.slice(0, itemsToRenderViewsCount || viewStore?.viewIds.length) || [];
if (!ids.includes(viewId)) {
ids.pop();
ids.push(viewId);
}
return ids;
}, [viewId, viewStore, itemsToRenderViewsCount]);
return (
<div className="relative flex justify-between px-5 gap-2">

View File

@ -1,4 +1,4 @@
import { FC, Fragment, ReactNode, useRef, useState } from "react";
import { FC, Fragment, ReactNode, useCallback, useRef, useState } from "react";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Placement } from "@popperjs/core";
@ -7,7 +7,7 @@ import { Plus, Search } from "lucide-react";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { useView } from "hooks/store";
// components
import { ViewDropdownItem } from "..";
import { ViewDropdownItem } from "../";
// types
import { TViewTypes } from "@plane/types";
import { TViewOperations } from "../types";
@ -63,14 +63,17 @@ export const ViewDropdown: FC<TViewDropdown> = (props) => {
],
});
const handleDropdownOpen = () => setDropdownToggle(true);
const handleDropdownClose = () => setDropdownToggle(false);
const handleDropdownToggle = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
event.stopPropagation();
if (!dropdownToggle) handleDropdownOpen();
else handleDropdownClose();
};
const handleDropdownOpen = useCallback(() => setDropdownToggle(true), []);
const handleDropdownClose = useCallback(() => setDropdownToggle(false), []);
const handleDropdownToggle = useCallback(
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
event.stopPropagation();
if (!dropdownToggle) handleDropdownOpen();
else handleDropdownClose();
},
[dropdownToggle, handleDropdownOpen, handleDropdownClose]
);
useOutsideClickDetector(dropdownRef, handleDropdownClose);

View File

@ -7,6 +7,7 @@ import {
TViewDisplayFiltersGrouped,
TViewDisplayFiltersOrderBy,
TViewDisplayFiltersType,
TViewDisplayFiltersExtraOptions,
} from "@plane/types";
// filters constants
@ -61,7 +62,7 @@ export const TYPE_PROPERTY: Record<TViewDisplayFiltersType | "null", { label: st
backlog: { label: "Backlog issues" },
};
export const EXTRA_OPTIONS_PROPERTY: Record<string, { label: string }> = {
export const EXTRA_OPTIONS_PROPERTY: Record<TViewDisplayFiltersExtraOptions, { label: string }> = {
sub_issue: { label: "Sub Issues" },
show_empty_groups: { label: "Show Empty Groups" },
};
@ -111,8 +112,9 @@ const ALL_FILTER_PERMISSIONS: TFilterPermissions["all"] = {
layouts: [EViewLayouts.SPREADSHEET],
[EViewLayouts.SPREADSHEET]: {
filters: ["project", "priority", "state_group", "assignees", "created_by", "labels", "start_date", "target_date"],
display_filters: ["type"],
// display_filters: ["type"],
// extra_options: [],
display_filters: ["group_by", "sub_group_by", "order_by", "type"],
extra_options: ["sub_issue", "show_empty_groups"],
display_properties: true,
},

View File

@ -30,6 +30,7 @@ import {
GROUP_BY_PROPERTY,
ORDER_BY_PROPERTY,
TYPE_PROPERTY,
EViewLayouts,
} from "constants/view/filters";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
@ -346,12 +347,19 @@ export const useViewFilter = (workspaceSlug: string, projectId: string | undefin
}
};
const displayFilterIdsWithKey = (displayFilterKey: keyof TViewDisplayFilters): string[] | undefined => {
const displayFilterIdsWithKey = (
displayFilterKey: keyof TViewDisplayFilters,
layout?: EViewLayouts
): string[] | undefined => {
if (!displayFilterKey) return undefined;
switch (displayFilterKey) {
case "group_by":
return Object.keys(GROUP_BY_PROPERTY) || undefined;
return (
Object.keys(GROUP_BY_PROPERTY).filter((property) =>
layout === EViewLayouts.KANBAN ? (property !== "null" ? false : true) : true
) || undefined
);
case "sub_group_by":
return Object.keys(GROUP_BY_PROPERTY) || undefined;
case "order_by":

View File

@ -1,9 +1,10 @@
import { ReactElement, useMemo } from "react";
import { useRouter } from "next/router";
import { CheckCircle } from "lucide-react";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { GlobalViewRoot } from "components/view";
import { GlobalViewRoot, ViewHeader } from "components/view";
// types
import { NextPageWithLayout } from "lib/types";
// constants
@ -31,8 +32,20 @@ const WorkspacePrivateViewPage: NextPageWithLayout = () => {
if (!workspaceSlug || !viewId) return <></>;
return (
<div className="h-full overflow-hidden bg-custom-background-100">
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
<div className="w-full h-full overflow-hidden bg-custom-background-100 relative flex flex-col">
<div className="flex-shrink-0 w-full">
{/* header */}
<div className="px-5 pt-4 pb-2 border-b border-custom-border-200">
<ViewHeader
projectId={undefined}
viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS}
titleIcon={<CheckCircle size={12} />}
title="All Issues"
workspaceViewTabOptions={workspaceViewTabOptions}
/>
</div>
{/* content */}
<GlobalViewRoot
workspaceSlug={workspaceSlug.toString()}
projectId={undefined}
@ -40,9 +53,12 @@ const WorkspacePrivateViewPage: NextPageWithLayout = () => {
viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS}
viewPageType={EViewPageType.ALL}
baseRoute={`/${workspaceSlug?.toString()}/views/private`}
workspaceViewTabOptions={workspaceViewTabOptions}
/>
</div>
<div className="w-full h-full overflow-hidden relative flex justify-center items-center text-sm text-custom-text-300">
Issues render placeholder
</div>
</div>
);
};

View File

@ -1,9 +1,10 @@
import { ReactElement, useMemo } from "react";
import { useRouter } from "next/router";
import { CheckCircle } from "lucide-react";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { GlobalViewRoot } from "components/view";
import { GlobalViewRoot, ViewHeader } from "components/view";
// types
import { NextPageWithLayout } from "lib/types";
// constants
@ -33,6 +34,18 @@ const WorkspacePublicViewPage: NextPageWithLayout = () => {
return (
<div className="w-full h-full overflow-hidden bg-custom-background-100 relative flex flex-col">
<div className="flex-shrink-0 w-full">
{/* header */}
<div className="px-5 pt-4 pb-2 border-b border-custom-border-200">
<ViewHeader
projectId={undefined}
viewType={VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS}
titleIcon={<CheckCircle size={12} />}
title="All Issues"
workspaceViewTabOptions={workspaceViewTabOptions}
/>
</div>
{/* content */}
<GlobalViewRoot
workspaceSlug={workspaceSlug.toString()}
projectId={undefined}
@ -40,10 +53,12 @@ const WorkspacePublicViewPage: NextPageWithLayout = () => {
viewType={VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS}
viewPageType={EViewPageType.ALL}
baseRoute={`/${workspaceSlug?.toString()}/views/public`}
workspaceViewTabOptions={workspaceViewTabOptions}
/>
</div>
<div className="w-full h-full overflow-hidden">Issues render</div>
<div className="w-full h-full overflow-hidden relative flex justify-center items-center text-sm text-custom-text-300">
Issues render placeholder
</div>
</div>
);
};

View File

@ -14,19 +14,19 @@ import { EViewPageType, viewPageDefaultLayoutsByPageType } from "constants/view"
export class FiltersHelper {
// computed filters
computedFilters = (filters: TViewFilters, defaultValues?: Partial<TViewFilters>): TViewFilters => ({
project: get(defaultValues, "project", get(filters, "project", [])),
module: get(defaultValues, "module", get(filters, "module", [])),
cycle: get(defaultValues, "cycle", get(filters, "cycle", [])),
priority: get(defaultValues, "priority", get(filters, "priority", [])),
state: get(defaultValues, "state", get(filters, "state", [])),
state_group: get(defaultValues, "state_group", get(filters, "state_group", [])),
assignees: get(defaultValues, "assignees", get(filters, "assignees", [])),
mentions: get(defaultValues, "mentions", get(filters, "mentions", [])),
subscriber: get(defaultValues, "subscriber", get(filters, "subscriber", [])),
created_by: get(defaultValues, "created_by", get(filters, "created_by", [])),
labels: get(defaultValues, "labels", get(filters, "labels", [])),
start_date: get(defaultValues, "start_date", get(filters, "start_date", [])),
target_date: get(defaultValues, "target_date", get(filters, "target_date", [])),
project: defaultValues?.project || filters?.project || [],
module: defaultValues?.module || filters?.module || [],
cycle: defaultValues?.cycle || filters?.cycle || [],
priority: defaultValues?.priority || filters?.priority || [],
state: defaultValues?.state || filters?.state || [],
state_group: defaultValues?.state_group || filters?.state_group || [],
assignees: defaultValues?.assignees || filters?.assignees || [],
mentions: defaultValues?.mentions || filters?.mentions || [],
subscriber: defaultValues?.subscriber || filters?.subscriber || [],
created_by: defaultValues?.created_by || filters?.created_by || [],
labels: defaultValues?.labels || filters?.labels || [],
start_date: defaultValues?.start_date || filters?.start_date || [],
target_date: defaultValues?.target_date || filters?.target_date || [],
});
// computed display filters

View File

@ -21,7 +21,7 @@ import {
// helpers
import { FiltersHelper } from "./helpers/filters_helpers";
// constants
import { EViewPageType, viewDefaultFilterParametersByViewTypeAndLayout } from "constants/view";
import { EViewLayouts, EViewPageType, viewDefaultFilterParametersByViewTypeAndLayout } from "constants/view";
type TLoader = "updating" | undefined;
@ -250,11 +250,11 @@ export class ViewStore extends FiltersHelper implements TViewStore {
const sub_issue = appliedFilters?.display_filters?.sub_issue;
if (group_by === undefined && display_filters.sub_group_by) display_filters.sub_group_by = undefined;
if (layout === "kanban") {
if (layout === EViewLayouts.KANBAN) {
if (sub_group_by === group_by) display_filters.group_by = undefined;
if (group_by === null) display_filters.group_by = "state";
}
if (layout === "spreadsheet" && sub_issue === true) display_filters.sub_issue = false;
if (layout === EViewLayouts.SPREADSHEET && sub_issue === true) display_filters.sub_issue = false;
runInAction(() => {
Object.keys(display_filters).forEach((key) => {