Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/app/api/workspace/app-display-names/route.ts
Original file line number Diff line number Diff line change
@@ -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<NextResponse<APIResponse>> => {
const user = authenticateHeaders(req.headers)

const assembly = new AssemblyClient(z.string().parse(user.token))
const displayNames = await assembly.getAppDisplayNames()
return NextResponse.json({ data: displayNames })
})
3 changes: 3 additions & 0 deletions src/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -53,13 +54,15 @@ export const authorizedRoutes: Record<string, RouteRule[]> = {
ROUTES.api.segment,
ROUTES.api.segmentConfig,
ROUTES.api.segmentStats,
ROUTES.api.appDisplayNames,
],
clientUsers: [
ROUTES.api.workspace,
ROUTES.api.clientContext,
ROUTES.client,
ROUTES.api.listCustomFields,
ROUTES.api.tasksAppId,
ROUTES.api.appDisplayNames,
{
path: ROUTES.api.settings,
methods: ['GET'],
Expand Down
3 changes: 3 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.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(),
Expand Down
8 changes: 6 additions & 2 deletions src/features/action-items/components/action-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
/>
<h3 className="text-heading-md dark-bg:text-white">{action.label}</h3>
<h3 className="text-heading-md dark-bg:text-white">{displayLabel}</h3>
</div>
<Icon
icon="ArrowRight"
Expand All @@ -78,7 +82,7 @@ export const ActionItem = ({ action, isLoading, mode, className, count }: Action
fallbackValue={count ?? 0}
/>
{mode === ViewMode.PREVIEW
? ` ${count === 1 ? action.singularLabel?.toLocaleLowerCase() : action.label.toLocaleLowerCase()}`
? ` ${count === 1 ? displaySingularLabel?.toLocaleLowerCase() : displayLabel.toLocaleLowerCase()}`
: null}
</div>
</button>
Expand Down
6 changes: 5 additions & 1 deletion src/features/action-items/components/actions-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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

Expand Down
20 changes: 20 additions & 0 deletions src/features/action-items/hooks/useAppDisplayNames.ts
Original file line number Diff line number Diff line change
@@ -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<Record<string, string>> => {
const res = await api.get<{ data: Record<string, string> }>('/api/workspace/app-display-names')
setAppDisplayNames(res.data.data)
return res.data.data
},
refetchInterval: 0,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
})
}
3 changes: 3 additions & 0 deletions src/features/editor/components/ClientEditorWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -36,6 +37,8 @@ export const ClientEditorWrapper = () => {
refetchOnWindowFocus: false,
})

useAppDisplayNames()

const isDark = isDarkColor(backgroundColor)

return (
Expand Down
2 changes: 2 additions & 0 deletions src/features/editor/components/EditorWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -43,6 +44,7 @@ export function EditorWrapper({ className }: EditorWrapperProps) {

useSegmentSettings()
useAppControls()
useAppDisplayNames()

return (
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { ActionDefinitions } from '@editor/components/Sidebar/Actions/constant'
import { useViewStore } from '@editor/stores/viewStore'
import { useSettingsStore } from '@settings/providers/settings.provider'

export const useActions = () => {
const actions = useSettingsStore((s) => s.actions)
const setActions = useSettingsStore((s) => s.setActions)
const appDisplayNames = useViewStore((s) => s.appDisplayNames)

const order = actions?.order ?? []

Expand All @@ -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: () => {
Expand Down
4 changes: 4 additions & 0 deletions src/features/editor/stores/viewStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface ViewStoreState {
displayMode: DisplayMode
workspace: WorkspaceResponse | null
tasksAppId: string | null
appDisplayNames: Record<string, string>
activeSegmentId: string | null
}

Expand All @@ -24,6 +25,7 @@ interface ViewStoreAction {
changeView: (data: Partial<ViewStoreState>) => void
setWorkspace: (workspace: WorkspaceResponse) => void
setTasksAppId: (id: string) => void
setAppDisplayNames: (names: Record<string, string>) => void
setActiveSegmentId: (segmentId: string | null) => void
}

Expand All @@ -32,6 +34,7 @@ const defaultState = {
displayMode: DisplayMode.DESKTOP,
workspace: null,
tasksAppId: null,
appDisplayNames: {},
activeSegmentId: null,
} as const satisfies Partial<ViewStoreState>

Expand All @@ -42,6 +45,7 @@ export const useViewStore = create<ViewStore>()((set) => ({
changeView: (data: Partial<ViewStoreState>) => set(data),
setWorkspace: (workspace: WorkspaceResponse) => set({ workspace }),
setTasksAppId: (tasksAppId: string | null) => set({ tasksAppId }),
setAppDisplayNames: (appDisplayNames: Record<string, string>) => set({ appDisplayNames }),
setActiveSegmentId: (activeSegmentId: string | null) => set({ activeSegmentId }),
reset: () => set(defaultState),
}))
Expand Down
23 changes: 23 additions & 0 deletions src/lib/assembly/assembly-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,28 @@ export default class AssemblyClient {
return installedApps.find((app) => app.appId === appDeploymentId)?.id || null
}

async _getAppDisplayNames(): Promise<Record<string, string>> {
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,
}
Comment on lines +89 to +94
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arpandhakal Something to do later, we could store them as config const somwhere else

Copy link
Collaborator Author

@arpandhakal arpandhakal Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the appIds are sensitive data. We should not expose them. Thats why I kept them in .env.


const displayNames: Record<string, string> = {}
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<ClientResponse> {
logger.info('AssemblyClient#_createClient', requestBody, sendInvite)
const assembly = await this.assemblyPromise
Expand Down Expand Up @@ -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)
}
Loading