diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index baf2abb70..69c2279dd 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -1,29 +1,42 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; +import { useTheme } from "next-themes"; // hooks import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { ExistingIssuesListModal } from "components/core"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // types import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; // constants import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { useTheme } from "next-themes"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { CYCLE_EMPTY_STATE_DETAILS, EMPTY_FILTER_STATE_DETAILS } from "constants/empty-state"; type Props = { workspaceSlug: string | undefined; projectId: string | undefined; cycleId: string | undefined; activeLayout: TIssueLayouts | undefined; + handleClearAllFilters: () => void; + isEmptyFilters?: boolean; }; +interface EmptyStateProps { + title: string; + image: string; + description?: string; + comicBox?: { title: string; description: string }; + primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; + secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; + size?: "lg" | "sm" | undefined; + disabled?: boolean | undefined; +} + export const CycleEmptyState: React.FC = observer((props) => { - const { workspaceSlug, projectId, cycleId, activeLayout } = props; + const { workspaceSlug, projectId, cycleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props; // states const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); // theme @@ -65,10 +78,41 @@ export const CycleEmptyState: React.FC = observer((props) => { const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["no-issues"]; const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); const emptyStateImage = getEmptyStateImagePath("cycle-issues", activeLayout ?? "list", isLightMode); const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER; + const emptyStateProps: EmptyStateProps = isEmptyFilters + ? { + title: EMPTY_FILTER_STATE_DETAILS["project"].title, + image: currentLayoutEmptyStateImagePath, + secondaryButton: { + text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text, + onClick: handleClearAllFilters, + }, + } + : { + title: emptyStateDetail.title, + description: emptyStateDetail.description, + image: emptyStateImage, + primaryButton: { + text: emptyStateDetail.primaryButton.text, + icon: , + onClick: () => { + setTrackElement("Cycle issue empty state"); + toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); + }, + }, + secondaryButton: { + text: emptyStateDetail.secondaryButton.text, + icon: , + onClick: () => setCycleIssuesListModal(true), + }, + size: "sm", + disabled: !isEditingAllowed, + }; + return ( <> = observer((props) => { handleOnSubmit={handleAddIssuesToCycle} />
- , - onClick: () => { - setTrackElement("Cycle issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); - }, - }} - secondaryButton={{ - text: emptyStateDetail.secondaryButton.text, - icon: , - onClick: () => setCycleIssuesListModal(true), - }} - size="sm" - disabled={!isEditingAllowed} - /> +
); diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index 4285368e9..ef7ec729c 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -1,29 +1,42 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; +import { useTheme } from "next-themes"; // hooks import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { ExistingIssuesListModal } from "components/core"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // types import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; // constants import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; -import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { useTheme } from "next-themes"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EMPTY_FILTER_STATE_DETAILS, MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; type Props = { workspaceSlug: string | undefined; projectId: string | undefined; moduleId: string | undefined; activeLayout: TIssueLayouts | undefined; + handleClearAllFilters: () => void; + isEmptyFilters?: boolean; }; +interface EmptyStateProps { + title: string; + image: string; + description?: string; + comicBox?: { title: string; description: string }; + primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; + secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; + size?: "lg" | "sm" | undefined; + disabled?: boolean | undefined; +} + export const ModuleEmptyState: React.FC = observer((props) => { - const { workspaceSlug, projectId, moduleId, activeLayout } = props; + const { workspaceSlug, projectId, moduleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props; // states const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); // theme @@ -59,10 +72,40 @@ export const ModuleEmptyState: React.FC = observer((props) => { const emptyStateDetail = MODULE_EMPTY_STATE_DETAILS["no-issues"]; const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("cycle-issues", activeLayout ?? "list", isLightMode); + const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); + const emptyStateImage = getEmptyStateImagePath("module-issues", activeLayout ?? "list", isLightMode); const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER; + const emptyStateProps: EmptyStateProps = isEmptyFilters + ? { + title: EMPTY_FILTER_STATE_DETAILS["project"].title, + image: currentLayoutEmptyStateImagePath, + secondaryButton: { + text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text, + onClick: handleClearAllFilters, + }, + } + : { + title: emptyStateDetail.title, + description: emptyStateDetail.description, + image: emptyStateImage, + primaryButton: { + text: emptyStateDetail.primaryButton.text, + icon: , + onClick: () => { + setTrackElement("Module issue empty state"); + toggleCreateIssueModal(true, EIssuesStoreType.MODULE); + }, + }, + secondaryButton: { + text: emptyStateDetail.secondaryButton.text, + icon: , + onClick: () => setModuleIssuesListModal(true), + }, + disabled: !isEditingAllowed, + }; + return ( <> = observer((props) => { handleOnSubmit={handleAddIssuesToModule} />
- , - onClick: () => { - setTrackElement("Module issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.MODULE); - }, - }} - secondaryButton={{ - text: emptyStateDetail.secondaryButton.text, - icon: , - onClick: () => setModuleIssuesListModal(true), - }} - disabled={!isEditingAllowed} - /> +
); diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index 402103072..4f7088c38 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -2,6 +2,7 @@ import React, { Fragment, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; +import size from "lodash/size"; // hooks import { useCycle, useIssues } from "hooks/store"; // components @@ -18,7 +19,9 @@ import { import { TransferIssues, TransferIssuesModal } from "components/cycles"; import { ActiveLoader } from "components/ui"; // constants -import { EIssuesStoreType } from "constants/issue"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +// types +import { IIssueFilterOptions } from "@plane/types"; export const CycleLayoutRoot: React.FC = observer(() => { const router = useRouter(); @@ -51,6 +54,31 @@ export const CycleLayoutRoot: React.FC = observer(() => { const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; const cycleStatus = cycleDetails?.status?.toLocaleLowerCase() ?? "draft"; + const userFilters = issuesFilter?.issueFilters?.filters; + + const issueFilterCount = size( + Object.fromEntries( + Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) + ) + ); + + const handleClearAllFilters = () => { + if (!workspaceSlug || !projectId || !cycleId) return; + const newFilters: IIssueFilterOptions = {}; + Object.keys(userFilters ?? {}).forEach((key) => { + newFilters[key as keyof IIssueFilterOptions] = null; + }); + issuesFilter.updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { + ...newFilters, + }, + cycleId.toString() + ); + }; + if (!workspaceSlug || !projectId || !cycleId) return <>; if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) { @@ -71,6 +99,8 @@ export const CycleLayoutRoot: React.FC = observer(() => { projectId={projectId.toString()} cycleId={cycleId.toString()} activeLayout={activeLayout} + handleClearAllFilters={handleClearAllFilters} + isEmptyFilters={issueFilterCount > 0} /> ) : ( diff --git a/web/components/issues/issue-layouts/roots/module-layout-root.tsx b/web/components/issues/issue-layouts/roots/module-layout-root.tsx index dfce3022f..b7978c7bc 100644 --- a/web/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -2,6 +2,7 @@ import React, { Fragment } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; +import size from "lodash/size"; // mobx store import { useIssues } from "hooks/store"; // components @@ -17,7 +18,9 @@ import { } from "components/issues"; import { ActiveLoader } from "components/ui"; // constants -import { EIssuesStoreType } from "constants/issue"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +// types +import { IIssueFilterOptions } from "@plane/types"; export const ModuleLayoutRoot: React.FC = observer(() => { // router @@ -43,6 +46,31 @@ export const ModuleLayoutRoot: React.FC = observer(() => { } ); + const userFilters = issuesFilter?.issueFilters?.filters; + + const issueFilterCount = size( + Object.fromEntries( + Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) + ) + ); + + const handleClearAllFilters = () => { + if (!workspaceSlug || !projectId || !moduleId) return; + const newFilters: IIssueFilterOptions = {}; + Object.keys(userFilters ?? {}).forEach((key) => { + newFilters[key as keyof IIssueFilterOptions] = null; + }); + issuesFilter.updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { + ...newFilters, + }, + moduleId.toString() + ); + }; + if (!workspaceSlug || !projectId || !moduleId) return <>; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined; @@ -62,6 +90,8 @@ export const ModuleLayoutRoot: React.FC = observer(() => { projectId={projectId.toString()} moduleId={moduleId.toString()} activeLayout={activeLayout} + handleClearAllFilters={handleClearAllFilters} + isEmptyFilters={issueFilterCount > 0} /> ) : (