[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:
@@ -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__
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ export const APPLICATION_FRAGMENT = gql`
|
||||
version
|
||||
universalIdentifier
|
||||
canBeUninstalled
|
||||
defaultRoleId
|
||||
availablePackages
|
||||
applicationVariables {
|
||||
id
|
||||
|
||||
+78
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
+4
-13
@@ -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
-15
@@ -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,
|
||||
|
||||
+20
@@ -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)};
|
||||
`;
|
||||
+7
@@ -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}
|
||||
|
||||
+49
-14
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+4
-6
@@ -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,
|
||||
|
||||
+13
-10
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+4
-6
@@ -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;
|
||||
+14
-1
@@ -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} />
|
||||
|
||||
+17
-6
@@ -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}
|
||||
/>
|
||||
) : (
|
||||
|
||||
+161
@@ -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>
|
||||
);
|
||||
};
|
||||
+117
@@ -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>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+55
@@ -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>
|
||||
);
|
||||
};
|
||||
+3
-3
@@ -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,
|
||||
|
||||
+130
-23
@@ -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
|
||||
|
||||
+386
@@ -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>;
|
||||
};
|
||||
+2
-3
@@ -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}
|
||||
|
||||
+148
@@ -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;
|
||||
};
|
||||
+13
@@ -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';
|
||||
|
||||
+210
-1
@@ -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;
|
||||
}
|
||||
|
||||
+122
-34
@@ -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(),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+204
@@ -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'],
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user