Fix application icons (#20142)

fixes application chip (icon Name) in all setting tables

## After
<img width="1200" height="896" alt="image"
src="https://github.com/user-attachments/assets/bd377f47-1d52-4142-b904-f2ce90c1db78"
/>
<img width="1200" height="917" alt="image"
src="https://github.com/user-attachments/assets/f49cc742-f11e-47e3-86ed-34beffe493c7"
/>
<img width="1234" height="878" alt="image"
src="https://github.com/user-attachments/assets/2ab459de-5f9d-4d39-9490-eec4ed9ee432"
/>
<img width="1239" height="845" alt="image"
src="https://github.com/user-attachments/assets/3c1bf258-285a-47b9-a60d-05ba1564334d"
/>
<img width="1183" height="907" alt="image"
src="https://github.com/user-attachments/assets/715b2470-2d88-48e3-88ac-d3daf3451717"
/>
<img width="1300" height="912" alt="image"
src="https://github.com/user-attachments/assets/d7c829fa-bf1d-4f19-82de-a8bf29e22bfa"
/>
This commit is contained in:
martmull
2026-04-30 11:32:04 +02:00
committed by GitHub
parent 842e679cc6
commit bddd23fd9c
59 changed files with 772 additions and 205 deletions
@@ -1,4 +1,4 @@
import { ApplicationDisplay } from '@/applications/components/ApplicationDisplay';
import { AppChip } from '@/applications/components/AppChip';
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@@ -173,7 +173,13 @@ const SettingsAdminAppsTableRow = ({
minWidth="0"
overflow="hidden"
>
<ApplicationDisplay application={registration} />
<AppChip
size="md"
fallbackApplicationData={{
logo: registration.logoUrl,
name: registration.name,
}}
/>
</TableCell>
<TableCell overflow="hidden" align="right">
{getFormattedSource(registration)}
@@ -25,8 +25,10 @@ describe('useComputeObjectAndFieldsContentForApplication', () => {
it('should return object rows for installed application objects', () => {
const installedApplication = {
id: APP_ID,
universalIdentifier: APP_ID,
objects: [{ id: personObject.id }],
name: 'Test App',
logo: null,
canBeUninstalled: true,
availablePackages: {},
applicationVariables: [],
@@ -53,8 +55,10 @@ describe('useComputeObjectAndFieldsContentForApplication', () => {
it('should return empty object rows when application has no objects', () => {
const installedApplication = {
id: APP_ID,
universalIdentifier: APP_ID,
objects: [],
name: 'Test App',
logo: null,
canBeUninstalled: true,
availablePackages: {},
applicationVariables: [],
@@ -79,8 +83,10 @@ describe('useComputeObjectAndFieldsContentForApplication', () => {
const installedApplication = {
id: fieldBelongingToApp.applicationId ?? APP_ID,
universalIdentifier: fieldBelongingToApp.applicationId ?? APP_ID,
objects: [{ id: personObject.id }],
name: 'Test App',
logo: null,
canBeUninstalled: true,
availablePackages: {},
applicationVariables: [],
@@ -106,8 +112,10 @@ describe('useComputeObjectAndFieldsContentForApplication', () => {
it('should exclude deny-listed objects from field rows', () => {
const installedApplication = {
id: APP_ID,
universalIdentifier: APP_ID,
objects: [],
name: 'Test App',
logo: null,
canBeUninstalled: true,
availablePackages: {},
applicationVariables: [],
@@ -261,8 +269,10 @@ describe('useComputeObjectAndFieldsContentForApplication', () => {
it('should use installed application data when both sources are provided', () => {
const installedApplication = {
id: APP_ID,
universalIdentifier: APP_ID,
objects: [{ id: personObject.id }],
name: 'Test App',
logo: null,
canBeUninstalled: true,
availablePackages: {},
applicationVariables: [],
@@ -67,6 +67,7 @@ export const useComputeObjectAndFieldsContentForApplication = ({
.map((field) => ({
key: `${item.id}-${field.id}`,
name: field.label,
applicationId: item?.applicationId ?? undefined,
icon: field.icon ?? undefined,
secondary: t`on ${item.labelSingular}`,
link: getSettingsPath(SettingsPath.ObjectFieldEdit, {
@@ -0,0 +1,54 @@
import { type ApplicationDisplayData } from '@/applications/types/applicationDisplayData.type';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { getInstalledApplicationObjectAndFieldRows } from '@/settings/applications/utils/getInstalledApplicationObjectAndFieldRows';
import { getManifestObjectAndFieldRows } from '@/settings/applications/utils/getManifestObjectAndFieldRows';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useMemo } from 'react';
import { type Manifest } from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
import { type Application } from '~/generated-metadata/graphql';
type InstalledApplicationForObjectRows = Omit<
Application,
'objects' | 'frontComponents'
> & {
objects: { id: string }[];
};
export const useObjectAndFieldRows = ({
installedApplication,
manifestContent,
applicationInfo,
}: {
applicationId: string;
installedApplication?: InstalledApplicationForObjectRows;
manifestContent?: Manifest;
applicationInfo?: ApplicationDisplayData;
}) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const installedApplications = currentWorkspace?.installedApplications;
return useMemo(() => {
if (isDefined(installedApplication)) {
return getInstalledApplicationObjectAndFieldRows({
installedApplication,
objectMetadataItems,
installedApplications,
});
}
return getManifestObjectAndFieldRows({
manifestContent,
objectMetadataItems,
applicationInfo,
});
}, [
installedApplication,
manifestContent,
objectMetadataItems,
installedApplications,
applicationInfo,
]);
};
@@ -0,0 +1,79 @@
import { type ApplicationDisplayData } from '@/applications/types/applicationDisplayData.type';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { type Application } from '~/generated-metadata/graphql';
import { type ApplicationDataTableRow } from '~/pages/settings/applications/types/applicationDataTableRow';
const FIELD_GROUP_DENY_LIST = ['timelineActivity', 'favorite'];
type InstalledApplication = Omit<Application, 'objects' | 'frontComponents'> & {
objects: { id: string }[];
};
export const getInstalledApplicationObjectAndFieldRows = ({
installedApplication,
objectMetadataItems,
installedApplications,
}: {
installedApplication: InstalledApplication;
objectMetadataItems: EnrichedObjectMetadataItem[];
installedApplications?: ApplicationDisplayData[];
}): {
objectRows: ApplicationDataTableRow[];
fieldGroupRows: ApplicationDataTableRow[];
} => {
const installedObjectIds = installedApplication.objects.map(
(object) => object.id,
);
const objectRows: ApplicationDataTableRow[] =
installedApplication.objects.length === 0
? []
: objectMetadataItems
.filter((item) => installedObjectIds.includes(item.id))
.map((item) => ({
key: item.nameSingular,
labelPlural: item.labelPlural,
icon: item.icon ?? undefined,
fieldsCount: item.fields.filter((f) => !isHiddenSystemField(f))
.length,
link: getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural: item.namePlural,
}),
application: {
id: installedApplication.id,
logo: installedApplication.logo,
name: installedApplication.name,
universalIdentifier: installedApplication.universalIdentifier,
},
}))
.sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
const fieldGroupRows: ApplicationDataTableRow[] = objectMetadataItems
.filter((item) => {
if (installedObjectIds.includes(item.id)) return false;
if (FIELD_GROUP_DENY_LIST.includes(item.nameSingular)) return false;
return item.fields.some(
(field) => field.applicationId === installedApplication.id,
);
})
.map((item) => ({
key: item.nameSingular,
labelPlural: item.labelPlural,
icon: item.icon ?? undefined,
fieldsCount: item.fields.filter(
(field) => field.applicationId === installedApplication.id,
).length,
link: getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural: item.namePlural,
}),
application:
installedApplications?.find((app) => app.id === item.applicationId) ??
{},
}));
return { objectRows, fieldGroupRows };
};
@@ -0,0 +1,102 @@
import { type ApplicationDisplayData } from '@/applications/types/applicationDisplayData.type';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type Manifest } from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
import { type ApplicationDataTableRow } from '~/pages/settings/applications/types/applicationDataTableRow';
import { findObjectNameByUniversalIdentifier } from '~/pages/settings/applications/utils/findObjectNameByUniversalIdentifier';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
const FIELD_GROUP_DENY_LIST = ['timelineActivity', 'favorite'];
export const getManifestObjectAndFieldRows = ({
manifestContent,
objectMetadataItems,
applicationInfo,
}: {
manifestContent?: Manifest;
objectMetadataItems: EnrichedObjectMetadataItem[];
applicationInfo?: ApplicationDisplayData;
}): {
objectRows: ApplicationDataTableRow[];
fieldGroupRows: ApplicationDataTableRow[];
} => {
const manifestObjects = manifestContent?.objects ?? [];
const manifestFields = manifestContent?.fields ?? [];
const objectRows: ApplicationDataTableRow[] = manifestObjects
.map((appObject) => ({
key: appObject.nameSingular,
labelPlural: appObject.labelPlural,
icon: appObject.icon ?? undefined,
fieldsCount: appObject.fields.filter((f) => !isHiddenSystemField(f))
.length,
application: applicationInfo ?? {},
}))
.sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
if (manifestFields.length === 0) {
return { objectRows, fieldGroupRows: [] };
}
const manifestObjectUids = new Set(
manifestObjects.map((obj) => obj.universalIdentifier),
);
const groupMap = new Map<
string,
{ objectUniversalIdentifier: string; count: number }
>();
for (const field of manifestFields) {
const objectUid = field.objectUniversalIdentifier;
if (manifestObjectUids.has(objectUid)) {
continue;
}
const existing = groupMap.get(objectUid);
if (isDefined(existing)) {
existing.count++;
} else {
groupMap.set(objectUid, {
objectUniversalIdentifier: objectUid,
count: 1,
});
}
}
const fieldGroupRows: ApplicationDataTableRow[] = Array.from(
groupMap.values(),
)
.map((group) => {
const standardObjectName = findObjectNameByUniversalIdentifier(
group.objectUniversalIdentifier,
);
const objectMetadataItem = isDefined(standardObjectName)
? objectMetadataItems.find(
(item) => item.nameSingular === standardObjectName,
)
: undefined;
if (!isDefined(objectMetadataItem)) {
return;
}
if (FIELD_GROUP_DENY_LIST.includes(objectMetadataItem.nameSingular)) {
return;
}
return {
key: objectMetadataItem.nameSingular,
labelPlural: objectMetadataItem.labelPlural,
icon: objectMetadataItem.icon ?? undefined,
fieldsCount: group.count,
application: applicationInfo ?? {},
};
})
.filter(isDefined);
return { objectRows, fieldGroupRows };
};