From ceefcc96b0d0e3d946059ac7d8e7b25d31f8823f Mon Sep 17 00:00:00 2001 From: arpandhakal Date: Fri, 20 Mar 2026 16:38:38 +0545 Subject: [PATCH 1/3] fix(OUT-3392): use app display names from API instead of hardcoded labels Fetch display names from Assembly's listAppInstalls endpoint and use them on action cards, with fallback to hardcoded labels. App deployment IDs are stored as env vars (FORMS_APP_ID, CONTRACTS_APP_ID, INVOICES_APP_ID). Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 3 +++ .../api/workspace/app-display-names/route.ts | 14 +++++++++++ src/app/routes.ts | 3 +++ src/config/env.ts | 3 +++ .../action-items/components/action-item.tsx | 8 +++++-- .../action-items/hooks/useAppDisplayNames.ts | 20 ++++++++++++++++ .../editor/components/ClientEditorWrapper.tsx | 3 +++ .../editor/components/EditorWrapper.tsx | 2 ++ .../components/Sidebar/Actions/useActions.tsx | 4 +++- src/features/editor/stores/viewStore.ts | 4 ++++ src/lib/assembly/assembly-client.ts | 23 +++++++++++++++++++ 11 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 src/app/api/workspace/app-display-names/route.ts create mode 100644 src/features/action-items/hooks/useAppDisplayNames.ts diff --git a/.env.example b/.env.example index bce96e06..cb914ded 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,9 @@ ASSEMBLY_API_KEY= TASKS_ASSEMBLY_API_KEY= TASKS_APP_ID= # Remains unchanged for dev +FORMS_APP_ID= +CONTRACTS_APP_ID= +INVOICES_APP_ID= COPILOT_DEBUG=true BLOB_READ_WRITE_TOKEN= # Blob rw token for old client home's vercel blob store diff --git a/src/app/api/workspace/app-display-names/route.ts b/src/app/api/workspace/app-display-names/route.ts new file mode 100644 index 00000000..08958b52 --- /dev/null +++ b/src/app/api/workspace/app-display-names/route.ts @@ -0,0 +1,14 @@ +import AssemblyClient from '@assembly/assembly-client' +import { authenticateHeaders } from '@auth/lib/authenticate' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import type { APIResponse } from '@/app/types' +import { withErrorHandler } from '@/lib/with-error-handler' + +export const GET = withErrorHandler(async (req: NextRequest): Promise> => { + const user = authenticateHeaders(req.headers) + + const assembly = new AssemblyClient(z.string().parse(user.token)) + const displayNames = await assembly.getAppDisplayNames() + return NextResponse.json({ data: displayNames }) +}) diff --git a/src/app/routes.ts b/src/app/routes.ts index e0557b12..f01c49a8 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -6,6 +6,7 @@ export const ROUTES = Object.freeze({ settings: '/api/settings', workspace: '/api/workspace', tasksAppId: '/api/workspace/tasks-app-id', + appDisplayNames: '/api/workspace/app-display-names', media: '/api/media', image: '/api/media/image', upload: '/api/media/upload', @@ -53,6 +54,7 @@ export const authorizedRoutes: Record = { ROUTES.api.segment, ROUTES.api.segmentConfig, ROUTES.api.segmentStats, + ROUTES.api.appDisplayNames, ], clientUsers: [ ROUTES.api.workspace, @@ -60,6 +62,7 @@ export const authorizedRoutes: Record = { ROUTES.client, ROUTES.api.listCustomFields, ROUTES.api.tasksAppId, + ROUTES.api.appDisplayNames, { path: ROUTES.api.settings, methods: ['GET'], diff --git a/src/config/env.ts b/src/config/env.ts index b6d73413..49026e23 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -7,6 +7,9 @@ const EnvSchema = z.object({ TASKS_ASSEMBLY_API_KEY: z.string().min(1), TASKS_APP_ID: z.uuid(), + FORMS_APP_ID: z.uuid(), + CONTRACTS_APP_ID: z.uuid(), + INVOICES_APP_ID: z.uuid(), VERCEL_ENV: z.enum(['production', 'preview', 'development']).optional(), // NOTE: We can add other custom environments here VERCEL_URL: z.url(), diff --git a/src/features/action-items/components/action-item.tsx b/src/features/action-items/components/action-item.tsx index 3f2ad8e9..85174b73 100644 --- a/src/features/action-items/components/action-item.tsx +++ b/src/features/action-items/components/action-item.tsx @@ -17,6 +17,10 @@ interface ActionItemProps { export const ActionItem = ({ action, isLoading, mode, className, count }: ActionItemProps) => { const clientId = useAuthStore((s) => s.clientId) const tasksAppId = useViewStore((s) => s.tasksAppId) + const appDisplayNames = useViewStore((s) => s.appDisplayNames) + + const displayLabel = appDisplayNames[action.key] ?? action.label + const displaySingularLabel = appDisplayNames[action.key] ?? action.singularLabel const handleClick = () => { if (!clientId) return @@ -60,7 +64,7 @@ export const ActionItem = ({ action, isLoading, mode, className, count }: Action icon={action.icon} className="size-4 transition-transform duration-300 group-hover:scale-110 dark-bg:text-white" /> -

{action.label}

+

{displayLabel}

{mode === ViewMode.PREVIEW - ? ` ${count === 1 ? action.singularLabel?.toLocaleLowerCase() : action.label.toLocaleLowerCase()}` + ? ` ${count === 1 ? displaySingularLabel?.toLocaleLowerCase() : displayLabel.toLocaleLowerCase()}` : null} diff --git a/src/features/action-items/hooks/useAppDisplayNames.ts b/src/features/action-items/hooks/useAppDisplayNames.ts new file mode 100644 index 00000000..329e1b14 --- /dev/null +++ b/src/features/action-items/hooks/useAppDisplayNames.ts @@ -0,0 +1,20 @@ +import { useViewStore } from '@editor/stores/viewStore' +import { useQuery } from '@tanstack/react-query' +import { api } from '@/lib/core/axios.instance' + +export const useAppDisplayNames = () => { + const setAppDisplayNames = useViewStore((store) => store.setAppDisplayNames) + + return useQuery({ + queryKey: ['app-display-names'], + queryFn: async (): Promise> => { + const res = await api.get<{ data: Record }>('/api/workspace/app-display-names') + setAppDisplayNames(res.data.data) + return res.data.data + }, + refetchInterval: 0, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + }) +} diff --git a/src/features/editor/components/ClientEditorWrapper.tsx b/src/features/editor/components/ClientEditorWrapper.tsx index 45034a83..0031adb9 100644 --- a/src/features/editor/components/ClientEditorWrapper.tsx +++ b/src/features/editor/components/ClientEditorWrapper.tsx @@ -5,6 +5,7 @@ import { useViewStore } from '@editor/stores/viewStore' import { useSettingsStore } from '@settings/providers/settings.provider' import { useQuery } from '@tanstack/react-query' import { ActionsCard } from '@/features/action-items/components/actions-card' +import { useAppDisplayNames } from '@/features/action-items/hooks/useAppDisplayNames' import { Banner } from '@/features/banner' import { getImageUrl } from '@/features/banner/lib/utils' import { api } from '@/lib/core/axios.instance' @@ -36,6 +37,8 @@ export const ClientEditorWrapper = () => { refetchOnWindowFocus: false, }) + useAppDisplayNames() + const isDark = isDarkColor(backgroundColor) return ( diff --git a/src/features/editor/components/EditorWrapper.tsx b/src/features/editor/components/EditorWrapper.tsx index 1aa36e60..7cfc6165 100644 --- a/src/features/editor/components/EditorWrapper.tsx +++ b/src/features/editor/components/EditorWrapper.tsx @@ -10,6 +10,7 @@ import { useSegmentSettings } from '@settings/hooks/useSegmentSettings' import { useSettingsStore } from '@settings/providers/settings.provider' import { Activity } from 'react' import { ActionsCard } from '@/features/action-items/components/actions-card' +import { useAppDisplayNames } from '@/features/action-items/hooks/useAppDisplayNames' import { Banner } from '@/features/banner' import { getImageUrl } from '@/features/banner/lib/utils' import { useSidebarStore } from '@/features/editor/stores/sidebarStore' @@ -43,6 +44,7 @@ export function EditorWrapper({ className }: EditorWrapperProps) { useSegmentSettings() useAppControls() + useAppDisplayNames() return (
{ const actions = useSettingsStore((s) => s.actions) const setActions = useSettingsStore((s) => s.setActions) + const appDisplayNames = useViewStore((s) => s.appDisplayNames) const order = actions?.order ?? [] @@ -16,7 +18,7 @@ export const useActions = () => { .map((item) => { return { key: item.key, - label: item.label, + label: appDisplayNames[item.key] ?? item.label, icon: item.icon, checked: actions?.[item.key] ?? false, onChange: () => { diff --git a/src/features/editor/stores/viewStore.ts b/src/features/editor/stores/viewStore.ts index 75314f10..bc0f78c9 100644 --- a/src/features/editor/stores/viewStore.ts +++ b/src/features/editor/stores/viewStore.ts @@ -16,6 +16,7 @@ interface ViewStoreState { displayMode: DisplayMode workspace: WorkspaceResponse | null tasksAppId: string | null + appDisplayNames: Record activeSegmentId: string | null } @@ -24,6 +25,7 @@ interface ViewStoreAction { changeView: (data: Partial) => void setWorkspace: (workspace: WorkspaceResponse) => void setTasksAppId: (id: string) => void + setAppDisplayNames: (names: Record) => void setActiveSegmentId: (segmentId: string | null) => void } @@ -32,6 +34,7 @@ const defaultState = { displayMode: DisplayMode.DESKTOP, workspace: null, tasksAppId: null, + appDisplayNames: {}, activeSegmentId: null, } as const satisfies Partial @@ -42,6 +45,7 @@ export const useViewStore = create()((set) => ({ changeView: (data: Partial) => set(data), setWorkspace: (workspace: WorkspaceResponse) => set({ workspace }), setTasksAppId: (tasksAppId: string | null) => set({ tasksAppId }), + setAppDisplayNames: (appDisplayNames: Record) => set({ appDisplayNames }), setActiveSegmentId: (activeSegmentId: string | null) => set({ activeSegmentId }), reset: () => set(defaultState), })) diff --git a/src/lib/assembly/assembly-client.ts b/src/lib/assembly/assembly-client.ts index 1f7f2a76..fad64111 100644 --- a/src/lib/assembly/assembly-client.ts +++ b/src/lib/assembly/assembly-client.ts @@ -82,6 +82,28 @@ export default class AssemblyClient { return installedApps.find((app) => app.appId === appDeploymentId)?.id || null } + async _getAppDisplayNames(): Promise> { + const assembly = await this.assemblyPromise + const installedApps = AppInstallsResponseSchema.parse(await assembly.listAppInstalls()) + + const appDeploymentIds = { + forms: env.FORMS_APP_ID, + contracts: env.CONTRACTS_APP_ID, + invoices: env.INVOICES_APP_ID, + tasks: env.TASKS_APP_ID, + } + + const displayNames: Record = {} + for (const [key, deploymentId] of Object.entries(appDeploymentIds)) { + const app = installedApps.find((a) => a.appId === deploymentId) + if (app?.displayName) { + displayNames[key] = app.displayName + } + } + + return displayNames + } + async _createClient(requestBody: ClientCreateRequest, sendInvite: boolean = false): Promise { logger.info('AssemblyClient#_createClient', requestBody, sendInvite) const assembly = await this.assemblyPromise @@ -240,4 +262,5 @@ export default class AssemblyClient { listCustomFields = this.wrapWithRetry(this._listCustomFields) listCustomFieldOptions = this.wrapWithRetry(this._listCustomFieldOptions) getAppId = this.wrapWithRetry(this._getAppId) + getAppDisplayNames = this.wrapWithRetry(this._getAppDisplayNames) } From 972b39fc6597bee66e83b661ce235a7fca53e0a0 Mon Sep 17 00:00:00 2001 From: arpandhakal Date: Fri, 20 Mar 2026 16:46:16 +0545 Subject: [PATCH 2/3] fix: uuid parse error --- src/config/env.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/env.ts b/src/config/env.ts index 49026e23..cb4af83d 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -7,9 +7,9 @@ const EnvSchema = z.object({ TASKS_ASSEMBLY_API_KEY: z.string().min(1), TASKS_APP_ID: z.uuid(), - FORMS_APP_ID: z.uuid(), - CONTRACTS_APP_ID: z.uuid(), - INVOICES_APP_ID: z.uuid(), + FORMS_APP_ID: z.string().min(1), + CONTRACTS_APP_ID: z.string().min(1), + INVOICES_APP_ID: z.string().min(1), VERCEL_ENV: z.enum(['production', 'preview', 'development']).optional(), // NOTE: We can add other custom environments here VERCEL_URL: z.url(), From 1e605f22caf0e4d1101f9a5dd7185eedd8a4581d Mon Sep 17 00:00:00 2001 From: arpandhakal Date: Mon, 23 Mar 2026 16:19:36 +0545 Subject: [PATCH 3/3] fix: applied loading for display app names --- src/features/action-items/components/actions-card.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/features/action-items/components/actions-card.tsx b/src/features/action-items/components/actions-card.tsx index c6034587..87b6d5a0 100644 --- a/src/features/action-items/components/actions-card.tsx +++ b/src/features/action-items/components/actions-card.tsx @@ -2,6 +2,7 @@ import { useViewStore, ViewMode } from '@editor/stores/viewStore' import { useNotificationCounts } from '@notification-counts/hooks/useNotificationCounts' import type { NotificationCountsDto } from '@notification-counts/notification-counts.dto' import { useEnabledActions } from '@settings/hooks/useEnabledActions' +import { useAppDisplayNames } from '@/features/action-items/hooks/useAppDisplayNames' import { HandleBarTemplate } from '@/features/handlebar-template/components/handle-bar-template' import { cn } from '@/utils/tailwind' import { ActionItem } from './action-item' @@ -14,7 +15,10 @@ export const ActionsCard = ({ readonly }: ActionCardProps) => { const { enabledActions } = useEnabledActions() const viewMode = useViewStore((store) => store.viewMode) const workspace = useViewStore((store) => store.workspace) - const { counts, isLoading } = useNotificationCounts() + const { counts, isLoading: isCountsLoading } = useNotificationCounts() + const { isLoading: isDisplayNamesLoading } = useAppDisplayNames() + + const isLoading = isCountsLoading || isDisplayNamesLoading const isPreviewMode = readonly || viewMode === ViewMode.PREVIEW