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:
@@ -8,7 +8,6 @@ export default defineApplication({
|
||||
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
displayName: 'Postcard App',
|
||||
description: 'Send postcards easily with Twenty',
|
||||
icon: 'IconWorld',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
|
||||
@@ -4,6 +4,5 @@ export default defineApplication({
|
||||
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000001',
|
||||
displayName: 'Root App',
|
||||
description: 'An app with all entities at root level',
|
||||
icon: 'IconFolder',
|
||||
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
displayName: 'Rich App',
|
||||
description: 'A simple rich app',
|
||||
icon: 'IconWorld',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
|
||||
@@ -580,6 +580,7 @@ type Application {
|
||||
id: UUID!
|
||||
name: String!
|
||||
description: String
|
||||
logo: String
|
||||
version: String
|
||||
universalIdentifier: String!
|
||||
packageJsonChecksum: String
|
||||
@@ -2202,7 +2203,6 @@ type MarketplaceApp {
|
||||
id: String!
|
||||
name: String!
|
||||
description: String!
|
||||
icon: String!
|
||||
author: String!
|
||||
category: String!
|
||||
logo: String
|
||||
|
||||
@@ -414,6 +414,7 @@ export interface Application {
|
||||
id: Scalars['UUID']
|
||||
name: Scalars['String']
|
||||
description?: Scalars['String']
|
||||
logo?: Scalars['String']
|
||||
version?: Scalars['String']
|
||||
universalIdentifier: Scalars['String']
|
||||
packageJsonChecksum?: Scalars['String']
|
||||
@@ -1940,7 +1941,6 @@ export interface MarketplaceApp {
|
||||
id: Scalars['String']
|
||||
name: Scalars['String']
|
||||
description: Scalars['String']
|
||||
icon: Scalars['String']
|
||||
author: Scalars['String']
|
||||
category: Scalars['String']
|
||||
logo?: Scalars['String']
|
||||
@@ -3314,6 +3314,7 @@ export interface ApplicationGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
logo?: boolean | number
|
||||
version?: boolean | number
|
||||
universalIdentifier?: boolean | number
|
||||
packageJsonChecksum?: boolean | number
|
||||
@@ -4923,7 +4924,6 @@ export interface MarketplaceAppGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
icon?: boolean | number
|
||||
author?: boolean | number
|
||||
category?: boolean | number
|
||||
logo?: boolean | number
|
||||
|
||||
@@ -1257,6 +1257,9 @@ export default {
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"logo": [
|
||||
1
|
||||
],
|
||||
"version": [
|
||||
1
|
||||
],
|
||||
@@ -4415,9 +4418,6 @@ export default {
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"icon": [
|
||||
1
|
||||
],
|
||||
"author": [
|
||||
1
|
||||
],
|
||||
|
||||
@@ -77,7 +77,6 @@ export default defineApplication({
|
||||
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
icon: 'IconWorld',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,10 +1,19 @@
|
||||
import { useApplicationChipData } from '@/applications/hooks/useApplicationChipData';
|
||||
import { styled } from '@linaria/react';
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
import {
|
||||
Avatar,
|
||||
type AvatarSize,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type AppChipProps = {
|
||||
applicationId: string;
|
||||
size?: AvatarSize;
|
||||
applicationId?: string | null;
|
||||
fallbackApplicationData?: {
|
||||
logo?: string | null;
|
||||
name?: string | null;
|
||||
};
|
||||
className?: string;
|
||||
};
|
||||
|
||||
@@ -20,29 +29,30 @@ const StyledContainer = styled.div`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const AppChip = ({ applicationId, className }: AppChipProps) => {
|
||||
const { applicationChipData } = useApplicationChipData({ applicationId });
|
||||
export const AppChip = ({
|
||||
applicationId,
|
||||
size = 'sm',
|
||||
fallbackApplicationData,
|
||||
className,
|
||||
}: AppChipProps) => {
|
||||
const { applicationChipData } = useApplicationChipData({
|
||||
applicationId,
|
||||
fallbackApplicationData,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledContainer className={className}>
|
||||
<Avatar
|
||||
type="app"
|
||||
size="sm"
|
||||
size={size}
|
||||
avatarUrl={applicationChipData.logo}
|
||||
placeholder={applicationChipData.name}
|
||||
placeholderColorSeed={applicationChipData.seed}
|
||||
color={applicationChipData.colors?.color}
|
||||
backgroundColor={applicationChipData.colors?.backgroundColor}
|
||||
borderColor={applicationChipData.colors?.borderColor}
|
||||
/>
|
||||
<StyledLabel title={applicationChipData.name}>
|
||||
{applicationChipData.name}
|
||||
</StyledLabel>
|
||||
<OverflowingTextWithTooltip text={applicationChipData.name} />
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,41 +1,29 @@
|
||||
import { useApplicationAvatarColors } from '@/applications/hooks/useApplicationAvatarColors';
|
||||
import { Avatar, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
|
||||
type ApplicationDisplayData = {
|
||||
id?: string | null;
|
||||
name?: string | null;
|
||||
universalIdentifier?: string | null;
|
||||
logoUrl?: string | null;
|
||||
applicationRegistration?: {
|
||||
logoUrl?: string | null;
|
||||
} | null;
|
||||
};
|
||||
import { type ApplicationDisplayData } from '@/applications/types/applicationDisplayData.type';
|
||||
import { AppChip } from '@/applications/components/AppChip';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
type ApplicationDisplayProps = {
|
||||
application: ApplicationDisplayData;
|
||||
application?: ApplicationDisplayData;
|
||||
};
|
||||
|
||||
const StyledAppChip = styled(AppChip)`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
export const ApplicationDisplay = ({
|
||||
application,
|
||||
}: ApplicationDisplayProps) => {
|
||||
const colors = useApplicationAvatarColors(application);
|
||||
const name = application.name ?? '';
|
||||
const logoUrl =
|
||||
application.logoUrl ?? application.applicationRegistration?.logoUrl;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Avatar
|
||||
type="app"
|
||||
size="md"
|
||||
avatarUrl={logoUrl ?? undefined}
|
||||
placeholder={name}
|
||||
placeholderColorSeed={application.universalIdentifier ?? name}
|
||||
color={colors?.color}
|
||||
backgroundColor={colors?.backgroundColor}
|
||||
borderColor={colors?.borderColor}
|
||||
/>
|
||||
<OverflowingTextWithTooltip text={name} />
|
||||
</>
|
||||
<StyledAppChip
|
||||
size="md"
|
||||
applicationId={application?.id}
|
||||
fallbackApplicationData={{
|
||||
logo: application?.logo,
|
||||
name: application?.name,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ export const APPLICATION_FRAGMENT = gql`
|
||||
id
|
||||
name
|
||||
description
|
||||
logo
|
||||
version
|
||||
universalIdentifier
|
||||
applicationRegistrationId
|
||||
|
||||
@@ -6,6 +6,7 @@ export const FIND_MANY_APPLICATIONS = gql`
|
||||
id
|
||||
name
|
||||
description
|
||||
logo
|
||||
version
|
||||
universalIdentifier
|
||||
applicationRegistrationId
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import { CurrentApplicationContext } from '@/applications/contexts/CurrentApplicationContext';
|
||||
import {
|
||||
useApplicationAvatarColors,
|
||||
type ApplicationAvatarColors,
|
||||
useApplicationAvatarColors,
|
||||
} from '@/applications/hooks/useApplicationAvatarColors';
|
||||
import { isTwentyStandardApplication } from '@/applications/utils/isTwentyStandardApplication';
|
||||
import { isWorkspaceCustomApplication } from '@/applications/utils/isWorkspaceCustomApplication';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { buildApplicationLogoUrl } from '@/applications/utils/buildApplicationLogoUrl';
|
||||
import CustomLogo from '~/pages/settings/applications/assets/custom-illustrations/custom-logo.webp';
|
||||
import StandardLogo from '~/pages/settings/applications/assets/standard-illustrations/standard-logo.webp';
|
||||
|
||||
type UseApplicationChipDataArgs = {
|
||||
applicationId: string;
|
||||
applicationId?: string | null;
|
||||
fallbackApplicationData?: {
|
||||
logo?: string | null;
|
||||
name?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
type ApplicationChipData = {
|
||||
name: string;
|
||||
seed: string;
|
||||
colors?: ApplicationAvatarColors;
|
||||
logo?: string;
|
||||
};
|
||||
|
||||
type UseApplicationChipDataReturnType = {
|
||||
@@ -27,8 +33,8 @@ type UseApplicationChipDataReturnType = {
|
||||
|
||||
export const useApplicationChipData = ({
|
||||
applicationId,
|
||||
fallbackApplicationData,
|
||||
}: UseApplicationChipDataArgs): UseApplicationChipDataReturnType => {
|
||||
const currentApplicationId = useContext(CurrentApplicationContext);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
|
||||
const application = currentWorkspace?.installedApplications.find(
|
||||
@@ -40,28 +46,39 @@ export const useApplicationChipData = ({
|
||||
if (!isDefined(application)) {
|
||||
return {
|
||||
applicationChipData: {
|
||||
name: '',
|
||||
seed: applicationId,
|
||||
name: fallbackApplicationData?.name ?? '',
|
||||
logo: fallbackApplicationData?.logo ?? '',
|
||||
seed: fallbackApplicationData?.name ?? '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const isCurrent =
|
||||
isDefined(currentApplicationId) && currentApplicationId === applicationId;
|
||||
const isStandard = isTwentyStandardApplication(application);
|
||||
|
||||
const displayName = isCurrent
|
||||
? t`This app`
|
||||
: isTwentyStandardApplication(application)
|
||||
? t`Standard`
|
||||
: isWorkspaceCustomApplication(application, currentWorkspace)
|
||||
? t`Custom`
|
||||
: application.name;
|
||||
const isCustom = isWorkspaceCustomApplication(application, currentWorkspace);
|
||||
|
||||
const displayName = isStandard
|
||||
? t`Standard`
|
||||
: isCustom
|
||||
? t`Custom`
|
||||
: application.name;
|
||||
|
||||
const logo = isStandard
|
||||
? new URL(StandardLogo, window.location.href).toString()
|
||||
: isCustom
|
||||
? new URL(CustomLogo, window.location.href).toString()
|
||||
: buildApplicationLogoUrl({
|
||||
applicationId: application.id,
|
||||
logo: application.logo,
|
||||
workspaceId: currentWorkspace?.id,
|
||||
});
|
||||
|
||||
return {
|
||||
applicationChipData: {
|
||||
name: displayName,
|
||||
seed: application.universalIdentifier ?? application.name,
|
||||
seed: application.name,
|
||||
colors,
|
||||
logo,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export type ApplicationDisplayData = {
|
||||
id?: string | null;
|
||||
name?: string | null;
|
||||
universalIdentifier?: string | null;
|
||||
logo?: string | null;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const buildApplicationLogoUrl = ({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
logo,
|
||||
}: {
|
||||
workspaceId?: string | null;
|
||||
applicationId?: string | null;
|
||||
logo?: string | null;
|
||||
}): string | undefined => {
|
||||
if (
|
||||
!isDefined(logo) ||
|
||||
logo.startsWith('http://') ||
|
||||
logo.startsWith('https://')
|
||||
) {
|
||||
return logo ?? undefined;
|
||||
}
|
||||
|
||||
if (!isDefined(workspaceId) || !isDefined(applicationId)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `${REACT_APP_SERVER_BASE_URL}/public-assets/${workspaceId}/${applicationId}/${logo}`;
|
||||
};
|
||||
@@ -47,7 +47,7 @@ export type CurrentWorkspace = Pick<
|
||||
workspaceCustomApplication: Pick<Application, 'id'> | null;
|
||||
installedApplications: Pick<
|
||||
Application,
|
||||
'id' | 'name' | 'universalIdentifier'
|
||||
'id' | 'name' | 'universalIdentifier' | 'logo'
|
||||
>[];
|
||||
};
|
||||
|
||||
|
||||
-1
@@ -5,7 +5,6 @@ export const MARKETPLACE_APP_FRAGMENT = gql`
|
||||
id
|
||||
name
|
||||
description
|
||||
icon
|
||||
author
|
||||
category
|
||||
logo
|
||||
|
||||
+8
-2
@@ -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)}
|
||||
|
||||
+10
@@ -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: [],
|
||||
|
||||
+1
@@ -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, {
|
||||
|
||||
+54
@@ -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,
|
||||
]);
|
||||
};
|
||||
+79
@@ -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 };
|
||||
};
|
||||
+102
@@ -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 };
|
||||
};
|
||||
@@ -70,6 +70,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
id
|
||||
name
|
||||
universalIdentifier
|
||||
logo
|
||||
}
|
||||
isCustomDomainEnabled
|
||||
workspaceUrls {
|
||||
|
||||
@@ -5,11 +5,13 @@ import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { StyledNameTableCell } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents';
|
||||
import { AppChip } from '@/applications/components/AppChip';
|
||||
|
||||
export type SettingsToolTableRowProps = {
|
||||
leftIcon: ReactNode;
|
||||
name: string;
|
||||
appLabel: string;
|
||||
applicationId: string;
|
||||
action?: ReactNode;
|
||||
link?: string;
|
||||
};
|
||||
@@ -25,7 +27,7 @@ const StyledIconContainer = styled.div`
|
||||
export const SettingsToolTableRow = ({
|
||||
leftIcon,
|
||||
name,
|
||||
appLabel,
|
||||
applicationId,
|
||||
action,
|
||||
link,
|
||||
}: SettingsToolTableRowProps) => {
|
||||
@@ -43,9 +45,9 @@ export const SettingsToolTableRow = ({
|
||||
<StyledIconContainer>{leftIcon}</StyledIconContainer>
|
||||
<OverflowingTextWithTooltip text={name} />
|
||||
</TableCell>
|
||||
<TableCell minWidth="0" overflow="hidden">
|
||||
<OverflowingTextWithTooltip text={appLabel} />
|
||||
</TableCell>
|
||||
<StyledNameTableCell minWidth="0" overflow="hidden">
|
||||
<AppChip applicationId={applicationId} />
|
||||
</StyledNameTableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
padding={`0 ${themeCssVariables.spacing[2]} 0 0`}
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
SettingsToolTableRow,
|
||||
TOOL_TABLE_ROW_GRID_TEMPLATE_COLUMNS,
|
||||
} from './SettingsToolTableRow';
|
||||
import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application';
|
||||
|
||||
type ToolItem = {
|
||||
identifier: string;
|
||||
@@ -53,6 +54,7 @@ const FIND_MANY_APPLICATIONS_FOR_TOOL_TABLE = gql`
|
||||
id
|
||||
name
|
||||
universalIdentifier
|
||||
logo
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -90,6 +92,7 @@ export const SettingsToolsTable = () => {
|
||||
id: string;
|
||||
name: string;
|
||||
universalIdentifier: string;
|
||||
logo?: string | null;
|
||||
}>;
|
||||
}>(FIND_MANY_APPLICATIONS_FOR_TOOL_TABLE);
|
||||
const { data: marketplaceAppsData } = useQuery<{
|
||||
@@ -119,6 +122,20 @@ export const SettingsToolsTable = () => {
|
||||
toolIdentifier: item.identifier,
|
||||
});
|
||||
|
||||
const getToolApplicationId = (item: ToolItem) => {
|
||||
if (isDefined(item.applicationId)) {
|
||||
return item.applicationId;
|
||||
}
|
||||
|
||||
return (
|
||||
currentWorkspace?.installedApplications?.find(
|
||||
(app) =>
|
||||
app.universalIdentifier ===
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
)?.id ?? ''
|
||||
);
|
||||
};
|
||||
|
||||
const allTools: ToolItem[] = useMemo(
|
||||
() => [
|
||||
...logicFunctions
|
||||
@@ -264,10 +281,6 @@ export const SettingsToolsTable = () => {
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const appLabel = isCustom(item)
|
||||
? (application?.name ?? t`Custom`)
|
||||
: t`Standard`;
|
||||
|
||||
return (
|
||||
<SettingsToolTableRow
|
||||
key={item.identifier}
|
||||
@@ -281,7 +294,7 @@ export const SettingsToolsTable = () => {
|
||||
/>
|
||||
}
|
||||
name={item.name}
|
||||
appLabel={appLabel}
|
||||
applicationId={getToolApplicationId(item)}
|
||||
action={
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
|
||||
+10
-4
@@ -89,7 +89,10 @@ export const SettingsApplicationDetails = () => {
|
||||
app?.displayName ?? application?.name ?? t`Application details`;
|
||||
const description = app?.description ?? resolvedDescription;
|
||||
const logoUrl =
|
||||
app?.logoUrl ?? application?.applicationRegistration?.logoUrl ?? undefined;
|
||||
app?.logoUrl ??
|
||||
application?.logo ??
|
||||
application?.applicationRegistration?.logoUrl ??
|
||||
undefined;
|
||||
|
||||
const getScreenshots = () => {
|
||||
if (app?.screenshots?.length) return app.screenshots;
|
||||
@@ -283,6 +286,12 @@ export const SettingsApplicationDetails = () => {
|
||||
applicationId={application.id}
|
||||
installedApplication={application}
|
||||
manifestContent={manifest}
|
||||
applicationInfo={{
|
||||
id: application.id,
|
||||
name: displayName,
|
||||
logo: application.logo,
|
||||
universalIdentifier: application.universalIdentifier,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case 'permissions':
|
||||
@@ -317,10 +326,7 @@ export const SettingsApplicationDetails = () => {
|
||||
<SettingsApplicationDetailTitle
|
||||
displayName={displayName}
|
||||
description={description}
|
||||
logoUrl={logoUrl}
|
||||
applicationId={application?.id}
|
||||
applicationName={application?.name}
|
||||
universalIdentifier={application?.universalIdentifier}
|
||||
/>
|
||||
}
|
||||
links={[
|
||||
|
||||
+5
-5
@@ -230,6 +230,11 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
<SettingsApplicationDetailContentTab
|
||||
applicationId={detail.universalIdentifier}
|
||||
manifestContent={manifest}
|
||||
applicationInfo={{
|
||||
name: displayName,
|
||||
logo: app?.logoUrl,
|
||||
universalIdentifier: detail.universalIdentifier,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case 'permissions':
|
||||
@@ -267,10 +272,7 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
<SettingsApplicationDetailTitle
|
||||
displayName={displayName}
|
||||
description={description}
|
||||
logoUrl={app?.logoUrl}
|
||||
applicationId={application?.id}
|
||||
applicationName={application?.name}
|
||||
universalIdentifier={detail.universalIdentifier}
|
||||
isUnlisted={isUnlisted}
|
||||
/>
|
||||
}
|
||||
@@ -279,9 +281,7 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
componentInstanceId={AVAILABLE_APPLICATION_DETAIL_ID}
|
||||
behaveAsLinks={false}
|
||||
/>
|
||||
|
||||
{renderActiveTabContent()}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 268 B |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 448 B |
+16
-1
@@ -13,23 +13,32 @@ import {
|
||||
useIcons,
|
||||
} from 'twenty-ui/display';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { AppChip } from '@/applications/components/AppChip';
|
||||
|
||||
export type ApplicationContentRow = {
|
||||
key: string;
|
||||
name: string;
|
||||
applicationId?: string;
|
||||
icon?: string;
|
||||
secondary?: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
const GRID_TEMPLATE_COLUMNS = '1fr auto 32px';
|
||||
const GRID_TEMPLATE_COLUMNS = '200px 1fr 160px 32px';
|
||||
|
||||
export const SettingsApplicationContentSubtable = ({
|
||||
title,
|
||||
rows,
|
||||
applicationId,
|
||||
fallbackApplicationData,
|
||||
}: {
|
||||
title: string;
|
||||
rows: ApplicationContentRow[];
|
||||
applicationId?: string;
|
||||
fallbackApplicationData?: {
|
||||
logo?: string | null;
|
||||
name?: string | null;
|
||||
};
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { getIcon } = useIcons();
|
||||
@@ -55,6 +64,12 @@ export const SettingsApplicationContentSubtable = ({
|
||||
)}
|
||||
<OverflowingTextWithTooltip text={row.name} />
|
||||
</StyledNameTableCell>
|
||||
<StyledNameTableCell minWidth="0" overflow="hidden" align={'right'}>
|
||||
<AppChip
|
||||
applicationId={row.applicationId ?? applicationId}
|
||||
fallbackApplicationData={fallbackApplicationData}
|
||||
/>
|
||||
</StyledNameTableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
color={themeCssVariables.font.color.secondary}
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { SettingsEmptyPlaceholder } from '@/settings/components/SettingsEmptyPlaceholder';
|
||||
import { SETTINGS_OBJECT_TABLE_COLUMN_WIDTH } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { TableSection } from '@/ui/layout/table/components/TableSection';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { SearchInput } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { SettingsApplicationDataTableRow } from '~/pages/settings/applications/components/SettingsApplicationDataTableRow';
|
||||
import { normalizeSearchText } from '~/utils/normalizeSearchText';
|
||||
import { type ApplicationDataTableRow } from '~/pages/settings/applications/types/applicationDataTableRow';
|
||||
|
||||
const MAIN_ROW_GRID_COLUMNS = `180px 1fr ${SETTINGS_OBJECT_TABLE_COLUMN_WIDTH} 36px`;
|
||||
|
||||
const StyledEmptyHeaderContainer = styled.div`
|
||||
> div {
|
||||
min-width: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSearchInputContainer = styled.div`
|
||||
padding-bottom: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
export const SettingsApplicationDataTable = ({
|
||||
objectRows,
|
||||
fieldGroupRows,
|
||||
}: {
|
||||
objectRows: ApplicationDataTableRow[];
|
||||
fieldGroupRows: ApplicationDataTableRow[];
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const filteredObjectRows = useMemo(() => {
|
||||
const normalizedSearch = normalizeSearchText(searchTerm);
|
||||
|
||||
if (normalizedSearch === '') {
|
||||
return objectRows;
|
||||
}
|
||||
|
||||
return objectRows.filter((row) =>
|
||||
normalizeSearchText(row.labelPlural).includes(normalizedSearch),
|
||||
);
|
||||
}, [objectRows, searchTerm]);
|
||||
|
||||
const filteredFieldGroupRows = useMemo(() => {
|
||||
const normalizedSearch = normalizeSearchText(searchTerm);
|
||||
|
||||
if (normalizedSearch === '') {
|
||||
return fieldGroupRows;
|
||||
}
|
||||
|
||||
return fieldGroupRows.filter((row) =>
|
||||
normalizeSearchText(row.labelPlural).includes(normalizedSearch),
|
||||
);
|
||||
}, [fieldGroupRows, searchTerm]);
|
||||
|
||||
const shouldDisplayObjects = filteredObjectRows.length > 0;
|
||||
const shouldDisplayFields = filteredFieldGroupRows.length > 0;
|
||||
const hasSearchTerm = searchTerm.trim().length > 0;
|
||||
const hasNoResults =
|
||||
hasSearchTerm && !shouldDisplayObjects && !shouldDisplayFields;
|
||||
|
||||
if (objectRows.length === 0 && fieldGroupRows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Data`}
|
||||
description={t`Objects and fields managed by this app`}
|
||||
/>
|
||||
<StyledSearchInputContainer>
|
||||
<SearchInput
|
||||
placeholder={t`Search an object...`}
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
/>
|
||||
</StyledSearchInputContainer>
|
||||
{hasNoResults ? (
|
||||
<SettingsEmptyPlaceholder>{t`No object found`}</SettingsEmptyPlaceholder>
|
||||
) : (
|
||||
<Table>
|
||||
<TableRow gridAutoColumns={MAIN_ROW_GRID_COLUMNS}>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader>{t`App`}</TableHeader>
|
||||
<TableHeader align="right">{t`Fields`}</TableHeader>
|
||||
<StyledEmptyHeaderContainer>
|
||||
<TableHeader />
|
||||
</StyledEmptyHeaderContainer>
|
||||
</TableRow>
|
||||
{shouldDisplayObjects && (
|
||||
<TableSection title={t`Objects`}>
|
||||
{filteredObjectRows.map((row) => (
|
||||
<SettingsApplicationDataTableRow key={row.key} row={row} />
|
||||
))}
|
||||
</TableSection>
|
||||
)}
|
||||
{shouldDisplayFields && (
|
||||
<TableSection title={t`Fields`}>
|
||||
{filteredFieldGroupRows.map((row) => (
|
||||
<SettingsApplicationDataTableRow key={row.key} row={row} />
|
||||
))}
|
||||
</TableSection>
|
||||
)}
|
||||
</Table>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
SETTINGS_OBJECT_TABLE_COLUMN_WIDTH,
|
||||
StyledActionTableCell,
|
||||
StyledNameTableCell,
|
||||
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronRight, useIcons } from 'twenty-ui/display';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { type ApplicationDataTableRow } from '~/pages/settings/applications/types/applicationDataTableRow';
|
||||
import { AppChip } from '@/applications/components/AppChip';
|
||||
|
||||
const MAIN_ROW_GRID_COLUMNS = `180px 1fr ${SETTINGS_OBJECT_TABLE_COLUMN_WIDTH} 36px`;
|
||||
|
||||
const StyledNameContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const StyledNameLabel = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const SettingsApplicationDataTableRow = ({
|
||||
row,
|
||||
}: {
|
||||
row: ApplicationDataTableRow;
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const Icon = getIcon(row.icon);
|
||||
|
||||
return (
|
||||
<TableRow gridAutoColumns={MAIN_ROW_GRID_COLUMNS} to={row.link}>
|
||||
<StyledNameTableCell minWidth="0" overflow="hidden">
|
||||
{isDefined(Icon) && (
|
||||
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||
)}
|
||||
<StyledNameContainer>
|
||||
<StyledNameLabel title={row.labelPlural}>
|
||||
{row.labelPlural}
|
||||
</StyledNameLabel>
|
||||
</StyledNameContainer>
|
||||
</StyledNameTableCell>
|
||||
<StyledNameTableCell minWidth="0" overflow="hidden">
|
||||
<AppChip
|
||||
applicationId={row.application?.id}
|
||||
fallbackApplicationData={row.application}
|
||||
/>
|
||||
</StyledNameTableCell>
|
||||
<TableCell align="right">{row.fieldsCount}</TableCell>
|
||||
<StyledActionTableCell>
|
||||
{row.link && (
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.light}
|
||||
/>
|
||||
)}
|
||||
</StyledActionTableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
+11
-15
@@ -1,10 +1,10 @@
|
||||
import { useApplicationAvatarColors } from '@/applications/hooks/useApplicationAvatarColors';
|
||||
import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Avatar, IconEyeOff } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { getApplicationDescriptionSummary } from '~/pages/settings/applications/utils/getApplicationDescriptionSummary';
|
||||
import { useApplicationChipData } from '@/applications/hooks/useApplicationChipData';
|
||||
|
||||
type SettingsApplicationDetailTitleProps = {
|
||||
displayName: string;
|
||||
@@ -72,19 +72,15 @@ const StyledUnlistedBanner = styled.div`
|
||||
export const SettingsApplicationDetailTitle = ({
|
||||
displayName,
|
||||
description,
|
||||
logoUrl,
|
||||
applicationId,
|
||||
applicationName,
|
||||
universalIdentifier,
|
||||
isUnlisted = false,
|
||||
}: SettingsApplicationDetailTitleProps) => {
|
||||
const colors = useApplicationAvatarColors({
|
||||
id: applicationId,
|
||||
name: applicationName,
|
||||
universalIdentifier,
|
||||
});
|
||||
const descriptionSummary = getApplicationDescriptionSummary(description);
|
||||
|
||||
const { applicationChipData } = useApplicationChipData({
|
||||
applicationId,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledTitleContainer>
|
||||
{isUnlisted && (
|
||||
@@ -99,12 +95,12 @@ export const SettingsApplicationDetailTitle = ({
|
||||
<Avatar
|
||||
type="app"
|
||||
size="lg"
|
||||
avatarUrl={logoUrl}
|
||||
placeholder={displayName}
|
||||
placeholderColorSeed={universalIdentifier ?? displayName}
|
||||
color={colors?.color}
|
||||
backgroundColor={colors?.backgroundColor}
|
||||
borderColor={colors?.borderColor}
|
||||
avatarUrl={applicationChipData.logo}
|
||||
placeholder={applicationChipData.name}
|
||||
placeholderColorSeed={applicationChipData.seed}
|
||||
color={applicationChipData.colors?.color}
|
||||
backgroundColor={applicationChipData.colors?.backgroundColor}
|
||||
borderColor={applicationChipData.colors?.borderColor}
|
||||
/>
|
||||
<StyledAppName>{displayName}</StyledAppName>
|
||||
</StyledHeaderTop>
|
||||
|
||||
+5
-15
@@ -9,18 +9,13 @@ import { Tag } from 'twenty-ui/components';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { getApplicationDescriptionSummary } from '~/pages/settings/applications/utils/getApplicationDescriptionSummary';
|
||||
import { type ApplicationDisplayData } from '@/applications/types/applicationDisplayData.type';
|
||||
import { StyledNameTableCell } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents';
|
||||
|
||||
export type SettingsApplicationTableRowProps = {
|
||||
action: ReactNode;
|
||||
application: {
|
||||
id?: string | null;
|
||||
name?: string | null;
|
||||
universalIdentifier?: string | null;
|
||||
application: ApplicationDisplayData & {
|
||||
description?: string | null;
|
||||
logoUrl?: string | null;
|
||||
applicationRegistration?: {
|
||||
logoUrl?: string | null;
|
||||
} | null;
|
||||
};
|
||||
hasUpdate?: boolean;
|
||||
link?: string;
|
||||
@@ -45,14 +40,9 @@ export const SettingsApplicationTableRow = ({
|
||||
key={application.id}
|
||||
to={link}
|
||||
>
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.primary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
minWidth="0"
|
||||
overflow="hidden"
|
||||
>
|
||||
<StyledNameTableCell minWidth="0" overflow="hidden">
|
||||
<ApplicationDisplay application={application} />
|
||||
</TableCell>
|
||||
</StyledNameTableCell>
|
||||
<TableCell gap={themeCssVariables.spacing[2]} minWidth="0">
|
||||
<OverflowingTextWithTooltip text={descriptionSummary} />
|
||||
{hasUpdate === true && (
|
||||
|
||||
+29
-1
@@ -10,6 +10,7 @@ import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { SearchInput } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { type ApplicationDisplayData } from '@/applications/types/applicationDisplayData.type';
|
||||
import { type Application } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
type ApplicationContentRow,
|
||||
@@ -19,7 +20,7 @@ import { normalizeSearchText } from '~/utils/normalizeSearchText';
|
||||
|
||||
type InstalledApplicationForContentTab = Omit<
|
||||
Application,
|
||||
'objects' | 'universalIdentifier' | 'frontComponents'
|
||||
'objects' | 'frontComponents'
|
||||
> & {
|
||||
objects: { id: string }[];
|
||||
frontComponents?: {
|
||||
@@ -33,6 +34,7 @@ type SettingsApplicationDetailContentTabProps = {
|
||||
applicationId: string;
|
||||
installedApplication?: InstalledApplicationForContentTab;
|
||||
manifestContent?: Manifest;
|
||||
applicationInfo: ApplicationDisplayData;
|
||||
};
|
||||
|
||||
const filterRows = (rows: ApplicationContentRow[], normalizedSearch: string) =>
|
||||
@@ -49,6 +51,7 @@ export const SettingsApplicationDetailContentTab = ({
|
||||
applicationId,
|
||||
installedApplication,
|
||||
manifestContent,
|
||||
applicationInfo,
|
||||
}: SettingsApplicationDetailContentTabProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -70,6 +73,11 @@ export const SettingsApplicationDetailContentTab = ({
|
||||
manifestContent,
|
||||
});
|
||||
|
||||
const fallbackApplicationData = {
|
||||
logo: applicationInfo?.logo,
|
||||
name: applicationInfo?.name,
|
||||
};
|
||||
|
||||
const lifecycleOptions = {
|
||||
postInstallUniversalIdentifier:
|
||||
manifestContent?.application?.postInstallLogicFunction
|
||||
@@ -167,10 +175,14 @@ export const SettingsApplicationDetailContentTab = ({
|
||||
<SettingsApplicationContentSubtable
|
||||
title={t`Objects`}
|
||||
rows={filtered.objects}
|
||||
applicationId={applicationId}
|
||||
fallbackApplicationData={fallbackApplicationData}
|
||||
/>
|
||||
<SettingsApplicationContentSubtable
|
||||
title={t`Fields added to other objects`}
|
||||
rows={filtered.fields}
|
||||
applicationId={applicationId}
|
||||
fallbackApplicationData={fallbackApplicationData}
|
||||
/>
|
||||
</Table>
|
||||
</Section>
|
||||
@@ -186,18 +198,26 @@ export const SettingsApplicationDetailContentTab = ({
|
||||
<SettingsApplicationContentSubtable
|
||||
title={t`Page layouts`}
|
||||
rows={filtered.pageLayouts}
|
||||
applicationId={applicationId}
|
||||
fallbackApplicationData={fallbackApplicationData}
|
||||
/>
|
||||
<SettingsApplicationContentSubtable
|
||||
title={t`Views`}
|
||||
rows={filtered.views}
|
||||
applicationId={applicationId}
|
||||
fallbackApplicationData={fallbackApplicationData}
|
||||
/>
|
||||
<SettingsApplicationContentSubtable
|
||||
title={t`Navigation menu items`}
|
||||
rows={filtered.navigation}
|
||||
applicationId={applicationId}
|
||||
fallbackApplicationData={fallbackApplicationData}
|
||||
/>
|
||||
<SettingsApplicationContentSubtable
|
||||
title={t`Front components`}
|
||||
rows={filtered.frontComponents}
|
||||
applicationId={applicationId}
|
||||
fallbackApplicationData={fallbackApplicationData}
|
||||
/>
|
||||
</Table>
|
||||
</Section>
|
||||
@@ -213,18 +233,26 @@ export const SettingsApplicationDetailContentTab = ({
|
||||
<SettingsApplicationContentSubtable
|
||||
title={t`Logic functions`}
|
||||
rows={filtered.logicFunctions}
|
||||
applicationId={applicationId}
|
||||
fallbackApplicationData={fallbackApplicationData}
|
||||
/>
|
||||
<SettingsApplicationContentSubtable
|
||||
title={t`Agents`}
|
||||
rows={filtered.agents}
|
||||
applicationId={applicationId}
|
||||
fallbackApplicationData={fallbackApplicationData}
|
||||
/>
|
||||
<SettingsApplicationContentSubtable
|
||||
title={t`Skills`}
|
||||
rows={filtered.skills}
|
||||
applicationId={applicationId}
|
||||
fallbackApplicationData={fallbackApplicationData}
|
||||
/>
|
||||
<SettingsApplicationContentSubtable
|
||||
title={t`Roles`}
|
||||
rows={filtered.roles}
|
||||
applicationId={applicationId}
|
||||
fallbackApplicationData={fallbackApplicationData}
|
||||
/>
|
||||
</Table>
|
||||
</Section>
|
||||
|
||||
+6
-34
@@ -1,4 +1,3 @@
|
||||
import { useApplicationAvatarColors } from '@/applications/hooks/useApplicationAvatarColors';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { SettingsEmptyPlaceholder } from '@/settings/components/SettingsEmptyPlaceholder';
|
||||
import {
|
||||
@@ -16,24 +15,21 @@ import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { FeatureFlagKey, SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import {
|
||||
Avatar,
|
||||
CommandBlock,
|
||||
H2Title,
|
||||
IconArrowUpRight,
|
||||
IconChevronRight,
|
||||
IconCopy,
|
||||
IconArrowUpRight,
|
||||
OverflowingTextWithTooltip,
|
||||
InlineBanner,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button, SearchInput } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import {
|
||||
type ApplicationRegistrationFragmentFragment,
|
||||
FeatureFlagKey,
|
||||
FindManyApplicationRegistrationsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
@@ -43,6 +39,8 @@ import {
|
||||
SettingsApplicationTableRow,
|
||||
} from '~/pages/settings/applications/components/SettingsApplicationTableRow';
|
||||
import { getApplicationDescriptionSummary } from '~/pages/settings/applications/utils/getApplicationDescriptionSummary';
|
||||
import { ApplicationDisplay } from '@/applications/components/ApplicationDisplay';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -61,31 +59,6 @@ const StyledTableRowsContainer = styled.div`
|
||||
|
||||
const NPM_PACKAGES_GRID_COLUMNS = '200px 1fr 36px';
|
||||
|
||||
type MarketplaceAppAvatarProps = {
|
||||
application: { id: string; name: string; logo?: string | null };
|
||||
};
|
||||
|
||||
const MarketplaceAppAvatar = ({ application }: MarketplaceAppAvatarProps) => {
|
||||
const colors = useApplicationAvatarColors({
|
||||
id: application.id,
|
||||
name: application.name,
|
||||
universalIdentifier: application.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
avatarUrl={application.logo || null}
|
||||
placeholder={application.name}
|
||||
placeholderColorSeed={application.id ?? application.name}
|
||||
size="md"
|
||||
type="app"
|
||||
color={colors?.color}
|
||||
backgroundColor={colors?.backgroundColor}
|
||||
borderColor={colors?.borderColor}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingsApplicationsDeveloperTab = () => {
|
||||
const { t } = useLingui();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
@@ -268,8 +241,7 @@ export const SettingsApplicationsDeveloperTab = () => {
|
||||
)}
|
||||
>
|
||||
<StyledNameTableCell>
|
||||
<MarketplaceAppAvatar application={application} />
|
||||
<OverflowingTextWithTooltip text={application.name} />
|
||||
<ApplicationDisplay application={application} />
|
||||
</StyledNameTableCell>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { type ApplicationDisplayData } from '@/applications/types/applicationDisplayData.type';
|
||||
|
||||
export type ApplicationDataTableRow = {
|
||||
key: string;
|
||||
labelPlural: string;
|
||||
icon?: string;
|
||||
fieldsCount: number;
|
||||
link?: string;
|
||||
application: ApplicationDisplayData;
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
|
||||
export const findObjectNameByUniversalIdentifier = (
|
||||
universalIdentifier: string,
|
||||
): string | undefined => {
|
||||
for (const [objectName, objectDefinition] of Object.entries(
|
||||
STANDARD_OBJECTS,
|
||||
)) {
|
||||
if (objectDefinition.universalIdentifier === universalIdentifier) {
|
||||
return objectName;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
-1
@@ -11,7 +11,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000001',
|
||||
displayName: 'Root App',
|
||||
description: 'An app with all entities at root level',
|
||||
icon: 'IconFolder',
|
||||
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
|
||||
packageJsonChecksum: '[checksum]',
|
||||
yarnLockChecksum: '[checksum]',
|
||||
|
||||
-1
@@ -69,7 +69,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
description: 'A simple rich app',
|
||||
displayName: 'Rich App',
|
||||
defaultRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
icon: 'IconWorld',
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e',
|
||||
packageJsonChecksum: '2851d0e2c3621a57e1fd103a245b6fde',
|
||||
|
||||
@@ -6,7 +6,6 @@ describe('defineApplication', () => {
|
||||
universalIdentifier: 'a9faf5f8-cf7e-4f24-9d37-fd523c30febe',
|
||||
displayName: 'My App',
|
||||
description: 'My app description',
|
||||
icon: 'IconWorld',
|
||||
defaultRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2',
|
||||
};
|
||||
|
||||
@@ -22,7 +21,6 @@ describe('defineApplication', () => {
|
||||
universalIdentifier: 'a9faf5f8-cf7e-4f24-9d37-fd523c30febe',
|
||||
displayName: 'My App',
|
||||
description: 'My app description',
|
||||
icon: 'IconWorld',
|
||||
applicationVariables: {
|
||||
API_KEY: {
|
||||
universalIdentifier: '3a327392-3a0f-4605-9223-0633f063eaf6',
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.2.0', 1777539664664)
|
||||
export class AddLogoToApplicationFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "core"."application" ADD "logo" text');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."application" DROP COLUMN "logo"',
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
@@ -19,6 +19,7 @@ import { AddIsPreInstalledToApplicationRegistrationFastInstanceCommand } from 's
|
||||
import { AddProviderExecutedToAgentMessagePartFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-1/2-1-instance-command-fast-1777012800000-add-provider-executed-to-agent-message-part';
|
||||
import { BackfillPageLayoutWidgetPositionSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-1/2-1-instance-command-slow-1795000002000-backfill-page-layout-widget-position';
|
||||
import { AddCacheTokensToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777455269302-add-cache-tokens-to-agent-chat-thread';
|
||||
import { AddLogoToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777539664664-add-logo-to-application';
|
||||
|
||||
export const INSTANCE_COMMANDS = [
|
||||
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
|
||||
@@ -40,4 +41,5 @@ export const INSTANCE_COMMANDS = [
|
||||
AddProviderExecutedToAgentMessagePartFastInstanceCommand,
|
||||
BackfillPageLayoutWidgetPositionSlowInstanceCommand,
|
||||
AddCacheTokensToAgentChatThreadFastInstanceCommand,
|
||||
AddLogoToApplicationFastInstanceCommand,
|
||||
];
|
||||
|
||||
+13
-4
@@ -5,6 +5,8 @@ import { promises as fs } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
import semver from 'semver';
|
||||
import { extractFileInfo } from 'src/engine/core-modules/file/utils/extract-file-info.utils';
|
||||
import { sanitizeFile } from 'src/engine/core-modules/file/utils/sanitize-file.utils';
|
||||
import { Manifest } from 'twenty-shared/application';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -141,6 +143,7 @@ export class ApplicationInstallService {
|
||||
existingApplication,
|
||||
universalIdentifier,
|
||||
name: resolvedPackage.manifest.application.displayName,
|
||||
logo: resolvedPackage.manifest.application.logoUrl ?? null,
|
||||
workspaceId: params.workspaceId,
|
||||
applicationRegistrationId: appRegistration.id,
|
||||
sourceType: appRegistration.sourceType,
|
||||
@@ -454,11 +457,15 @@ export class ApplicationInstallService {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: mimeType should be defined, default to application/octet-stream, which won't be displayed
|
||||
// inline by the browser (forced download) due to Content-Disposition security headers.
|
||||
const { mimeType, ext } = await extractFileInfo({
|
||||
file: content,
|
||||
filename: relativePath,
|
||||
});
|
||||
const sanitizedContent = sanitizeFile({ file: content, ext, mimeType });
|
||||
|
||||
await this.fileStorageService.writeFile({
|
||||
sourceFile: content,
|
||||
mimeType: undefined,
|
||||
sourceFile: sanitizedContent,
|
||||
mimeType,
|
||||
fileFolder,
|
||||
applicationUniversalIdentifier,
|
||||
workspaceId,
|
||||
@@ -506,6 +513,7 @@ export class ApplicationInstallService {
|
||||
existingApplication: ApplicationEntity | null;
|
||||
universalIdentifier: string;
|
||||
name: string;
|
||||
logo: string | null;
|
||||
workspaceId: string;
|
||||
applicationRegistrationId: string;
|
||||
sourceType: ApplicationRegistrationSourceType;
|
||||
@@ -517,6 +525,7 @@ export class ApplicationInstallService {
|
||||
return await this.applicationService.create({
|
||||
universalIdentifier: params.universalIdentifier,
|
||||
name: params.name,
|
||||
logo: params.logo,
|
||||
sourcePath: params.universalIdentifier,
|
||||
sourceType: params.sourceType,
|
||||
applicationRegistrationId: params.applicationRegistrationId,
|
||||
|
||||
+1
@@ -159,6 +159,7 @@ export class ApplicationSyncService {
|
||||
return await this.applicationService.update(application.id, {
|
||||
name,
|
||||
description: manifest.application.description,
|
||||
logo: manifest.application.logoUrl ?? null,
|
||||
version: packageJson.version,
|
||||
packageJsonChecksum: manifest.application.packageJsonChecksum,
|
||||
yarnLockChecksum: manifest.application.yarnLockChecksum,
|
||||
|
||||
-4
@@ -25,10 +25,6 @@ export class MarketplaceAppDTO {
|
||||
@MaxLength(160)
|
||||
description: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
icon: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
author: string;
|
||||
|
||||
-1
@@ -87,7 +87,6 @@ export class MarketplaceQueryService {
|
||||
id: registration.universalIdentifier,
|
||||
name: app?.displayName ?? registration.name,
|
||||
description: app?.description ?? '',
|
||||
icon: app?.icon ?? 'IconApps',
|
||||
author: `${app?.author ?? 'Unknown'}`,
|
||||
category: app?.category ?? '',
|
||||
logo: app?.logoUrl ?? undefined,
|
||||
|
||||
+9
-8
@@ -278,6 +278,14 @@ export class ApplicationRegistrationService {
|
||||
params.universalIdentifier,
|
||||
);
|
||||
|
||||
const curatedIdentifiers = new Set(
|
||||
MARKETPLACE_CURATED_APPLICATIONS.map(
|
||||
(entry) => entry.universalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
const isFeatured = curatedIdentifiers.has(params.universalIdentifier);
|
||||
|
||||
if (isDefined(existing)) {
|
||||
await this.applicationRegistrationRepository.save({
|
||||
...existing,
|
||||
@@ -286,16 +294,9 @@ export class ApplicationRegistrationService {
|
||||
sourcePackage: params.sourcePackage,
|
||||
latestAvailableVersion: params.latestAvailableVersion,
|
||||
manifest: params.manifest,
|
||||
isFeatured,
|
||||
});
|
||||
} else {
|
||||
const curatedIdentifiers = new Set(
|
||||
MARKETPLACE_CURATED_APPLICATIONS.map(
|
||||
(entry) => entry.universalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
const isFeatured = curatedIdentifiers.has(params.universalIdentifier);
|
||||
|
||||
const registration = this.applicationRegistrationRepository.create({
|
||||
universalIdentifier: params.universalIdentifier,
|
||||
name: params.name,
|
||||
|
||||
@@ -50,6 +50,9 @@ export class ApplicationEntity extends WorkspaceRelatedEntity {
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
description: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
logo: string | null;
|
||||
|
||||
// TODO should not be nullable
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
version: string | null;
|
||||
|
||||
@@ -34,6 +34,11 @@ export class ApplicationDTO {
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
logo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
|
||||
+2
@@ -5,6 +5,7 @@ export const fromFlatApplicationToApplicationDto = ({
|
||||
canBeUninstalled,
|
||||
description,
|
||||
id,
|
||||
logo,
|
||||
name,
|
||||
packageJsonChecksum,
|
||||
packageJsonFileId,
|
||||
@@ -19,6 +20,7 @@ export const fromFlatApplicationToApplicationDto = ({
|
||||
canBeUninstalled,
|
||||
description: description ?? undefined,
|
||||
id,
|
||||
logo: logo ?? undefined,
|
||||
name,
|
||||
objects: [],
|
||||
packageJsonChecksum: packageJsonChecksum ?? undefined,
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ const MOCK_FLAT_APPLICATION: FlatApplication = {
|
||||
universalIdentifier: '20202020-81ee-42da-a281-668632f32fe7',
|
||||
name: 'Workspace Custom Application',
|
||||
description: null,
|
||||
logo: null,
|
||||
workspaceId: 'workspace-id',
|
||||
version: null,
|
||||
sourceType: ApplicationRegistrationSourceType.LOCAL,
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ const MOCK_FLAT_APPLICATION: FlatApplication = {
|
||||
universalIdentifier: '20202020-81ee-42da-a281-668632f32fe7',
|
||||
name: 'Workspace Custom Application',
|
||||
description: null,
|
||||
logo: null,
|
||||
version: null,
|
||||
workspaceId: 'workspace-id',
|
||||
sourceType: ApplicationRegistrationSourceType.LOCAL,
|
||||
|
||||
@@ -10,6 +10,7 @@ export const APPLICATION_GQL_FIELDS = `
|
||||
id
|
||||
name
|
||||
description
|
||||
logo
|
||||
version
|
||||
universalIdentifier
|
||||
canBeUninstalled
|
||||
|
||||
-3
@@ -13,7 +13,6 @@ const MARKETPLACE_QUERY = `
|
||||
description
|
||||
author
|
||||
sourcePackage
|
||||
icon
|
||||
category
|
||||
logo
|
||||
isFeatured
|
||||
@@ -135,7 +134,6 @@ describe('Marketplace Catalog Sync (integration)', () => {
|
||||
sourcePackage: '@twentyhq/app-data-enrichment',
|
||||
manifest: {
|
||||
application: {
|
||||
icon: 'IconSparkles',
|
||||
category: 'Data',
|
||||
},
|
||||
},
|
||||
@@ -175,7 +173,6 @@ describe('Marketplace Catalog Sync (integration)', () => {
|
||||
expect(curatedApp).toBeDefined();
|
||||
expect(curatedApp.name).toBe('Data Enrichment');
|
||||
expect(curatedApp.category).toBe('Data');
|
||||
expect(curatedApp.icon).toBe('IconSparkles');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
-1
@@ -14,7 +14,6 @@ export const buildBaseManifest = ({
|
||||
defaultRoleUniversalIdentifier: roleId,
|
||||
displayName: 'Test Application',
|
||||
description: 'Test application',
|
||||
icon: 'IconTestPipe',
|
||||
applicationVariables: {},
|
||||
packageJsonChecksum: null,
|
||||
yarnLockChecksum: null,
|
||||
|
||||
@@ -8,7 +8,6 @@ export type ApplicationManifest = SyncableEntityOptions & {
|
||||
defaultRoleUniversalIdentifier: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
applicationVariables?: ApplicationVariables;
|
||||
serverVariables?: ServerVariables;
|
||||
author?: string;
|
||||
|
||||
@@ -150,13 +150,15 @@ export const Avatar = ({
|
||||
|
||||
const showBackgroundColor = showPlaceholder;
|
||||
|
||||
const showBorderColor = showPlaceholder;
|
||||
|
||||
return (
|
||||
<StyledAvatar
|
||||
size={size}
|
||||
backgroundColor={
|
||||
Icon ? 'inherit' : showBackgroundColor ? fixedBackgroundColor : 'none'
|
||||
}
|
||||
borderColor={fixedBorderColor}
|
||||
borderColor={showBorderColor ? fixedBorderColor : undefined}
|
||||
color={fixedColor}
|
||||
clickable={!isUndefined(onClick)}
|
||||
rounded={type === 'rounded'}
|
||||
|
||||
Reference in New Issue
Block a user