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:
@@ -581,7 +581,6 @@ type Application {
|
||||
id: UUID!
|
||||
name: String!
|
||||
description: String
|
||||
logo: String
|
||||
logoFileId: UUID
|
||||
version: String
|
||||
universalIdentifier: String!
|
||||
@@ -2086,7 +2085,7 @@ type MarketplaceApp {
|
||||
description: String!
|
||||
author: String!
|
||||
category: String!
|
||||
logo: String
|
||||
logoUrl: String
|
||||
sourcePackage: String
|
||||
isVetted: Boolean!
|
||||
}
|
||||
@@ -2136,7 +2135,7 @@ type MarketplaceAppDetail {
|
||||
description: String
|
||||
author: String
|
||||
category: String
|
||||
logo: String
|
||||
logoUrl: String
|
||||
websiteUrl: String
|
||||
aboutDescription: String
|
||||
termsUrl: String
|
||||
|
||||
@@ -392,7 +392,6 @@ export interface Application {
|
||||
id: Scalars['UUID']
|
||||
name: Scalars['String']
|
||||
description?: Scalars['String']
|
||||
logo?: Scalars['String']
|
||||
logoFileId?: Scalars['UUID']
|
||||
version?: Scalars['String']
|
||||
universalIdentifier: Scalars['String']
|
||||
@@ -1752,7 +1751,7 @@ export interface MarketplaceApp {
|
||||
description: Scalars['String']
|
||||
author: Scalars['String']
|
||||
category: Scalars['String']
|
||||
logo?: Scalars['String']
|
||||
logoUrl?: Scalars['String']
|
||||
sourcePackage?: Scalars['String']
|
||||
isVetted: Scalars['Boolean']
|
||||
__typename: 'MarketplaceApp'
|
||||
@@ -1806,7 +1805,7 @@ export interface MarketplaceAppDetail {
|
||||
description?: Scalars['String']
|
||||
author?: Scalars['String']
|
||||
category?: Scalars['String']
|
||||
logo?: Scalars['String']
|
||||
logoUrl?: Scalars['String']
|
||||
websiteUrl?: Scalars['String']
|
||||
aboutDescription?: Scalars['String']
|
||||
termsUrl?: Scalars['String']
|
||||
@@ -3539,7 +3538,6 @@ export interface ApplicationGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
logo?: boolean | number
|
||||
logoFileId?: boolean | number
|
||||
version?: boolean | number
|
||||
universalIdentifier?: boolean | number
|
||||
@@ -4948,7 +4946,7 @@ export interface MarketplaceAppGenqlSelection{
|
||||
description?: boolean | number
|
||||
author?: boolean | number
|
||||
category?: boolean | number
|
||||
logo?: boolean | number
|
||||
logoUrl?: boolean | number
|
||||
sourcePackage?: boolean | number
|
||||
isVetted?: boolean | number
|
||||
__typename?: boolean | number
|
||||
@@ -5006,7 +5004,7 @@ export interface MarketplaceAppDetailGenqlSelection{
|
||||
description?: boolean | number
|
||||
author?: boolean | number
|
||||
category?: boolean | number
|
||||
logo?: boolean | number
|
||||
logoUrl?: boolean | number
|
||||
websiteUrl?: boolean | number
|
||||
aboutDescription?: boolean | number
|
||||
termsUrl?: boolean | number
|
||||
|
||||
@@ -1136,9 +1136,6 @@ export default {
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"logo": [
|
||||
1
|
||||
],
|
||||
"logoFileId": [
|
||||
4
|
||||
],
|
||||
@@ -4116,7 +4113,7 @@ export default {
|
||||
"category": [
|
||||
1
|
||||
],
|
||||
"logo": [
|
||||
"logoUrl": [
|
||||
1
|
||||
],
|
||||
"sourcePackage": [
|
||||
@@ -4250,7 +4247,7 @@ export default {
|
||||
"category": [
|
||||
1
|
||||
],
|
||||
"logo": [
|
||||
"logoUrl": [
|
||||
1
|
||||
],
|
||||
"websiteUrl": [
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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,
|
||||
}}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ export const APPLICATION_FRAGMENT = gql`
|
||||
id
|
||||
name
|
||||
description
|
||||
logo
|
||||
logoUrl
|
||||
version
|
||||
universalIdentifier
|
||||
applicationRegistrationId
|
||||
|
||||
+1
-1
@@ -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'
|
||||
>[];
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ export const MARKETPLACE_APP_DETAIL_FRAGMENT = gql`
|
||||
description
|
||||
author
|
||||
category
|
||||
logo
|
||||
logoUrl
|
||||
websiteUrl
|
||||
aboutDescription
|
||||
termsUrl
|
||||
|
||||
+1
-1
@@ -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.`,
|
||||
});
|
||||
|
||||
+1
-1
@@ -269,7 +269,7 @@ const SettingsAdminAppsTableRow = ({
|
||||
<ApplicationDisplay
|
||||
application={{
|
||||
name: registration.name,
|
||||
logo: registration.logoUrl,
|
||||
logoUrl: registration.logoUrl,
|
||||
}}
|
||||
/>
|
||||
</StyledNameTableCell>
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -37,7 +37,7 @@ export const InstallApps = () => {
|
||||
);
|
||||
|
||||
return isDefined(marketplaceApp)
|
||||
? [{ ...app, logo: marketplaceApp.logo ?? null }]
|
||||
? [{ ...app, logoUrl: marketplaceApp.logoUrl ?? null }]
|
||||
: [];
|
||||
});
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ const StyledInstallButton = styled.div`
|
||||
`;
|
||||
|
||||
type InstallAppsContentProps = {
|
||||
apps: (OnboardingInstallableApp & { logo: string | null })[];
|
||||
apps: (OnboardingInstallableApp & { logoUrl: string | null })[];
|
||||
selectedUniversalIdentifiers: string[];
|
||||
creditsRewardPerApp?: number;
|
||||
isCompleting: boolean;
|
||||
@@ -160,7 +160,7 @@ export const InstallAppsContent = ({
|
||||
return (
|
||||
<StyledAppRow key={app.universalIdentifier}>
|
||||
<Avatar
|
||||
avatarUrl={getAbsoluteImageUrl(app.logo)}
|
||||
avatarUrl={getAbsoluteImageUrl(app.logoUrl)}
|
||||
placeholder={labelText}
|
||||
placeholderColorSeed={app.universalIdentifier}
|
||||
size="lg"
|
||||
|
||||
+2
-2
@@ -321,7 +321,7 @@ export const SettingsApplicationDetails = () => {
|
||||
applicationInfo={{
|
||||
id: application.id,
|
||||
name: displayName,
|
||||
logo: application.logo,
|
||||
logoUrl: application.logoUrl,
|
||||
universalIdentifier: application.universalIdentifier,
|
||||
}}
|
||||
/>
|
||||
@@ -349,8 +349,8 @@ export const SettingsApplicationDetails = () => {
|
||||
isDefined(application) ? (
|
||||
<AppChip
|
||||
applicationId={application.id}
|
||||
logoUrl={application.logoUrl}
|
||||
fallbackApplicationData={{
|
||||
logo: application.logo,
|
||||
name: displayName,
|
||||
}}
|
||||
size="md"
|
||||
|
||||
+3
-3
@@ -255,7 +255,7 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
manifestContent={manifest}
|
||||
applicationInfo={{
|
||||
name: displayName,
|
||||
logo: detail.logo,
|
||||
logoUrl: detail.logoUrl,
|
||||
universalIdentifier: detail.universalIdentifier,
|
||||
}}
|
||||
/>
|
||||
@@ -295,7 +295,7 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
icon={
|
||||
<AppChip
|
||||
applicationId={application?.id}
|
||||
logoUrl={detail?.logo}
|
||||
logoUrl={detail?.logoUrl}
|
||||
fallbackApplicationData={{
|
||||
name: displayName,
|
||||
}}
|
||||
@@ -321,7 +321,7 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
<SettingsApplicationInstallPermissionValidationModal
|
||||
modalInstanceId={modalInstanceId}
|
||||
appDisplayName={displayName}
|
||||
appLogoUrl={detail?.logo ?? undefined}
|
||||
appLogoUrl={detail?.logoUrl ?? undefined}
|
||||
defaultRole={defaultRole}
|
||||
onAuthorize={handleInstall}
|
||||
isInstalling={isInstalling}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ export const SettingsApplicationContentSubtable = ({
|
||||
rows: ApplicationContentRow[];
|
||||
applicationId?: string;
|
||||
fallbackApplicationData?: {
|
||||
logo?: string | null;
|
||||
logoUrl?: string | null;
|
||||
name?: string | null;
|
||||
};
|
||||
}) => {
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ export const SettingsApplicationRegistrationShareLinkButtons = ({
|
||||
<SettingsApplicationInstallPermissionValidationModal
|
||||
modalInstanceId={modalInstanceId}
|
||||
appDisplayName={displayName}
|
||||
appLogoUrl={detail?.logo ?? undefined}
|
||||
appLogoUrl={detail?.logoUrl ?? undefined}
|
||||
defaultRole={defaultRole}
|
||||
onAuthorize={handleInstall}
|
||||
isInstalling={isInstalling}
|
||||
|
||||
+1
-4
@@ -101,10 +101,7 @@ export const SettingsApplicationsTable = ({
|
||||
return (
|
||||
<SettingsApplicationTableRow
|
||||
key={application.id}
|
||||
application={{
|
||||
...application,
|
||||
logoUrl: application.applicationRegistration?.logoUrl,
|
||||
}}
|
||||
application={application}
|
||||
hasUpdate={hasUpdate}
|
||||
sourceType={application.applicationRegistration?.sourceType}
|
||||
action={
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ export const SettingsAvailableApplicationCard = ({
|
||||
<Card rounded fullWidth>
|
||||
<StyledSettingsCardContent alignItems="flex-start" fullHeight>
|
||||
<Avatar
|
||||
avatarUrl={getAbsoluteImageUrl(application.logo || null)}
|
||||
avatarUrl={getAbsoluteImageUrl(application.logoUrl || null)}
|
||||
placeholder={application.name}
|
||||
placeholderColorSeed={application.name}
|
||||
size="lg"
|
||||
|
||||
+3
-1
@@ -210,7 +210,9 @@ export const SettingsClaimApplicationSection = () => {
|
||||
availableApplicationId: app.universalIdentifier,
|
||||
})}
|
||||
>
|
||||
<ApplicationDisplay application={{ name: app.name, logo: app.logoUrl }} />
|
||||
<ApplicationDisplay
|
||||
application={{ name: app.name, logoUrl: app.logoUrl }}
|
||||
/>
|
||||
</StyledResultTitleLink>
|
||||
);
|
||||
|
||||
|
||||
-1
@@ -31,7 +31,6 @@ const buildApplication = (variableValue: string): Application => ({
|
||||
id: APP_ID,
|
||||
name: 'Test App',
|
||||
description: null,
|
||||
logo: null,
|
||||
version: '1.0.0',
|
||||
universalIdentifier: 'test-app',
|
||||
applicationRegistrationId: null,
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ export const SettingsApplicationDetailContentTab = ({
|
||||
});
|
||||
|
||||
const fallbackApplicationData = {
|
||||
logo: applicationInfo?.logo,
|
||||
logoUrl: applicationInfo?.logoUrl,
|
||||
name: applicationInfo?.name,
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ export type ApplicationWithoutRelation = Pick<
|
||||
| 'id'
|
||||
| 'name'
|
||||
| 'description'
|
||||
| 'logoUrl'
|
||||
| 'version'
|
||||
| 'universalIdentifier'
|
||||
| 'applicationRegistrationId'
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ export class MarketplaceAppDetailDTO {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
logo?: string;
|
||||
logoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ export class MarketplaceAppDTO {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
logo?: string;
|
||||
logoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ export class MarketplaceCatalogCacheProviderService extends CoreEntityCacheProvi
|
||||
description: catalogCard.description ?? '',
|
||||
author: catalogCard.author ?? 'Unknown',
|
||||
category: catalogCard.category ?? '',
|
||||
logo: catalogCard.logoUrl ?? undefined,
|
||||
logoUrl: catalogCard.logoUrl ?? undefined,
|
||||
sourcePackage: catalogCard.sourcePackage ?? undefined,
|
||||
isVetted: catalogCard.isVetted,
|
||||
};
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@ export class MarketplaceQueryService {
|
||||
registration.category ??
|
||||
registration.manifest?.application?.category ??
|
||||
undefined,
|
||||
logo:
|
||||
logoUrl:
|
||||
this.applicationRegistrationAssetUrlService.buildLogoUrl(
|
||||
registration,
|
||||
) ?? undefined,
|
||||
|
||||
@@ -35,9 +35,10 @@ export class ApplicationDTO {
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
// Package-relative path of the logo bundled in the application, not
|
||||
// displayable on its own: exposed to clients through the logoUrl field
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
logo?: string;
|
||||
|
||||
@IsOptional()
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ export const APPLICATION_GQL_FIELDS = `
|
||||
id
|
||||
name
|
||||
description
|
||||
logo
|
||||
logoUrl
|
||||
version
|
||||
universalIdentifier
|
||||
canBeUninstalled
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ const MARKETPLACE_QUERY = `
|
||||
author
|
||||
sourcePackage
|
||||
category
|
||||
logo
|
||||
logoUrl
|
||||
isVetted
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user