diff --git a/packages/types/src/view/filter.d.ts b/packages/types/src/view/filter.d.ts index 2fd11b5d4..1794e407b 100644 --- a/packages/types/src/view/filter.d.ts +++ b/packages/types/src/view/filter.d.ts @@ -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"; diff --git a/web/components/view/applied-filters/filter-item.tsx b/web/components/view/applied-filters/filter-item.tsx index 3dd36f856..4c3cf54c0 100644 --- a/web/components/view/applied-filters/filter-item.tsx +++ b/web/components/view/applied-filters/filter-item.tsx @@ -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 = (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 (
= 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 ( diff --git a/web/components/view/applied-filters/root.tsx b/web/components/view/applied-filters/root.tsx index eabb2ba77..fa64982c3 100644 --- a/web/components/view/applied-filters/root.tsx +++ b/web/components/view/applied-filters/root.tsx @@ -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 = 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 ( diff --git a/web/components/view/display-filters/extra-options.tsx b/web/components/view/display-filters/extra-options.tsx new file mode 100644 index 000000000..a8066c3fe --- /dev/null +++ b/web/components/view/display-filters/extra-options.tsx @@ -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 = 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 ( + +
+
+ {isSelected && } +
+
+ {optionTitle || "Extra Option"} +
+
+
+ ); +}); diff --git a/web/components/view/display-filters/root.tsx b/web/components/view/display-filters/root.tsx index 39665bb63..ad471c2d2 100644 --- a/web/components/view/display-filters/root.tsx +++ b/web/components/view/display-filters/root.tsx @@ -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 = observer((pro const [filterVisibility, setFilterVisibility] = useState<(Partial | "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 (
@@ -67,7 +71,7 @@ export const ViewDisplayFiltersRoot: FC = observer((pro
{filtersProperties.map((filterKey) => ( -
+
{filterKey.replaceAll("_", " ")} @@ -91,10 +95,16 @@ export const ViewDisplayFiltersRoot: FC = observer((pro
))} - {/* extra options */} -
-
Show sub issues
-
Show Empty groups
+
+ {filtersExtraProperties.map((option) => ( + + ))}
); diff --git a/web/components/view/display-properties/property-selection.tsx b/web/components/view/display-properties/property-selection.tsx index 75a3fc2d4..492dc711c 100644 --- a/web/components/view/display-properties/property-selection.tsx +++ b/web/components/view/display-properties/property-selection.tsx @@ -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 = 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 (
= observer((props) => ], }); - const handleDropdownOpen = () => setDropdownToggle(true); - const handleDropdownClose = () => setDropdownToggle(false); - const handleDropdownToggle = (event: React.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) => { + event.preventDefault(); + event.stopPropagation(); + if (!dropdownToggle) handleDropdownOpen(); + else handleDropdownClose(); + }, + [dropdownToggle, handleDropdownOpen, handleDropdownClose] + ); useOutsideClickDetector(dropdownRef, () => dateCustomFilterToggle === undefined && handleDropdownClose()); diff --git a/web/components/view/filters/filter-item-root.tsx b/web/components/view/filters/filter-item-root.tsx index 86d2f33e9..ff56a4a53 100644 --- a/web/components/view/filters/filter-item-root.tsx +++ b/web/components/view/filters/filter-item-root.tsx @@ -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 = 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
No items are available.
; diff --git a/web/components/view/filters/filter-selection.tsx b/web/components/view/filters/filter-selection.tsx index 8fb0f0483..9b26dd08d 100644 --- a/web/components/view/filters/filter-selection.tsx +++ b/web/components/view/filters/filter-selection.tsx @@ -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 = 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 = 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 (
= observer((props) => { const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); // state const [filterVisibility, setFilterVisibility] = useState[]>([]); - 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 (
{filtersProperties.map((filterKey) => ( diff --git a/web/components/view/header-tabs.tsx b/web/components/view/header-tabs.tsx new file mode 100644 index 000000000..36ab27054 --- /dev/null +++ b/web/components/view/header-tabs.tsx @@ -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 = (props) => { + const { projectId, viewType, titleIcon, title, workspaceViewTabOptions } = props; + // hooks + const { getProjectById } = useProject(); + + const projectDetails = useMemo( + () => (projectId ? getProjectById(projectId) : undefined), + [projectId, getProjectById] + ); + + return ( +
+ {projectDetails && ( + +
+
+ {projectDetails?.icon_prop ? projectDetails?.icon_prop.toString() : } +
+
+ {projectDetails?.name ? projectDetails?.name : "Project Issues"} +
+
+
+ +
+
+ )} + +
+
+ {titleIcon ? titleIcon : } +
+
+ {title ? title : "All Issues"} +
+
+ +
+
+ {workspaceViewTabOptions.map((tab) => ( + + {tab.title} + + ))} +
+
+
+ ); +}; diff --git a/web/components/view/index.ts b/web/components/view/index.ts index 116a15bcb..9379e3e7b 100644 --- a/web/components/view/index.ts +++ b/web/components/view/index.ts @@ -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"; diff --git a/web/components/view/layout.tsx b/web/components/view/layout.tsx index 415a0eb0b..db1e851f1 100644 --- a/web/components/view/layout.tsx +++ b/web/components/view/layout.tsx @@ -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 = 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 ( diff --git a/web/components/view/root.tsx b/web/components/view/root.tsx index 00aefe265..ab61e7c5c 100644 --- a/web/components/view/root.tsx +++ b/web/components/view/root.tsx @@ -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 = 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 = 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 = observer((props) => { } }, }), - [viewStore, viewDetailStore, setToastAlert, viewDetailCreateEditStore] + [viewStore, viewDetailStore, setToastAlert, viewDetailCreateEditStore, handleViewOperationsToggle] ); // fetch all views @@ -174,43 +173,13 @@ export const GlobalViewRoot: FC = observer((props) => { return (
-
-
-
- -
-
- All Issues -
-
- -
-
- {workspaceViewTabOptions.map((tab) => ( - - {tab.title} - - ))} -
-
-
- {viewStore?.loader && viewStore?.loader === "init-loader" ? (
) : ( <> -
+
= 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 = 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 (
diff --git a/web/components/view/views/view-dropdown.tsx b/web/components/view/views/view-dropdown.tsx index 22f3bde13..92676f23d 100644 --- a/web/components/view/views/view-dropdown.tsx +++ b/web/components/view/views/view-dropdown.tsx @@ -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 = (props) => { ], }); - const handleDropdownOpen = () => setDropdownToggle(true); - const handleDropdownClose = () => setDropdownToggle(false); - const handleDropdownToggle = (event: React.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) => { + event.preventDefault(); + event.stopPropagation(); + if (!dropdownToggle) handleDropdownOpen(); + else handleDropdownClose(); + }, + [dropdownToggle, handleDropdownOpen, handleDropdownClose] + ); useOutsideClickDetector(dropdownRef, handleDropdownClose); diff --git a/web/constants/view/filters.ts b/web/constants/view/filters.ts index 8f84ac386..8c5ba1d7a 100644 --- a/web/constants/view/filters.ts +++ b/web/constants/view/filters.ts @@ -7,6 +7,7 @@ import { TViewDisplayFiltersGrouped, TViewDisplayFiltersOrderBy, TViewDisplayFiltersType, + TViewDisplayFiltersExtraOptions, } from "@plane/types"; // filters constants @@ -61,7 +62,7 @@ export const TYPE_PROPERTY: Record = { +export const EXTRA_OPTIONS_PROPERTY: Record = { 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, }, diff --git a/web/hooks/store/views/use-view-filters.tsx b/web/hooks/store/views/use-view-filters.tsx index 7ed774ba0..d55e2b658 100644 --- a/web/hooks/store/views/use-view-filters.tsx +++ b/web/hooks/store/views/use-view-filters.tsx @@ -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": diff --git a/web/pages/[workspaceSlug]/views/private/[viewId].tsx b/web/pages/[workspaceSlug]/views/private/[viewId].tsx index 0dac0cf9c..15bc3a345 100644 --- a/web/pages/[workspaceSlug]/views/private/[viewId].tsx +++ b/web/pages/[workspaceSlug]/views/private/[viewId].tsx @@ -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 ( -
-
+
+
+ {/* header */} +
+ } + title="All Issues" + workspaceViewTabOptions={workspaceViewTabOptions} + /> +
+ + {/* content */} { viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS} viewPageType={EViewPageType.ALL} baseRoute={`/${workspaceSlug?.toString()}/views/private`} - workspaceViewTabOptions={workspaceViewTabOptions} />
+ +
+ Issues render placeholder +
); }; diff --git a/web/pages/[workspaceSlug]/views/public/[viewId].tsx b/web/pages/[workspaceSlug]/views/public/[viewId].tsx index 657975b18..e1a3ae3fa 100644 --- a/web/pages/[workspaceSlug]/views/public/[viewId].tsx +++ b/web/pages/[workspaceSlug]/views/public/[viewId].tsx @@ -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 (
+ {/* header */} +
+ } + title="All Issues" + workspaceViewTabOptions={workspaceViewTabOptions} + /> +
+ + {/* content */} { viewType={VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS} viewPageType={EViewPageType.ALL} baseRoute={`/${workspaceSlug?.toString()}/views/public`} - workspaceViewTabOptions={workspaceViewTabOptions} />
-
Issues render
+ +
+ Issues render placeholder +
); }; diff --git a/web/store/view/helpers/filters_helpers.ts b/web/store/view/helpers/filters_helpers.ts index 9aceec4c6..5b0d43096 100644 --- a/web/store/view/helpers/filters_helpers.ts +++ b/web/store/view/helpers/filters_helpers.ts @@ -14,19 +14,19 @@ import { EViewPageType, viewPageDefaultLayoutsByPageType } from "constants/view" export class FiltersHelper { // computed filters computedFilters = (filters: TViewFilters, defaultValues?: Partial): 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 diff --git a/web/store/view/view.store.ts b/web/store/view/view.store.ts index 1dd46329c..119c3d5a1 100644 --- a/web/store/view/view.store.ts +++ b/web/store/view/view.store.ts @@ -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) => {