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; }