[WEB-5230 | WEB-5231] chore: new empty state implementation (#7972)

This commit is contained in:
Anmol Singh Bhatia 2025-10-24 17:21:14 +05:30 committed by GitHub
parent a60d74a3c0
commit 68fd2463f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
72 changed files with 5260 additions and 746 deletions

View file

@ -6,6 +6,7 @@ import { useParams } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, CYCLE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import type { TCycleFilters } from "@plane/types";
import { EUserProjectRoles } from "@plane/types";
// components
@ -15,7 +16,6 @@ import { PageHead } from "@/components/core/page-title";
import { CycleAppliedFiltersList } from "@/components/cycles/applied-filters";
import { CyclesView } from "@/components/cycles/cycles-view";
import { CycleCreateUpdateModal } from "@/components/cycles/modal";
import { ComicBoxButton } from "@/components/empty-state/comic-box-button";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader";
// hooks
@ -96,22 +96,19 @@ const ProjectCyclesPage = observer(() => {
/>
{totalCycles === 0 ? (
<div className="h-full place-items-center">
<DetailedEmptyState
title={t("project_cycles.empty_state.general.title")}
description={t("project_cycles.empty_state.general.description")}
assetPath={resolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("project_cycles.empty_state.general.primary_button.text")}
title={t("project_cycles.empty_state.general.primary_button.comic.title")}
description={t("project_cycles.empty_state.general.primary_button.comic.description")}
data-ph-element={CYCLE_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON}
onClick={() => {
setCreateModal(true);
}}
disabled={!hasMemberLevelPermission}
/>
}
<EmptyStateDetailed
assetKey="cycle"
title={t("project.cycles.title")}
description={t("project.cycles.description")}
actions={[
{
label: t("project.cycles.cta_primary"),
onClick: () => setCreateModal(true),
variant: "primary",
disabled: !hasMemberLevelPermission,
"data-ph-element": CYCLE_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON,
},
]}
/>
</div>
) : (

View file

@ -7,18 +7,17 @@ import useSWR from "swr";
import { PROFILE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// component
import { EmptyStateCompact } from "@plane/propel/empty-state";
import { APITokenService } from "@plane/services";
import { CreateApiTokenModal } from "@/components/api-token/modal/create-token-modal";
import { ApiTokenListItem } from "@/components/api-token/token-list-item";
import { PageHead } from "@/components/core/page-title";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { SettingsHeading } from "@/components/settings/heading";
import { APITokenSettingsLoader } from "@/components/ui/loader/settings/api-token";
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
// store hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
const apiTokenService = new APITokenService();
@ -30,8 +29,6 @@ const ApiTokensPage = observer(() => {
const { t } = useTranslation();
// store hooks
const { currentWorkspace } = useWorkspace();
// derived values
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/api-tokens" });
const { data: tokens } = useSWR(API_TOKENS_LIST, () => apiTokenService.list());
@ -70,7 +67,7 @@ const ApiTokensPage = observer(() => {
</div>
</>
) : (
<div className="flex h-full w-full flex-col">
<div className="flex h-full w-full flex-col py-">
<SettingsHeading
title={t("account_settings.api_tokens.heading")}
description={t("account_settings.api_tokens.description")}
@ -84,24 +81,26 @@ const ApiTokensPage = observer(() => {
},
}}
/>
<div className="h-full w-full flex items-center justify-center">
<DetailedEmptyState
title=""
description=""
assetPath={resolvedPath}
className="w-full !p-0 justify-center mx-auto"
size="md"
primaryButton={{
text: t("workspace_settings.settings.api_tokens.add_token"),
<EmptyStateCompact
assetKey="token"
assetClassName="size-20"
title={t("settings.tokens.title")}
description={t("settings.tokens.description")}
actions={[
{
label: t("settings.tokens.cta_primary"),
onClick: () => {
captureClick({
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_PAT_BUTTON,
});
setIsCreateTokenModalOpen(true);
},
}}
/>
</div>
},
]}
align="start"
rootClassName="py-20"
/>
</div>
)}
</section>

View file

@ -14,18 +14,16 @@ import {
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Search, X } from "lucide-react";
// plane package imports
import { useTranslation } from "@plane/i18n";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@plane/propel/table";
import { cn } from "@plane/utils";
// plane web components
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import AnalyticsEmptyState from "../empty-state";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
@ -42,7 +40,6 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
const { t } = useTranslation();
const inputRef = React.useRef<HTMLInputElement>(null);
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-table" });
const table = useReactTable({
data,
@ -156,14 +153,12 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
) : (
<TableRow>
<TableCell colSpan={columns.length} className="p-0">
<div className="flex h-[350px] w-full items-center justify-center border border-custom-border-100 ">
<AnalyticsEmptyState
title={t("workspace_analytics.empty_state.customized_insights.title")}
description={t("workspace_analytics.empty_state.customized_insights.description")}
className="border-0"
assetPath={resolvedPath}
/>
</div>
<EmptyStateCompact
assetKey="unknown"
assetClassName="size-20"
rootClassName="border border-custom-border-100 px-5 py-10 md:py-20 md:px-20"
title={t("workspace.analytics_work_items.title")}
/>
</TableCell>
</TableRow>
)}

View file

@ -4,15 +4,14 @@ import { useParams } from "next/navigation";
import useSWR from "swr";
// plane package imports
import { useTranslation } from "@plane/i18n";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import type { TChartData } from "@plane/types";
// hooks
import { useAnalytics } from "@/hooks/store/use-analytics";
// services
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { AnalyticsService } from "@/services/analytics.service";
// plane web components
import AnalyticsSectionWrapper from "../analytics-section-wrapper";
import AnalyticsEmptyState from "../empty-state";
import { ProjectInsightsLoader } from "../loaders";
const RadarChart = dynamic(() =>
@ -29,7 +28,6 @@ const ProjectInsights = observer(() => {
const workspaceSlug = params.workspaceSlug.toString();
const { selectedDuration, selectedDurationLabel, selectedProjects, selectedCycle, selectedModule, isPeekView } =
useAnalytics();
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-radar" });
const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR(
`radar-chart-project-insights-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`,
@ -56,11 +54,11 @@ const ProjectInsights = observer(() => {
{isLoadingProjectInsight ? (
<ProjectInsightsLoader />
) : projectInsightsData && projectInsightsData?.length == 0 ? (
<AnalyticsEmptyState
title={t("workspace_analytics.empty_state.project_insights.title")}
description={t("workspace_analytics.empty_state.project_insights.description")}
className="h-[300px]"
assetPath={resolvedPath}
<EmptyStateCompact
assetKey="unknown"
assetClassName="size-20"
rootClassName="border border-custom-border-100 px-5 py-10 md:py-20 md:px-20"
title={t("workspace.analytics_work_items.title")}
/>
) : (
<div className="gap-8 lg:flex">

View file

@ -5,16 +5,15 @@ import useSWR from "swr";
// plane package imports
import { useTranslation } from "@plane/i18n";
import { AreaChart } from "@plane/propel/charts/area-chart";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import type { IChartResponse, TChartData } from "@plane/types";
import { renderFormattedDate } from "@plane/utils";
// hooks
import { useAnalytics } from "@/hooks/store/use-analytics";
// services
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { AnalyticsService } from "@/services/analytics.service";
// plane web components
import AnalyticsSectionWrapper from "../analytics-section-wrapper";
import AnalyticsEmptyState from "../empty-state";
import { ChartLoader } from "../loaders";
const analyticsService = new AnalyticsService();
@ -31,7 +30,6 @@ const CreatedVsResolved = observer(() => {
const params = useParams();
const { t } = useTranslation();
const workspaceSlug = params.workspaceSlug.toString();
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-area" });
const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR(
`created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}-${isEpic}`,
() =>
@ -121,11 +119,11 @@ const CreatedVsResolved = observer(() => {
}}
/>
) : (
<AnalyticsEmptyState
title={t("workspace_analytics.empty_state.created_vs_resolved.title")}
description={t("workspace_analytics.empty_state.created_vs_resolved.description")}
className="h-[350px]"
assetPath={resolvedPath}
<EmptyStateCompact
assetKey="unknown"
assetClassName="size-20"
rootClassName="border border-custom-border-100 px-5 py-10 md:py-20 md:px-20"
title={t("workspace.analytics_work_items.title")}
/>
)}
</AnalyticsSectionWrapper>

View file

@ -11,15 +11,14 @@ import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, CHART_COLOR_PALETTES,
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { BarChart } from "@plane/propel/charts/bar-chart";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import type { TBarItem, TChart, TChartDatum, ChartXAxisProperty, ChartYAxisMetric } from "@plane/types";
// plane web components
import { generateExtendedColors, parseChartData } from "@/components/chart/utils";
// hooks
import { useAnalytics } from "@/hooks/store/use-analytics";
import { useProjectState } from "@/hooks/store/use-project-state";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { AnalyticsService } from "@/services/analytics.service";
import AnalyticsEmptyState from "../empty-state";
import { exportCSV } from "../export";
import { DataTable } from "../insight-table/data-table";
import { ChartLoader } from "../loaders";
@ -46,7 +45,6 @@ const analyticsService = new AnalyticsService();
const PriorityChart = observer((props: Props) => {
const { x_axis, y_axis, group_by } = props;
const { t } = useTranslation();
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-bar" });
// store hooks
const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView, isEpic } = useAnalytics();
const { workspaceStates } = useProjectState();
@ -232,11 +230,11 @@ const PriorityChart = observer((props: Props) => {
/>
</>
) : (
<AnalyticsEmptyState
title={t("workspace_analytics.empty_state.customized_insights.title")}
description={t("workspace_analytics.empty_state.customized_insights.description")}
className="h-[350px]"
assetPath={resolvedPath}
<EmptyStateCompact
assetKey="unknown"
assetClassName="size-20"
rootClassName="border border-custom-border-100 px-5 py-10 md:py-20 md:px-20"
title={t("workspace.analytics_work_items.title")}
/>
)}
</div>

View file

@ -5,6 +5,7 @@ import useSWR from "swr";
// plane imports
import { useTranslation } from "@plane/i18n";
// hooks
import { EmptyStateCompact } from "@plane/propel/empty-state";
import { useProjectEstimates } from "@/hooks/store/estimates";
import { useProject } from "@/hooks/store/use-project";
// plane web components
@ -13,7 +14,6 @@ import { UpdateEstimateModal } from "@/plane-web/components/estimates";
import { SettingsHeading } from "../settings/heading";
import { CreateEstimateModal } from "./create/modal";
import { DeleteEstimateModal } from "./delete/modal";
import { EstimateEmptyScreen } from "./empty-screen";
import { EstimateDisableSwitch } from "./estimate-disable-switch";
import { EstimateList } from "./estimate-list";
import { EstimateLoaderScreen } from "./loader-screen";
@ -76,7 +76,20 @@ export const EstimateRoot: FC<TEstimateRoot> = observer((props) => {
/>
</div>
) : (
<EstimateEmptyScreen onButtonClick={() => setIsEstimateCreateModalOpen(true)} />
<EmptyStateCompact
assetKey="estimate"
assetClassName="size-20"
title={t("settings.estimates.title")}
description={t("settings.estimates.description")}
actions={[
{
label: t("settings.estimates.cta_primary"),
onClick: () => setIsEstimateCreateModalOpen(true),
},
]}
align="start"
rootClassName="py-20"
/>
)}
{/* archived estimates section */}

View file

@ -1,14 +1,15 @@
import { Link2 } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { EmptyStateCompact } from "@plane/propel/empty-state";
export const LinksEmptyState = () => {
const { t } = useTranslation();
return (
<div className="min-h-[110px] flex w-full justify-center py-6 bg-custom-border-100 rounded">
<div className="m-auto flex gap-2">
<Link2 size={30} className="text-custom-text-400/40 -rotate-45" />
<div className="text-custom-text-400 text-sm text-center my-auto">{t("home.quick_links.empty")}</div>
</div>
<div className="flex items-center justify-center py-10 bg-custom-background-90 w-full">
<EmptyStateCompact
assetKey="link"
assetClassName="w-20 h-20"
title={t("workspace.home_widget_quick_links.title")}
/>
</div>
);
};

View file

@ -1,41 +1,40 @@
import { History } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { PageIcon, ProjectIcon, WorkItemsIcon } from "@plane/propel/icons";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import type { CompactAssetType } from "@plane/propel/empty-state";
const getDisplayContent = (type: string) => {
const getDisplayContent = (type: string): { assetKey: CompactAssetType; text: string } => {
switch (type) {
case "project":
return {
icon: <ProjectIcon height={30} width={30} className="text-custom-text-400/40" />,
assetKey: "project",
text: "home.recents.empty.project",
};
case "page":
return {
icon: <PageIcon height={30} width={30} className="text-custom-text-400/40" />,
assetKey: "note",
text: "home.recents.empty.page",
};
case "issue":
return {
icon: <WorkItemsIcon className="text-custom-text-400/40 w-[30px] h-[30px]" />,
assetKey: "work-item",
text: "home.recents.empty.issue",
};
default:
return {
icon: <History height={30} width={30} className="text-custom-text-400/40" />,
assetKey: "work-item",
text: "home.recents.empty.default",
};
}
};
export const RecentsEmptyState = ({ type }: { type: string }) => {
const { t } = useTranslation();
const { icon, text } = getDisplayContent(type);
const { assetKey, text } = getDisplayContent(type);
return (
<div className="min-h-[120px] flex w-full justify-center py-6 bg-custom-border-100 rounded">
<div className="m-auto flex gap-2">
{icon} <div className="text-custom-text-400 text-sm text-center my-auto">{t(text)}</div>
</div>
<div className="flex items-center justify-center py-10 bg-custom-background-90 w-full">
<EmptyStateCompact assetKey={assetKey} assetClassName="size-20" title={t(text)} />
</div>
);
};

View file

@ -1,15 +1,11 @@
// plane ui
import { useTranslation } from "@plane/i18n";
import { RecentStickyIcon } from "@plane/propel/icons";
import { EmptyStateCompact } from "@plane/propel/empty-state";
export const StickiesEmptyState = () => {
const { t } = useTranslation();
return (
<div className="min-h-[110px] flex w-full justify-center py-6 bg-custom-border-100 rounded">
<div className="m-auto flex gap-2">
<RecentStickyIcon className="h-[30px] w-[30px] text-custom-text-400/40" />
<div className="text-custom-text-400 text-sm text-center my-auto">{t("stickies.empty_state.simple")}</div>
</div>
<div className="flex items-center justify-center py-10 bg-custom-background-90 w-full">
<EmptyStateCompact assetKey="note" assetClassName="size-20" title={t("stickies.empty_state.simple")} />
</div>
);
};

View file

@ -4,11 +4,11 @@ import { observer } from "mobx-react";
import { PanelLeft } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import { IntakeIcon } from "@plane/propel/icons";
import { EInboxIssueCurrentTab } from "@plane/types";
import { cn } from "@plane/utils";
// components
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
import { InboxContentRoot } from "@/components/inbox/content";
import { InboxSidebar } from "@/components/inbox/sidebar";
import { InboxLayoutLoader } from "@/components/ui/loader/layouts/project-inbox/inbox-layout-loader";
@ -101,9 +101,7 @@ export const InboxIssueRoot: FC<TInboxIssueRoot> = observer((props) => {
inboxIssueId={inboxIssueId.toString()}
/>
) : (
<div className="w-full h-full relative flex justify-center items-center">
<SimpleEmptyState title={t("inbox_issue.empty_state.detail.title")} assetPath={resolvedPath} />
</div>
<EmptyStateCompact assetKey="intake" title={t("project.intake_main.title")} assetClassName="size-20" />
)}
</div>
</>

View file

@ -4,20 +4,19 @@ import type { FC } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import type { TInboxIssueCurrentTab } from "@plane/types";
import { EInboxIssueCurrentTab } from "@plane/types";
// plane imports
import { Header, Loader, EHeaderVariant } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
import { InboxSidebarLoader } from "@/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useProjectInbox } from "@/hooks/store/use-project-inbox";
import { useAppRouter } from "@/hooks/use-app-router";
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// local imports
import { FiltersRoot } from "../inbox-filter";
import { InboxIssueAppliedFilters } from "../inbox-filter/applied-filters/root";
@ -62,11 +61,6 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
getAppliedFiltersCount,
} = useProjectInbox();
// derived values
const sidebarAssetPath = useResolvedAssetPath({ basePath: "/empty-state/intake/intake-issue" });
const sidebarFilterAssetPath = useResolvedAssetPath({
basePath: "/empty-state/intake/filter-issue",
});
const fetchNextPages = useCallback(() => {
if (!workspaceSlug || !projectId) return;
fetchInboxPaginationIssues(workspaceSlug.toString(), projectId.toString());
@ -141,22 +135,33 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
) : (
<div className="flex items-center justify-center h-full w-full">
{getAppliedFiltersCount > 0 ? (
<SimpleEmptyState
title={t("inbox_issue.empty_state.sidebar_filter.title")}
description={t("inbox_issue.empty_state.sidebar_filter.description")}
assetPath={sidebarFilterAssetPath}
<EmptyStateDetailed
assetKey="search"
title={t("common.search.title")}
description={t("common.search.description")}
assetClassName="size-20"
/>
) : currentTab === EInboxIssueCurrentTab.OPEN ? (
<SimpleEmptyState
title={t("inbox_issue.empty_state.sidebar_open_tab.title")}
description={t("inbox_issue.empty_state.sidebar_open_tab.description")}
assetPath={sidebarAssetPath}
<EmptyStateDetailed
assetKey="inbox"
title={t("project.intake_sidebar.title")}
description={t("project.intake_sidebar.description")}
assetClassName="size-20"
actions={[
{
label: t("project.intake_sidebar.cta_primary"),
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/intake`),
variant: "primary",
},
]}
/>
) : (
<SimpleEmptyState
title={t("inbox_issue.empty_state.sidebar_closed_tab.title")}
description={t("inbox_issue.empty_state.sidebar_closed_tab.description")}
assetPath={sidebarAssetPath}
// TODO: Add translation
<EmptyStateDetailed
assetKey="inbox"
title="No request closed yet"
description="All the work items whether accepted or declined can be found here."
assetClassName="size-20"
/>
)}
</div>

View file

@ -7,19 +7,18 @@ import { useParams } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { ISearchIssueResponse } from "@plane/types";
import { EIssuesStoreType, EUserProjectRoles } from "@plane/types";
// components
import { ExistingIssuesListModal } from "@/components/core/modals/existing-issues-list-modal";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useCycle } from "@/hooks/store/use-cycle";
import { useIssues } from "@/hooks/store/use-issues";
import { useUserPermissions } from "@/hooks/store/user";
import { useWorkItemFilterInstance } from "@/hooks/store/work-item-filters/use-work-item-filter-instance";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const CycleEmptyState: React.FC = observer(() => {
// router
@ -33,31 +32,18 @@ export const CycleEmptyState: React.FC = observer(() => {
const { t } = useTranslation();
// store hooks
const { getCycleById } = useCycle();
const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
const { issues } = useIssues(EIssuesStoreType.CYCLE);
const { toggleCreateIssueModal } = useCommandPalette();
const { allowPermissions } = useUserPermissions();
// derived values
const cycleWorkItemFilter = cycleId ? useWorkItemFilterInstance(EIssuesStoreType.CYCLE, cycleId) : undefined;
const cycleDetails = cycleId ? getCycleById(cycleId) : undefined;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {});
const isCompletedAndEmpty = isCompletedCycleSnapshotAvailable || cycleDetails?.status?.toLowerCase() === "completed";
const additionalPath = activeLayout ?? "list";
const canPerformEmptyStateActions = allowPermissions(
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
EUserPermissionsLevel.PROJECT
);
const emptyFilterResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/empty-filters/",
additionalPath: additionalPath,
});
const noIssueResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/cycle-issues/",
additionalPath: additionalPath,
});
const completedNoIssuesResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/cycle/completed-no-issues",
});
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId || !cycleId) return;
@ -94,39 +80,50 @@ export const CycleEmptyState: React.FC = observer(() => {
/>
<div className="grid h-full w-full place-items-center">
{isCompletedAndEmpty ? (
<DetailedEmptyState
// TODO: Empty state ux copy needs to be updated
<EmptyStateDetailed
assetKey="work-item"
title={t("project_cycles.empty_state.completed_no_issues.title")}
description={t("project_cycles.empty_state.completed_no_issues.description")}
assetPath={completedNoIssuesResolvedPath}
/>
) : cycleWorkItemFilter?.hasActiveFilters ? (
<DetailedEmptyState
title={t("project_issues.empty_state.issues_empty_filter.title")}
assetPath={emptyFilterResolvedPath}
secondaryButton={{
text: t("project_issues.empty_state.issues_empty_filter.secondary_button.text"),
onClick: cycleWorkItemFilter?.clearFilters,
disabled: !canPerformEmptyStateActions || !cycleWorkItemFilter,
}}
<EmptyStateDetailed
assetKey="search"
title={t("common.search.title")}
description={t("common.search.description")}
actions={[
{
label: t("common.search.cta_secondary"),
onClick: cycleWorkItemFilter?.clearFilters,
disabled: !canPerformEmptyStateActions || !cycleWorkItemFilter,
variant: "outline-primary",
},
]}
/>
) : (
<DetailedEmptyState
title={t("project_cycles.empty_state.no_issues.title")}
description={t("project_cycles.empty_state.no_issues.description")}
assetPath={noIssueResolvedPath}
primaryButton={{
text: t("project_cycles.empty_state.no_issues.primary_button.text"),
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.CYCLE });
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
<EmptyStateDetailed
assetKey="work-item"
title={t("project.cycle_work_items.title")}
description={t("project.cycle_work_items.description")}
actions={[
{
label: t("project.cycle_work_items.cta_primary"),
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.CYCLE });
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
},
disabled: !canPerformEmptyStateActions,
variant: "primary",
"data-ph-element": WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.CYCLE,
},
disabled: !canPerformEmptyStateActions,
}}
secondaryButton={{
text: t("project_cycles.empty_state.no_issues.secondary_button.text"),
onClick: () => setCycleIssuesListModal(true),
disabled: !canPerformEmptyStateActions,
}}
{
label: t("project.cycle_work_items.cta_secondary"),
onClick: () => setCycleIssuesListModal(true),
disabled: !canPerformEmptyStateActions,
variant: "outline-primary",
"data-ph-element": WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.CYCLE,
},
]}
/>
)}
</div>

View file

@ -1,21 +1,16 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { EIssuesStoreType, EUserWorkspaceRoles } from "@plane/types";
// components
import { ComicBoxButton } from "@/components/empty-state/comic-box-button";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const GlobalViewEmptyState: React.FC = observer(() => {
const { globalViewId } = useParams();
// plane imports
const { t } = useTranslation();
// store hooks
@ -27,56 +22,46 @@ export const GlobalViewEmptyState: React.FC = observer(() => {
[EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId?.toString() ?? "");
const currentView = isDefaultView && globalViewId ? globalViewId : "custom-view";
const resolvedCurrentView = currentView?.toString();
const noProjectResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" });
const globalViewsResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/all-issues/",
additionalPath: resolvedCurrentView,
});
if (workspaceProjectIds?.length === 0) {
return (
<DetailedEmptyState
size="sm"
<EmptyStateDetailed
title={t("workspace_projects.empty_state.no_projects.title")}
description={t("workspace_projects.empty_state.no_projects.description")}
assetPath={noProjectResolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("workspace_projects.empty_state.no_projects.primary_button.text")}
title={t("workspace_projects.empty_state.no_projects.primary_button.comic.title")}
description={t("workspace_projects.empty_state.no_projects.primary_button.comic.description")}
onClick={() => {
assetKey="project"
assetClassName="size-40"
actions={[
{
label: t("workspace_projects.empty_state.no_projects.primary_button.text"),
onClick: () => {
toggleCreateProjectModal(true);
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.GLOBAL_VIEW });
}}
disabled={!hasMemberLevelPermission}
/>
}
},
disabled: !hasMemberLevelPermission,
variant: "primary",
},
]}
/>
);
}
return (
<DetailedEmptyState
size="sm"
title={t(`workspace_views.empty_state.${resolvedCurrentView}.title`)}
description={t(`workspace_views.empty_state.${resolvedCurrentView}.description`)}
assetPath={globalViewsResolvedPath}
primaryButton={
["subscribed", "custom-view"].includes(resolvedCurrentView) === false
? {
text: t(`workspace_views.empty_state.${resolvedCurrentView}.primary_button.text`),
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.GLOBAL_VIEW });
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
disabled: !hasMemberLevelPermission,
}
: undefined
}
<EmptyStateDetailed
title={t(`workspace.views.title`)}
description={t(`workspace.views.description`)}
assetKey="project"
assetClassName="size-40"
actions={[
{
label: t(`workspace.views.cta_primary`),
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.GLOBAL_VIEW });
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
disabled: !hasMemberLevelPermission,
variant: "primary",
},
]}
/>
);
});

View file

@ -6,19 +6,18 @@ import { useParams } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { ISearchIssueResponse } from "@plane/types";
import { EIssuesStoreType, EUserProjectRoles } from "@plane/types";
// components
import { ExistingIssuesListModal } from "@/components/core/modals/existing-issues-list-modal";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useIssues } from "@/hooks/store/use-issues";
import { useUserPermissions } from "@/hooks/store/user";
import { useWorkItemFilterInstance } from "@/hooks/store/work-item-filters/use-work-item-filter-instance";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const ModuleEmptyState: React.FC = observer(() => {
// router
@ -31,25 +30,15 @@ export const ModuleEmptyState: React.FC = observer(() => {
// plane hooks
const { t } = useTranslation();
// store hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE);
const { issues } = useIssues(EIssuesStoreType.MODULE);
const { toggleCreateIssueModal } = useCommandPalette();
const { allowPermissions } = useUserPermissions();
// derived values
const moduleWorkItemFilter = moduleId ? useWorkItemFilterInstance(EIssuesStoreType.MODULE, moduleId) : undefined;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const additionalPath = activeLayout ?? "list";
const canPerformEmptyStateActions = allowPermissions(
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
EUserPermissionsLevel.PROJECT
);
const emptyFilterResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/empty-filters/",
additionalPath: additionalPath,
});
const moduleIssuesResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/module-issues/",
additionalPath: additionalPath,
});
const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId || !moduleId) return;
@ -85,33 +74,41 @@ export const ModuleEmptyState: React.FC = observer(() => {
/>
<div className="grid h-full w-full place-items-center">
{moduleWorkItemFilter?.hasActiveFilters ? (
<DetailedEmptyState
title={t("project_issues.empty_state.issues_empty_filter.title")}
assetPath={emptyFilterResolvedPath}
secondaryButton={{
text: t("project_issues.empty_state.issues_empty_filter.secondary_button.text"),
onClick: moduleWorkItemFilter?.clearFilters,
disabled: !canPerformEmptyStateActions || !moduleWorkItemFilter,
}}
<EmptyStateDetailed
assetKey="search"
title={t("common.search.title")}
description={t("common.search.description")}
actions={[
{
label: t("common.search.cta_secondary"),
onClick: moduleWorkItemFilter?.clearFilters,
disabled: !canPerformEmptyStateActions || !moduleWorkItemFilter,
variant: "outline-primary",
},
]}
/>
) : (
<DetailedEmptyState
title={t("project_module.empty_state.no_issues.title")}
description={t("project_module.empty_state.no_issues.description")}
assetPath={moduleIssuesResolvedPath}
primaryButton={{
text: t("project_module.empty_state.no_issues.primary_button.text"),
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.MODULE });
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
<EmptyStateDetailed
assetKey="work-item"
title={t("project.module_work_items.title")}
description={t("project.module_work_items.description")}
actions={[
{
label: t("project.module_work_items.cta_primary"),
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.MODULE });
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
},
disabled: !canPerformEmptyStateActions,
variant: "primary",
},
disabled: !canPerformEmptyStateActions,
}}
secondaryButton={{
text: t("project_module.empty_state.no_issues.secondary_button.text"),
onClick: () => setModuleIssuesListModal(true),
disabled: !canPerformEmptyStateActions,
}}
{
label: t("project.module_work_items.cta_secondary"),
onClick: () => setModuleIssuesListModal(true),
disabled: !canPerformEmptyStateActions,
variant: "outline-primary",
},
]}
/>
)}
</div>

View file

@ -3,17 +3,14 @@ import { useParams } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { EIssuesStoreType, EUserProjectRoles } from "@plane/types";
// components
import { ComicBoxButton } from "@/components/empty-state/comic-box-button";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useIssues } from "@/hooks/store/use-issues";
import { useUserPermissions } from "@/hooks/store/user";
import { useWorkItemFilterInstance } from "@/hooks/store/work-item-filters/use-work-item-filter-instance";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const ProjectEmptyState: React.FC = observer(() => {
// router
@ -23,53 +20,47 @@ export const ProjectEmptyState: React.FC = observer(() => {
const { t } = useTranslation();
// store hooks
const { toggleCreateIssueModal } = useCommandPalette();
const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
const { allowPermissions } = useUserPermissions();
// derived values
const projectWorkItemFilter = projectId ? useWorkItemFilterInstance(EIssuesStoreType.PROJECT, projectId) : undefined;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const additionalPath = projectWorkItemFilter?.hasActiveFilters ? (activeLayout ?? "list") : undefined;
const canPerformEmptyStateActions = allowPermissions(
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
EUserPermissionsLevel.PROJECT
);
const emptyFilterResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/empty-filters/",
additionalPath: additionalPath,
});
const projectIssuesResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/onboarding/issues",
});
return (
<div className="relative h-full w-full overflow-y-auto">
{projectWorkItemFilter?.hasActiveFilters ? (
<DetailedEmptyState
title={t("project_issues.empty_state.issues_empty_filter.title")}
assetPath={emptyFilterResolvedPath}
secondaryButton={{
text: t("project_issues.empty_state.issues_empty_filter.secondary_button.text"),
onClick: projectWorkItemFilter?.clearFilters,
disabled: !canPerformEmptyStateActions || !projectWorkItemFilter,
}}
<EmptyStateDetailed
assetKey="search"
title={t("common.search.title")}
description={t("common.search.description")}
actions={[
{
label: t("project_issues.empty_state.issues_empty_filter.secondary_button.text"),
onClick: projectWorkItemFilter?.clearFilters,
disabled: !canPerformEmptyStateActions || !projectWorkItemFilter,
variant: "outline-primary",
},
]}
/>
) : (
<DetailedEmptyState
title={t("project_issues.empty_state.no_issues.title")}
description={t("project_issues.empty_state.no_issues.description")}
assetPath={projectIssuesResolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("project_issues.empty_state.no_issues.primary_button.text")}
title={t("project_issues.empty_state.no_issues.primary_button.comic.title")}
description={t("project_issues.empty_state.no_issues.primary_button.comic.description")}
onClick={() => {
<EmptyStateDetailed
assetKey="work-item"
title={t("project.work_items.title")}
description={t("project.work_items.description")}
actions={[
{
label: t("project.work_items.cta_primary"),
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.WORK_ITEMS });
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}}
disabled={!canPerformEmptyStateActions}
/>
}
},
disabled: !canPerformEmptyStateActions,
variant: "primary",
},
]}
/>
)}
</div>

View file

@ -1,15 +1,12 @@
import { observer } from "mobx-react";
import { PlusIcon } from "lucide-react";
// components
import { EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { EIssuesStoreType } from "@plane/types";
import { EmptyState } from "@/components/common/empty-state";
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useUserPermissions } from "@/hooks/store/user";
// assets
import emptyIssue from "@/public/empty-state/issue.svg";
export const ProjectViewEmptyState: React.FC = observer(() => {
// store hooks
@ -23,24 +20,22 @@ export const ProjectViewEmptyState: React.FC = observer(() => {
);
return (
<div className="grid h-full w-full place-items-center">
<EmptyState
title="View work items will appear here"
description="Work items help you track individual pieces of work. With work items, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={
isCreatingIssueAllowed
? {
text: "New work item",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.PROJECT_VIEW });
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW);
},
}
: undefined
}
/>
</div>
// TODO: Add translation
<EmptyStateDetailed
assetKey="work-item"
title="View work items will appear here"
description="Work items help you track individual pieces of work. With work items, keep track of what's going on, who is working on it, and what's done."
actions={[
{
label: "New work item",
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.PROJECT_VIEW });
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW);
},
disabled: !isCreatingIssueAllowed,
variant: "primary",
},
]}
/>
);
});

View file

@ -6,12 +6,11 @@ import { Fragment, useState } from "react";
import { observer } from "mobx-react";
import { EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { EIssuesStoreType, EUserWorkspaceRoles } from "@plane/types";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { CreateUpdateIssueModal } from "@/components/issues/issue-modal/modal";
// constants
import { useUserPermissions } from "@/hooks/store/user";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const WorkspaceDraftEmptyState: FC = observer(() => {
// state
@ -24,7 +23,6 @@ export const WorkspaceDraftEmptyState: FC = observer(() => {
[EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/cycles" });
return (
<Fragment>
@ -35,17 +33,21 @@ export const WorkspaceDraftEmptyState: FC = observer(() => {
isDraft
/>
<div className="relative h-full w-full overflow-y-auto">
<DetailedEmptyState
title={t("workspace_draft_issues.empty_state.title")}
description={t("workspace_draft_issues.empty_state.description")}
assetPath={resolvedPath}
primaryButton={{
text: t("workspace_draft_issues.empty_state.primary_button.text"),
onClick: () => {
setIsDraftIssueModalOpen(true);
<EmptyStateDetailed
title={t("workspace.drafts.title")}
description={t("workspace.drafts.description")}
assetKey="draft"
assetClassName="size-20"
actions={[
{
label: t("workspace.drafts.cta_primary"),
onClick: () => {
setIsDraftIssueModalOpen(true);
},
disabled: !canPerformEmptyStateActions,
variant: "primary",
},
disabled: !canPerformEmptyStateActions,
}}
]}
/>
</div>
</Fragment>

View file

@ -7,11 +7,10 @@ import useSWR from "swr";
// plane imports
import { EUserPermissionsLevel, EDraftIssuePaginationType, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { EUserWorkspaceRoles } from "@plane/types";
// components
import { cn } from "@plane/utils";
import { ComicBoxButton } from "@/components/empty-state/comic-box-button";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { captureClick } from "@/helpers/event-tracker.helper";
// constants
@ -70,23 +69,22 @@ export const WorkspaceDraftIssuesRoot: FC<TWorkspaceDraftIssuesRoot> = observer(
if (workspaceProjectIds?.length === 0)
return (
<DetailedEmptyState
size="sm"
<EmptyStateDetailed
title={t("workspace_projects.empty_state.no_projects.title")}
description={t("workspace_projects.empty_state.no_projects.description")}
assetPath={noProjectResolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("workspace_projects.empty_state.no_projects.primary_button.text")}
title={t("workspace_projects.empty_state.no_projects.primary_button.comic.title")}
description={t("workspace_projects.empty_state.no_projects.primary_button.comic.description")}
onClick={() => {
assetKey="project"
assetClassName="size-40"
actions={[
{
label: t("workspace_projects.empty_state.no_projects.primary_button.text"),
onClick: () => {
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
}}
disabled={!hasMemberLevelPermission}
/>
}
},
disabled: !hasMemberLevelPermission,
variant: "primary",
},
]}
/>
);

View file

@ -6,9 +6,9 @@ import { useParams } from "next/navigation";
// plane imports
import { EUserPermissions, EUserPermissionsLevel, PROJECT_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import type { IIssueLabel } from "@plane/types";
import { Loader } from "@plane/ui";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import type { TLabelOperationsCallbacks } from "@/components/labels";
import {
CreateUpdateLabelInline,
@ -20,7 +20,6 @@ import {
import { captureClick } from "@/helpers/event-tracker.helper";
import { useLabel } from "@/hooks/store/use-label";
import { useUserPermissions } from "@/hooks/store/user";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// local imports
import { SettingsHeading } from "../settings/heading";
@ -40,7 +39,6 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
const { allowPermissions } = useUserPermissions();
// derived values
const isEditable = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/project-settings/labels" });
const labelOperationsCallbacks: TLabelOperationsCallbacks = {
createLabel: (data: Partial<IIssueLabel>) => createLabel(workspaceSlug?.toString(), projectId?.toString(), data),
updateLabel: (labelId: string, data: Partial<IIssueLabel>) =>
@ -111,24 +109,25 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
)}
{projectLabels ? (
projectLabels.length === 0 && !showLabelForm ? (
<div className="flex items-center justify-center h-full w-full">
<DetailedEmptyState
title={""}
description={""}
primaryButton={{
text: "Create your first label",
<EmptyStateCompact
assetKey="label"
assetClassName="size-20"
title={t("settings.labels.title")}
description={t("settings.labels.description")}
actions={[
{
label: t("settings.labels.cta_primary"),
onClick: () => {
newLabel();
captureClick({
elementName: PROJECT_SETTINGS_TRACKER_ELEMENTS.LABELS_EMPTY_STATE_CREATE_BUTTON,
});
},
}}
assetPath={resolvedPath}
className="w-full !px-0 !py-0"
size="md"
/>
</div>
},
]}
align="start"
rootClassName="py-20"
/>
) : (
projectLabelsTree && (
<div className="mt-3">

View file

@ -1,14 +1,12 @@
import { observer } from "mobx-react";
import Image from "next/image";
import { useParams, useSearchParams } from "next/navigation";
// components
import { EUserPermissionsLevel, MODULE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { EUserProjectRoles } from "@plane/types";
import { ContentWrapper, Row, ERowVariant } from "@plane/ui";
import { ListLayout } from "@/components/core/list";
import { ComicBoxButton } from "@/components/empty-state/comic-box-button";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "@/components/modules";
import { CycleModuleBoardLayoutLoader } from "@/components/ui/loader/cycle-module-board-loader";
import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader";
@ -18,9 +16,6 @@ import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useModule } from "@/hooks/store/use-module";
import { useModuleFilter } from "@/hooks/store/use-module-filter";
import { useUserPermissions } from "@/hooks/store/user";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import AllFiltersImage from "@/public/empty-state/module/all-filters.svg";
import NameFilterImage from "@/public/empty-state/module/name-filter.svg";
export const ModulesListView: React.FC = observer(() => {
// router
@ -32,7 +27,7 @@ export const ModulesListView: React.FC = observer(() => {
// store hooks
const { toggleCreateModuleModal } = useCommandPalette();
const { getProjectModuleIds, getFilteredModuleIds, loader } = useModule();
const { currentProjectDisplayFilters: displayFilters, searchQuery } = useModuleFilter();
const { currentProjectDisplayFilters: displayFilters } = useModuleFilter();
const { allowPermissions } = useUserPermissions();
// derived values
const projectModuleIds = projectId ? getProjectModuleIds(projectId.toString()) : undefined;
@ -41,9 +36,6 @@ export const ModulesListView: React.FC = observer(() => {
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
EUserPermissionsLevel.PROJECT
);
const generalViewResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/onboarding/modules",
});
if (loader || !projectModuleIds || !filteredModuleIds)
return (
@ -56,42 +48,29 @@ export const ModulesListView: React.FC = observer(() => {
if (projectModuleIds.length === 0)
return (
<DetailedEmptyState
title={t("project_module.empty_state.general.title")}
description={t("project_module.empty_state.general.description")}
assetPath={generalViewResolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("project_module.empty_state.general.primary_button.text")}
title={t("project_module.empty_state.general.primary_button.comic.title")}
description={t("project_module.empty_state.general.primary_button.comic.description")}
data-ph-element={MODULE_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON}
onClick={() => {
toggleCreateModuleModal(true);
}}
disabled={!canPerformEmptyStateActions}
/>
}
<EmptyStateDetailed
assetKey="module"
title={t("project.modules.title")}
description={t("project.modules.description")}
actions={[
{
label: t("project.modules.cta_primary"),
onClick: () => toggleCreateModuleModal(true),
disabled: !canPerformEmptyStateActions,
variant: "primary",
"data-ph-element": MODULE_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON,
},
]}
/>
);
if (filteredModuleIds.length === 0)
return (
<div className="grid h-full w-full place-items-center">
<div className="text-center">
<Image
src={searchQuery.trim() === "" ? AllFiltersImage : NameFilterImage}
className="mx-auto h-36 w-36 sm:h-48 sm:w-48"
alt="No matching modules"
/>
<h5 className="mb-1 mt-7 text-xl font-medium">No matching modules</h5>
<p className="text-base text-custom-text-400">
{searchQuery.trim() === ""
? "Remove the filters to see all modules"
: "Remove the search criteria to see all modules"}
</p>
</div>
</div>
<EmptyStateDetailed
assetKey="search"
title={t("common.search.title")}
description={t("common.search.description")}
/>
);
return (

View file

@ -1,7 +1,6 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
// plane imports
import { useParams, useRouter } from "next/navigation";
import {
@ -11,16 +10,15 @@ import {
PROJECT_PAGE_TRACKER_EVENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TPage, TPageNavigationTabs } from "@plane/types";
import { EUserProjectRoles } from "@plane/types";
// components
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { PageLoader } from "@/components/pages/loaders/page-loader";
import { captureClick, captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// plane web hooks
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
@ -52,23 +50,6 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
EUserPermissionsLevel.PROJECT
);
const generalPageResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/onboarding/pages",
});
const publicPageResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/wiki/public",
});
const privatePageResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/wiki/private",
});
const archivedPageResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/wiki/archived",
});
const resolvedFiltersImage = useResolvedAssetPath({ basePath: "/empty-state/wiki/all-filters", extension: "svg" });
const resolvedNameFilterImage = useResolvedAssetPath({
basePath: "/empty-state/wiki/name-filter",
extension: "svg",
});
// handle page create
const handleCreatePage = async () => {
@ -111,80 +92,79 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
if (!isAnyPageAvailable || pageIds?.length === 0) {
if (!isAnyPageAvailable) {
return (
<DetailedEmptyState
title={t("project_page.empty_state.general.title")}
description={t("project_page.empty_state.general.description")}
assetPath={generalPageResolvedPath}
primaryButton={{
text: isCreatingPage ? t("creating") : t("project_page.empty_state.general.primary_button.text"),
onClick: () => {
handleCreatePage();
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
<EmptyStateDetailed
assetKey="page"
title={t("project.pages.title")}
description={t("project.pages.description")}
actions={[
{
label: t("project.pages.cta_primary"),
onClick: () => {
handleCreatePage();
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
},
variant: "primary",
disabled: !canPerformEmptyStateActions || isCreatingPage,
},
disabled: !canPerformEmptyStateActions || isCreatingPage,
}}
]}
/>
);
}
if (pageType === "public")
return (
<DetailedEmptyState
title={t("project_page.empty_state.public.title")}
description={t("project_page.empty_state.public.description")}
assetPath={publicPageResolvedPath}
primaryButton={{
text: isCreatingPage ? t("creating") : t("project_page.empty_state.public.primary_button.text"),
onClick: () => {
handleCreatePage();
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
<EmptyStateDetailed
assetKey="page"
title={t("project.pages.title")}
description={t("project.pages.description")}
actions={[
{
label: t("project.pages.cta_primary"),
onClick: () => {
handleCreatePage();
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
},
variant: "primary",
disabled: !canPerformEmptyStateActions || isCreatingPage,
},
disabled: !canPerformEmptyStateActions || isCreatingPage,
}}
]}
/>
);
if (pageType === "private")
return (
<DetailedEmptyState
title={t("project_page.empty_state.private.title")}
description={t("project_page.empty_state.private.description")}
assetPath={privatePageResolvedPath}
primaryButton={{
text: isCreatingPage ? t("creating") : t("project_page.empty_state.private.primary_button.text"),
onClick: () => {
handleCreatePage();
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
<EmptyStateDetailed
assetKey="page"
title={t("project.pages.title")}
description={t("project.pages.description")}
actions={[
{
label: t("project.pages.cta_primary"),
onClick: () => {
handleCreatePage();
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
},
variant: "primary",
disabled: !canPerformEmptyStateActions || isCreatingPage,
},
disabled: !canPerformEmptyStateActions || isCreatingPage,
}}
]}
/>
);
if (pageType === "archived")
return (
<DetailedEmptyState
title={t("project_page.empty_state.archived.title")}
description={t("project_page.empty_state.archived.description")}
assetPath={archivedPageResolvedPath}
<EmptyStateDetailed
assetKey="page"
title={t("project.archive_pages.title")}
description={t("project.archive_pages.description")}
/>
);
}
// if no pages match the filter criteria
if (filteredPageIds?.length === 0)
return (
<div className="h-full w-full grid place-items-center">
<div className="text-center">
<Image
src={filters.searchQuery.length > 0 ? resolvedNameFilterImage : resolvedFiltersImage}
className="h-36 sm:h-48 w-36 sm:w-48 mx-auto"
alt="No matching modules"
/>
<h5 className="text-xl font-medium mt-7 mb-1">No matching pages</h5>
<p className="text-custom-text-400 text-base">
{filters.searchQuery.length > 0
? "Remove the search criteria to see all pages"
: "Remove the filters to see all pages"}
</p>
</div>
</div>
<EmptyStateDetailed
assetKey="search"
title={t("common.search.title")}
description={t("common.search.description")}
/>
);
return <div className="h-full w-full overflow-hidden">{children}</div>;

View file

@ -5,6 +5,7 @@ import { useParams } from "next/navigation";
import useSWR from "swr";
// ui
import { useTranslation } from "@plane/i18n";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import { Loader, Card } from "@plane/ui";
import { calculateTimeAgo, getFileURL } from "@plane/utils";
// components
@ -83,11 +84,7 @@ export const ProfileActivity = observer(() => {
))}
</div>
) : (
<ProfileEmptyState
title={t("no_data_yet")}
description={t("profile.stats.recent_activity.empty")}
image={recentActivityEmptyState}
/>
<EmptyStateCompact title={t("no_data_yet")} assetKey="unknown" assetClassName="size-20" />
)
) : (
<Loader className="space-y-5">

View file

@ -3,13 +3,10 @@
// plane imports
import { useTranslation } from "@plane/i18n";
import { BarChart } from "@plane/propel/charts/bar-chart";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import type { IUserProfileData } from "@plane/types";
import { Loader, Card } from "@plane/ui";
import { capitalizeFirstLetter } from "@plane/utils";
// components
import { ProfileEmptyState } from "@/components/ui/profile-empty-state";
// assets
import emptyBarGraph from "@/public/empty-state/empty_bar_graph.svg";
type Props = {
userProfile: IUserProfileData | undefined;
@ -62,13 +59,11 @@ export const ProfilePriorityDistribution: React.FC<Props> = ({ userProfile }) =>
barSize={20}
/>
) : (
<div className="flex-grow p-7">
<ProfileEmptyState
title={t("no_data_yet")}
description={t("profile.stats.priority_distribution.empty")}
image={emptyBarGraph}
/>
</div>
<EmptyStateCompact
assetKey="priority"
assetClassName="size-20"
title={t("workspace.your_work_by_priority.title")}
/>
)}
</Card>
) : (

View file

@ -2,13 +2,10 @@
import { STATE_GROUPS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { PieChart } from "@plane/propel/charts/pie-chart";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import type { IUserProfileData, IUserStateDistribution } from "@plane/types";
import { Card } from "@plane/ui";
import { capitalizeFirstLetter } from "@plane/utils";
// components
import { ProfileEmptyState } from "@/components/ui/profile-empty-state";
// assets
import stateGraph from "@/public/empty-state/state_graph.svg";
type Props = {
stateDistribution: IUserStateDistribution[];
@ -74,10 +71,10 @@ export const ProfileStateDistribution: React.FC<Props> = ({ stateDistribution, u
</div>
</div>
) : (
<ProfileEmptyState
title={t("no_data_yet")}
description={t("profile.stats.state_distribution.empty")}
image={stateGraph}
<EmptyStateCompact
assetKey="priority"
assetClassName="size-20"
title={t("workspace.your_work_by_priority.title")}
/>
)}
</Card>

View file

@ -1,12 +1,11 @@
import { observer } from "mobx-react";
import Image from "next/image";
// plane imports
import { EUserPermissionsLevel, EUserPermissions, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { ContentWrapper } from "@plane/ui";
// components
import { ComicBoxButton } from "@/components/empty-state/comic-box-button";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { calculateTotalFilters } from "@plane/utils";
import { ProjectsLoader } from "@/components/ui/loader/projects-loader";
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
@ -14,7 +13,6 @@ import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useProject } from "@/hooks/store/use-project";
import { useProjectFilter } from "@/hooks/store/use-project-filter";
import { useUserPermissions } from "@/hooks/store/user";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// local imports
import { ProjectCard } from "./card";
@ -36,20 +34,9 @@ export const ProjectCardList = observer((props: TProjectCardListProps) => {
filteredProjectIds: storeFilteredProjectIds,
getProjectById,
} = useProject();
const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter();
const { currentWorkspaceDisplayFilters, currentWorkspaceFilters } = useProjectFilter();
const { allowPermissions } = useUserPermissions();
// helper hooks
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" });
const resolvedFiltersImage = useResolvedAssetPath({
basePath: "/empty-state/project/all-filters",
extension: "svg",
});
const resolvedNameFilterImage = useResolvedAssetPath({
basePath: "/empty-state/project/name-filter",
extension: "svg",
});
// derived values
const workspaceProjectIds = totalProjectIdsProps ?? storeWorkspaceProjectIds;
const filteredProjectIds = filteredProjectIdsProps ?? storeFilteredProjectIds;
@ -65,42 +52,48 @@ export const ProjectCardList = observer((props: TProjectCardListProps) => {
if (workspaceProjectIds?.length === 0 && !currentWorkspaceDisplayFilters?.archived_projects)
return (
<DetailedEmptyState
<EmptyStateDetailed
title={t("workspace_projects.empty_state.general.title")}
description={t("workspace_projects.empty_state.general.description")}
assetPath={resolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("workspace_projects.empty_state.general.primary_button.text")}
title={t("workspace_projects.empty_state.general.primary_button.comic.title")}
description={t("workspace_projects.empty_state.general.primary_button.comic.description")}
onClick={() => {
assetKey="project"
assetClassName="size-40"
actions={[
{
label: t("workspace_projects.empty_state.general.primary_button.text"),
onClick: () => {
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
}}
disabled={!canPerformEmptyStateActions}
/>
}
},
disabled: !canPerformEmptyStateActions,
variant: "primary",
},
]}
/>
);
if (filteredProjectIds.length === 0)
return (
<div className="grid h-full w-full place-items-center">
<div className="text-center">
<Image
src={searchQuery.trim() === "" ? resolvedFiltersImage : resolvedNameFilterImage}
className="mx-auto h-36 w-36 sm:h-48 sm:w-48"
alt="No matching projects"
/>
<h5 className="mb-1 mt-7 text-xl font-medium">{t("workspace_projects.empty_state.filter.title")}</h5>
<p className="whitespace-pre-line text-base text-custom-text-400">
{searchQuery.trim() === ""
? t("workspace_projects.empty_state.filter.description")
: t("workspace_projects.empty_state.search.description")}
</p>
</div>
</div>
<EmptyStateDetailed
title={
currentWorkspaceDisplayFilters?.archived_projects &&
calculateTotalFilters(currentWorkspaceFilters ?? {}) === 0
? t("workspace.projects_archived.title")
: t("common.search.title")
}
description={
currentWorkspaceDisplayFilters?.archived_projects &&
calculateTotalFilters(currentWorkspaceFilters ?? {}) === 0
? t("workspace.projects_archived.description")
: t("common.search.description")
}
assetKey={
currentWorkspaceDisplayFilters?.archived_projects &&
calculateTotalFilters(currentWorkspaceFilters ?? {}) === 0
? "archived-work-item"
: "search"
}
assetClassName="size-40"
/>
);
return (

View file

@ -1,21 +1,17 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
import { EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { EUserProjectRoles } from "@plane/types";
// components
import { ListLayout } from "@/components/core/list";
import { ComicBoxButton } from "@/components/empty-state/comic-box-button";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
import { ViewListLoader } from "@/components/ui/loader/view-list-loader";
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useProjectView } from "@/hooks/store/use-project-view";
import { useUserPermissions } from "@/hooks/store/user";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// local imports
import { ProjectViewListItem } from "./view-list-item";
@ -34,24 +30,16 @@ export const ProjectViewsList = observer(() => {
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER, EUserProjectRoles.GUEST],
EUserPermissionsLevel.PROJECT
);
const generalViewResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/onboarding/views",
});
const filteredViewResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/search/views",
});
if (loader || !projectViews || !filteredProjectViews) return <ViewListLoader />;
if (filteredProjectViews.length === 0 && projectViews.length > 0) {
return (
<div className="flex items-center justify-center h-full w-full">
<SimpleEmptyState
title={t("project_views.empty_state.filter.title")}
description={t("project_views.empty_state.filter.description")}
assetPath={filteredViewResolvedPath}
/>
</div>
<EmptyStateDetailed
assetKey="search"
title={t("common.search.title")}
description={t("common.search.description")}
/>
);
}
@ -68,22 +56,18 @@ export const ProjectViewsList = observer(() => {
</ListLayout>
</div>
) : (
<DetailedEmptyState
title={t("project_views.empty_state.general.title")}
description={t("project_views.empty_state.general.description")}
assetPath={generalViewResolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("project_views.empty_state.general.primary_button.text")}
title={t("project_views.empty_state.general.primary_button.comic.title")}
description={t("project_views.empty_state.general.primary_button.comic.description")}
onClick={() => {
toggleCreateViewModal(true);
captureClick({ elementName: PROJECT_VIEW_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
}}
disabled={!canPerformEmptyStateActions}
/>
}
<EmptyStateDetailed
assetKey="view"
title={t("project.views.title")}
description={t("project.views.description")}
actions={[
{
label: t("project.views.cta_primary"),
onClick: () => toggleCreateViewModal(true),
disabled: !canPerformEmptyStateActions,
variant: "primary",
},
]}
/>
)}
</>

View file

@ -6,10 +6,10 @@ import useSWR from "swr";
// plane imports
import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import { cn } from "@plane/utils";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
// hooks
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
import { useWorkspace } from "@/hooks/store/use-workspace";
@ -87,8 +87,8 @@ export const NotificationsRoot = observer(({ workspaceSlug }: NotificationsRootP
return (
<div className={cn("w-full h-full overflow-hidden ", isWorkItem && "overflow-y-auto")}>
{!currentSelectedNotificationId ? (
<div className="w-full h-screen flex justify-center items-center">
<SimpleEmptyState title={t("notification.empty_state.detail.title")} assetPath={resolvedPath} />
<div className="flex justify-center items-center size-full">
<EmptyStateCompact assetKey="unknown" assetClassName="size-20" />
</div>
) : (
<>

View file

@ -4,33 +4,29 @@ import type { FC } from "react";
import { observer } from "mobx-react";
// plane imports
import { ENotificationTab } from "@plane/constants";
// components
import { useTranslation } from "@plane/i18n";
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
// constants
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { EmptyStateCompact } from "@plane/propel/empty-state";
export const NotificationEmptyState: FC = observer(() => {
type TNotificationEmptyStateProps = {
currentNotificationTab: ENotificationTab;
};
export const NotificationEmptyState: FC<TNotificationEmptyStateProps> = observer(({ currentNotificationTab }) => {
// plane imports
const { t } = useTranslation();
// derived values
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/notification" });
return (
<>
{ENotificationTab.ALL ? (
<SimpleEmptyState
title={t("notification.empty_state.all.title")}
description={t("notification.empty_state.all.description")}
assetPath={resolvedPath}
/>
) : (
<SimpleEmptyState
title={t("notification.empty_state.mentions.title")}
description={t("notification.empty_state.mentions.description")}
assetPath={resolvedPath}
/>
)}
<EmptyStateCompact
assetKey="inbox"
assetClassName="size-24"
title={
currentNotificationTab === ENotificationTab.ALL
? t("workspace.inbox_sidebar_all.title")
: t("workspace.inbox_sidebar_mentions.title")
}
className="max-w-56"
/>
</>
);
});

View file

@ -107,7 +107,7 @@ export const NotificationsSidebarRoot: FC = observer(() => {
</ContentWrapper>
) : (
<div className="relative w-full h-full flex justify-center items-center">
<NotificationEmptyState />
<NotificationEmptyState currentNotificationTab={currentNotificationTab} />
</div>
)}
</>

View file

@ -7,12 +7,11 @@ import useSWR from "swr";
// plane imports
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { EProjectNetwork } from "@plane/types";
// components
import { JoinProject } from "@/components/auth-screens/project/join-project";
import { LogoSpinner } from "@/components/common/logo-spinner";
import { ComicBoxButton } from "@/components/empty-state/comic-box-button";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { ETimeLineTypeType } from "@/components/gantt-chart/contexts";
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
@ -26,7 +25,6 @@ import { useProject } from "@/hooks/store/use-project";
import { useProjectState } from "@/hooks/store/use-project-state";
import { useProjectView } from "@/hooks/store/use-project-view";
import { useUserPermissions } from "@/hooks/store/user";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { useTimeLineChart } from "@/hooks/use-timeline-chart";
// local
import { persistence } from "@/local-db/storage.sqlite";
@ -58,9 +56,6 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
const { fetchProjectLabels } = useLabel();
const { getProjectEstimates } = useProjectEstimates();
// helper hooks
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" });
// derived values
const projectExists = projectId ? getProjectById(projectId.toString()) : null;
const projectMemberInfo = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
@ -184,22 +179,22 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
if (loader === "loaded" && projectId && !!hasPermissionToCurrentProject === false)
return (
<div className="grid h-full place-items-center bg-custom-background-100">
<DetailedEmptyState
<EmptyStateDetailed
title={t("workspace_projects.empty_state.general.title")}
description={t("workspace_projects.empty_state.general.description")}
assetPath={resolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("workspace_projects.empty_state.general.primary_button.text")}
title={t("workspace_projects.empty_state.general.primary_button.comic.title")}
description={t("workspace_projects.empty_state.general.primary_button.comic.description")}
onClick={() => {
assetKey="project"
assetClassName="size-40"
actions={[
{
label: t("workspace_projects.empty_state.general.primary_button.text"),
onClick: () => {
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
}}
disabled={!canPerformEmptyStateActions}
/>
}
},
disabled: !canPerformEmptyStateActions,
variant: "primary",
},
]}
/>
</div>
);