fix(applications): display the installed application icon (#23411)

## Problem

After installing an app, its icon is missing across the UI, while
application *registration* icons render fine.

`Application.logo` holds the manifest path (`public/logo.svg`), which is
package-relative and not displayable. The server exposes a `logoUrl`
resolve field that turns it into
`/public-assets/{workspaceId}/{applicationId}/{logo}`, but on the front
end:

- `APPLICATION_FRAGMENT` and `FIND_MANY_APPLICATIONS` never selected
`Application.logoUrl`.
- So the only source of a usable logo url was
`currentWorkspace.installedApplications`, which is fetched by
`GetCurrentUser` at bootstrap. Nothing refreshed it after
`installApplication`, so a freshly installed app was absent from that
list.
- `useApplicationChipData` then fell through to
`fallbackApplicationData`, which callers populated with the raw `logo`
path. `getAbsoluteImageUrl('public/logo.svg')` yields
`{serverUrl}/public/logo.svg`, which 404s, so the avatar rendered as a
letter placeholder.

## Before / After

An app installed while the applications page is open, so the workspace
snapshot loaded at bootstrap does not know about it yet:

| Before | After |
|---|---|
| <img
src="https://raw.githubusercontent.com/twentyhq/twenty/e27a817fd95c5e44c1fbb64fd4fd68525daeee61/.pr-assets/app-icon-before.png"
width="480"> | <img
src="https://raw.githubusercontent.com/twentyhq/twenty/e27a817fd95c5e44c1fbb64fd4fd68525daeee61/.pr-assets/app-icon-after.png"
width="480"> |

## Changes

- Select `logoUrl` on `Application` in `APPLICATION_FRAGMENT` and
`FIND_MANY_APPLICATIONS`.
- Drop `logo` from `ApplicationDisplayData` and from the `AppChip` /
subtable fallback props, so a package-relative path can no longer reach
an `img` src. Call sites that already passed a url under `logo` now pass
`logoUrl`.
- `SettingsApplicationDetails` and `SettingsApplicationsTable` pass the
application's own `logoUrl`.
- On install, add the returned application to
`currentWorkspace.installedApplications` instead of reloading the
current user, so the chips that resolve by `applicationId` only (nav
menu items, object/field tables, tool rows, workflow nodes) pick it up.
- Stop exposing `logo` on the `Application` GraphQL type: nothing
selects it anymore, and having both `logo` (package-relative path) and
`logoUrl` (display url) was the source of the bug. The column is still
read server-side to build `logoUrl`.
- Regenerated `generated-metadata/graphql.ts`.

## Verification

Ran the stack locally against a seeded workspace with an installed app
whose logo lives at `public/logo.png`:

- `findManyApplications` returns a `logoUrl` under `/public-assets/...`,
and that url serves `200 image/png`.
- Reproduced the bug and the fix in the browser with the scenario shown
above (screenshots taken on the base commit and on this branch).
- `npx nx typecheck twenty-front`, `npx nx typecheck twenty-server`,
`npx nx lint:diff-with-main` on both, and the application settings jest
suites pass.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01N8r4z2dZ553nAnCNe7GxMH)_

[Review in
cubic](https://cubic.dev/pr/twentyhq/twenty/pull/23411?utm_source=github)
This commit is contained in:
martmull
2026-07-28 16:59:13 +02:00
committed by GitHub
parent 4f31265927
commit 942755d0dd
37 changed files with 89 additions and 77 deletions
@@ -12,7 +12,7 @@ type AppChipProps = {
// over the logo computed from the installed application.
logoUrl?: string | null;
fallbackApplicationData?: {
logo?: string | null;
logoUrl?: string | null;
name?: string | null;
};
className?: string;
@@ -22,7 +22,6 @@ export const ApplicationDisplay = ({
applicationId={application?.id}
logoUrl={application?.logoUrl}
fallbackApplicationData={{
logo: application?.logo,
name: application?.name,
}}
/>
@@ -11,7 +11,7 @@ export const APPLICATION_FRAGMENT = gql`
id
name
description
logo
logoUrl
version
universalIdentifier
applicationRegistrationId
@@ -6,7 +6,7 @@ export const FIND_MANY_APPLICATIONS = gql`
id
name
description
logo
logoUrl
version
universalIdentifier
applicationRegistrationId
@@ -14,7 +14,7 @@ import StandardLogo from '~/pages/settings/applications/assets/standard-illustra
type UseApplicationChipDataArgs = {
applicationId?: string | null;
fallbackApplicationData?: {
logo?: string | null;
logoUrl?: string | null;
name?: string | null;
};
};
@@ -46,7 +46,7 @@ export const useApplicationChipData = ({
return {
applicationChipData: {
name: fallbackApplicationData?.name ?? '',
logo: fallbackApplicationData?.logo ?? '',
logo: fallbackApplicationData?.logoUrl ?? '',
seed: fallbackApplicationData?.name ?? '',
},
};
@@ -2,8 +2,5 @@ export type ApplicationDisplayData = {
id?: string | null;
name?: string | null;
universalIdentifier?: string | null;
logo?: string | null;
// Resolved display url (the registration's logoUrl resolve field); takes
// precedence over the logo computed from the installed application.
logoUrl?: string | null;
};
@@ -48,7 +48,7 @@ export type CurrentWorkspace = Pick<
workspaceCustomApplication: Pick<Application, 'id'> | null;
installedApplications: Pick<
Application,
'id' | 'name' | 'universalIdentifier' | 'logo' | 'logoUrl'
'id' | 'name' | 'universalIdentifier' | 'logoUrl'
>[];
};
@@ -13,7 +13,7 @@ export const MARKETPLACE_APP_DETAIL_FRAGMENT = gql`
description
author
category
logo
logoUrl
websiteUrl
aboutDescription
termsUrl
@@ -7,7 +7,7 @@ export const MARKETPLACE_APP_FRAGMENT = gql`
description
author
category
logo
logoUrl
sourcePackage
isVetted
}
@@ -4,6 +4,9 @@ export const INSTALL_APPLICATION = gql`
mutation InstallApplication($universalIdentifier: String!) {
installApplication(universalIdentifier: $universalIdentifier) {
id
name
universalIdentifier
logoUrl
}
}
`;
@@ -1,4 +1,6 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { useMutation } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
@@ -12,6 +14,7 @@ export const useInstallMarketplaceApp = () => {
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
const [isInstalling, setIsInstalling] = useState(false);
const [installApplicationMutation] = useMutation(InstallApplicationDocument);
const setCurrentWorkspace = useSetAtomState(currentWorkspaceState);
const install = async (variables: {
universalIdentifier: string;
@@ -23,6 +26,24 @@ export const useInstallMarketplaceApp = () => {
const result = await installApplicationMutation({ variables });
if (isDefined(result.data)) {
const installedApplication = result.data.installApplication;
// the workspace carries the applications app chips resolve their logo
// from, so the freshly installed one has to be added to it
setCurrentWorkspace((currentWorkspace) =>
isDefined(currentWorkspace)
? {
...currentWorkspace,
installedApplications: [
...currentWorkspace.installedApplications.filter(
(application) => application.id !== installedApplication.id,
),
installedApplication,
],
}
: currentWorkspace,
);
enqueueSuccessSnackBar({
message: t`Application installed successfully.`,
});
@@ -269,7 +269,7 @@ const SettingsAdminAppsTableRow = ({
<ApplicationDisplay
application={{
name: registration.name,
logo: registration.logoUrl,
logoUrl: registration.logoUrl,
}}
/>
</StyledNameTableCell>
@@ -44,7 +44,7 @@ export const getInstalledApplicationObjectAndFieldRows = ({
}),
application: {
id: installedApplication.id,
logo: installedApplication.logo,
logoUrl: installedApplication.logoUrl,
name: installedApplication.name,
universalIdentifier: installedApplication.universalIdentifier,
},
@@ -70,7 +70,6 @@ export const USER_QUERY_FRAGMENT = gql`
id
name
universalIdentifier
logo
logoUrl
}
isCustomDomainEnabled