From 3faf7681126108d5caa933880256fdf95ebae714 Mon Sep 17 00:00:00 2001
From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Date: Mon, 27 Oct 2025 17:03:35 +0530
Subject: [PATCH] [WEB-3567] chore: empty state refactoring and translation
fix (#8014)
* chore: empty state component improvement and code refactor
* chore: translation code refactor
* chore: empty state code refactor
---
.../settings/(workspace)/webhooks/page.tsx | 34 +++++------
.../cycles/archived-cycles/root.tsx | 12 ++--
.../core/components/exporter/prev-exports.tsx | 17 +++---
.../core/components/inbox/sidebar/root.tsx | 1 +
.../empty-states/archived-issues.tsx | 56 +++++++++----------
.../modules/archived-modules/root.tsx | 12 ++--
packages/i18n/src/constants/language.ts | 2 +-
packages/i18n/src/locales/index.ts | 38 ++++++-------
.../src/empty-state/assets/asset-registry.tsx | 2 +
.../src/empty-state/assets/asset-types.ts | 1 +
.../assets/vertical-stack/constant.tsx | 5 ++
.../src/empty-state/compact-empty-state.tsx | 28 ++++++----
.../src/empty-state/detailed-empty-state.tsx | 28 ++++++----
packages/propel/src/empty-state/types.ts | 1 +
14 files changed, 121 insertions(+), 116 deletions(-)
diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx
index 06be0fd85..efbe979ba 100644
--- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx
+++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx
@@ -8,9 +8,9 @@ import useSWR from "swr";
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components
+import { EmptyStateCompact } from "@plane/propel/empty-state";
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
-import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { SettingsHeading } from "@/components/settings/heading";
import { WebhookSettingsLoader } from "@/components/ui/loader/settings/web-hook";
@@ -20,7 +20,6 @@ import { captureClick } from "@/helpers/event-tracker.helper";
import { useWebhook } from "@/hooks/store/use-webhook";
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUserPermissions } from "@/hooks/store/user";
-import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
const WebhooksListPage = observer(() => {
// states
@@ -35,7 +34,6 @@ const WebhooksListPage = observer(() => {
const { currentWorkspace } = useWorkspace();
// derived values
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
- const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/webhooks" });
useSWR(
workspaceSlug && canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null,
@@ -90,21 +88,23 @@ const WebhooksListPage = observer(() => {
) : (
- {
- captureClick({
- elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_WEBHOOK_BUTTON,
- });
- setShowCreateWebhookModal(true);
+ {
+ captureClick({
+ elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_WEBHOOK_BUTTON,
+ });
+ setShowCreateWebhookModal(true);
+ },
},
- }}
+ ]}
+ align="start"
+ rootClassName="py-20"
/>
diff --git a/apps/web/core/components/cycles/archived-cycles/root.tsx b/apps/web/core/components/cycles/archived-cycles/root.tsx
index 93c18a048..fdf527de8 100644
--- a/apps/web/core/components/cycles/archived-cycles/root.tsx
+++ b/apps/web/core/components/cycles/archived-cycles/root.tsx
@@ -4,15 +4,14 @@ import { useParams } from "next/navigation";
import useSWR from "swr";
// plane imports
import { useTranslation } from "@plane/i18n";
+import { EmptyStateDetailed } from "@plane/propel/empty-state";
import type { TCycleFilters } from "@plane/types";
import { calculateTotalFilters } from "@plane/utils";
// components
-import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader";
// hooks
import { useCycle } from "@/hooks/store/use-cycle";
import { useCycleFilter } from "@/hooks/store/use-cycle-filter";
-import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// local imports
import { CycleAppliedFiltersList } from "../applied-filters";
import { ArchivedCyclesView } from "./view";
@@ -28,7 +27,6 @@ export const ArchivedCycleLayoutRoot: React.FC = observer(() => {
const { clearAllFilters, currentProjectArchivedFilters, updateFilters } = useCycleFilter();
// derived values
const totalArchivedCycles = currentProjectArchivedCycleIds?.length ?? 0;
- const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/archived/empty-cycles" });
useSWR(
workspaceSlug && projectId ? `ARCHIVED_CYCLES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
@@ -69,10 +67,10 @@ export const ArchivedCycleLayoutRoot: React.FC = observer(() => {
)}
{totalArchivedCycles === 0 ? (
-
) : (
diff --git a/apps/web/core/components/exporter/prev-exports.tsx b/apps/web/core/components/exporter/prev-exports.tsx
index 5ee589e7d..8498a6ca2 100644
--- a/apps/web/core/components/exporter/prev-exports.tsx
+++ b/apps/web/core/components/exporter/prev-exports.tsx
@@ -4,15 +4,13 @@ import useSWR, { mutate } from "swr";
import { MoveLeft, MoveRight, RefreshCw } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
+import { EmptyStateCompact } from "@plane/propel/empty-state";
import type { IExportData } from "@plane/types";
import { Table } from "@plane/ui";
// components
-import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { ImportExportSettingsLoader } from "@/components/ui/loader/settings/import-and-export";
// constants
import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys";
-// hooks
-import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// services
import { IntegrationService } from "@/services/integrations";
// local imports
@@ -35,7 +33,6 @@ export const PrevExports = observer((props: Props) => {
// hooks
const { t } = useTranslation();
const columns = useExportColumns();
- const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/exports" });
const { data: exporterServices } = useSWR(
workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null,
@@ -125,12 +122,12 @@ export const PrevExports = observer((props: Props) => {
) : (
-
)
diff --git a/apps/web/core/components/inbox/sidebar/root.tsx b/apps/web/core/components/inbox/sidebar/root.tsx
index 88b59dc62..ba43a7a93 100644
--- a/apps/web/core/components/inbox/sidebar/root.tsx
+++ b/apps/web/core/components/inbox/sidebar/root.tsx
@@ -162,6 +162,7 @@ export const InboxSidebar: FC = observer((props) => {
title="No request closed yet"
description="All the work items whether accepted or declined can be found here."
assetClassName="size-20"
+ className="px-10"
/>
)}
diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx
index a54aff362..5b0c5728a 100644
--- a/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx
+++ b/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx
@@ -3,15 +3,12 @@ import { useParams } from "next/navigation";
// plane imports
import { EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
+import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { EIssuesStoreType, EUserProjectRoles } from "@plane/types";
-// components
-import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
// hooks
-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 { useAppRouter } from "@/hooks/use-app-router";
-import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const ProjectArchivedEmptyState: React.FC = observer(() => {
// router
@@ -22,48 +19,45 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => {
// plane hooks
const { t } = useTranslation();
// store hooks
- const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
const { allowPermissions } = useUserPermissions();
// derived values
const archivedWorkItemFilter = projectId
? useWorkItemFilterInstance(EIssuesStoreType.ARCHIVED, projectId)
: undefined;
- const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
- const additionalPath = archivedWorkItemFilter?.hasActiveFilters ? (activeLayout ?? "list") : undefined;
const canPerformEmptyStateActions = allowPermissions(
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
EUserPermissionsLevel.PROJECT
);
- const emptyFilterResolvedPath = useResolvedAssetPath({
- basePath: "/empty-state/empty-filters/",
- additionalPath: additionalPath,
- });
- const archivedIssuesResolvedPath = useResolvedAssetPath({
- basePath: "/empty-state/archived/empty-issues",
- });
return (
{archivedWorkItemFilter?.hasActiveFilters ? (
-
) : (
- router.push(`/${workspaceSlug}/settings/projects/${projectId}/automations`),
- disabled: !canPerformEmptyStateActions,
- }}
+ router.push(`/${workspaceSlug}/settings/projects/${projectId}/automations`),
+ disabled: !canPerformEmptyStateActions,
+ variant: "primary",
+ },
+ ]}
/>
)}
diff --git a/apps/web/core/components/modules/archived-modules/root.tsx b/apps/web/core/components/modules/archived-modules/root.tsx
index 1628192a9..31fb196ca 100644
--- a/apps/web/core/components/modules/archived-modules/root.tsx
+++ b/apps/web/core/components/modules/archived-modules/root.tsx
@@ -4,17 +4,16 @@ import { useParams } from "next/navigation";
import useSWR from "swr";
// plane imports
import { useTranslation } from "@plane/i18n";
+import { EmptyStateDetailed } from "@plane/propel/empty-state";
import type { TModuleFilters } from "@plane/types";
// components
import { calculateTotalFilters } from "@plane/utils";
-import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { ArchivedModulesView, ModuleAppliedFiltersList } from "@/components/modules";
import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader";
// helpers
// hooks
import { useModule } from "@/hooks/store/use-module";
import { useModuleFilter } from "@/hooks/store/use-module-filter";
-import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const ArchivedModuleLayoutRoot: React.FC = observer(() => {
// router
@@ -26,7 +25,6 @@ export const ArchivedModuleLayoutRoot: React.FC = observer(() => {
const { clearAllFilters, currentProjectArchivedFilters, updateFilters } = useModuleFilter();
// derived values
const totalArchivedModules = projectArchivedModuleIds?.length ?? 0;
- const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/archived/empty-modules" });
useSWR(
workspaceSlug && projectId ? `ARCHIVED_MODULES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
@@ -72,10 +70,10 @@ export const ArchivedModuleLayoutRoot: React.FC = observer(() => {
)}
{totalArchivedModules === 0 ? (
-
) : (
diff --git a/packages/i18n/src/constants/language.ts b/packages/i18n/src/constants/language.ts
index efcc9aada..8e54c189d 100644
--- a/packages/i18n/src/constants/language.ts
+++ b/packages/i18n/src/constants/language.ts
@@ -32,7 +32,7 @@ export enum ETranslationFiles {
TRANSLATIONS = "translations",
ACCESSIBILITY = "accessibility",
EDITOR = "editor",
- EMPTY_STATE = "emptyState",
+ EMPTY_STATE = "empty-state",
}
export const LANGUAGE_STORAGE_KEY = "userLanguage";
diff --git a/packages/i18n/src/locales/index.ts b/packages/i18n/src/locales/index.ts
index 50188889e..6bd62cf41 100644
--- a/packages/i18n/src/locales/index.ts
+++ b/packages/i18n/src/locales/index.ts
@@ -12,114 +12,114 @@ export const locales = {
translations: () => import("./en/translations"),
accessibility: () => import("./en/accessibility"),
editor: () => import("./en/editor"),
- emptyState: () => import("./en/empty-state"),
+ "empty-state": () => import("./en/empty-state"),
},
fr: {
translations: () => import("./fr/translations"),
accessibility: () => import("./fr/accessibility"),
editor: () => import("./fr/editor"),
- emptyState: () => import("./fr/empty-state"),
+ "empty-state": () => import("./fr/empty-state"),
},
es: {
translations: () => import("./es/translations"),
accessibility: () => import("./es/accessibility"),
editor: () => import("./es/editor"),
- emptyState: () => import("./es/empty-state"),
+ "empty-state": () => import("./es/empty-state"),
},
ja: {
translations: () => import("./ja/translations"),
accessibility: () => import("./ja/accessibility"),
editor: () => import("./ja/editor"),
- emptyState: () => import("./ja/empty-state"),
+ "empty-state": () => import("./ja/empty-state"),
},
"zh-CN": {
translations: () => import("./zh-CN/translations"),
accessibility: () => import("./zh-CN/accessibility"),
editor: () => import("./zh-CN/editor"),
- emptyState: () => import("./zh-CN/empty-state"),
+ "empty-state": () => import("./zh-CN/empty-state"),
},
"zh-TW": {
translations: () => import("./zh-TW/translations"),
accessibility: () => import("./zh-TW/accessibility"),
editor: () => import("./zh-TW/editor"),
- emptyState: () => import("./zh-TW/empty-state"),
+ "empty-state": () => import("./zh-TW/empty-state"),
},
ru: {
translations: () => import("./ru/translations"),
accessibility: () => import("./ru/accessibility"),
editor: () => import("./ru/editor"),
- emptyState: () => import("./ru/empty-state"),
+ "empty-state": () => import("./ru/empty-state"),
},
it: {
translations: () => import("./it/translations"),
accessibility: () => import("./it/accessibility"),
editor: () => import("./it/editor"),
- emptyState: () => import("./it/empty-state"),
+ "empty-state": () => import("./it/empty-state"),
},
cs: {
translations: () => import("./cs/translations"),
accessibility: () => import("./cs/accessibility"),
editor: () => import("./cs/editor"),
- emptyState: () => import("./cs/empty-state"),
+ "empty-state": () => import("./cs/empty-state"),
},
sk: {
translations: () => import("./sk/translations"),
accessibility: () => import("./sk/accessibility"),
editor: () => import("./sk/editor"),
- emptyState: () => import("./sk/empty-state"),
+ "empty-state": () => import("./sk/empty-state"),
},
de: {
translations: () => import("./de/translations"),
accessibility: () => import("./de/accessibility"),
editor: () => import("./de/editor"),
- emptyState: () => import("./de/empty-state"),
+ "empty-state": () => import("./de/empty-state"),
},
ua: {
translations: () => import("./ua/translations"),
accessibility: () => import("./ua/accessibility"),
editor: () => import("./ua/editor"),
- emptyState: () => import("./ua/empty-state"),
+ "empty-state": () => import("./ua/empty-state"),
},
pl: {
translations: () => import("./pl/translations"),
accessibility: () => import("./pl/accessibility"),
editor: () => import("./pl/editor"),
- emptyState: () => import("./pl/empty-state"),
+ "empty-state": () => import("./pl/empty-state"),
},
ko: {
translations: () => import("./ko/translations"),
accessibility: () => import("./ko/accessibility"),
editor: () => import("./ko/editor"),
- emptyState: () => import("./ko/empty-state"),
+ "empty-state": () => import("./ko/empty-state"),
},
"pt-BR": {
translations: () => import("./pt-BR/translations"),
accessibility: () => import("./pt-BR/accessibility"),
editor: () => import("./pt-BR/editor"),
- emptyState: () => import("./pt-BR/empty-state"),
+ "empty-state": () => import("./pt-BR/empty-state"),
},
id: {
translations: () => import("./id/translations"),
accessibility: () => import("./id/accessibility"),
editor: () => import("./id/editor"),
- emptyState: () => import("./id/empty-state"),
+ "empty-state": () => import("./id/empty-state"),
},
ro: {
translations: () => import("./ro/translations"),
accessibility: () => import("./ro/accessibility"),
editor: () => import("./ro/editor"),
- emptyState: () => import("./ro/empty-state"),
+ "empty-state": () => import("./ro/empty-state"),
},
"vi-VN": {
translations: () => import("./vi-VN/translations"),
accessibility: () => import("./vi-VN/accessibility"),
editor: () => import("./vi-VN/editor"),
- emptyState: () => import("./vi-VN/empty-state"),
+ "empty-state": () => import("./vi-VN/empty-state"),
},
"tr-TR": {
translations: () => import("./tr-TR/translations"),
accessibility: () => import("./tr-TR/accessibility"),
editor: () => import("./tr-TR/editor"),
- emptyState: () => import("./tr-TR/empty-state"),
+ "empty-state": () => import("./tr-TR/empty-state"),
},
};
diff --git a/packages/propel/src/empty-state/assets/asset-registry.tsx b/packages/propel/src/empty-state/assets/asset-registry.tsx
index 42ac821e6..5ad23bb09 100644
--- a/packages/propel/src/empty-state/assets/asset-registry.tsx
+++ b/packages/propel/src/empty-state/assets/asset-registry.tsx
@@ -39,6 +39,7 @@ import {
DraftVerticalStackIllustration,
EpicVerticalStackIllustration,
Error404VerticalStackIllustration,
+ InitiativeVerticalStackIllustration,
InvalidLinkVerticalStackIllustration,
ModuleVerticalStackIllustration,
NoAccessVerticalStackIllustration,
@@ -85,6 +86,7 @@ export const VERTICAL_STACK_ASSETS: Record,
title: "Error404VerticalStackIllustration",
},
+ {
+ asset: ,
+ title: "InitiativeVerticalStackIllustration",
+ },
{
asset: ,
title: "InvalidLinkVerticalStackIllustration",
diff --git a/packages/propel/src/empty-state/compact-empty-state.tsx b/packages/propel/src/empty-state/compact-empty-state.tsx
index 33f135d97..5a4523d1f 100644
--- a/packages/propel/src/empty-state/compact-empty-state.tsx
+++ b/packages/propel/src/empty-state/compact-empty-state.tsx
@@ -15,6 +15,7 @@ export const EmptyStateCompact: React.FC = ({
rootClassName,
assetClassName,
align = "center",
+ customButton,
}) => {
// Determine which asset to use: assetKey takes precedence, fallback to custom asset
const resolvedAsset = assetKey ? getCompactAsset(assetKey as CompactAssetType, assetClassName) : asset;
@@ -39,18 +40,21 @@ export const EmptyStateCompact: React.FC = ({
title && {title}
)}
- {actions && actions.length > 0 && (
-
- {actions.map((action, index) => {
- const { label, variant, ...rest } = action;
- return (
-
- );
- })}
-
- )}
+ {customButton
+ ? customButton
+ : actions &&
+ actions.length > 0 && (
+
+ {actions.map((action, index) => {
+ const { label, variant, ...rest } = action;
+ return (
+
+ );
+ })}
+
+ )}
diff --git a/packages/propel/src/empty-state/detailed-empty-state.tsx b/packages/propel/src/empty-state/detailed-empty-state.tsx
index b6fd4af0c..9680aea71 100644
--- a/packages/propel/src/empty-state/detailed-empty-state.tsx
+++ b/packages/propel/src/empty-state/detailed-empty-state.tsx
@@ -14,6 +14,7 @@ export const EmptyStateDetailed: React.FC = ({
className,
rootClassName,
assetClassName,
+ customButton,
}) => {
// Determine which asset to use: assetKey takes precedence, fallback to custom asset
const resolvedAsset = assetKey ? getDetailedAsset(assetKey as DetailedAssetType, assetClassName) : asset;
@@ -31,18 +32,21 @@ export const EmptyStateDetailed: React.FC = ({
)}
- {actions && actions.length > 0 && (
-
- {actions.map((action, index) => {
- const { label, variant, ...rest } = action;
- return (
-
- );
- })}
-
- )}
+ {customButton
+ ? customButton
+ : actions &&
+ actions.length > 0 && (
+
+ {actions.map((action, index) => {
+ const { label, variant, ...rest } = action;
+ return (
+
+ );
+ })}
+
+ )}
diff --git a/packages/propel/src/empty-state/types.ts b/packages/propel/src/empty-state/types.ts
index 8c2886799..ae161bdb0 100644
--- a/packages/propel/src/empty-state/types.ts
+++ b/packages/propel/src/empty-state/types.ts
@@ -21,4 +21,5 @@ export interface BaseEmptyStateCommonProps {
assetKey?: CompactAssetType | DetailedAssetType;
asset?: React.ReactNode;
align?: TAlign;
+ customButton?: React.ReactNode;
}