[Apps] Content + permission tabs for marketplace and installed apps (#17888)

In this PR
- Content tab for marketplace apps and installed apps: complies with
[figma](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=34845-126856);
addition of field section showing fields added to standard objects;
initiative: arrow opens a sub-table of fields rows. (suggested because
1/ for marketplace apps we cannot redirect to the actual object page in
settings since the object does not exist yet in the workspace 2/ since
we dont have an quick "go back to application page" option, it can be
annoying to be redirected to a different setting page when we just want
to look at the content of the app objects)
- Permission tab for marketplace apps and installed apps
- left to do - settings tab (in another PR)

There are breaking changes but it's behind a feature flag not exposed
(access to marketplace) so not problematic


marketplace apps

https://github.com/user-attachments/assets/4c660101-50fc-47ce-b90a-8d6f17db5e74

installed apps

https://github.com/user-attachments/assets/c9229ee1-e75f-4cad-8766-758b2c5b37b4
This commit is contained in:
Marie
2026-02-12 18:38:53 +01:00
committed by GitHub
parent 13c2234856
commit 90a30263ac
29 changed files with 1967 additions and 199 deletions
@@ -2027,11 +2027,16 @@ export type MarketplaceApp = {
aboutDescription: Scalars['String'];
author: Scalars['String'];
category: Scalars['String'];
defaultRole?: Maybe<MarketplaceAppDefaultRole>;
description: Scalars['String'];
fields: Array<MarketplaceAppField>;
frontComponents: Array<MarketplaceAppFrontComponent>;
icon: Scalars['String'];
id: Scalars['String'];
logicFunctions: Array<MarketplaceAppLogicFunction>;
logo?: Maybe<Scalars['String']>;
name: Scalars['String'];
objects: Array<MarketplaceAppObject>;
providers: Array<Scalars['String']>;
screenshots: Array<Scalars['String']>;
termsUrl?: Maybe<Scalars['String']>;
@@ -2039,6 +2044,75 @@ export type MarketplaceApp = {
websiteUrl?: Maybe<Scalars['String']>;
};
export type MarketplaceAppDefaultRole = {
__typename?: 'MarketplaceAppDefaultRole';
canAccessAllTools: Scalars['Boolean'];
canDestroyAllObjectRecords: Scalars['Boolean'];
canReadAllObjectRecords: Scalars['Boolean'];
canSoftDeleteAllObjectRecords: Scalars['Boolean'];
canUpdateAllObjectRecords: Scalars['Boolean'];
canUpdateAllSettings: Scalars['Boolean'];
description?: Maybe<Scalars['String']>;
fieldPermissions: Array<MarketplaceAppRoleFieldPermission>;
id: Scalars['String'];
label: Scalars['String'];
objectPermissions: Array<MarketplaceAppRoleObjectPermission>;
permissionFlags: Array<Scalars['String']>;
};
export type MarketplaceAppField = {
__typename?: 'MarketplaceAppField';
description?: Maybe<Scalars['String']>;
icon?: Maybe<Scalars['String']>;
label: Scalars['String'];
name: Scalars['String'];
objectUniversalIdentifier?: Maybe<Scalars['String']>;
type: Scalars['String'];
universalIdentifier?: Maybe<Scalars['String']>;
};
export type MarketplaceAppFrontComponent = {
__typename?: 'MarketplaceAppFrontComponent';
description?: Maybe<Scalars['String']>;
name: Scalars['String'];
};
export type MarketplaceAppLogicFunction = {
__typename?: 'MarketplaceAppLogicFunction';
description?: Maybe<Scalars['String']>;
name: Scalars['String'];
timeoutSeconds?: Maybe<Scalars['Int']>;
};
export type MarketplaceAppObject = {
__typename?: 'MarketplaceAppObject';
description?: Maybe<Scalars['String']>;
fields: Array<MarketplaceAppField>;
icon?: Maybe<Scalars['String']>;
labelPlural: Scalars['String'];
labelSingular: Scalars['String'];
namePlural: Scalars['String'];
nameSingular: Scalars['String'];
universalIdentifier: Scalars['String'];
};
export type MarketplaceAppRoleFieldPermission = {
__typename?: 'MarketplaceAppRoleFieldPermission';
canReadFieldValue?: Maybe<Scalars['Boolean']>;
canUpdateFieldValue?: Maybe<Scalars['Boolean']>;
fieldUniversalIdentifier: Scalars['String'];
objectUniversalIdentifier: Scalars['String'];
};
export type MarketplaceAppRoleObjectPermission = {
__typename?: 'MarketplaceAppRoleObjectPermission';
canDestroyObjectRecords?: Maybe<Scalars['Boolean']>;
canReadObjectRecords?: Maybe<Scalars['Boolean']>;
canSoftDeleteObjectRecords?: Maybe<Scalars['Boolean']>;
canUpdateObjectRecords?: Maybe<Scalars['Boolean']>;
objectUniversalIdentifier: Scalars['String'];
};
export enum ModelProvider {
ANTHROPIC = 'ANTHROPIC',
GROQ = 'GROQ',
@@ -5261,7 +5335,7 @@ export type UpdateOneApplicationVariableMutationVariables = Exact<{
export type UpdateOneApplicationVariableMutation = { __typename?: 'Mutation', updateOneApplicationVariable: boolean };
export type ApplicationFieldsFragment = { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, availablePackages: any, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array<string>, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, logicFunctions: Array<{ __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string }> };
export type ApplicationFieldsFragment = { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, defaultRoleId?: string | null, availablePackages: any, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array<string>, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, logicFunctions: Array<{ __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string }> };
export type FindManyApplicationsQueryVariables = Exact<{ [key: string]: never; }>;
@@ -5273,7 +5347,7 @@ export type FindOneApplicationQueryVariables = Exact<{
}>;
export type FindOneApplicationQuery = { __typename?: 'Query', findOneApplication: { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, availablePackages: any, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array<string>, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, logicFunctions: Array<{ __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string }> } };
export type FindOneApplicationQuery = { __typename?: 'Query', findOneApplication: { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, defaultRoleId?: string | null, availablePackages: any, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array<string>, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, logicFunctions: Array<{ __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string }> } };
export type UploadFileMutationVariables = Exact<{
file: Scalars['Upload'];
@@ -5672,10 +5746,12 @@ export type GetLogicFunctionSourceCodeQueryVariables = Exact<{
export type GetLogicFunctionSourceCodeQuery = { __typename?: 'Query', getLogicFunctionSourceCode?: string | null };
export type MarketplaceAppFieldsFragment = { __typename?: 'MarketplaceApp', id: string, name: string, description: string, icon: string, version: string, author: string, category: string, logo?: string | null, screenshots: Array<string>, aboutDescription: string, providers: Array<string>, websiteUrl?: string | null, termsUrl?: string | null, objects: Array<{ __typename?: 'MarketplaceAppObject', universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, fields: Array<{ __typename?: 'MarketplaceAppField', universalIdentifier?: string | null, name: string, type: string, label: string, description?: string | null, icon?: string | null }> }>, fields: Array<{ __typename?: 'MarketplaceAppField', name: string, type: string, label: string, description?: string | null, icon?: string | null, objectUniversalIdentifier?: string | null }>, logicFunctions: Array<{ __typename?: 'MarketplaceAppLogicFunction', name: string, description?: string | null, timeoutSeconds?: number | null }>, frontComponents: Array<{ __typename?: 'MarketplaceAppFrontComponent', name: string, description?: string | null }>, defaultRole?: { __typename?: 'MarketplaceAppDefaultRole', id: string, label: string, description?: string | null, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canUpdateAllSettings: boolean, canAccessAllTools: boolean, permissionFlags: Array<string>, objectPermissions: Array<{ __typename?: 'MarketplaceAppRoleObjectPermission', objectUniversalIdentifier: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }>, fieldPermissions: Array<{ __typename?: 'MarketplaceAppRoleFieldPermission', objectUniversalIdentifier: string, fieldUniversalIdentifier: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null }> } | null };
export type FindManyMarketplaceAppsQueryVariables = Exact<{ [key: string]: never; }>;
export type FindManyMarketplaceAppsQuery = { __typename?: 'Query', findManyMarketplaceApps: Array<{ __typename?: 'MarketplaceApp', id: string, name: string, description: string, icon: string, version: string, author: string, category: string, logo?: string | null, screenshots: Array<string>, aboutDescription: string, providers: Array<string>, websiteUrl?: string | null, termsUrl?: string | null }> };
export type FindManyMarketplaceAppsQuery = { __typename?: 'Query', findManyMarketplaceApps: Array<{ __typename?: 'MarketplaceApp', id: string, name: string, description: string, icon: string, version: string, author: string, category: string, logo?: string | null, screenshots: Array<string>, aboutDescription: string, providers: Array<string>, websiteUrl?: string | null, termsUrl?: string | null, objects: Array<{ __typename?: 'MarketplaceAppObject', universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, fields: Array<{ __typename?: 'MarketplaceAppField', universalIdentifier?: string | null, name: string, type: string, label: string, description?: string | null, icon?: string | null }> }>, fields: Array<{ __typename?: 'MarketplaceAppField', name: string, type: string, label: string, description?: string | null, icon?: string | null, objectUniversalIdentifier?: string | null }>, logicFunctions: Array<{ __typename?: 'MarketplaceAppLogicFunction', name: string, description?: string | null, timeoutSeconds?: number | null }>, frontComponents: Array<{ __typename?: 'MarketplaceAppFrontComponent', name: string, description?: string | null }>, defaultRole?: { __typename?: 'MarketplaceAppDefaultRole', id: string, label: string, description?: string | null, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canUpdateAllSettings: boolean, canAccessAllTools: boolean, permissionFlags: Array<string>, objectPermissions: Array<{ __typename?: 'MarketplaceAppRoleObjectPermission', objectUniversalIdentifier: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }>, fieldPermissions: Array<{ __typename?: 'MarketplaceAppRoleFieldPermission', objectUniversalIdentifier: string, fieldUniversalIdentifier: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null }> } | null }> };
export type NavigationMenuItemFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string };
@@ -6788,6 +6864,7 @@ export const ApplicationFieldsFragmentDoc = gql`
version
universalIdentifier
canBeUninstalled
defaultRoleId
availablePackages
applicationVariables {
id
@@ -7097,6 +7174,82 @@ export const PageLayoutFragmentFragmentDoc = gql`
}
}
${PageLayoutTabFragmentFragmentDoc}`;
export const MarketplaceAppFieldsFragmentDoc = gql`
fragment MarketplaceAppFields on MarketplaceApp {
id
name
description
icon
version
author
category
logo
screenshots
aboutDescription
providers
websiteUrl
termsUrl
objects {
universalIdentifier
nameSingular
namePlural
labelSingular
labelPlural
description
icon
fields {
universalIdentifier
name
type
label
description
icon
}
}
fields {
name
type
label
description
icon
objectUniversalIdentifier
}
logicFunctions {
name
description
timeoutSeconds
}
frontComponents {
name
description
}
defaultRole {
id
label
description
canReadAllObjectRecords
canUpdateAllObjectRecords
canSoftDeleteAllObjectRecords
canDestroyAllObjectRecords
canUpdateAllSettings
canAccessAllTools
objectPermissions {
objectUniversalIdentifier
canReadObjectRecords
canUpdateObjectRecords
canSoftDeleteObjectRecords
canDestroyObjectRecords
}
fieldPermissions {
objectUniversalIdentifier
fieldUniversalIdentifier
canReadFieldValue
canUpdateFieldValue
}
permissionFlags
}
}
`;
export const NavigationMenuItemFieldsFragmentDoc = gql`
fragment NavigationMenuItemFields on NavigationMenuItem {
id
@@ -10463,22 +10616,10 @@ export type GetLogicFunctionSourceCodeQueryResult = Apollo.QueryResult<GetLogicF
export const FindManyMarketplaceAppsDocument = gql`
query FindManyMarketplaceApps {
findManyMarketplaceApps {
id
name
description
icon
version
author
category
logo
screenshots
aboutDescription
providers
websiteUrl
termsUrl
...MarketplaceAppFields
}
}
`;
${MarketplaceAppFieldsFragmentDoc}`;
/**
* __useFindManyMarketplaceAppsQuery__
@@ -14,6 +14,7 @@ export const APPLICATION_FRAGMENT = gql`
version
universalIdentifier
canBeUninstalled
defaultRoleId
availablePackages
applicationVariables {
id
@@ -0,0 +1,78 @@
import gql from 'graphql-tag';
export const MARKETPLACE_APP_FRAGMENT = gql`
fragment MarketplaceAppFields on MarketplaceApp {
id
name
description
icon
version
author
category
logo
screenshots
aboutDescription
providers
websiteUrl
termsUrl
objects {
universalIdentifier
nameSingular
namePlural
labelSingular
labelPlural
description
icon
fields {
universalIdentifier
name
type
label
description
icon
}
}
fields {
name
type
label
description
icon
objectUniversalIdentifier
}
logicFunctions {
name
description
timeoutSeconds
}
frontComponents {
name
description
}
defaultRole {
id
label
description
canReadAllObjectRecords
canUpdateAllObjectRecords
canSoftDeleteAllObjectRecords
canDestroyAllObjectRecords
canUpdateAllSettings
canAccessAllTools
objectPermissions {
objectUniversalIdentifier
canReadObjectRecords
canUpdateObjectRecords
canSoftDeleteObjectRecords
canDestroyObjectRecords
}
fieldPermissions {
objectUniversalIdentifier
fieldUniversalIdentifier
canReadFieldValue
canUpdateFieldValue
}
permissionFlags
}
}
`;
@@ -1,21 +1,12 @@
import gql from 'graphql-tag';
import { MARKETPLACE_APP_FRAGMENT } from '@/marketplace/graphql/fragments/marketplaceAppFragment';
export const FIND_MANY_MARKETPLACE_APPS = gql`
${MARKETPLACE_APP_FRAGMENT}
query FindManyMarketplaceApps {
findManyMarketplaceApps {
id
name
description
icon
version
author
category
logo
screenshots
aboutDescription
providers
websiteUrl
termsUrl
...MarketplaceAppFields
}
}
`;
@@ -5,8 +5,12 @@ import { type ReactNode } from 'react';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
import {
StyledActionTableCell,
StyledNameTableCell,
StyledObjectTableRow,
} 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 { useIcons } from 'twenty-ui/display';
export type SettingsObjectMetadataItemTableRowProps = {
@@ -16,15 +20,6 @@ export type SettingsObjectMetadataItemTableRowProps = {
totalObjectCount: number;
};
export const StyledObjectTableRow = styled(TableRow)`
grid-template-columns: 180px 98.7px 98.7px 98.7px 36px;
`;
const StyledNameTableCell = styled(TableCell)`
color: ${({ theme }) => theme.font.color.primary};
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledNameContainer = styled.div`
display: flex;
align-items: center;
@@ -54,11 +49,6 @@ const StyledInactiveLabel = styled.span`
}
`;
const StyledActionTableCell = styled(TableCell)`
justify-content: center;
padding-right: ${({ theme }) => theme.spacing(1)};
`;
export const SettingsObjectMetadataItemTableRow = ({
action,
objectMetadataItem,
@@ -0,0 +1,20 @@
import styled from '@emotion/styled';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
export const SETTINGS_OBJECT_TABLE_COLUMN_WIDTH = '98.7px';
export const StyledObjectTableRow = styled(TableRow)`
grid-template-columns: 180px ${SETTINGS_OBJECT_TABLE_COLUMN_WIDTH} ${SETTINGS_OBJECT_TABLE_COLUMN_WIDTH} ${SETTINGS_OBJECT_TABLE_COLUMN_WIDTH} 36px;
`;
export const StyledNameTableCell = styled(TableCell)`
color: ${({ theme }) => theme.font.color.primary};
gap: ${({ theme }) => theme.spacing(2)};
`;
export const StyledActionTableCell = styled(TableCell)`
justify-content: center;
padding-right: ${({ theme }) => theme.spacing(1)};
`;
@@ -1,3 +1,4 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsRolePermissionsObjectLevelSection } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection';
import { SettingsRolePermissionsObjectsSection } from '@/settings/roles/role-permissions/objects-permissions/components/SettingsRolePermissionsObjectsSection';
import { SettingsRolePermissionsSettingsSection } from '@/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsSection';
@@ -14,12 +15,14 @@ type SettingsRolePermissionsProps = {
roleId: string;
isEditable: boolean;
fromAgentId?: string;
objectMetadataItemsFromMarketplaceApp?: ObjectMetadataItem[];
};
export const SettingsRolePermissions = ({
roleId,
isEditable,
fromAgentId,
objectMetadataItemsFromMarketplaceApp,
}: SettingsRolePermissionsProps) => {
return (
<StyledRolePermissionsContainer>
@@ -30,6 +33,10 @@ export const SettingsRolePermissions = ({
<SettingsRolePermissionsObjectLevelSection
roleId={roleId}
fromAgentId={fromAgentId}
isEditable={isEditable}
objectMetadataItemsFromMarketplaceApp={
objectMetadataItemsFromMarketplaceApp
}
/>
<SettingsRolePermissionsSettingsSection
roleId={roleId}
@@ -1,3 +1,4 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsRolePermissionsObjectLevelTableHeader } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableHeader';
import { SettingsRolePermissionsObjectLevelTableRow } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRow';
import { useFilterObjectMetadataItemsWithPermissionOverride } from '@/settings/roles/role-permissions/object-level-permissions/hooks/useFilterObjectWithPermissionOverride';
@@ -7,6 +8,7 @@ import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { SettingsPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -30,7 +32,9 @@ const StyledTableRows = styled.div`
type SettingsRolePermissionsObjectLevelSectionProps = {
roleId: string;
isEditable: boolean;
fromAgentId?: string;
objectMetadataItemsFromMarketplaceApp?: ObjectMetadataItem[];
};
const StyledNoOverride = styled(TableCell)`
@@ -40,6 +44,8 @@ const StyledNoOverride = styled(TableCell)`
export const SettingsRolePermissionsObjectLevelSection = ({
roleId,
fromAgentId,
isEditable,
objectMetadataItemsFromMarketplaceApp,
}: SettingsRolePermissionsObjectLevelSectionProps) => {
const navigateSettings = useNavigateSettings();
@@ -50,19 +56,45 @@ export const SettingsRolePermissionsObjectLevelSection = ({
const { objectMetadataItemsThatCanHavePermission } =
useObjectMetadataItemsThatCanHavePermission();
const allObjectMetadataItems = useMemo(() => {
if (
!isDefined(objectMetadataItemsFromMarketplaceApp) ||
objectMetadataItemsFromMarketplaceApp.length === 0
) {
return objectMetadataItemsThatCanHavePermission;
}
const existingIds = new Set(
objectMetadataItemsThatCanHavePermission.map((item) => item.id),
);
const additionalObjectMetadataItemsFromApp =
objectMetadataItemsFromMarketplaceApp.filter(
(item) => !existingIds.has(item.id),
);
return [
...objectMetadataItemsThatCanHavePermission,
...additionalObjectMetadataItemsFromApp,
];
}, [
objectMetadataItemsThatCanHavePermission,
objectMetadataItemsFromMarketplaceApp,
]);
const { filterObjectMetadataItemsWithPermissionOverride } =
useFilterObjectMetadataItemsWithPermissionOverride({
roleId,
});
const objectMetadataItemsWithPermissionOverride =
objectMetadataItemsThatCanHavePermission.filter(
allObjectMetadataItems.filter(
filterObjectMetadataItemsWithPermissionOverride,
);
const allObjectsHaveSetPermission =
objectMetadataItemsWithPermissionOverride.length ===
objectMetadataItemsThatCanHavePermission.length;
allObjectMetadataItems.length;
const handleAddRule = () => {
navigateSettings(
@@ -91,6 +123,7 @@ export const SettingsRolePermissionsObjectLevelSection = ({
objectMetadataItem={objectMetadataItem}
roleId={roleId}
fromAgentId={fromAgentId}
isEditable={isEditable}
/>
),
)
@@ -101,18 +134,20 @@ export const SettingsRolePermissionsObjectLevelSection = ({
)}
</StyledTableRows>
</Table>
<StyledCreateObjectOverrideSection>
<Button
Icon={IconPlus}
title={t`Add rule`}
variant="secondary"
size="small"
disabled={
!settingsDraftRole.isEditable || allObjectsHaveSetPermission
}
onClick={handleAddRule}
/>
</StyledCreateObjectOverrideSection>
{isEditable && (
<StyledCreateObjectOverrideSection>
<Button
Icon={IconPlus}
title={t`Add rule`}
variant="secondary"
size="small"
disabled={
!settingsDraftRole.isEditable || allObjectsHaveSetPermission
}
onClick={handleAddRule}
/>
</StyledCreateObjectOverrideSection>
)}
</Section>
);
};
@@ -1,4 +1,4 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useObjectPermissionDerivedStates } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useObjectPermissionDerivedStates';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useLingui } from '@lingui/react/macro';
@@ -6,12 +6,12 @@ import { useRecoilValue } from 'recoil';
type SettingsRolePermissionsObjectLevelSeeFieldsValueForObjectProps = {
roleId: string;
objectMetadataItemId: string;
objectMetadataItem: ObjectMetadataItem;
};
export const SettingsRolePermissionsObjectLevelSeeFieldsValueForObject = ({
roleId,
objectMetadataItemId,
objectMetadataItem,
}: SettingsRolePermissionsObjectLevelSeeFieldsValueForObjectProps) => {
const { t } = useLingui();
@@ -19,9 +19,7 @@ export const SettingsRolePermissionsObjectLevelSeeFieldsValueForObject = ({
settingsDraftRoleFamilyState(roleId),
);
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataItemId,
});
const objectMetadataItemId = objectMetadataItem.id;
const restrictableFieldMetadataItems = objectMetadataItem.fields.filter(
(fieldMetadataItem) => !fieldMetadataItem.isSystem,
@@ -29,12 +29,14 @@ const StyledNameLabel = styled.div`
type SettingsRolePermissionsObjectLevelTableRowProps = {
objectMetadataItem: ObjectMetadataItem;
roleId: string;
isEditable?: boolean;
fromAgentId?: string;
};
export const SettingsRolePermissionsObjectLevelTableRow = ({
objectMetadataItem,
roleId,
isEditable = true,
fromAgentId,
}: SettingsRolePermissionsObjectLevelTableRowProps) => {
const { getIcon } = useIcons();
@@ -43,7 +45,6 @@ export const SettingsRolePermissionsObjectLevelTableRow = ({
const Icon = getIcon(objectMetadataItem.icon);
const objectLabelPlural = objectMetadataItem.labelPlural;
const navigationPath = getSettingsPath(SettingsPath.RoleObjectLevel, {
roleId: roleId,
objectMetadataId: objectMetadataItem.id,
@@ -55,7 +56,7 @@ export const SettingsRolePermissionsObjectLevelTableRow = ({
return (
<TableRow
to={navigationUrl}
to={isEditable ? navigationUrl : undefined}
gridAutoColumns={OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS}
>
<StyledNameTableCell>
@@ -80,21 +81,23 @@ export const SettingsRolePermissionsObjectLevelTableRow = ({
<TableCell>
<SettingsRolePermissionsObjectLevelSeeFieldsValueForObject
roleId={roleId}
objectMetadataItemId={objectMetadataItem.id}
objectMetadataItem={objectMetadataItem}
/>
</TableCell>
<TableCell>
<SettingsRolePermissionsObjectLevelUpdateFieldsValueForObject
roleId={roleId}
objectMetadataItemId={objectMetadataItem.id}
/>
</TableCell>
<TableCell align="right">
<IconChevronRight
size={theme.icon.size.md}
color={theme.font.color.tertiary}
objectMetadataItem={objectMetadataItem}
/>
</TableCell>
{isEditable && (
<TableCell align="right">
<IconChevronRight
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
</TableCell>
)}
</TableRow>
);
};
@@ -1,4 +1,4 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { filterUserFacingFieldMetadataItems } from '@/object-metadata/utils/filterUserFacingFieldMetadataItems';
import { useObjectPermissionDerivedStates } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useObjectPermissionDerivedStates';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
@@ -7,12 +7,12 @@ import { useRecoilValue } from 'recoil';
type SettingsRolePermissionsObjectLevelUpdateFieldsValueForObjectProps = {
roleId: string;
objectMetadataItemId: string;
objectMetadataItem: ObjectMetadataItem;
};
export const SettingsRolePermissionsObjectLevelUpdateFieldsValueForObject = ({
roleId,
objectMetadataItemId,
objectMetadataItem,
}: SettingsRolePermissionsObjectLevelUpdateFieldsValueForObjectProps) => {
const { t } = useLingui();
@@ -20,9 +20,7 @@ export const SettingsRolePermissionsObjectLevelUpdateFieldsValueForObject = ({
settingsDraftRoleFamilyState(roleId),
);
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataItemId,
});
const objectMetadataItemId = objectMetadataItem.id;
const restrictableFieldMetadataItems = objectMetadataItem.fields.filter(
filterUserFacingFieldMetadataItems,
@@ -0,0 +1,9 @@
import styled from '@emotion/styled';
import { TableRow } from '@/ui/layout/table/components/TableRow';
const StyledTableSubRow = styled(TableRow)`
padding-left: ${({ theme }) => theme.spacing(4)};
`;
export const TableSubRow = StyledTableSubRow;
@@ -5,7 +5,12 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBa
import { useParams } from 'react-router-dom';
import { useFindOneApplicationQuery } from '~/generated-metadata/graphql';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { IconInfoCircle, IconSettings, IconBox } from 'twenty-ui/display';
import {
IconBox,
IconInfoCircle,
IconLock,
IconSettings,
} from 'twenty-ui/display';
import { SettingsApplicationDetailSkeletonLoader } from '~/pages/settings/applications/components/SettingsApplicationDetailSkeletonLoader';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
@@ -13,6 +18,7 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
import { SettingsApplicationDetailContentTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailContentTab';
import { SettingsApplicationDetailAboutTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailAboutTab';
import { SettingsApplicationDetailSettingsTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailSettingsTab';
import { SettingsApplicationPermissionsTab } from '~/pages/settings/applications/tabs/SettingsApplicationPermissionsTab';
const APPLICATION_DETAIL_ID = 'application-detail-id';
@@ -40,6 +46,7 @@ export const SettingsApplicationDetails = () => {
const tabs = [
{ id: 'about', title: t`About`, Icon: IconInfoCircle },
{ id: 'content', title: t`Content`, Icon: IconBox },
{ id: 'permissions', title: t`Permissions`, Icon: IconLock },
{ id: 'settings', title: t`Settings`, Icon: IconSettings },
];
@@ -51,6 +58,12 @@ export const SettingsApplicationDetails = () => {
return (
<SettingsApplicationDetailContentTab application={application} />
);
case 'permissions':
return (
<SettingsApplicationPermissionsTab
defaultRoleId={application?.defaultRoleId}
/>
);
case 'settings':
return (
<SettingsApplicationDetailSettingsTab application={application} />
@@ -28,6 +28,8 @@ import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { PermissionFlagType } from '~/generated-metadata/graphql';
import { useMarketplaceApps } from '~/pages/settings/applications/hooks/useMarketplaceApps';
import { SettingsApplicationPermissionsTab } from '~/pages/settings/applications/tabs/SettingsApplicationPermissionsTab';
import { SettingsAvailableApplicationDetailContentTab } from '~/pages/settings/applications/tabs/SettingsAvailableApplicationDetailContentTab';
const AVAILABLE_APPLICATION_DETAIL_ID = 'available-application-detail';
@@ -371,7 +373,7 @@ export const SettingsAvailableApplicationDetails = () => {
<StyledSidebarSection>
<StyledSidebarLabel>{t`Developers links`}</StyledSidebarLabel>
<StyledLink
href={application.websiteUrl}
href={application.websiteUrl ?? undefined}
target="_blank"
rel="noopener noreferrer"
>
@@ -379,7 +381,7 @@ export const SettingsAvailableApplicationDetails = () => {
{t`Website`}
</StyledLink>
<StyledLink
href={application.termsUrl}
href={application.termsUrl ?? undefined}
target="_blank"
rel="noopener noreferrer"
>
@@ -392,9 +394,18 @@ export const SettingsAvailableApplicationDetails = () => {
</>
);
case 'content':
return <div>{t`Content tab`}</div>;
return (
<SettingsAvailableApplicationDetailContentTab
application={application}
/>
);
case 'permissions':
return <div>{t`Permissions tab`}</div>;
return (
<SettingsApplicationPermissionsTab
marketplaceAppDefaultRole={application.defaultRole}
marketplaceAppObjects={application.objects}
/>
);
case 'settings':
return <div>{t`Settings tab`}</div>;
default:
@@ -425,9 +436,9 @@ export const SettingsAvailableApplicationDetails = () => {
<StyledHeader>
<StyledHeaderLeft>
<StyledLogo>
{application.logoPath ? (
{application.logo ? (
<StyledLogoImage
src={application.logoPath}
src={application.logo}
alt={application.name}
/>
) : (
@@ -0,0 +1,161 @@
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 '@emotion/styled';
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 { SettingsApplicationDataTableRow } from '~/pages/settings/applications/components/SettingsApplicationDataTableRow';
import { normalizeSearchText } from '~/utils/normalizeSearchText';
export type ApplicationDataTableFieldItem = {
key: string;
label: string;
icon?: string;
type: string;
};
export type ApplicationDataTableRow = {
key: string;
labelPlural: string;
icon?: string;
fieldsCount: number;
link?: string;
fields?: ApplicationDataTableFieldItem[];
tagItem: {
isCustom?: boolean;
isRemote?: boolean;
applicationId?: string | null;
};
};
const MAIN_ROW_GRID_COLUMNS = `180px 1fr ${SETTINGS_OBJECT_TABLE_COLUMN_WIDTH} 36px`;
const StyledEmptyHeader = styled(TableHeader)`
min-width: 0;
`;
const StyledSearchInputContainer = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledEmptyState = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
padding: ${({ theme }) => theme.spacing(8)};
text-align: center;
`;
export const SettingsApplicationDataTable = ({
objectRows,
fieldGroupRows,
}: {
objectRows: ApplicationDataTableRow[];
fieldGroupRows: ApplicationDataTableRow[];
}) => {
const [searchTerm, setSearchTerm] = useState('');
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
const toggleRow = (key: string) => {
setExpandedRows((previous) => {
const next = new Set(previous);
if (next.has(key)) {
next.delete(key);
} else {
next.add(key);
}
return next;
});
};
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 ? (
<StyledEmptyState>{t`No object found`}</StyledEmptyState>
) : (
<Table>
<TableRow gridAutoColumns={MAIN_ROW_GRID_COLUMNS}>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`App`}</TableHeader>
<TableHeader align="right">{t`Fields`}</TableHeader>
<StyledEmptyHeader />
</TableRow>
{shouldDisplayObjects && (
<TableSection title={t`Objects`}>
{filteredObjectRows.map((row) => (
<SettingsApplicationDataTableRow
key={row.key}
row={row}
isExpanded={expandedRows.has(row.key)}
onToggle={() => toggleRow(row.key)}
/>
))}
</TableSection>
)}
{shouldDisplayFields && (
<TableSection title={t`Fields`}>
{filteredFieldGroupRows.map((row) => (
<SettingsApplicationDataTableRow
key={row.key}
row={row}
isExpanded={expandedRows.has(row.key)}
onToggle={() => toggleRow(row.key)}
/>
))}
</TableSection>
)}
</Table>
)}
</Section>
);
};
@@ -0,0 +1,117 @@
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
import { SettingsObjectFieldDataType } from '@/settings/data-model/object-details/components/SettingsObjectFieldDataType';
import {
StyledActionTableCell,
StyledNameTableCell,
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents';
import { type SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { TableSubRow } from '@/ui/layout/table/components/TableSubRow';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronDown, IconChevronRight, useIcons } from 'twenty-ui/display';
import { type ApplicationDataTableRow } from '~/pages/settings/applications/components/SettingsApplicationDataTable';
const MAIN_ROW_GRID_COLUMNS = '180px 1fr 98.7px 36px';
const FIELD_SUB_ROW_GRID_COLUMNS = '180px 1fr';
const StyledFieldDivider = styled.div`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
`;
const StyledFieldNameTableCell = styled(StyledNameTableCell)`
color: ${({ theme }) => theme.font.color.secondary};
`;
export const SettingsApplicationDataTableRow = ({
row,
isExpanded,
onToggle,
}: {
row: ApplicationDataTableRow;
isExpanded: boolean;
onToggle: () => void;
}) => {
const theme = useTheme();
const { getIcon } = useIcons();
const Icon = getIcon(row.icon);
const hasFields = isDefined(row.fields) && row.fields.length > 0;
return (
<>
<TableRow
gridAutoColumns={MAIN_ROW_GRID_COLUMNS}
onClick={hasFields ? onToggle : undefined}
to={hasFields ? undefined : row.link}
>
<StyledNameTableCell>
{isDefined(Icon) && (
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
)}
{row.labelPlural}
</StyledNameTableCell>
<TableCell>
<SettingsItemTypeTag item={row.tagItem} />
</TableCell>
<TableCell align="right">{row.fieldsCount}</TableCell>
<StyledActionTableCell>
{hasFields && isExpanded ? (
<IconChevronDown
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
) : (
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
)}
</StyledActionTableCell>
</TableRow>
{hasFields && isExpanded && (
<>
<StyledFieldDivider />
<TableSubRow gridAutoColumns={FIELD_SUB_ROW_GRID_COLUMNS}>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Type`}</TableHeader>
</TableSubRow>
</>
)}
{hasFields &&
isExpanded &&
row.fields?.map((field) => {
const FieldIcon = getIcon(field.icon);
return (
<TableSubRow
key={field.key}
gridAutoColumns={FIELD_SUB_ROW_GRID_COLUMNS}
>
<StyledFieldNameTableCell>
{isDefined(FieldIcon) && (
<FieldIcon
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
)}
{field.label}
</StyledFieldNameTableCell>
<TableCell>
<SettingsObjectFieldDataType
value={field.type as SettingsFieldType}
/>
</TableCell>
</TableSubRow>
);
})}
</>
);
};
@@ -0,0 +1,55 @@
import { TableCell } from '@/ui/layout/table/components/TableCell';
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 { Table } from '@/ui/layout/table/components/Table';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
type SettingsApplicationNameDescriptionTableItem = {
name: string;
description?: string | null;
};
const StyledNameTableCell = styled(TableCell)`
color: ${({ theme }) => theme.font.color.primary};
gap: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsApplicationNameDescriptionTable = ({
title,
description,
sectionTitle,
items,
}: {
title: string;
description: string;
sectionTitle: string;
items: SettingsApplicationNameDescriptionTableItem[];
}) => {
if (items.length === 0) {
return null;
}
return (
<Section>
<H2Title title={title} description={description} />
<Table>
<TableRow gridAutoColumns="180px 1fr">
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Description`}</TableHeader>
</TableRow>
<TableSection title={sectionTitle}>
{items.map((item) => (
<TableRow key={item.name} gridAutoColumns="180px 1fr">
<StyledNameTableCell>{item.name}</StyledNameTableCell>
<TableCell>{item.description ?? ''}</TableCell>
</TableRow>
))}
</TableSection>
</Table>
</Section>
);
};
@@ -11,10 +11,10 @@ import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { Avatar } from 'twenty-ui/display';
import { Card } from 'twenty-ui/layout';
import { type AvailableApplication } from '~/pages/settings/applications/types/availableApplication';
import { type MarketplaceApp } from '~/generated-metadata/graphql';
type SettingsAvailableApplicationCardProps = {
application: AvailableApplication;
application: MarketplaceApp;
};
const StyledLink = styled(Link)`
@@ -41,7 +41,7 @@ export const SettingsAvailableApplicationCard = ({
<Card rounded fullWidth>
<StyledSettingsCardContent alignItems="flex-start" fullHeight>
<Avatar
avatarUrl={application.logoPath || null}
avatarUrl={application.logo || null}
placeholder={application.name}
placeholderColorSeed={application.name}
size="lg"
@@ -1,30 +1,38 @@
import { useFindManyMarketplaceAppsQuery } from '~/generated-metadata/graphql';
import { type AvailableApplication } from '~/pages/settings/applications/types/availableApplication';
import {
useFindManyMarketplaceAppsQuery,
type MarketplaceApp,
} from '~/generated-metadata/graphql';
export type MarketplaceAppWithContentCounts = MarketplaceApp & {
content: {
objects: number;
fields: number;
functions: number;
frontComponents: number;
};
};
export const useMarketplaceApps = () => {
const { data, loading, error } = useFindManyMarketplaceAppsQuery();
const marketplaceApps: AvailableApplication[] =
data?.findManyMarketplaceApps.map((app) => ({
id: app.id,
name: app.name,
description: app.description,
author: app.author,
logoPath: app.logo ?? '',
category: app.category,
aboutDescription: app.aboutDescription,
providers: app.providers,
screenshots: app.screenshots,
content: {
objects: 0,
fields: 0,
functions: 0,
frontComponents: 0,
},
version: app.version,
websiteUrl: app.websiteUrl ?? '',
termsUrl: app.termsUrl ?? '',
})) ?? [];
const marketplaceApps: MarketplaceAppWithContentCounts[] =
data?.findManyMarketplaceApps.map((app) => {
const totalFieldsCount =
(app.objects ?? []).reduce(
(count, appObject) => count + appObject.fields.length,
0,
) + (app.fields ?? []).length;
return {
...app,
content: {
objects: (app.objects ?? []).length,
fields: totalFieldsCount,
functions: (app.logicFunctions ?? []).length,
frontComponents: (app.frontComponents ?? []).length,
},
};
}) ?? [];
return {
data: marketplaceApps,
@@ -1,13 +1,19 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { SettingsLogicFunctionsTable } from '@/settings/logic-functions/components/SettingsLogicFunctionsTable';
import { t } from '@lingui/core/macro';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { type Application } from '~/generated-metadata/graphql';
import { SettingsAIAgentsTable } from '~/pages/settings/ai/components/SettingsAIAgentsTable';
import { SettingsObjectTable } from '~/pages/settings/data-model/SettingsObjectTable';
import {
SettingsApplicationDataTable,
type ApplicationDataTableFieldItem,
type ApplicationDataTableRow,
} from '~/pages/settings/applications/components/SettingsApplicationDataTable';
export const SettingsApplicationDetailContentTab = ({
application,
@@ -18,39 +24,140 @@ export const SettingsApplicationDetailContentTab = ({
}) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const applicationObjectIds = useMemo(
() => application?.objects.map((object) => object.id) ?? [],
[application?.objects],
);
const objectRows = useMemo((): ApplicationDataTableRow[] => {
if (!isDefined(application) || application.objects.length === 0) {
return [];
}
return objectMetadataItems
.filter((objectMetadataItem) =>
applicationObjectIds.includes(objectMetadataItem.id),
)
.map((objectMetadataItem) => {
const nonSystemFields = objectMetadataItem.fields.filter(
(field) => !field.isSystem,
);
const fields: ApplicationDataTableFieldItem[] = nonSystemFields.map(
(field) => ({
key: field.id,
label: field.label,
icon: field.icon ?? undefined,
type: field.type,
}),
);
return {
key: objectMetadataItem.nameSingular,
labelPlural: objectMetadataItem.labelPlural,
icon: objectMetadataItem.icon ?? undefined,
fieldsCount: nonSystemFields.length,
fields,
link: getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural: objectMetadataItem.namePlural,
}),
tagItem: {
isCustom: objectMetadataItem.isCustom,
isRemote: objectMetadataItem.isRemote,
applicationId: objectMetadataItem.applicationId,
},
};
});
}, [application, objectMetadataItems, applicationObjectIds]);
const fieldGroupRows = useMemo((): ApplicationDataTableRow[] => {
if (!isDefined(application)) {
return [];
}
const FIELD_GROUP_DENY_LIST = ['timelineActivity', 'favorite'];
const fieldGroupMap = new Map<
string,
{
objectMetadataItemId: string;
fields: ApplicationDataTableFieldItem[];
}
>();
for (const objectMetadataItem of objectMetadataItems) {
if (applicationObjectIds.includes(objectMetadataItem.id)) {
continue;
}
if (FIELD_GROUP_DENY_LIST.includes(objectMetadataItem.nameSingular)) {
continue;
}
const appFields = objectMetadataItem.fields.filter(
(field) => field.applicationId === application.id,
);
if (appFields.length > 0) {
fieldGroupMap.set(objectMetadataItem.id, {
objectMetadataItemId: objectMetadataItem.id,
fields: appFields.map((field) => ({
key: field.id,
label: field.label,
icon: field.icon ?? undefined,
type: field.type,
})),
});
}
}
return Array.from(fieldGroupMap.values())
.map((group) => {
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === group.objectMetadataItemId,
);
if (!isDefined(objectMetadataItem)) {
return undefined;
}
return {
key: objectMetadataItem.nameSingular ?? group.objectMetadataItemId,
labelPlural: objectMetadataItem.labelPlural,
icon: objectMetadataItem.icon ?? undefined,
fieldsCount: group.fields.length,
fields: group.fields,
link: isDefined(objectMetadataItem)
? getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural: objectMetadataItem.namePlural,
})
: undefined,
tagItem: {
isCustom: objectMetadataItem?.isCustom,
isRemote: objectMetadataItem?.isRemote,
applicationId: objectMetadataItem?.applicationId,
},
};
})
.filter(isDefined);
}, [objectMetadataItems, applicationObjectIds, application]);
if (!isDefined(application)) {
return null;
}
const { logicFunctions, agents, objects } = application;
const { logicFunctions, agents } = application;
const shouldDisplayLogicFunctions =
isDefined(logicFunctions) && logicFunctions?.length > 0;
const shouldDisplayAgents = isDefined(agents) && agents.length > 0;
const shouldDisplayObjects = isDefined(objects) && objects.length > 0;
const applicationObjectMetadataItems = shouldDisplayObjects
? objectMetadataItems.filter((objectMetadataItem) =>
objects.map((object) => object.id).includes(objectMetadataItem.id),
)
: [];
return (
<>
{shouldDisplayObjects && (
<Section>
<H2Title
title={t`Objects`}
description={t`Objects managed by this app`}
/>
<SettingsObjectTable
objectMetadataItems={applicationObjectMetadataItems}
withSearchBar={false}
/>
</Section>
)}
<SettingsApplicationDataTable
objectRows={objectRows}
fieldGroupRows={fieldGroupRows}
/>
{shouldDisplayLogicFunctions && (
<Section>
<H2Title
@@ -0,0 +1,386 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsRolesQueryEffect } from '@/settings/roles/components/SettingsRolesQueryEffect';
import { SettingsRolePermissions } from '@/settings/roles/role-permissions/components/SettingsRolePermissions';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { type RoleWithPartialMembers } from '@/settings/roles/types/RoleWithPartialMembers';
import { t } from '@lingui/core/macro';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
import {
FieldMetadataType,
type MarketplaceAppDefaultRole,
type MarketplaceAppField,
type MarketplaceAppObject,
type PermissionFlagType,
} from '~/generated-metadata/graphql';
import { findObjectNameByUniversalIdentifier } from '~/pages/settings/applications/utils/findObjectNameByUniversalIdentifier';
type SettingsApplicationPermissionsTabProps = {
defaultRoleId?: string | null;
marketplaceAppDefaultRole?: MarketplaceAppDefaultRole | null;
marketplaceAppObjects?: MarketplaceAppObject[];
};
const resolvePermissionIds = (
defaultRole: MarketplaceAppDefaultRole,
objectMetadataItems: ObjectMetadataItem[],
): {
objectUniversalIdToIdMap: Record<string, string>;
fieldUniversalIdToIdMap: Record<string, string>;
} => {
const objectUniversalIdToIdMap: Record<string, string> = {};
const fieldUniversalIdToIdMap: Record<string, string> = {};
const allObjectUniversalIds = new Set<string>();
for (const permission of defaultRole.objectPermissions) {
allObjectUniversalIds.add(permission.objectUniversalIdentifier);
}
for (const permission of defaultRole.fieldPermissions) {
allObjectUniversalIds.add(permission.objectUniversalIdentifier);
}
for (const universalId of allObjectUniversalIds) {
const standardObjectName = findObjectNameByUniversalIdentifier(universalId);
if (isDefined(standardObjectName)) {
const workspaceObject = objectMetadataItems.find(
(item) => item.nameSingular === standardObjectName,
);
if (isDefined(workspaceObject)) {
objectUniversalIdToIdMap[universalId] = workspaceObject.id;
}
}
}
for (const permission of defaultRole.fieldPermissions) {
const objectName = findObjectNameByUniversalIdentifier(
permission.objectUniversalIdentifier,
);
if (isDefined(objectName)) {
const standardObject =
STANDARD_OBJECTS[objectName as keyof typeof STANDARD_OBJECTS];
if (isDefined(standardObject)) {
for (const [fieldName, fieldDef] of Object.entries(
standardObject.fields,
)) {
if (
(fieldDef as { universalIdentifier: string })
.universalIdentifier === permission.fieldUniversalIdentifier
) {
const workspaceObject = objectMetadataItems.find(
(item) => item.nameSingular === objectName,
);
const workspaceField = workspaceObject?.fields.find(
(field) => field.name === fieldName,
);
if (isDefined(workspaceField)) {
fieldUniversalIdToIdMap[permission.fieldUniversalIdentifier] =
workspaceField.id;
}
break;
}
}
}
}
}
return { objectUniversalIdToIdMap, fieldUniversalIdToIdMap };
};
const buildSyntheticRole = (
defaultRole: MarketplaceAppDefaultRole,
objectUniversalIdToIdMap: Record<string, string>,
fieldUniversalIdToIdMap: Record<string, string>,
): RoleWithPartialMembers => ({
__typename: 'Role',
id: defaultRole.id,
label: defaultRole.label,
description: defaultRole.description ?? '',
icon: '',
isEditable: false,
canReadAllObjectRecords: defaultRole.canReadAllObjectRecords,
canUpdateAllObjectRecords: defaultRole.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords: defaultRole.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords: defaultRole.canDestroyAllObjectRecords,
canUpdateAllSettings: defaultRole.canUpdateAllSettings,
canAccessAllTools: defaultRole.canAccessAllTools,
canBeAssignedToUsers: false,
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: false,
workspaceMembers: [],
agents: [],
apiKeys: [],
rowLevelPermissionPredicates: [],
rowLevelPermissionPredicateGroups: [],
objectPermissions: defaultRole.objectPermissions.map((permission) => ({
__typename: 'ObjectPermission' as const,
objectMetadataId:
objectUniversalIdToIdMap[permission.objectUniversalIdentifier] ??
permission.objectUniversalIdentifier,
canReadObjectRecords: permission.canReadObjectRecords,
canUpdateObjectRecords: permission.canUpdateObjectRecords,
canSoftDeleteObjectRecords: permission.canSoftDeleteObjectRecords,
canDestroyObjectRecords: permission.canDestroyObjectRecords,
})),
fieldPermissions: defaultRole.fieldPermissions.map((permission) => ({
__typename: 'FieldPermission' as const,
id: uuidv4(),
roleId: defaultRole.id,
objectMetadataId:
objectUniversalIdToIdMap[permission.objectUniversalIdentifier] ??
permission.objectUniversalIdentifier,
fieldMetadataId:
fieldUniversalIdToIdMap[permission.fieldUniversalIdentifier] ??
permission.fieldUniversalIdentifier,
canReadFieldValue: permission.canReadFieldValue,
canUpdateFieldValue: permission.canUpdateFieldValue,
})),
permissionFlags: defaultRole.permissionFlags.map((flag) => ({
__typename: 'PermissionFlag' as const,
id: uuidv4(),
roleId: defaultRole.id,
flag: flag as PermissionFlagType,
})),
});
const buildFieldMetadataItemFromMarketplaceField = (
field: MarketplaceAppField,
): FieldMetadataItem => {
const now = new Date().toISOString();
return {
id: field.universalIdentifier ?? uuidv4(),
name: field.name,
label: field.label,
type: (field.type as FieldMetadataType) ?? FieldMetadataType.TEXT,
description: field.description ?? '',
icon: field.icon ?? 'IconField',
isActive: true,
isCustom: true,
isSystem: false,
isNullable: true,
isUnique: false,
isUIReadOnly: false,
createdAt: now,
updatedAt: now,
defaultValue: null,
options: null,
relation: null,
settings: null,
};
};
const buildobjectMetadataItemsFromMarketplaceApp = (
defaultRole: MarketplaceAppDefaultRole,
objectUniversalIdToIdMap: Record<string, string>,
marketplaceAppObjects: MarketplaceAppObject[],
): ObjectMetadataItem[] => {
const unresolvedUniversalIds = new Set<string>();
for (const permission of defaultRole.objectPermissions) {
if (
!isDefined(objectUniversalIdToIdMap[permission.objectUniversalIdentifier])
) {
unresolvedUniversalIds.add(permission.objectUniversalIdentifier);
}
}
for (const permission of defaultRole.fieldPermissions) {
if (
!isDefined(objectUniversalIdToIdMap[permission.objectUniversalIdentifier])
) {
unresolvedUniversalIds.add(permission.objectUniversalIdentifier);
}
}
const appObjectsByUniversalId = new Map(
marketplaceAppObjects.map((obj) => [obj.universalIdentifier, obj]),
);
return [...unresolvedUniversalIds]
.map((universalId) => {
const appObject = appObjectsByUniversalId.get(universalId);
if (!isDefined(appObject)) {
return undefined;
}
const fields = appObject.fields.map(
buildFieldMetadataItemFromMarketplaceField,
);
const objectFieldPermissions = defaultRole.fieldPermissions.filter(
(permission) => permission.objectUniversalIdentifier === universalId,
);
const nonReadableFieldIds = new Set(
objectFieldPermissions
.filter((permission) => permission.canReadFieldValue === false)
.map((permission) => permission.fieldUniversalIdentifier),
);
const nonUpdatableFieldIds = new Set(
objectFieldPermissions
.filter((permission) => permission.canUpdateFieldValue === false)
.map((permission) => permission.fieldUniversalIdentifier),
);
const item: ObjectMetadataItem = {
__typename: 'Object',
id: universalId,
nameSingular: appObject.nameSingular,
namePlural: appObject.namePlural,
labelSingular: appObject.labelSingular,
labelPlural: appObject.labelPlural,
description: appObject.description ?? '',
icon: appObject.icon ?? 'IconBox',
isCustom: true,
isRemote: false,
isActive: true,
isSystem: false,
isSearchable: false,
isUIReadOnly: false,
isLabelSyncedWithName: false,
labelIdentifierFieldMetadataId: '',
fields,
readableFields: fields.filter(
(field) => !nonReadableFieldIds.has(field.id),
),
updatableFields: fields.filter(
(field) => !nonUpdatableFieldIds.has(field.id),
),
indexMetadatas: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
return item;
})
.filter(isDefined);
};
const MarketplaceRoleEffect = ({
defaultRole,
marketplaceAppObjects,
onObjectMetadataItemsFromMarketplaceApp,
}: {
defaultRole: MarketplaceAppDefaultRole;
marketplaceAppObjects: MarketplaceAppObject[];
onObjectMetadataItemsFromMarketplaceApp: (
items: ObjectMetadataItem[],
) => void;
}) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const setDraftRole = useSetRecoilState(
settingsDraftRoleFamilyState(defaultRole.id),
);
const { resolvedRole, objectMetadataItemsFromMarketplaceApp } =
useMemo(() => {
const { objectUniversalIdToIdMap, fieldUniversalIdToIdMap } =
resolvePermissionIds(defaultRole, objectMetadataItems);
return {
resolvedRole: buildSyntheticRole(
defaultRole,
objectUniversalIdToIdMap,
fieldUniversalIdToIdMap,
),
objectMetadataItemsFromMarketplaceApp:
buildobjectMetadataItemsFromMarketplaceApp(
defaultRole,
objectUniversalIdToIdMap,
marketplaceAppObjects,
),
};
}, [defaultRole, objectMetadataItems, marketplaceAppObjects]);
useEffect(() => {
setDraftRole(resolvedRole);
}, [resolvedRole, setDraftRole]);
useEffect(() => {
onObjectMetadataItemsFromMarketplaceApp(
objectMetadataItemsFromMarketplaceApp,
);
}, [
objectMetadataItemsFromMarketplaceApp,
onObjectMetadataItemsFromMarketplaceApp,
]);
return null;
};
const MarketplaceAppPermissions = ({
defaultRole,
marketplaceAppObjects,
}: {
defaultRole: MarketplaceAppDefaultRole;
marketplaceAppObjects: MarketplaceAppObject[];
}) => {
const [
objectMetadataItemsFromMarketplaceApp,
setObjectMetadataItemsFromMarketplaceApp,
] = useState<ObjectMetadataItem[]>([]);
const handleObjectsFromMarketplaceApp = useCallback(
(objects: ObjectMetadataItem[]) =>
setObjectMetadataItemsFromMarketplaceApp(objects),
[],
);
return (
<>
<MarketplaceRoleEffect
defaultRole={defaultRole}
marketplaceAppObjects={marketplaceAppObjects}
onObjectMetadataItemsFromMarketplaceApp={
handleObjectsFromMarketplaceApp
}
/>
<SettingsRolePermissions
roleId={defaultRole.id}
isEditable={false}
objectMetadataItemsFromMarketplaceApp={
objectMetadataItemsFromMarketplaceApp
}
/>
</>
);
};
export const SettingsApplicationPermissionsTab = ({
defaultRoleId,
marketplaceAppDefaultRole,
marketplaceAppObjects,
}: SettingsApplicationPermissionsTabProps) => {
if (isDefined(marketplaceAppDefaultRole)) {
return (
<MarketplaceAppPermissions
defaultRole={marketplaceAppDefaultRole}
marketplaceAppObjects={marketplaceAppObjects ?? []}
/>
);
}
if (isDefined(defaultRoleId)) {
return (
<>
<SettingsRolesQueryEffect />
<SettingsRolePermissions roleId={defaultRoleId} isEditable={false} />
</>
);
}
return <div>{t`No permissions configured for this application.`}</div>;
};
@@ -5,7 +5,6 @@ import { SearchInput } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { SettingsAvailableApplicationCard } from '~/pages/settings/applications/components/SettingsAvailableApplicationCard';
import { useMarketplaceApps } from '~/pages/settings/applications/hooks/useMarketplaceApps';
import { type AvailableApplication } from '~/pages/settings/applications/types/availableApplication';
const StyledSearchInputContainer = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)};
@@ -37,7 +36,7 @@ export const SettingsApplicationsAvailableTab = () => {
const filteredApplications = useMemo(() => {
return applications.filter(
(application: AvailableApplication) =>
(application) =>
application.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
application.description
.toLowerCase()
@@ -68,7 +67,7 @@ export const SettingsApplicationsAvailableTab = () => {
<StyledEmptyState>{t`No applications available`}</StyledEmptyState>
) : (
<StyledCardsGrid>
{filteredApplications.map((application: AvailableApplication) => (
{filteredApplications.map((application) => (
<SettingsAvailableApplicationCard
key={application.id}
application={application}
@@ -0,0 +1,148 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { t } from '@lingui/core/macro';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { type MarketplaceApp } from '~/generated-metadata/graphql';
import {
type ApplicationDataTableFieldItem,
type ApplicationDataTableRow,
SettingsApplicationDataTable,
} from '~/pages/settings/applications/components/SettingsApplicationDataTable';
import { SettingsApplicationNameDescriptionTable } from '~/pages/settings/applications/components/SettingsApplicationNameDescriptionTable';
import { findObjectNameByUniversalIdentifier } from '~/pages/settings/applications/utils/findObjectNameByUniversalIdentifier';
export const SettingsAvailableApplicationDetailContentTab = ({
application,
}: {
application: MarketplaceApp;
}) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objects, fields, logicFunctions, frontComponents } = application;
const objectRows = useMemo(
(): ApplicationDataTableRow[] =>
(objects ?? []).map((appObject) => ({
key: appObject.nameSingular,
labelPlural: appObject.labelPlural,
icon: appObject.icon ?? undefined,
fieldsCount: appObject.fields.length,
fields: appObject.fields.map(
(field): ApplicationDataTableFieldItem => ({
key: field.name,
label: field.label,
icon: field.icon ?? undefined,
type: field.type,
}),
),
tagItem: { applicationId: application.id },
})),
[objects, application.id],
);
const fieldGroupRows = useMemo((): ApplicationDataTableRow[] => {
if (!isDefined(fields) || fields.length === 0) {
return [];
}
const groupMap = new Map<
string,
{
objectUniversalIdentifier: string;
fieldItems: ApplicationDataTableFieldItem[];
}
>();
for (const field of fields) {
if (!isDefined(field.objectUniversalIdentifier)) {
continue;
}
const existing = groupMap.get(field.objectUniversalIdentifier);
const fieldItem: ApplicationDataTableFieldItem = {
key: field.name,
label: field.label,
icon: field.icon ?? undefined,
type: field.type,
};
if (isDefined(existing)) {
existing.fieldItems.push(fieldItem);
} else {
groupMap.set(field.objectUniversalIdentifier, {
objectUniversalIdentifier: field.objectUniversalIdentifier,
fieldItems: [fieldItem],
});
}
}
return Array.from(groupMap.values()).map((group) => {
const appObject = objects?.find(
(appObj) =>
appObj.universalIdentifier === group.objectUniversalIdentifier,
);
if (isDefined(appObject)) {
return {
key: appObject.nameSingular,
labelPlural: appObject.labelPlural,
icon: appObject.icon ?? undefined,
fieldsCount: group.fieldItems.length,
fields: group.fieldItems,
tagItem: { applicationId: application.id },
};
}
const standardObjectName = findObjectNameByUniversalIdentifier(
group.objectUniversalIdentifier,
);
const objectMetadataItem = isDefined(standardObjectName)
? objectMetadataItems.find(
(item) => item.nameSingular === standardObjectName,
)
: undefined;
if (!isDefined(objectMetadataItem)) {
throw new Error(
`Could not resolve object for universalIdentifier: ${group.objectUniversalIdentifier}`,
);
}
return {
key: objectMetadataItem.nameSingular,
labelPlural: objectMetadataItem.labelPlural,
icon: objectMetadataItem.icon ?? undefined,
fieldsCount: group.fieldItems.length,
fields: group.fieldItems,
link: getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural: objectMetadataItem.namePlural,
}),
tagItem: {},
};
});
}, [fields, objectMetadataItems, objects, application.id]);
return (
<>
<SettingsApplicationDataTable
objectRows={objectRows}
fieldGroupRows={fieldGroupRows}
/>
<SettingsApplicationNameDescriptionTable
title={t`Functions`}
description={t`Logic functions provided by this app`}
sectionTitle={t`Logic functions`}
items={logicFunctions}
/>
<SettingsApplicationNameDescriptionTable
title={t`Front components`}
description={t`UI components provided by this app`}
sectionTitle={t`Front components`}
items={frontComponents}
/>
</>
);
};
@@ -1,20 +0,0 @@
export type AvailableApplication = {
id: string;
name: string;
description: string;
author: string;
logoPath: string;
category: string;
aboutDescription: string;
providers: string[];
screenshots: string[];
content: {
objects: number;
fields: number;
functions: number;
frontComponents: number;
};
version: string;
websiteUrl: string;
termsUrl: string;
};
@@ -0,0 +1,13 @@
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
export const findObjectNameByUniversalIdentifier = (
universalIdentifier: string,
): string | undefined => {
for (const [objectName, objectConfig] of Object.entries(STANDARD_OBJECTS)) {
if (objectConfig.universalIdentifier === universalIdentifier) {
return objectName;
}
}
return undefined;
};
@@ -3,10 +3,8 @@ import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDelet
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useCombinedGetTotalCount } from '@/object-record/multiple-objects/hooks/useCombinedGetTotalCount';
import {
SettingsObjectMetadataItemTableRow,
StyledObjectTableRow,
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
import { SettingsObjectMetadataItemTableRow } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
import { StyledObjectTableRow } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents';
import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown';
import { getItemTagInfo } from '@/settings/data-model/utils/getItemTagInfo';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@@ -1,13 +1,202 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { Field, Int, ObjectType } from '@nestjs/graphql';
import {
IsArray,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
MaxLength,
} from 'class-validator';
@ObjectType('MarketplaceAppField')
export class MarketplaceAppFieldDTO {
@IsString()
@Field()
name: string;
@IsString()
@Field()
type: string;
@IsString()
@Field()
label: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
description?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
icon?: string;
@IsString()
@Field({ nullable: true })
objectUniversalIdentifier: string;
@IsString()
@Field({ nullable: true })
universalIdentifier: string;
}
@ObjectType('MarketplaceAppObject')
export class MarketplaceAppObjectDTO {
@IsString()
@Field()
universalIdentifier: string;
@IsString()
@Field()
nameSingular: string;
@IsString()
@Field()
namePlural: string;
@IsString()
@Field()
labelSingular: string;
@IsString()
@Field()
labelPlural: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
description?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
icon?: string;
@IsArray()
@Field(() => [MarketplaceAppFieldDTO])
fields: MarketplaceAppFieldDTO[];
}
@ObjectType('MarketplaceAppLogicFunction')
export class MarketplaceAppLogicFunctionDTO {
@IsString()
@Field()
name: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
description?: string;
@IsOptional()
@IsNumber()
@Field(() => Int, { nullable: true })
timeoutSeconds?: number;
}
@ObjectType('MarketplaceAppFrontComponent')
export class MarketplaceAppFrontComponentDTO {
@IsString()
@Field()
name: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
description?: string;
}
@ObjectType('MarketplaceAppRoleObjectPermission')
export class MarketplaceAppRoleObjectPermissionDTO {
@IsString()
@Field()
objectUniversalIdentifier: string;
@IsOptional()
@Field(() => Boolean, { nullable: true })
canReadObjectRecords?: boolean;
@IsOptional()
@Field(() => Boolean, { nullable: true })
canUpdateObjectRecords?: boolean;
@IsOptional()
@Field(() => Boolean, { nullable: true })
canSoftDeleteObjectRecords?: boolean;
@IsOptional()
@Field(() => Boolean, { nullable: true })
canDestroyObjectRecords?: boolean;
}
@ObjectType('MarketplaceAppRoleFieldPermission')
export class MarketplaceAppRoleFieldPermissionDTO {
@IsString()
@Field()
objectUniversalIdentifier: string;
@IsString()
@Field()
fieldUniversalIdentifier: string;
@IsOptional()
@Field(() => Boolean, { nullable: true })
canReadFieldValue?: boolean;
@IsOptional()
@Field(() => Boolean, { nullable: true })
canUpdateFieldValue?: boolean;
}
@ObjectType('MarketplaceAppDefaultRole')
export class MarketplaceAppDefaultRoleDTO {
@IsString()
@IsNotEmpty()
@Field()
id: string;
@IsString()
@Field()
label: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
description?: string;
@Field(() => Boolean)
canReadAllObjectRecords: boolean;
@Field(() => Boolean)
canUpdateAllObjectRecords: boolean;
@Field(() => Boolean)
canSoftDeleteAllObjectRecords: boolean;
@Field(() => Boolean)
canDestroyAllObjectRecords: boolean;
@Field(() => Boolean)
canUpdateAllSettings: boolean;
@Field(() => Boolean)
canAccessAllTools: boolean;
@IsArray()
@Field(() => [MarketplaceAppRoleObjectPermissionDTO])
objectPermissions: MarketplaceAppRoleObjectPermissionDTO[];
@IsArray()
@Field(() => [MarketplaceAppRoleFieldPermissionDTO])
fieldPermissions: MarketplaceAppRoleFieldPermissionDTO[];
@IsArray()
@Field(() => [String])
permissionFlags: string[];
}
@ObjectType('MarketplaceApp')
export class MarketplaceAppDTO {
@IsString()
@@ -67,4 +256,24 @@ export class MarketplaceAppDTO {
@IsString()
@Field({ nullable: true })
termsUrl?: string;
@IsArray()
@Field(() => [MarketplaceAppObjectDTO])
objects: MarketplaceAppObjectDTO[];
@IsArray()
@Field(() => [MarketplaceAppFieldDTO])
fields: MarketplaceAppFieldDTO[];
@IsArray()
@Field(() => [MarketplaceAppLogicFunctionDTO])
logicFunctions: MarketplaceAppLogicFunctionDTO[];
@IsArray()
@Field(() => [MarketplaceAppFrontComponentDTO])
frontComponents: MarketplaceAppFrontComponentDTO[];
@IsOptional()
@Field(() => MarketplaceAppDefaultRoleDTO, { nullable: true })
defaultRole?: MarketplaceAppDefaultRoleDTO;
}
@@ -5,7 +5,15 @@ import { lowerCase, upperFirst } from 'lodash';
import { type Manifest } from 'twenty-shared/application';
import { PackageJson } from 'type-fest';
import { MarketplaceAppDTO } from 'src/engine/core-modules/application/dtos/marketplace-app.dto';
import {
MarketplaceAppDTO,
MarketplaceAppDefaultRoleDTO,
MarketplaceAppFieldDTO,
MarketplaceAppFrontComponentDTO,
MarketplaceAppLogicFunctionDTO,
MarketplaceAppObjectDTO,
} from 'src/engine/core-modules/application/dtos/marketplace-app.dto';
import { MOCKED_MARKETPLACE_APP } from 'src/engine/core-modules/application/services/mocked-marketplace-app.constant';
type GitHubContent = {
name: string;
@@ -52,7 +60,7 @@ export class MarketplaceService {
private async fetchAllMarketplaceApps(): Promise<MarketplaceAppDTO[]> {
const apps: MarketplaceAppDTO[] = [];
apps.push(this.loadMockApp());
apps.push(MOCKED_MARKETPLACE_APP); // To remove once we have apps on marketplace
try {
const appDirs = await this.getAppDirectoriesFromGitHub();
@@ -82,6 +90,10 @@ export class MarketplaceService {
providers: [],
websiteUrl: '',
termsUrl: '',
objects: [],
fields: [],
logicFunctions: [],
frontComponents: [],
});
}
} catch (error) {
@@ -99,44 +111,12 @@ export class MarketplaceService {
return apps;
}
private loadMockApp(): MarketplaceAppDTO {
// SVG logo as data URL
const logoSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="#1a2744"><ellipse cx="38" cy="20" rx="28" ry="10"/><rect x="10" y="20" width="56" height="50"/><ellipse cx="38" cy="70" rx="28" ry="10"/><ellipse cx="38" cy="35" rx="28" ry="10" fill="none" stroke="#fff" stroke-width="3"/><ellipse cx="38" cy="52" rx="28" ry="10" fill="none" stroke="#fff" stroke-width="3"/><circle cx="72" cy="62" r="22" fill="#1a2744"/><circle cx="72" cy="62" r="18" fill="#fff"/><path d="M72 50 L72 74 M62 58 L72 48 L82 58" stroke="#1a2744" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`;
const logoDataUrl = `data:image/svg+xml,${encodeURIComponent(logoSvg)}`;
return {
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'Data Enrichment',
description: 'Enrich your data easily. Choose your provider.',
icon: 'IconSparkles',
version: '1.0.0',
author: 'Cosmos Labs',
category: 'Data',
logo: logoDataUrl,
screenshots: [
'https://placehold.co/800x400/f5f5f5/666?text=Screenshot+1',
'https://placehold.co/800x400/f5f5f5/666?text=Screenshot+2',
'https://placehold.co/800x400/f5f5f5/666?text=Screenshot+3',
],
aboutDescription:
'Enhance your workspace with automated data intelligence. This app monitors your new records and automatically populates missing details such as job titles, company size, social profiles, and industry insights.',
providers: ['Clearbit', 'Apollo', 'Hunter.io'],
websiteUrl: 'https://google.com',
termsUrl: 'https://google.com',
};
}
private async getAppDirectoriesFromGitHub(): Promise<string[]> {
const directories: string[] = [];
const rootContents = await this.fetchGitHubDirectory('');
for (const entry of rootContents) {
// if (entry.type !== 'dir') continue;
// if (entry.name.startsWith('.')) continue;
// if (entry.name === 'node_modules') continue;
// if (entry.name === 'internal') continue;
if (entry.name === 'community') {
const communityContents = await this.fetchGitHubDirectory('community');
@@ -217,6 +197,61 @@ export class MarketplaceService {
return null;
}
const objects: MarketplaceAppObjectDTO[] = (manifest.objects ?? []).map(
(manifestObject) => ({
universalIdentifier: manifestObject.universalIdentifier,
nameSingular: manifestObject.nameSingular,
namePlural: manifestObject.namePlural,
labelSingular: manifestObject.labelSingular,
labelPlural: manifestObject.labelPlural,
description: manifestObject.description,
icon: manifestObject.icon,
fields: (manifestObject.fields ?? []).map((field) => ({
name: field.name ?? '',
type: field.type,
label: field.label ?? '',
description: field.description,
icon: field.icon,
universalIdentifier: field.universalIdentifier,
objectUniversalIdentifier: manifestObject.universalIdentifier,
})),
}),
);
const fields: MarketplaceAppFieldDTO[] = (manifest.fields ?? []).map(
(manifestField) => {
return {
name: manifestField.name,
type: manifestField.type,
label: manifestField.label,
description: manifestField.description,
icon: manifestField.icon,
objectUniversalIdentifier: manifestField.objectUniversalIdentifier,
universalIdentifier: manifestField.universalIdentifier,
};
},
);
const logicFunctions: MarketplaceAppLogicFunctionDTO[] = (
manifest.logicFunctions ?? []
).map((manifestLogicFunction) => ({
name: manifestLogicFunction.name ?? '',
description: manifestLogicFunction.description,
timeoutSeconds: manifestLogicFunction.timeoutSeconds,
}));
const frontComponents: MarketplaceAppFrontComponentDTO[] = (
manifest.frontComponents ?? []
).map((manifestFrontComponent) => ({
name: manifestFrontComponent.name ?? '',
description: manifestFrontComponent.description,
}));
const defaultRole = this.resolveDefaultRole(
manifest,
application.defaultRoleUniversalIdentifier,
);
return {
id: application.universalIdentifier,
name: application.displayName,
@@ -231,6 +266,59 @@ export class MarketplaceService {
providers: marketplaceData.providers ?? [],
websiteUrl: marketplaceData.websiteUrl,
termsUrl: marketplaceData.termsUrl,
objects,
fields,
logicFunctions,
frontComponents,
defaultRole,
};
}
private resolveDefaultRole(
manifest: Manifest,
defaultRoleUniversalIdentifier: string,
): MarketplaceAppDefaultRoleDTO | undefined {
const roleManifest = manifest.roles?.find(
(role) => role.universalIdentifier === defaultRoleUniversalIdentifier,
);
if (!roleManifest) {
return undefined;
}
return {
id: roleManifest.universalIdentifier,
label: roleManifest.label,
description: roleManifest.description,
canReadAllObjectRecords: roleManifest.canReadAllObjectRecords ?? false,
canUpdateAllObjectRecords:
roleManifest.canUpdateAllObjectRecords ?? false,
canSoftDeleteAllObjectRecords:
roleManifest.canSoftDeleteAllObjectRecords ?? false,
canDestroyAllObjectRecords:
roleManifest.canDestroyAllObjectRecords ?? false,
canUpdateAllSettings: roleManifest.canUpdateAllSettings ?? false,
canAccessAllTools: roleManifest.canAccessAllTools ?? false,
objectPermissions: (roleManifest.objectPermissions ?? []).map(
(permission) => ({
objectUniversalIdentifier: permission.objectUniversalIdentifier,
canReadObjectRecords: permission.canReadObjectRecords,
canUpdateObjectRecords: permission.canUpdateObjectRecords,
canSoftDeleteObjectRecords: permission.canSoftDeleteObjectRecords,
canDestroyObjectRecords: permission.canDestroyObjectRecords,
}),
),
fieldPermissions: (roleManifest.fieldPermissions ?? []).map(
(permission) => ({
objectUniversalIdentifier: permission.objectUniversalIdentifier,
fieldUniversalIdentifier: permission.fieldUniversalIdentifier,
canReadFieldValue: permission.canReadFieldValue,
canUpdateFieldValue: permission.canUpdateFieldValue,
}),
),
permissionFlags: (roleManifest.permissionFlags ?? []).map((flag) =>
flag.toString(),
),
};
}
@@ -0,0 +1,204 @@
import { type MarketplaceAppDTO } from 'src/engine/core-modules/application/dtos/marketplace-app.dto';
// Readable mock identifiers for the Data Enrichment app
const MOCK_APP_ID = 'a1b2c3d4-0000-0000-0000-000000000001';
const MOCK_ROLE_ID = 'a1b2c3d4-0000-0000-0000-000000000010';
const MOCK_ENRICHMENT_JOB_UNIVERSAL_ID = 'a1b2c3d4-0000-0000-0000-000000000100';
// Standard object universalIdentifiers from STANDARD_OBJECTS
const COMPANY_UNIVERSAL_ID = '20202020-b374-4779-a561-80086cb2e17f';
const PERSON_UNIVERSAL_ID = '20202020-e674-48e5-a542-72570eee7213';
// Standard field universalIdentifiers
const COMPANY_NAME_FIELD_UNIVERSAL_ID = '20202020-4d99-4e2e-a84c-4a27837b1ece';
// Enrichment job field universalIdentifiers
const MOCK_ENRICHMENT_JOB_STATUS_FIELD_UNIVERSAL_ID =
'a1b2c3d4-0000-0000-0000-000000000101';
const MOCK_ENRICHMENT_JOB_PROVIDER_FIELD_UNIVERSAL_ID =
'a1b2c3d4-0000-0000-0000-000000000102';
const MOCK_ENRICHMENT_JOB_ENRICHED_AT_FIELD_UNIVERSAL_ID =
'a1b2c3d4-0000-0000-0000-000000000103';
const MOCK_ENRICHMENT_JOB_RECORD_ID_FIELD_UNIVERSAL_ID =
'a1b2c3d4-0000-0000-0000-000000000104';
// App-provided field universalIdentifiers on standard objects
const COMPANY_INDUSTRY_FIELD_UNIVERSAL_ID =
'a1b2c3d4-0000-0000-0000-000000000201';
const COMPANY_EMPLOYEE_COUNT_FIELD_UNIVERSAL_ID =
'a1b2c3d4-0000-0000-0000-000000000202';
const PERSON_LINKEDIN_URL_FIELD_UNIVERSAL_ID =
'a1b2c3d4-0000-0000-0000-000000000203';
const PERSON_JOB_TITLE_FIELD_UNIVERSAL_ID =
'a1b2c3d4-0000-0000-0000-000000000204';
const MOCK_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="#1a2744"><ellipse cx="38" cy="20" rx="28" ry="10"/><rect x="10" y="20" width="56" height="50"/><ellipse cx="38" cy="70" rx="28" ry="10"/><ellipse cx="38" cy="35" rx="28" ry="10" fill="none" stroke="#fff" stroke-width="3"/><ellipse cx="38" cy="52" rx="28" ry="10" fill="none" stroke="#fff" stroke-width="3"/><circle cx="72" cy="62" r="22" fill="#1a2744"/><circle cx="72" cy="62" r="18" fill="#fff"/><path d="M72 50 L72 74 M62 58 L72 48 L82 58" stroke="#1a2744" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`;
export const MOCKED_MARKETPLACE_APP: MarketplaceAppDTO = {
id: MOCK_APP_ID,
name: 'Data Enrichment',
description: 'Enrich your data easily. Choose your provider.',
icon: 'IconSparkles',
version: '1.0.0',
author: 'Cosmos Labs',
category: 'Data',
logo: `data:image/svg+xml,${encodeURIComponent(MOCK_LOGO_SVG)}`,
screenshots: [
'https://placehold.co/800x400/f5f5f5/666?text=Screenshot+1',
'https://placehold.co/800x400/f5f5f5/666?text=Screenshot+2',
'https://placehold.co/800x400/f5f5f5/666?text=Screenshot+3',
],
aboutDescription:
'Enhance your workspace with automated data intelligence. This app monitors your new records and automatically populates missing details such as job titles, company size, social profiles, and industry insights.',
providers: ['Clearbit', 'Apollo', 'Hunter.io'],
websiteUrl: 'https://google.com',
termsUrl: 'https://google.com',
objects: [
{
universalIdentifier: MOCK_ENRICHMENT_JOB_UNIVERSAL_ID,
nameSingular: 'enrichmentJob',
namePlural: 'enrichmentJobs',
labelSingular: 'Enrichment Job',
labelPlural: 'Enrichment Jobs',
description: 'Tracks data enrichment requests and their status',
icon: 'IconSparkles',
fields: [
{
name: 'status',
type: 'SELECT',
label: 'Status',
description: 'Current status of the enrichment job',
icon: 'IconProgressCheck',
universalIdentifier: MOCK_ENRICHMENT_JOB_STATUS_FIELD_UNIVERSAL_ID,
objectUniversalIdentifier: MOCK_ENRICHMENT_JOB_UNIVERSAL_ID,
},
{
name: 'provider',
type: 'TEXT',
label: 'Provider',
description: 'Enrichment provider used',
icon: 'IconCloud',
universalIdentifier: MOCK_ENRICHMENT_JOB_PROVIDER_FIELD_UNIVERSAL_ID,
objectUniversalIdentifier: MOCK_ENRICHMENT_JOB_UNIVERSAL_ID,
},
{
name: 'enrichedAt',
type: 'DATE_TIME',
label: 'Enriched At',
description: 'When the enrichment was completed',
icon: 'IconCalendar',
universalIdentifier:
MOCK_ENRICHMENT_JOB_ENRICHED_AT_FIELD_UNIVERSAL_ID,
objectUniversalIdentifier: MOCK_ENRICHMENT_JOB_UNIVERSAL_ID,
},
{
name: 'recordId',
type: 'TEXT',
label: 'Record ID',
description: 'ID of the enriched record',
icon: 'IconKey',
universalIdentifier: MOCK_ENRICHMENT_JOB_RECORD_ID_FIELD_UNIVERSAL_ID,
objectUniversalIdentifier: MOCK_ENRICHMENT_JOB_UNIVERSAL_ID,
},
],
},
],
fields: [
{
name: 'industry',
type: 'TEXT',
label: 'Industry',
description: 'Company industry from enrichment',
icon: 'IconBuildingFactory2',
objectUniversalIdentifier: COMPANY_UNIVERSAL_ID,
universalIdentifier: COMPANY_INDUSTRY_FIELD_UNIVERSAL_ID,
},
{
name: 'employeeCount',
type: 'NUMBER',
label: 'Employee Count',
description: 'Number of employees from enrichment',
icon: 'IconUsers',
objectUniversalIdentifier: COMPANY_UNIVERSAL_ID,
universalIdentifier: COMPANY_EMPLOYEE_COUNT_FIELD_UNIVERSAL_ID,
},
{
name: 'linkedInUrl',
type: 'LINKS',
label: 'LinkedIn URL',
description: 'LinkedIn profile URL from enrichment',
icon: 'IconBrandLinkedin',
objectUniversalIdentifier: PERSON_UNIVERSAL_ID,
universalIdentifier: PERSON_LINKEDIN_URL_FIELD_UNIVERSAL_ID,
},
{
name: 'jobTitle',
type: 'TEXT',
label: 'Job Title',
description: 'Job title from enrichment',
icon: 'IconBriefcase',
objectUniversalIdentifier: PERSON_UNIVERSAL_ID,
universalIdentifier: PERSON_JOB_TITLE_FIELD_UNIVERSAL_ID,
},
],
logicFunctions: [
{
name: 'enrich-on-create',
description: 'Automatically enriches new records when they are created',
timeoutSeconds: 30,
},
],
frontComponents: [],
defaultRole: {
id: MOCK_ROLE_ID,
label: 'Data Enrichment default role',
description: 'Default permissions for the Data Enrichment app',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
canUpdateAllSettings: false,
canAccessAllTools: false,
objectPermissions: [
{
// Company: revoke soft-delete
objectUniversalIdentifier: COMPANY_UNIVERSAL_ID,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
{
// Person: no overrides (matches defaults)
objectUniversalIdentifier: PERSON_UNIVERSAL_ID,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: false,
},
{
// Enrichment Job (app-custom object): revoke soft-delete
objectUniversalIdentifier: MOCK_ENRICHMENT_JOB_UNIVERSAL_ID,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier: COMPANY_UNIVERSAL_ID,
fieldUniversalIdentifier: COMPANY_NAME_FIELD_UNIVERSAL_ID,
canReadFieldValue: true,
canUpdateFieldValue: false,
},
{
objectUniversalIdentifier: PERSON_UNIVERSAL_ID,
fieldUniversalIdentifier: PERSON_JOB_TITLE_FIELD_UNIVERSAL_ID,
canReadFieldValue: true,
canUpdateFieldValue: false,
},
],
permissionFlags: ['DATA_MODEL', 'API_KEYS_AND_WEBHOOKS'],
},
};