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"
| "-sub_issues_count"; | "-sub_issues_count";
export type TViewDisplayFiltersExtraOptions = "sub_issue" | "show_empty_groups";
export type TViewDisplayFiltersType = "active" | "backlog"; export type TViewDisplayFiltersType = "active" | "backlog";
export type TViewCalendarLayouts = "month" | "week"; 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"; import { ImagePlus, X } from "lucide-react";
// hooks // hooks
import { useViewDetail, useViewFilter } from "hooks/store"; import { useViewDetail, useViewFilter } from "hooks/store";
@ -22,9 +22,10 @@ export const ViewAppliedFiltersItem: FC<TViewAppliedFiltersItem> = (props) => {
const propertyDetail = viewFilterHelper?.propertyDetails(filterKey, propertyId) || undefined; const propertyDetail = viewFilterHelper?.propertyDetails(filterKey, propertyId) || undefined;
const removeFilterOption = () => { const removeFilterOption = useCallback(
viewDetailStore?.setFilters(filterKey, propertyId); () => viewDetailStore?.setFilters(filterKey, propertyId),
}; [viewDetailStore, filterKey, propertyId]
);
return ( return (
<div <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 { observer } from "mobx-react-lite";
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
import { X } from "lucide-react"; import { X } from "lucide-react";
@ -24,14 +24,23 @@ export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => {
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const viewFilterStore = useViewFilter(workspaceSlug, projectId); const viewFilterStore = useViewFilter(workspaceSlug, projectId);
const currentDefaultFilterDetails = viewFilterStore?.propertyDefaultDetails(filterKey); const currentDefaultFilterDetails = useMemo(
() => viewFilterStore?.propertyDefaultDetails(filterKey),
[viewFilterStore, filterKey]
);
const propertyValues = const propertyValues = useMemo(
viewDetailStore?.appliedFilters?.filters && !isEmpty(viewDetailStore?.appliedFilters?.filters) () =>
? viewDetailStore?.appliedFilters?.filters?.[filterKey] || undefined viewDetailStore?.appliedFilters?.filters && !isEmpty(viewDetailStore?.appliedFilters?.filters)
: undefined; ? 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 <></>; if (!propertyValues || propertyValues.length <= 0) return <></>;
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 { observer } from "mobx-react-lite";
import { X } from "lucide-react"; import { X } from "lucide-react";
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
@ -23,12 +23,15 @@ export const ViewAppliedFiltersRoot: FC<TViewAppliedFiltersRoot> = observer((pro
// hooks // hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
const filterKeys = const filterKeys = useMemo(
viewDetailStore?.filtersToUpdate && !isEmpty(viewDetailStore?.filtersToUpdate?.filters) () =>
? Object.keys(viewDetailStore?.filtersToUpdate?.filters) viewDetailStore?.filtersToUpdate && !isEmpty(viewDetailStore?.filtersToUpdate?.filters)
: undefined; ? 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) if (!filterKeys || !viewDetailStore?.isFiltersApplied)
return ( 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 { observer } from "mobx-react-lite";
import { ChevronUp, ChevronDown } from "lucide-react"; import { ChevronUp, ChevronDown } from "lucide-react";
import filter from "lodash/filter"; import filter from "lodash/filter";
@ -7,9 +7,9 @@ import uniq from "lodash/uniq";
// hooks // hooks
import { useViewDetail } from "hooks/store"; import { useViewDetail } from "hooks/store";
// components // components
import { ViewDisplayPropertiesRoot, ViewDisplayFiltersItemRoot } from "../"; import { ViewDisplayPropertiesRoot, ViewDisplayFiltersItemRoot, DisplayFilterExtraOptions } from "../";
// types // types
import { TViewDisplayFilters, TViewTypes } from "@plane/types"; import { TViewDisplayFilters, TViewDisplayFiltersExtraOptions, TViewTypes } from "@plane/types";
// constants // constants
import { EViewPageType, viewDefaultFilterParametersByViewTypeAndLayout } from "constants/view"; 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 [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 = []) => { setFilterVisibility((prevData = []) => {
if (prevData.includes(key)) return filter(prevData, (item) => item !== key); if (prevData.includes(key)) return filter(prevData, (item) => item !== key);
return uniq(concat(prevData, [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 const filtersExtraProperties = useMemo(() => {
? viewDefaultFilterParametersByViewTypeAndLayout(viewPageType, layout, "display_filters") const layout = viewDetailStore?.appliedFilters?.display_filters?.layout;
: []; return layout ? viewDefaultFilterParametersByViewTypeAndLayout(viewPageType, layout, "extra_options") : [];
}, [viewDetailStore, viewPageType]);
return ( return (
<div className="space-y-1 divide-y divide-custom-border-300"> <div className="space-y-1 divide-y divide-custom-border-300">
@ -67,7 +71,7 @@ export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = observer((pro
</div> </div>
{filtersProperties.map((filterKey) => ( {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="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"> <div className="font-medium text-xs text-custom-text-300 capitalize py-1">
{filterKey.replaceAll("_", " ")} {filterKey.replaceAll("_", " ")}
@ -91,10 +95,16 @@ export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = observer((pro
</div> </div>
))} ))}
{/* extra options */} <div className="pt-1 pb-0">
<div> {filtersExtraProperties.map((option) => (
<div>Show sub issues</div> <DisplayFilterExtraOptions
<div>Show Empty groups</div> workspaceSlug={workspaceSlug}
projectId={projectId}
viewId={viewId}
viewType={viewType}
filterKey={option as TViewDisplayFiltersExtraOptions}
/>
))}
</div> </div>
</div> </div>
); );

View File

@ -1,4 +1,4 @@
import { FC } from "react"; import { FC, useCallback } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useViewDetail } from "hooks/store"; import { useViewDetail } from "hooks/store";
@ -18,9 +18,12 @@ export const ViewDisplayPropertySelection: FC<TViewDisplayPropertySelection> = o
// hooks // hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); 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 ( return (
<div <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 { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
@ -64,14 +64,17 @@ export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) =>
], ],
}); });
const handleDropdownOpen = () => setDropdownToggle(true); const handleDropdownOpen = useCallback(() => setDropdownToggle(true), []);
const handleDropdownClose = () => setDropdownToggle(false); const handleDropdownClose = useCallback(() => setDropdownToggle(false), []);
const handleDropdownToggle = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { const handleDropdownToggle = useCallback(
event.preventDefault(); (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.stopPropagation(); event.preventDefault();
if (!dropdownToggle) handleDropdownOpen(); event.stopPropagation();
else handleDropdownClose(); if (!dropdownToggle) handleDropdownOpen();
}; else handleDropdownClose();
},
[dropdownToggle, handleDropdownOpen, handleDropdownClose]
);
useOutsideClickDetector(dropdownRef, () => dateCustomFilterToggle === undefined && 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"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useViewDetail, useViewFilter } from "hooks/store"; import { useViewDetail, useViewFilter } from "hooks/store";
@ -27,28 +27,37 @@ export const ViewFiltersItemRoot: FC<TViewFiltersItemRoot> = observer((props) =>
// state // state
const [viewAll, setViewAll] = useState(false); 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) => { const handlePropertySelection = useCallback(
if (["start_date", "target_date"].includes(filterKey)) { (_propertyId: string) => {
if (_propertyId === "custom") { if (["start_date", "target_date"].includes(filterKey)) {
const _propertyIds = viewDetailStore?.appliedFilters?.filters?.[filterKey] || []; if (_propertyId === "custom") {
const selectedDates = _propertyIds.filter((id) => id.includes("-")); const _propertyIds = viewDetailStore?.appliedFilters?.filters?.[filterKey] || [];
if (selectedDates.length > 0) const selectedDates = _propertyIds.filter((id) => id.includes("-"));
selectedDates.forEach((date: string) => viewDetailStore?.setFilters(filterKey, date)); if (selectedDates.length > 0)
else setDateCustomFilterToggle(filterKey); 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);
} else viewDetailStore?.setFilters(filterKey, _propertyId); },
}; [filterKey, viewDetailStore, setDateCustomFilterToggle]
);
const handleCustomDateSelection = (selectedDates: string[]) => { const handleCustomDateSelection = useCallback(
selectedDates.forEach((date: string) => { (selectedDates: string[]) => {
viewDetailStore?.setFilters(filterKey, date); selectedDates.forEach((date: string) => {
setDateCustomFilterToggle(undefined); viewDetailStore?.setFilters(filterKey, date);
}); setDateCustomFilterToggle(undefined);
}; });
},
[filterKey, viewDetailStore, setDateCustomFilterToggle]
);
if (propertyIds.length <= 0) if (propertyIds.length <= 0)
return <div className="text-xs italic py-1 text-custom-text-300">No items are available.</div>; 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 { Check } from "lucide-react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
@ -20,7 +20,10 @@ export const ViewFilterSelection: FC<TViewFilterSelection> = observer((props) =>
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); 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) const isSelected = ["start_date", "target_date"].includes(filterKey)
? propertyId === "custom" ? propertyId === "custom"
@ -30,6 +33,18 @@ export const ViewFilterSelection: FC<TViewFilterSelection> = observer((props) =>
: propertyIds?.includes(propertyId) : propertyIds?.includes(propertyId)
: propertyIds?.includes(propertyId) || false; : 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 ( return (
<div <div
className={`flex-shrink-0 w-3 h-3 flex justify-center items-center border rounded text-bold ${ 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 { observer } from "mobx-react-lite";
import { ChevronDown, ChevronUp } from "lucide-react"; import { ChevronDown, ChevronUp } from "lucide-react";
import concat from "lodash/concat"; import concat from "lodash/concat";
@ -36,20 +36,19 @@ export const ViewFiltersRoot: FC<TViewFiltersRoot> = observer((props) => {
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
// state // state
const [filterVisibility, setFilterVisibility] = useState<Partial<keyof TViewFilters>[]>([]); const [filterVisibility, setFilterVisibility] = useState<Partial<keyof TViewFilters>[]>([]);
const handleFilterVisibility = (key: keyof TViewFilters) => { const handleFilterVisibility = useCallback((key: keyof TViewFilters) => {
setFilterVisibility((prevData = []) => { setFilterVisibility((prevData = []) => {
if (prevData.includes(key)) return filter(prevData, (item) => item !== key); if (prevData.includes(key)) return filter(prevData, (item) => item !== key);
return uniq(concat(prevData, [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 if (filtersProperties.length <= 0) return <></>;
? viewDefaultFilterParametersByViewTypeAndLayout(viewPageType, layout, "filters")
: [];
if (!layout || filtersProperties.length <= 0) return <></>;
return ( return (
<div className="space-y-1 divide-y divide-custom-border-300"> <div className="space-y-1 divide-y divide-custom-border-300">
{filtersProperties.map((filterKey) => ( {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 "./root";
export * from "./header-tabs";
// views // views
export * from "./views/root"; export * from "./views/root";
export * from "./views/view-item"; 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-root";
export * from "./display-filters/display-filter-item"; export * from "./display-filters/display-filter-item";
export * from "./display-filters/display-filter-selection"; export * from "./display-filters/display-filter-selection";
export * from "./display-filters/extra-options";
// view display properties // view display properties
export * from "./display-properties/root"; 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 { observer } from "mobx-react-lite";
import { LucideIcon, List, Kanban, Calendar, Sheet, GanttChartSquare } from "lucide-react"; import { LucideIcon, List, Kanban, Calendar, Sheet, GanttChartSquare } from "lucide-react";
// hooks // hooks
@ -18,20 +18,23 @@ type TViewLayoutRoot = {
viewPageType: EViewPageType; 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) => { export const ViewLayoutRoot: FC<TViewLayoutRoot> = observer((props) => {
const { workspaceSlug, projectId, viewId, viewType, viewPageType } = props; const { workspaceSlug, projectId, viewId, viewType, viewPageType } = props;
// hooks // hooks
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); 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 <></>; if (!viewDetailStore || validLayouts.length <= 1) return <></>;
return ( return (

View File

@ -1,7 +1,5 @@
import { FC, Fragment, useEffect, useMemo, useState } from "react"; import { FC, Fragment, useCallback, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { CheckCircle } from "lucide-react";
import { v4 as uuidV4 } from "uuid"; import { v4 as uuidV4 } from "uuid";
import cloneDeep from "lodash/cloneDeep"; import cloneDeep from "lodash/cloneDeep";
// hooks // hooks
@ -35,7 +33,6 @@ type TGlobalViewRoot = {
viewType: TViewTypes; viewType: TViewTypes;
viewPageType: EViewPageType; viewPageType: EViewPageType;
baseRoute: string; baseRoute: string;
workspaceViewTabOptions: { key: TViewTypes; title: string; href: string }[];
}; };
type TViewOperationsToggle = { type TViewOperationsToggle = {
@ -44,7 +41,7 @@ type TViewOperationsToggle = {
}; };
export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => { 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 // hooks
const viewStore = useView(workspaceSlug, projectId, viewType); const viewStore = useView(workspaceSlug, projectId, viewType);
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
@ -54,9 +51,11 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
type: undefined, type: undefined,
viewId: undefined, viewId: undefined,
}); });
const handleViewOperationsToggle = (type: TViewOperationsToggle["type"], viewId: string | undefined) => const handleViewOperationsToggle = useCallback(
setViewOperationsToggle({ type, viewId }); (type: TViewOperationsToggle["type"], viewId: string | undefined) => setViewOperationsToggle({ type, viewId }),
[]
);
// hooks
const viewDetailCreateEditStore = useViewDetail( const viewDetailCreateEditStore = useViewDetail(
workspaceSlug, workspaceSlug,
projectId, projectId,
@ -153,7 +152,7 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
} }
}, },
}), }),
[viewStore, viewDetailStore, setToastAlert, viewDetailCreateEditStore] [viewStore, viewDetailStore, setToastAlert, viewDetailCreateEditStore, handleViewOperationsToggle]
); );
// fetch all views // fetch all views
@ -174,43 +173,13 @@ export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
return ( return (
<div className="relative w-full h-full"> <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" ? ( {viewStore?.loader && viewStore?.loader === "init-loader" ? (
<div className="relative w-full h-full flex justify-center items-center"> <div className="relative w-full h-full flex justify-center items-center">
<Spinner /> <Spinner />
</div> </div>
) : ( ) : (
<> <>
<div className="border-b border-custom-border-200"> <div className="border-b border-custom-border-200 pt-2">
<ViewRoot <ViewRoot
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} 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 { observer } from "mobx-react-lite";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
// hooks // hooks
@ -31,7 +31,7 @@ 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 = 116; const itemWidth = 124;
if (!tabContainer || !tabItemViewMore) return; if (!tabContainer || !tabItemViewMore) return;
const containerWidth = tabContainer.clientWidth; const containerWidth = tabContainer.clientWidth;
@ -49,12 +49,14 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
return () => window.removeEventListener("resize", () => handleViewTabsVisibility()); return () => window.removeEventListener("resize", () => handleViewTabsVisibility());
}, [viewStore?.viewIds]); }, [viewStore?.viewIds]);
const viewIds = viewStore?.viewIds?.slice(0, itemsToRenderViewsCount || viewStore?.viewIds.length) || []; const viewIds = useMemo(() => {
const ids = viewStore?.viewIds?.slice(0, itemsToRenderViewsCount || viewStore?.viewIds.length) || [];
if (!viewIds.includes(viewId)) { if (!ids.includes(viewId)) {
viewIds.pop(); ids.pop();
viewIds.push(viewId); ids.push(viewId);
} }
return ids;
}, [viewId, viewStore, itemsToRenderViewsCount]);
return ( return (
<div className="relative flex justify-between px-5 gap-2"> <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 { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { Placement } from "@popperjs/core"; import { Placement } from "@popperjs/core";
@ -7,7 +7,7 @@ import { Plus, Search } from "lucide-react";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { useView } from "hooks/store"; import { useView } from "hooks/store";
// components // components
import { ViewDropdownItem } from ".."; import { ViewDropdownItem } from "../";
// types // types
import { TViewTypes } from "@plane/types"; import { TViewTypes } from "@plane/types";
import { TViewOperations } from "../types"; import { TViewOperations } from "../types";
@ -63,14 +63,17 @@ export const ViewDropdown: FC<TViewDropdown> = (props) => {
], ],
}); });
const handleDropdownOpen = () => setDropdownToggle(true); const handleDropdownOpen = useCallback(() => setDropdownToggle(true), []);
const handleDropdownClose = () => setDropdownToggle(false); const handleDropdownClose = useCallback(() => setDropdownToggle(false), []);
const handleDropdownToggle = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { const handleDropdownToggle = useCallback(
event.preventDefault(); (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.stopPropagation(); event.preventDefault();
if (!dropdownToggle) handleDropdownOpen(); event.stopPropagation();
else handleDropdownClose(); if (!dropdownToggle) handleDropdownOpen();
}; else handleDropdownClose();
},
[dropdownToggle, handleDropdownOpen, handleDropdownClose]
);
useOutsideClickDetector(dropdownRef, handleDropdownClose); useOutsideClickDetector(dropdownRef, handleDropdownClose);

View File

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

View File

@ -30,6 +30,7 @@ import {
GROUP_BY_PROPERTY, GROUP_BY_PROPERTY,
ORDER_BY_PROPERTY, ORDER_BY_PROPERTY,
TYPE_PROPERTY, TYPE_PROPERTY,
EViewLayouts,
} from "constants/view/filters"; } from "constants/view/filters";
// helpers // helpers
import { renderEmoji } from "helpers/emoji.helper"; 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; if (!displayFilterKey) return undefined;
switch (displayFilterKey) { switch (displayFilterKey) {
case "group_by": 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": case "sub_group_by":
return Object.keys(GROUP_BY_PROPERTY) || undefined; return Object.keys(GROUP_BY_PROPERTY) || undefined;
case "order_by": case "order_by":

View File

@ -1,9 +1,10 @@
import { ReactElement, useMemo } from "react"; import { ReactElement, useMemo } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { CheckCircle } from "lucide-react";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { GlobalViewRoot } from "components/view"; import { GlobalViewRoot, ViewHeader } from "components/view";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants // constants
@ -31,8 +32,20 @@ const WorkspacePrivateViewPage: NextPageWithLayout = () => {
if (!workspaceSlug || !viewId) return <></>; if (!workspaceSlug || !viewId) return <></>;
return ( return (
<div className="h-full overflow-hidden bg-custom-background-100"> <div className="w-full h-full overflow-hidden bg-custom-background-100 relative flex flex-col">
<div className="flex h-full w-full flex-col border-b border-custom-border-300"> <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 <GlobalViewRoot
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={undefined} projectId={undefined}
@ -40,9 +53,12 @@ const WorkspacePrivateViewPage: NextPageWithLayout = () => {
viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS} viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS}
viewPageType={EViewPageType.ALL} viewPageType={EViewPageType.ALL}
baseRoute={`/${workspaceSlug?.toString()}/views/private`} baseRoute={`/${workspaceSlug?.toString()}/views/private`}
workspaceViewTabOptions={workspaceViewTabOptions}
/> />
</div> </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> </div>
); );
}; };

View File

@ -1,9 +1,10 @@
import { ReactElement, useMemo } from "react"; import { ReactElement, useMemo } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { CheckCircle } from "lucide-react";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { GlobalViewRoot } from "components/view"; import { GlobalViewRoot, ViewHeader } from "components/view";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants // constants
@ -33,6 +34,18 @@ const WorkspacePublicViewPage: NextPageWithLayout = () => {
return ( return (
<div className="w-full h-full overflow-hidden bg-custom-background-100 relative flex flex-col"> <div className="w-full h-full overflow-hidden bg-custom-background-100 relative flex flex-col">
<div className="flex-shrink-0 w-full"> <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 <GlobalViewRoot
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={undefined} projectId={undefined}
@ -40,10 +53,12 @@ const WorkspacePublicViewPage: NextPageWithLayout = () => {
viewType={VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS} viewType={VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS}
viewPageType={EViewPageType.ALL} viewPageType={EViewPageType.ALL}
baseRoute={`/${workspaceSlug?.toString()}/views/public`} baseRoute={`/${workspaceSlug?.toString()}/views/public`}
workspaceViewTabOptions={workspaceViewTabOptions}
/> />
</div> </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> </div>
); );
}; };

View File

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

View File

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