1518 extensibility front add an application section in settings (#15056)

Protected by IS_APPLICATION_ENABLED featureFlag

Add `Application` section in settings

<img width="301" height="137" alt="image"
src="https://github.com/user-attachments/assets/ee53bdd2-36f6-45c6-8646-17b1e08abf00"
/>


A `settings/applications` route listing all installed applications

<img width="661" height="428" alt="image"
src="https://github.com/user-attachments/assets/69d534c4-4e9e-452a-a3d9-ded0223bb457"
/>

Introduce a new Tag for application managed items

<img width="885" height="759" alt="image"
src="https://github.com/user-attachments/assets/19767be5-61e5-4bd2-a51d-54ed9bfb1923"
/>



A `settings/applications/<application_id>` details setting page listing
all objects, serverlessFunctions and agents created by the application:

<img width="917" height="778" alt="image"
src="https://github.com/user-attachments/assets/7fc056a6-1d73-4242-b2eb-6f8955d8597d"
/>

A `settings/applications/<application_id>/<serverless_function_id>`

<img width="905" height="652" alt="image"
src="https://github.com/user-attachments/assets/56ca0021-26bf-42cb-9abf-34879f16050a"
/>

Add trigger tab in serverless function details (readonly for now)

<img width="899" height="724" alt="image"
src="https://github.com/user-attachments/assets/5eeefa35-f2a4-4fd8-a640-7b5c5891f226"
/>

Set object, serverless and agent setting detail pages readonly for
managed items
<img width="1075" height="859" alt="image"
src="https://github.com/user-attachments/assets/57c73d69-4980-47a2-b752-8dc5ab494530"
/>
<img width="648" height="582" alt="image"
src="https://github.com/user-attachments/assets/5ad5f3f7-3bc3-4e40-870a-4981c6492524"
/>
<img width="982" height="692" alt="image"
src="https://github.com/user-attachments/assets/7ad756c4-5d33-4a0a-9eb8-416c040362b9"
/>
<img width="1077" height="647" alt="image"
src="https://github.com/user-attachments/assets/e086b9f5-4062-4d10-82a9-4023de3cad3f"
/>
This commit is contained in:
martmull
2025-10-15 17:13:29 +02:00
committed by GitHub
parent c0ed246a03
commit b16ab1b7c9
109 changed files with 2464 additions and 1357 deletions
@@ -9,6 +9,7 @@ module.exports = {
'./src/modules/users/graphql/**/*.{ts,tsx}',
'./src/modules/views/graphql/**/*.{ts,tsx}',
'./src/modules/ai/graphql/**/*.{ts,tsx}',
'./src/modules/applications/graphql/**/*.{ts,tsx}',
'./src/modules/workspace/graphql/**/*.{ts,tsx}',
'./src/modules/workspace-member/graphql/**/*.{ts,tsx}',
@@ -51,6 +51,7 @@ export type AdminPanelWorkerQueueHealth = {
export type Agent = {
__typename?: 'Agent';
applicationId?: Maybe<Scalars['UUID']>;
createdAt: Scalars['DateTime'];
description?: Maybe<Scalars['String']>;
icon?: Maybe<Scalars['String']>;
@@ -200,6 +201,16 @@ export type AppTokenEdge = {
node: AppToken;
};
export type Application = {
__typename?: 'Application';
agents: Array<Agent>;
description: Scalars['String'];
id: Scalars['UUID'];
name: Scalars['String'];
objects: Array<Object>;
serverlessFunctions: Array<ServerlessFunction>;
};
export type ApprovedAccessDomain = {
__typename?: 'ApprovedAccessDomain';
createdAt: Scalars['DateTime'];
@@ -972,7 +983,6 @@ export type CronTrigger = {
__typename?: 'CronTrigger';
createdAt: Scalars['DateTime'];
id: Scalars['UUID'];
serverlessFunctionId: Scalars['String'];
settings: Scalars['JSON'];
updatedAt: Scalars['DateTime'];
};
@@ -1006,7 +1016,6 @@ export type DatabaseEventTrigger = {
__typename?: 'DatabaseEventTrigger';
createdAt: Scalars['DateTime'];
id: Scalars['UUID'];
serverlessFunctionId: Scalars['String'];
settings: Scalars['JSON'];
updatedAt: Scalars['DateTime'];
};
@@ -1226,6 +1235,7 @@ export type FeatureFlagDto = {
export enum FeatureFlagKey {
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
IS_AI_ENABLED = 'IS_AI_ENABLED',
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
IS_CALENDAR_VIEW_ENABLED = 'IS_CALENDAR_VIEW_ENABLED',
IS_COMMON_API_ENABLED = 'IS_COMMON_API_ENABLED',
IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED',
@@ -2757,6 +2767,7 @@ export type NumberChartConfiguration = {
export type Object = {
__typename?: 'Object';
applicationId?: Maybe<Scalars['UUID']>;
createdAt: Scalars['DateTime'];
description?: Maybe<Scalars['String']>;
duplicateCriteria?: Maybe<Array<Array<Scalars['String']>>>;
@@ -3071,6 +3082,7 @@ export type Query = {
findAgentHandoffs: Array<AgentHandoffDto>;
findDistantTablesWithStatus: Array<RemoteTable>;
findManyAgents: Array<Agent>;
findManyApplications: Array<Application>;
findManyCronTriggers: Array<CronTrigger>;
findManyDatabaseEventTriggers: Array<DatabaseEventTrigger>;
findManyPublicDomains: Array<PublicDomain>;
@@ -3078,6 +3090,7 @@ export type Query = {
findManyRouteTriggers: Array<RouteTrigger>;
findManyServerlessFunctions: Array<ServerlessFunction>;
findOneAgent: Agent;
findOneApplication: Application;
findOneCronTrigger: CronTrigger;
findOneDatabaseEventTrigger: DatabaseEventTrigger;
findOneRemoteServerById: RemoteServer;
@@ -3211,6 +3224,11 @@ export type QueryFindOneAgentArgs = {
};
export type QueryFindOneApplicationArgs = {
id: Scalars['UUID'];
};
export type QueryFindOneCronTriggerArgs = {
input: CronTriggerIdInput;
};
@@ -3596,9 +3614,7 @@ export type RouteTrigger = {
id: Scalars['ID'];
isAuthRequired: Scalars['Boolean'];
path: Scalars['String'];
serverlessFunctionId: Scalars['String'];
updatedAt: Scalars['DateTime'];
workspaceId: Scalars['String'];
};
export type RouteTriggerIdInput = {
@@ -3684,11 +3700,14 @@ export type Sentry = {
export type ServerlessFunction = {
__typename?: 'ServerlessFunction';
createdAt: Scalars['DateTime'];
cronTriggers?: Maybe<Array<CronTrigger>>;
databaseEventTriggers?: Maybe<Array<DatabaseEventTrigger>>;
description?: Maybe<Scalars['String']>;
id: Scalars['UUID'];
latestVersion?: Maybe<Scalars['String']>;
name: Scalars['String'];
publishedVersions: Array<Scalars['String']>;
routeTriggers?: Maybe<Array<RouteTrigger>>;
runtime: Scalars['String'];
timeoutSeconds: Scalars['Float'];
updatedAt: Scalars['DateTime'];
@@ -4648,7 +4667,7 @@ export type WorkspaceUrlsAndId = {
workspaceUrls: WorkspaceUrls;
};
export type AgentFieldsFragment = { __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, createdAt: string, updatedAt: string };
export type AgentFieldsFragment = { __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, applicationId?: string | null, createdAt: string, updatedAt: string };
export type AssignRoleToAgentMutationVariables = Exact<{
agentId: Scalars['UUID'];
@@ -4677,14 +4696,14 @@ export type CreateOneAgentMutationVariables = Exact<{
}>;
export type CreateOneAgentMutation = { __typename?: 'Mutation', createOneAgent: { __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, createdAt: string, updatedAt: string } };
export type CreateOneAgentMutation = { __typename?: 'Mutation', createOneAgent: { __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, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type DeleteOneAgentMutationVariables = Exact<{
input: AgentIdInput;
}>;
export type DeleteOneAgentMutation = { __typename?: 'Mutation', deleteOneAgent: { __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, createdAt: string, updatedAt: string } };
export type DeleteOneAgentMutation = { __typename?: 'Mutation', deleteOneAgent: { __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, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type RemoveAgentHandoffMutationVariables = Exact<{
input: RemoveAgentHandoffInput;
@@ -4705,7 +4724,7 @@ export type UpdateOneAgentMutationVariables = Exact<{
}>;
export type UpdateOneAgentMutation = { __typename?: 'Mutation', updateOneAgent: { __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, createdAt: string, updatedAt: string } };
export type UpdateOneAgentMutation = { __typename?: 'Mutation', updateOneAgent: { __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, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type FindAgentHandoffTargetsQueryVariables = Exact<{
input: AgentIdInput;
@@ -4724,14 +4743,14 @@ export type FindAgentHandoffsQuery = { __typename?: 'Query', findAgentHandoffs:
export type FindManyAgentsQueryVariables = Exact<{ [key: string]: never; }>;
export type FindManyAgentsQuery = { __typename?: 'Query', findManyAgents: 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, createdAt: string, updatedAt: string }> };
export type FindManyAgentsQuery = { __typename?: 'Query', findManyAgents: 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, applicationId?: string | null, createdAt: string, updatedAt: string }> };
export type FindOneAgentQueryVariables = Exact<{
id: Scalars['UUID'];
}>;
export type FindOneAgentQuery = { __typename?: 'Query', findOneAgent: { __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, createdAt: string, updatedAt: string } };
export type FindOneAgentQuery = { __typename?: 'Query', findOneAgent: { __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, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type GetAgentChatMessagesQueryVariables = Exact<{
threadId: Scalars['UUID'];
@@ -4757,6 +4776,20 @@ export type TrackAnalyticsMutationVariables = Exact<{
export type TrackAnalyticsMutation = { __typename?: 'Mutation', trackAnalytics: { __typename?: 'Analytics', success: boolean } };
export type ApplicationFieldsFragment = { __typename?: 'Application', id: string, name: string, description: string, 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, 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 | null, 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, 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 }> }>, serverlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null }> };
export type FindManyApplicationsQueryVariables = Exact<{ [key: string]: never; }>;
export type FindManyApplicationsQuery = { __typename?: 'Query', findManyApplications: Array<{ __typename?: 'Application', id: string, name: string, description: string }> };
export type FindOneApplicationQueryVariables = Exact<{
id: Scalars['UUID'];
}>;
export type FindOneApplicationQuery = { __typename?: 'Query', findOneApplication: { __typename?: 'Application', id: string, name: string, description: string, 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, 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 | null, 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, 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 }> }>, serverlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null }> } };
export type UploadFileMutationVariables = Exact<{
file: Scalars['Upload'];
fileFolder?: InputMaybe<FileFolder>;
@@ -5132,6 +5165,8 @@ export type DeleteFileMutationVariables = Exact<{
export type DeleteFileMutation = { __typename?: 'Mutation', deleteFile: { __typename?: 'File', id: string, name: string, fullPath: string, size: number, type: string, createdAt: string } };
export type ObjectMetadataFieldsFragment = { __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 | null, 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, 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 }> };
export type CreateOneObjectMetadataItemMutationVariables = Exact<{
input: CreateOneObjectInput;
}>;
@@ -5179,7 +5214,7 @@ export type DeleteOneFieldMetadataItemMutation = { __typename?: 'Mutation', dele
export type ObjectMetadataItemsQueryVariables = Exact<{ [key: string]: never; }>;
export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'ObjectEdge', node: { __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, 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, 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 }> } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } };
export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'ObjectEdge', node: { __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 | null, 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, 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 }> } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } };
export type SkipBookOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>;
@@ -5492,7 +5527,7 @@ export type UpsertPermissionFlagsMutation = { __typename?: 'Mutation', upsertPer
export type GetRolesQueryVariables = Exact<{ [key: string]: never; }>;
export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, 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, createdAt: string, updatedAt: string }>, apiKeys: Array<{ __typename?: 'ApiKeyForRole', id: string, name: string, expiresAt: string, revokedAt?: string | null }>, permissionFlags?: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, fieldPermissions?: Array<{ __typename?: 'FieldPermission', objectMetadataId: string, fieldMetadataId: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null, id: string, roleId: string }> | null }> };
export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, 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, applicationId?: string | null, createdAt: string, updatedAt: string }>, apiKeys: Array<{ __typename?: 'ApiKeyForRole', id: string, name: string, expiresAt: string, revokedAt?: string | null }>, permissionFlags?: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, fieldPermissions?: Array<{ __typename?: 'FieldPermission', objectMetadataId: string, fieldMetadataId: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null, id: string, roleId: string }> | null }> };
export type CreateApprovedAccessDomainMutationVariables = Exact<{
input: CreateApprovedAccessDomainInput;
@@ -5553,21 +5588,21 @@ export type GetSsoIdentityProvidersQueryVariables = Exact<{ [key: string]: never
export type GetSsoIdentityProvidersQuery = { __typename?: 'Query', getSSOIdentityProviders: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdentityProviderType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> };
export type ServerlessFunctionFieldsFragment = { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string };
export type ServerlessFunctionFieldsFragment = { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null };
export type CreateOneServerlessFunctionItemMutationVariables = Exact<{
input: CreateServerlessFunctionInput;
}>;
export type CreateOneServerlessFunctionItemMutation = { __typename?: 'Mutation', createOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string } };
export type CreateOneServerlessFunctionItemMutation = { __typename?: 'Mutation', createOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } };
export type DeleteOneServerlessFunctionMutationVariables = Exact<{
input: ServerlessFunctionIdInput;
}>;
export type DeleteOneServerlessFunctionMutation = { __typename?: 'Mutation', deleteOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string } };
export type DeleteOneServerlessFunctionMutation = { __typename?: 'Mutation', deleteOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } };
export type ExecuteOneServerlessFunctionMutationVariables = Exact<{
input: ExecuteServerlessFunctionInput;
@@ -5581,14 +5616,14 @@ export type PublishOneServerlessFunctionMutationVariables = Exact<{
}>;
export type PublishOneServerlessFunctionMutation = { __typename?: 'Mutation', publishServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string } };
export type PublishOneServerlessFunctionMutation = { __typename?: 'Mutation', publishServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } };
export type UpdateOneServerlessFunctionMutationVariables = Exact<{
input: UpdateServerlessFunctionInput;
}>;
export type UpdateOneServerlessFunctionMutation = { __typename?: 'Mutation', updateOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string } };
export type UpdateOneServerlessFunctionMutation = { __typename?: 'Mutation', updateOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } };
export type FindManyAvailablePackagesQueryVariables = Exact<{
input: ServerlessFunctionIdInput;
@@ -5600,14 +5635,14 @@ export type FindManyAvailablePackagesQuery = { __typename?: 'Query', getAvailabl
export type GetManyServerlessFunctionsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetManyServerlessFunctionsQuery = { __typename?: 'Query', findManyServerlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string }> };
export type GetManyServerlessFunctionsQuery = { __typename?: 'Query', findManyServerlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null }> };
export type GetOneServerlessFunctionQueryVariables = Exact<{
input: ServerlessFunctionIdInput;
}>;
export type GetOneServerlessFunctionQuery = { __typename?: 'Query', findOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string } };
export type GetOneServerlessFunctionQuery = { __typename?: 'Query', findOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array<string>, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } };
export type FindOneServerlessFunctionSourceCodeQueryVariables = Exact<{
input: GetServerlessFunctionSourceCodeInput;
@@ -6103,10 +6138,166 @@ export const AgentFieldsFragmentDoc = gql`
roleId
isCustom
modelConfiguration
applicationId
createdAt
updatedAt
}
`;
export const ObjectMetadataFieldsFragmentDoc = gql`
fragment ObjectMetadataFields on Object {
id
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isRemote
isActive
isSystem
isUIReadOnly
createdAt
updatedAt
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
applicationId
shortcut
isLabelSyncedWithName
isSearchable
duplicateCriteria
indexMetadataList {
id
createdAt
updatedAt
name
indexWhereClause
indexType
isUnique
isCustom
indexFieldMetadataList {
id
fieldMetadataId
createdAt
updatedAt
order
}
}
fieldsList {
id
type
name
label
description
icon
isCustom
isActive
isSystem
isUIReadOnly
isNullable
isUnique
createdAt
updatedAt
defaultValue
options
settings
isLabelSyncedWithName
relation {
type
sourceObjectMetadata {
id
nameSingular
namePlural
}
targetObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetFieldMetadata {
id
name
}
}
morphRelations {
type
sourceObjectMetadata {
id
nameSingular
namePlural
}
targetObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetFieldMetadata {
id
name
}
}
}
}
`;
export const ServerlessFunctionFieldsFragmentDoc = gql`
fragment ServerlessFunctionFields on ServerlessFunction {
id
name
description
runtime
timeoutSeconds
latestVersion
publishedVersions
cronTriggers {
id
settings
createdAt
updatedAt
}
databaseEventTriggers {
id
settings
createdAt
updatedAt
}
routeTriggers {
id
path
isAuthRequired
httpMethod
createdAt
updatedAt
}
createdAt
updatedAt
}
`;
export const ApplicationFieldsFragmentDoc = gql`
fragment ApplicationFields on Application {
id
name
description
agents {
...AgentFields
}
objects {
...ObjectMetadataFields
}
serverlessFunctions {
...ServerlessFunctionFields
}
}
${AgentFieldsFragmentDoc}
${ObjectMetadataFieldsFragmentDoc}
${ServerlessFunctionFieldsFragmentDoc}`;
export const AuthTokenFragmentFragmentDoc = gql`
fragment AuthTokenFragment on AuthToken {
token
@@ -6226,19 +6417,6 @@ export const PermissionFlagFragmentFragmentDoc = gql`
roleId
}
`;
export const ServerlessFunctionFieldsFragmentDoc = gql`
fragment ServerlessFunctionFields on ServerlessFunction {
id
name
description
runtime
timeoutSeconds
latestVersion
publishedVersions
createdAt
updatedAt
}
`;
export const WorkspaceMemberQueryFragmentFragmentDoc = gql`
fragment WorkspaceMemberQueryFragment on WorkspaceMember {
id
@@ -7144,6 +7322,77 @@ export function useTrackAnalyticsMutation(baseOptions?: Apollo.MutationHookOptio
export type TrackAnalyticsMutationHookResult = ReturnType<typeof useTrackAnalyticsMutation>;
export type TrackAnalyticsMutationResult = Apollo.MutationResult<TrackAnalyticsMutation>;
export type TrackAnalyticsMutationOptions = Apollo.BaseMutationOptions<TrackAnalyticsMutation, TrackAnalyticsMutationVariables>;
export const FindManyApplicationsDocument = gql`
query FindManyApplications {
findManyApplications {
id
name
description
}
}
`;
/**
* __useFindManyApplicationsQuery__
*
* To run a query within a React component, call `useFindManyApplicationsQuery` and pass it any options that fit your needs.
* When your component renders, `useFindManyApplicationsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useFindManyApplicationsQuery({
* variables: {
* },
* });
*/
export function useFindManyApplicationsQuery(baseOptions?: Apollo.QueryHookOptions<FindManyApplicationsQuery, FindManyApplicationsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindManyApplicationsQuery, FindManyApplicationsQueryVariables>(FindManyApplicationsDocument, options);
}
export function useFindManyApplicationsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindManyApplicationsQuery, FindManyApplicationsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FindManyApplicationsQuery, FindManyApplicationsQueryVariables>(FindManyApplicationsDocument, options);
}
export type FindManyApplicationsQueryHookResult = ReturnType<typeof useFindManyApplicationsQuery>;
export type FindManyApplicationsLazyQueryHookResult = ReturnType<typeof useFindManyApplicationsLazyQuery>;
export type FindManyApplicationsQueryResult = Apollo.QueryResult<FindManyApplicationsQuery, FindManyApplicationsQueryVariables>;
export const FindOneApplicationDocument = gql`
query FindOneApplication($id: UUID!) {
findOneApplication(id: $id) {
...ApplicationFields
}
}
${ApplicationFieldsFragmentDoc}`;
/**
* __useFindOneApplicationQuery__
*
* To run a query within a React component, call `useFindOneApplicationQuery` and pass it any options that fit your needs.
* When your component renders, `useFindOneApplicationQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useFindOneApplicationQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useFindOneApplicationQuery(baseOptions: Apollo.QueryHookOptions<FindOneApplicationQuery, FindOneApplicationQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindOneApplicationQuery, FindOneApplicationQueryVariables>(FindOneApplicationDocument, options);
}
export function useFindOneApplicationLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindOneApplicationQuery, FindOneApplicationQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FindOneApplicationQuery, FindOneApplicationQueryVariables>(FindOneApplicationDocument, options);
}
export type FindOneApplicationQueryHookResult = ReturnType<typeof useFindOneApplicationQuery>;
export type FindOneApplicationLazyQueryHookResult = ReturnType<typeof useFindOneApplicationLazyQuery>;
export type FindOneApplicationQueryResult = Apollo.QueryResult<FindOneApplicationQuery, FindOneApplicationQueryVariables>;
export const UploadFileDocument = gql`
mutation uploadFile($file: Upload!, $fileFolder: FileFolder) {
uploadFile(file: $file, fileFolder: $fileFolder) {
@@ -9285,105 +9534,7 @@ export const ObjectMetadataItemsDocument = gql`
objects(paging: {first: 1000}) {
edges {
node {
id
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isRemote
isActive
isSystem
isUIReadOnly
createdAt
updatedAt
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
shortcut
isLabelSyncedWithName
isSearchable
duplicateCriteria
indexMetadataList {
id
createdAt
updatedAt
name
indexWhereClause
indexType
isUnique
isCustom
indexFieldMetadataList {
id
fieldMetadataId
createdAt
updatedAt
order
}
}
fieldsList {
id
type
name
label
description
icon
isCustom
isActive
isSystem
isUIReadOnly
isNullable
isUnique
createdAt
updatedAt
defaultValue
options
settings
isLabelSyncedWithName
relation {
type
sourceObjectMetadata {
id
nameSingular
namePlural
}
targetObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetFieldMetadata {
id
name
}
}
morphRelations {
type
sourceObjectMetadata {
id
nameSingular
namePlural
}
targetObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetFieldMetadata {
id
name
}
}
}
...ObjectMetadataFields
}
}
pageInfo {
@@ -9394,7 +9545,7 @@ export const ObjectMetadataItemsDocument = gql`
}
}
}
`;
${ObjectMetadataFieldsFragmentDoc}`;
/**
* __useObjectMetadataItemsQuery__
+23 -4
View File
@@ -51,6 +51,7 @@ export type AdminPanelWorkerQueueHealth = {
export type Agent = {
__typename?: 'Agent';
applicationId?: Maybe<Scalars['UUID']>;
createdAt: Scalars['DateTime'];
description?: Maybe<Scalars['String']>;
icon?: Maybe<Scalars['String']>;
@@ -200,6 +201,16 @@ export type AppTokenEdge = {
node: AppToken;
};
export type Application = {
__typename?: 'Application';
agents: Array<Agent>;
description: Scalars['String'];
id: Scalars['UUID'];
name: Scalars['String'];
objects: Array<Object>;
serverlessFunctions: Array<ServerlessFunction>;
};
export type ApprovedAccessDomain = {
__typename?: 'ApprovedAccessDomain';
createdAt: Scalars['DateTime'];
@@ -936,7 +947,6 @@ export type CronTrigger = {
__typename?: 'CronTrigger';
createdAt: Scalars['DateTime'];
id: Scalars['UUID'];
serverlessFunctionId: Scalars['String'];
settings: Scalars['JSON'];
updatedAt: Scalars['DateTime'];
};
@@ -970,7 +980,6 @@ export type DatabaseEventTrigger = {
__typename?: 'DatabaseEventTrigger';
createdAt: Scalars['DateTime'];
id: Scalars['UUID'];
serverlessFunctionId: Scalars['String'];
settings: Scalars['JSON'];
updatedAt: Scalars['DateTime'];
};
@@ -1190,6 +1199,7 @@ export type FeatureFlagDto = {
export enum FeatureFlagKey {
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
IS_AI_ENABLED = 'IS_AI_ENABLED',
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
IS_CALENDAR_VIEW_ENABLED = 'IS_CALENDAR_VIEW_ENABLED',
IS_COMMON_API_ENABLED = 'IS_COMMON_API_ENABLED',
IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED',
@@ -2668,6 +2678,7 @@ export type NumberChartConfiguration = {
export type Object = {
__typename?: 'Object';
applicationId?: Maybe<Scalars['UUID']>;
createdAt: Scalars['DateTime'];
description?: Maybe<Scalars['String']>;
duplicateCriteria?: Maybe<Array<Array<Scalars['String']>>>;
@@ -2981,12 +2992,14 @@ export type Query = {
findAgentHandoffTargets: Array<Agent>;
findAgentHandoffs: Array<AgentHandoffDto>;
findManyAgents: Array<Agent>;
findManyApplications: Array<Application>;
findManyCronTriggers: Array<CronTrigger>;
findManyDatabaseEventTriggers: Array<DatabaseEventTrigger>;
findManyPublicDomains: Array<PublicDomain>;
findManyRouteTriggers: Array<RouteTrigger>;
findManyServerlessFunctions: Array<ServerlessFunction>;
findOneAgent: Agent;
findOneApplication: Application;
findOneCronTrigger: CronTrigger;
findOneDatabaseEventTrigger: DatabaseEventTrigger;
findOneRouteTrigger: RouteTrigger;
@@ -3098,6 +3111,11 @@ export type QueryFindOneAgentArgs = {
};
export type QueryFindOneApplicationArgs = {
id: Scalars['UUID'];
};
export type QueryFindOneCronTriggerArgs = {
input: CronTriggerIdInput;
};
@@ -3442,9 +3460,7 @@ export type RouteTrigger = {
id: Scalars['ID'];
isAuthRequired: Scalars['Boolean'];
path: Scalars['String'];
serverlessFunctionId: Scalars['String'];
updatedAt: Scalars['DateTime'];
workspaceId: Scalars['String'];
};
export type RouteTriggerIdInput = {
@@ -3530,11 +3546,14 @@ export type Sentry = {
export type ServerlessFunction = {
__typename?: 'ServerlessFunction';
createdAt: Scalars['DateTime'];
cronTriggers?: Maybe<Array<CronTrigger>>;
databaseEventTriggers?: Maybe<Array<DatabaseEventTrigger>>;
description?: Maybe<Scalars['String']>;
id: Scalars['UUID'];
latestVersion?: Maybe<Scalars['String']>;
name: Scalars['String'];
publishedVersions: Array<Scalars['String']>;
routeTriggers?: Maybe<Array<RouteTrigger>>;
runtime: Scalars['String'];
timeoutSeconds: Scalars['Float'];
updatedAt: Scalars['DateTime'];
@@ -13,6 +13,7 @@ export const AGENT_FRAGMENT = gql`
roleId
isCustom
modelConfiguration
applicationId
createdAt
updatedAt
}
@@ -5,7 +5,7 @@ import { SettingsProtectedRouteWrapper } from '@/settings/components/SettingsPro
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { SettingPublicDomain } from '@/settings/domains/components/SettingPublicDomain';
import { SettingsPath } from 'twenty-shared/types';
import { PermissionFlagType } from '~/generated/graphql';
import { FeatureFlagKey, PermissionFlagType } from '~/generated/graphql';
const SettingsGraphQLPlayground = lazy(() =>
import(
@@ -105,12 +105,6 @@ const SettingsDevelopersApiKeysNew = lazy(() =>
})),
);
const SettingsServerlessFunctions = lazy(() =>
import(
'~/pages/settings/serverless-functions/SettingsServerlessFunctions'
).then((module) => ({ default: module.SettingsServerlessFunctions })),
);
const SettingsServerlessFunctionDetail = lazy(() =>
import(
'~/pages/settings/serverless-functions/SettingsServerlessFunctionDetail'
@@ -119,14 +113,6 @@ const SettingsServerlessFunctionDetail = lazy(() =>
})),
);
const SettingsServerlessFunctionsNew = lazy(() =>
import(
'~/pages/settings/serverless-functions/SettingsServerlessFunctionsNew'
).then((module) => ({
default: module.SettingsServerlessFunctionsNew,
})),
);
const SettingsWorkspace = lazy(() =>
import('~/pages/settings/SettingsWorkspace').then((module) => ({
default: module.SettingsWorkspace,
@@ -157,6 +143,22 @@ const SettingsAI = lazy(() =>
})),
);
const SettingsApplications = lazy(() =>
import('~/pages/settings/applications/SettingsApplications').then(
(module) => ({
default: module.SettingsApplications,
}),
),
);
const SettingsApplicationDetails = lazy(() =>
import('~/pages/settings/applications/SettingsApplicationDetails').then(
(module) => ({
default: module.SettingsApplicationDetails,
}),
),
);
const SettingsAgentForm = lazy(() =>
import('~/pages/settings/ai/SettingsAgentForm').then((module) => ({
default: module.SettingsAgentForm,
@@ -399,10 +401,7 @@ type SettingsRoutesProps = {
isAdminPageEnabled?: boolean;
};
export const SettingsRoutes = ({
isFunctionSettingsEnabled,
isAdminPageEnabled,
}: SettingsRoutesProps) => (
export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
<Suspense fallback={<SettingsSkeletonLoader />}>
<Routes>
<Route path={SettingsPath.ProfilePage} element={<SettingsProfile />} />
@@ -590,22 +589,27 @@ export const SettingsRoutes = ({
element={<SettingsIntegrationMCP />}
/>
</Route>
{isFunctionSettingsEnabled && (
<>
<Route
path={SettingsPath.ServerlessFunctions}
element={<SettingsServerlessFunctions />}
<Route
element={
<SettingsProtectedRouteWrapper
requiredFeatureFlag={FeatureFlagKey.IS_APPLICATION_ENABLED}
/>
<Route
path={SettingsPath.NewServerlessFunction}
element={<SettingsServerlessFunctionsNew />}
/>
<Route
path={SettingsPath.ServerlessFunctionDetail}
element={<SettingsServerlessFunctionDetail />}
/>
</>
)}
}
>
<Route
path={SettingsPath.Applications}
element={<SettingsApplications />}
/>
<Route
path={SettingsPath.ApplicationDetail}
element={<SettingsApplicationDetails />}
/>
<Route
path={SettingsPath.ApplicationServerlessFunctionDetail}
element={<SettingsServerlessFunctionDetail />}
/>
</Route>
<Route
element={
@@ -0,0 +1,24 @@
import { gql } from '@apollo/client';
import { AGENT_FRAGMENT } from '@/ai/graphql/fragments/agentFragment';
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
import { OBJECT_METADATA_FRAGMENT } from '@/object-metadata/graphql/fragment';
export const APPLICATION_FRAGMENT = gql`
${AGENT_FRAGMENT}
${SERVERLESS_FUNCTION_FRAGMENT}
${OBJECT_METADATA_FRAGMENT}
fragment ApplicationFields on Application {
id
name
description
agents {
...AgentFields
}
objects {
...ObjectMetadataFields
}
serverlessFunctions {
...ServerlessFunctionFields
}
}
`;
@@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
export const FIND_MANY_APPLICATIONS = gql`
query FindManyApplications {
findManyApplications {
id
name
description
}
}
`;
@@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
import { APPLICATION_FRAGMENT } from '../fragments/applicationFragment';
export const FIND_ONE_APPLICATION = gql`
${APPLICATION_FRAGMENT}
query FindOneApplication($id: UUID!) {
findOneApplication(id: $id) {
...ApplicationFields
}
}
`;
@@ -0,0 +1,106 @@
import { gql } from '@apollo/client';
export const OBJECT_METADATA_FRAGMENT = gql`
fragment ObjectMetadataFields on Object {
id
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isRemote
isActive
isSystem
isUIReadOnly
createdAt
updatedAt
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
applicationId
shortcut
isLabelSyncedWithName
isSearchable
duplicateCriteria
indexMetadataList {
id
createdAt
updatedAt
name
indexWhereClause
indexType
isUnique
isCustom
indexFieldMetadataList {
id
fieldMetadataId
createdAt
updatedAt
order
}
}
fieldsList {
id
type
name
label
description
icon
isCustom
isActive
isSystem
isUIReadOnly
isNullable
isUnique
createdAt
updatedAt
defaultValue
options
settings
isLabelSyncedWithName
relation {
type
sourceObjectMetadata {
id
nameSingular
namePlural
}
targetObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetFieldMetadata {
id
name
}
}
morphRelations {
type
sourceObjectMetadata {
id
nameSingular
namePlural
}
targetObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetFieldMetadata {
id
name
}
}
}
}
`;
@@ -1,109 +1,13 @@
import { gql } from '@apollo/client';
import { OBJECT_METADATA_FRAGMENT } from '@/object-metadata/graphql/fragment';
export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
${OBJECT_METADATA_FRAGMENT}
query ObjectMetadataItems {
objects(paging: { first: 1000 }) {
edges {
node {
id
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isRemote
isActive
isSystem
isUIReadOnly
createdAt
updatedAt
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
shortcut
isLabelSyncedWithName
isSearchable
duplicateCriteria
indexMetadataList {
id
createdAt
updatedAt
name
indexWhereClause
indexType
isUnique
isCustom
indexFieldMetadataList {
id
fieldMetadataId
createdAt
updatedAt
order
}
}
fieldsList {
id
type
name
label
description
icon
isCustom
isActive
isSystem
isUIReadOnly
isNullable
isUnique
createdAt
updatedAt
defaultValue
options
settings
isLabelSyncedWithName
relation {
type
sourceObjectMetadata {
id
nameSingular
namePlural
}
targetObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetFieldMetadata {
id
name
}
}
morphRelations {
type
sourceObjectMetadata {
id
nameSingular
namePlural
}
targetObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetFieldMetadata {
id
name
}
}
}
...ObjectMetadataFields
}
}
pageInfo {
@@ -64,4 +64,28 @@ describe('isObjectMetadataReadOnly', () => {
expect(result).toBe(true);
});
it('should return true if object is managed by application', () => {
const result = isObjectMetadataReadOnly({
objectMetadataItem: {
applicationId: 'applicationId',
isUIReadOnly: false,
isRemote: false,
},
});
expect(result).toBe(true);
});
it('should return false if object is custom', () => {
const result = isObjectMetadataReadOnly({
objectMetadataItem: {
applicationId: undefined,
isUIReadOnly: false,
isRemote: false,
},
});
expect(result).toBe(false);
});
});
@@ -1,9 +1,13 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ObjectPermission } from '~/generated/graphql';
import { isDefined } from 'twenty-shared/utils';
type IsObjectMetadataReadOnlyParams = {
objectPermissions: ObjectPermission;
objectMetadataItem: Pick<ObjectMetadataItem, 'isUIReadOnly' | 'isRemote'>;
objectPermissions?: ObjectPermission;
objectMetadataItem?: Pick<
ObjectMetadataItem,
'isUIReadOnly' | 'isRemote' | 'applicationId'
>;
};
export const isObjectMetadataReadOnly = ({
@@ -11,8 +15,10 @@ export const isObjectMetadataReadOnly = ({
objectMetadataItem,
}: IsObjectMetadataReadOnlyParams) => {
return (
!objectPermissions.canUpdateObjectRecords ||
objectMetadataItem.isUIReadOnly ||
objectMetadataItem.isRemote
(isDefined(objectPermissions) &&
!objectPermissions.canUpdateObjectRecords) ||
objectMetadataItem?.isUIReadOnly ||
objectMetadataItem?.isRemote ||
isDefined(objectMetadataItem?.applicationId)
);
};
@@ -0,0 +1 @@
export type Sources = { [key: string]: string | Sources };
@@ -0,0 +1,146 @@
import { computeNewSources } from '@/serverless-functions/utils/computeNewSources';
describe('computeNewSources', () => {
it('should compute new code input root 0', () => {
const previousCodeInput = {
'index.ts': 'export const toto = () => {}',
};
const filePath = 'index.ts';
const value = 'export const totoUpdated = () => {}';
const expectedResult = {
'index.ts': 'export const totoUpdated = () => {}',
};
expect(
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
).toEqual(expectedResult);
});
it('should compute new code input root 0 file changed', () => {
const previousCodeInput = {
'.env': 'ENV=env',
'index.ts': 'export const toto = () => {}',
};
const filePath = '.env';
const value = 'ENV=env\nENV2=env2';
const expectedResult = {
'.env': 'ENV=env\nENV2=env2',
'index.ts': 'export const toto = () => {}',
};
expect(
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
).toEqual(expectedResult);
});
it('should compute new code input root 0 with multiple files', () => {
const previousCodeInput = {
'index.ts': 'export const toto = () => {}',
'.env': 'ENV',
};
const filePath = 'index.ts';
const value = 'export const totoUpdated = () => {}';
const expectedResult = {
'index.ts': 'export const totoUpdated = () => {}',
'.env': 'ENV',
};
expect(
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
).toEqual(expectedResult);
});
it('should compute new code input root 1', () => {
const previousCodeInput = {
src: { 'index.ts': 'export const toto = () => {}' },
};
const filePath = 'src/index.ts';
const value = 'export const totoUpdated = () => {}';
const expectedResult = {
src: { 'index.ts': 'export const totoUpdated = () => {}' },
};
expect(
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
).toEqual(expectedResult);
});
it('should compute new code input root 1 with multiple files', () => {
const previousCodeInput = {
src: {
'index.ts': 'export const toto = () => {}',
'index2.ts': 'export const toto2 = () => {}',
},
};
const filePath = 'src/index.ts';
const value = 'export const totoUpdated = () => {}';
const expectedResult = {
src: {
'index.ts': 'export const totoUpdated = () => {}',
'index2.ts': 'export const toto2 = () => {}',
},
};
expect(
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
).toEqual(expectedResult);
});
it('should compute new code input root 1 with added files', () => {
const previousCodeInput = {
src: {
'index.ts': 'export const toto = () => {}',
},
};
const filePath = 'src/index2.ts';
const value = 'export const toto2 = () => {}';
const expectedResult = {
src: {
'index.ts': 'export const toto = () => {}',
'index2.ts': 'export const toto2 = () => {}',
},
};
expect(
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
).toEqual(expectedResult);
});
it('should compute new code input multiple roots', () => {
const previousCodeInput = {
'.env': 'ENV=env',
src: { 'index.ts': 'export const toto = () => {}' },
};
const filePath = 'src/index.ts';
const value = 'export const totoUpdated = () => {}';
const expectedResult = {
src: { 'index.ts': 'export const totoUpdated = () => {}' },
'.env': 'ENV=env',
};
expect(
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
).toEqual(expectedResult);
});
});
@@ -0,0 +1,58 @@
// IA Generated
import { type Sources } from '@/serverless-functions/types/sources.type';
import { flattenSources } from '@/serverless-functions/utils/flattenSources';
describe('flattenSources', () => {
it('flattens nested sources with root files', () => {
const input: Sources = {
'.env': 'KEY=VALUE',
src: {
'index.ts': 'export const a = 1',
lib: {
'util.ts': 'export const util = () => {}',
},
},
docs: {
'README.md': '# Hello',
},
};
const result = flattenSources(input);
expect(result).toEqual([
{ path: '.env', content: 'KEY=VALUE' },
{ path: 'docs/README.md', content: '# Hello' },
{ path: 'src/index.ts', content: 'export const a = 1' },
{ path: 'src/lib/util.ts', content: 'export const util = () => {}' },
]);
});
it('handles deep nesting and preserves file contents', () => {
const input: Sources = {
a: { b: { c: { d: { 'file.ts': 'content' } } } },
};
expect(flattenSources(input)).toEqual([
{ path: 'a/b/c/d/file.ts', content: 'content' },
]);
});
it('ignores empty folders and non-string leaves', () => {
const input: Sources = {
empty: {},
weird: {
oops: 42,
} as unknown as Sources,
file: 'ok',
};
const res = flattenSources(input);
expect(res).toEqual([{ path: 'file', content: 'ok' }]);
});
it('accepts a custom basePath prefix', () => {
const input: Sources = { src: { 'index.ts': 'x' } };
const res = flattenSources(input, 'pkg');
expect(res).toEqual([{ path: 'pkg/src/index.ts', content: 'x' }]);
});
});
@@ -0,0 +1,48 @@
import { type Sources } from '@/serverless-functions/types/sources.type';
export const computeNewSources = ({
previousCode,
filePath,
value,
}: {
previousCode: Sources;
filePath: string;
value: string;
}): Sources => {
const result = { ...previousCode };
const parts = filePath.split('/').filter(Boolean);
if (parts.length === 0) {
return result;
}
if (parts.length === 1) {
result[filePath] = value;
return result;
}
const [root, ...rest] = parts;
const newFilePath = rest.join('/');
if (
typeof result?.[root] === 'string' ||
typeof previousCode[root] === 'string'
) {
throw Error('Cannot compute new code input');
}
return {
...previousCode,
[root]: {
...previousCode[root],
...computeNewSources({
previousCode: result?.[root] ?? {},
filePath: newFilePath,
value,
}),
},
};
};
@@ -0,0 +1,28 @@
// IA Generated
import { type Sources } from '@/serverless-functions/types/sources.type';
type FlatSource = { path: string; content: string };
export const flattenSources = (
sources: Sources,
basePath = '',
): FlatSource[] => {
const out: FlatSource[] = [];
const join = (a: string, b: string) => (a ? `${a}/${b}` : b);
const walk = (node: Sources, prefix: string) => {
for (const [name, value] of Object.entries(node)) {
if (typeof value === 'string') {
out.push({ path: join(prefix, name), content: value });
} else if (value && typeof value === 'object') {
walk(value as Sources, join(prefix, name));
}
}
};
walk(sources, basePath);
out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
return out;
};
@@ -0,0 +1,112 @@
import { Select } from '@/ui/input/components/Select';
import { isDefined } from 'twenty-shared/utils';
import { IconButton, type SelectOption } from 'twenty-ui/input';
import {
IconBox,
IconNorthStar,
IconPlus,
IconTrash,
useIcons,
} from 'twenty-ui/display';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import styled from '@emotion/styled';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
const OBJECT_DROPDOWN_WIDTH = 340;
const ACTION_DROPDOWN_WIDTH = 140;
const OBJECT_MOBILE_WIDTH = 150;
const ACTION_MOBILE_WIDTH = 140;
const StyledFilterRow = styled.div<{ isMobile: boolean }>`
display: grid;
grid-template-columns: ${({ isMobile }) =>
isMobile
? `${OBJECT_MOBILE_WIDTH}px ${ACTION_MOBILE_WIDTH}px auto`
: `${OBJECT_DROPDOWN_WIDTH}px ${ACTION_DROPDOWN_WIDTH}px auto`};
gap: ${({ theme }) => theme.spacing(2)};
margin-bottom: ${({ theme }) => theme.spacing(2)};
align-items: center;
`;
const StyledPlaceholder = styled.div`
height: ${({ theme }) => theme.spacing(8)};
width: ${({ theme }) => theme.spacing(8)};
`;
export const SettingsDatabaseEventsForm = ({
events,
updateOperation,
removeOperation,
disabled = false,
}: {
events: { object: string | null; action: string }[];
updateOperation?: (
index: number,
field: 'object' | 'action',
value: string | null,
) => void;
removeOperation?: (index: number) => void;
disabled?: boolean;
}) => {
const isMobile = useIsMobile();
const { objectMetadataItems } = useObjectMetadataItems();
const { getIcon } = useIcons();
const objectOptions: SelectOption<string>[] = [
{ label: 'All Objects', value: '*', Icon: IconNorthStar },
...objectMetadataItems.map((item) => ({
label: item.labelPlural,
value: item.nameSingular,
Icon: getIcon(item.icon),
})),
];
const actionOptions: SelectOption<string>[] = [
{ label: 'All', value: '*', Icon: IconNorthStar },
{ label: 'Created', value: 'created', Icon: IconPlus },
{ label: 'Updated', value: 'updated', Icon: IconBox },
{ label: 'Deleted', value: 'deleted', Icon: IconTrash },
];
return (
<>
{events.map((operation, index) => (
<StyledFilterRow key={index} isMobile={isMobile}>
<Select
dropdownId={`object-webhook-type-select-${index}`}
value={operation.object}
options={objectOptions}
onChange={(newValue) =>
updateOperation?.(index, 'object', newValue)
}
fullWidth
emptyOption={{ label: 'Object', value: null }}
disabled={disabled}
/>
<Select
dropdownId={`operation-webhook-type-select-${index}`}
value={operation.action}
options={actionOptions}
onChange={(newValue) =>
updateOperation?.(index, 'action', newValue)
}
fullWidth
disabled={disabled}
/>
{isDefined(operation.object) && !disabled ? (
<IconButton
Icon={IconTrash}
variant="tertiary"
size="medium"
onClick={() => removeOperation?.(index)}
/>
) : (
<StyledPlaceholder />
)}
</StyledFilterRow>
))}
</>
);
};
@@ -0,0 +1,27 @@
import { Tag } from 'twenty-ui/components';
import { getItemTagInfo } from '@/settings/data-model/utils/getItemTagInfo';
type SettingsItemTypeTagProps = {
item: {
isCustom?: boolean;
isRemote?: boolean;
applicationId?: string | null;
};
className?: string;
};
export const SettingsItemTypeTag = ({
className,
item: { isCustom, isRemote, applicationId },
}: SettingsItemTypeTagProps) => {
const itemTagInfo = getItemTagInfo({ isCustom, isRemote, applicationId });
return (
<Tag
className={className}
color={itemTagInfo.labelColor}
text={itemTagInfo.labelText}
weight="medium"
/>
);
};
@@ -9,6 +9,7 @@ type SettingsDataModelPreviewFormCardProps = {
className?: string;
preview: ReactNode;
form?: ReactNode;
disabled?: boolean;
};
const StyledPreviewContainer = styled(CardContent)`
@@ -18,9 +18,11 @@ export type SettingsDataModelFieldBooleanFormValues = z.infer<
type SettingsDataModelFieldBooleanFormProps = {
existingFieldMetadataId: string;
disabled?: boolean;
};
export const SettingsDataModelFieldBooleanForm = ({
disabled,
existingFieldMetadataId,
}: SettingsDataModelFieldBooleanFormProps) => {
const { t } = useLingui();
@@ -46,6 +48,7 @@ export const SettingsDataModelFieldBooleanForm = ({
onChange={onChange}
dropdownId="object-field-default-value-select-boolean"
dropdownWidth={120}
disabled={disabled}
needIconCheck={false}
options={BOOLEAN_DATA_MODEL_SELECT_OPTIONS.map((option) => ({
...option,
@@ -12,11 +12,13 @@ import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data
type SettingsDataModelFieldBooleanSettingsFormCardProps = {
existingFieldMetadataId: string;
objectNameSingular: string;
disabled?: boolean;
};
export const SettingsDataModelFieldBooleanSettingsFormCard = ({
existingFieldMetadataId,
objectNameSingular,
disabled = false,
}: SettingsDataModelFieldBooleanSettingsFormCardProps) => {
const { watch } = useFormContext<
SettingsDataModelFieldBooleanFormValues &
@@ -38,6 +40,7 @@ export const SettingsDataModelFieldBooleanSettingsFormCard = ({
}
form={
<SettingsDataModelFieldBooleanForm
disabled={disabled}
existingFieldMetadataId={existingFieldMetadataId}
/>
}
@@ -76,12 +76,14 @@ type SettingsDataModelFieldIconLabelFormProps = {
fieldMetadataItem?: FieldMetadataItem;
maxLength?: number;
isCreationMode?: boolean;
readonly?: boolean;
};
export const SettingsDataModelFieldIconLabelForm = ({
isCreationMode = false,
fieldMetadataItem,
maxLength,
readonly = false,
}: SettingsDataModelFieldIconLabelFormProps) => {
const {
control,
@@ -150,6 +152,7 @@ export const SettingsDataModelFieldIconLabelForm = ({
selectedIconKey={value ?? 'IconUsers'}
onChange={({ iconKey }) => onChange(iconKey)}
variant="primary"
disabled={readonly}
/>
)}
/>
@@ -162,7 +165,7 @@ export const SettingsDataModelFieldIconLabelForm = ({
instanceId={labelTextInputId}
placeholder={t`Employees`}
value={value}
disabled={!isLabelEditEnabled}
disabled={!isLabelEditEnabled || readonly}
onChange={(value) => {
onChange(value);
trigger('label');
@@ -199,6 +202,7 @@ export const SettingsDataModelFieldIconLabelForm = ({
placeholder={t`employees`}
value={value}
onChange={onChange}
readOnly={readonly}
disabled={!isNameEditEnabled}
fullWidth
maxLength={DATABASE_IDENTIFIER_MAXIMUM_LENGTH}
@@ -245,6 +249,7 @@ export const SettingsDataModelFieldIconLabelForm = ({
title={t`Synchronize Field Label and API Name`}
description={t`Should changing a field's label also change the API name?`}
checked={value ?? true}
disabled={readonly}
advancedMode
onChange={(value) => {
onChange(value);
@@ -18,12 +18,14 @@ type SettingsDataModelFieldIsUniqueFormProps = {
objectNameSingular: string;
fieldType: FieldMetadataType;
existingFieldMetadataId: string;
disabled?: boolean;
};
export const SettingsDataModelFieldIsUniqueForm = ({
fieldType,
existingFieldMetadataId,
objectNameSingular,
disabled = false,
}: SettingsDataModelFieldIsUniqueFormProps) => {
const { control } =
useFormContext<SettingsDataModelFieldIsUniqueFormValues>();
@@ -74,7 +76,7 @@ export const SettingsDataModelFieldIsUniqueForm = ({
toggleSize="small"
value={isUnique}
onChange={(value) => onChange(value)}
disabled={hasStandardUniqueIndex}
disabled={disabled || hasStandardUniqueIndex}
/>
</SettingsOptionCardContentSelect>
</>
@@ -160,6 +160,7 @@ type SettingsDataModelFieldSettingsFormCardProps = {
existingFieldMetadataId: string;
fieldType: FieldMetadataType;
objectNameSingular: string;
disabled?: boolean;
};
const previewableTypes = [
@@ -188,6 +189,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
existingFieldMetadataId,
fieldType,
objectNameSingular,
disabled = false,
}: SettingsDataModelFieldSettingsFormCardProps) => {
const { watch } = useFormContext<SettingsDataModelFieldEditFormValues>();
@@ -200,6 +202,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldBooleanSettingsFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -209,6 +212,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldCurrencySettingsFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -222,6 +226,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
existingFieldMetadataId={existingFieldMetadataId}
fieldType={fieldType}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -231,6 +236,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldRelationSettingsFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -240,6 +246,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldMorphRelationFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -249,6 +256,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldNumberSettingsFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -258,6 +266,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldTextSettingsFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -267,6 +276,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldAddressSettingsFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -276,6 +286,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldPhonesSettingsFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -289,6 +300,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
existingFieldMetadataId={existingFieldMetadataId}
fieldType={fieldType}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -318,6 +330,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldMaxValuesForm
existingFieldMetadataId={existingFieldMetadataId}
fieldType={fieldType}
disabled={disabled}
/>
<Separator />
</>
@@ -326,6 +339,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
fieldType={fieldType}
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
</>
}
@@ -42,6 +42,7 @@ export const SettingsDataModelFieldTextSettingsFormCard = ({
fieldType={FieldMetadataType.TEXT}
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
</>
}
@@ -55,6 +55,7 @@ export const SettingsDataModelFieldDateSettingsFormCard = ({
fieldType={fieldType}
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
</>
}
@@ -39,10 +39,12 @@ export type SettingsDataModelFieldMorphRelationFormValues = z.infer<
type SettingsDataModelFieldMorphRelationFormProps = {
existingFieldMetadataId: string;
disabled?: boolean;
};
export const SettingsDataModelFieldMorphRelationForm = ({
existingFieldMetadataId,
disabled = false,
}: SettingsDataModelFieldMorphRelationFormProps) => {
const { t } = useLingui();
const { control } =
@@ -90,7 +92,7 @@ export const SettingsDataModelFieldMorphRelationForm = ({
label={t`Relation type`}
dropdownId="relation-type-select"
fullWidth
disabled={disableRelationEdition}
disabled={disabled || disableRelationEdition}
value={value}
options={RELATION_TYPE_OPTIONS}
onChange={onChange}
@@ -18,11 +18,13 @@ import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data
type SettingsDataModelFieldMorphRelationFormCardProps = {
existingFieldMetadataId: string;
objectNameSingular: string;
disabled?: boolean;
};
export const SettingsDataModelFieldMorphRelationFormCard = ({
existingFieldMetadataId,
objectNameSingular,
disabled = false,
}: SettingsDataModelFieldMorphRelationFormCardProps) => {
const { watch } = useFormContext<
SettingsDataModelFieldMorphRelationFormValues &
@@ -107,6 +109,7 @@ export const SettingsDataModelFieldMorphRelationFormCard = ({
form={
<SettingsDataModelFieldMorphRelationForm
existingFieldMetadataId={existingFieldMetadataId}
disabled={disabled}
/>
}
/>
@@ -42,6 +42,7 @@ export const SettingsDataModelFieldNumberSettingsFormCard = ({
fieldType={FieldMetadataType.NUMBER}
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
</>
}
@@ -58,6 +58,7 @@ export const SettingsDataModelFieldPhonesSettingsFormCard = ({
fieldType={FieldMetadataType.PHONES}
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
</>
}
@@ -47,6 +47,7 @@ export type SettingsDataModelFieldRelationFormValues = z.infer<
type SettingsDataModelFieldRelationFormProps = {
existingFieldMetadataId: string;
objectMetadataItem?: ObjectMetadataItem;
disabled?: boolean;
};
export const StyledContainer = styled.div`
@@ -84,6 +85,7 @@ export const RELATION_TYPE_OPTIONS = Object.entries(RELATION_TYPES).map(
export const SettingsDataModelFieldRelationForm = ({
existingFieldMetadataId,
objectMetadataItem,
disabled,
}: SettingsDataModelFieldRelationFormProps) => {
const { t } = useLingui();
const { control, watch: watchFormValue } =
@@ -129,7 +131,7 @@ export const SettingsDataModelFieldRelationForm = ({
label={t`Relation type`}
dropdownId="relation-type-select"
fullWidth
disabled={disableRelationEdition}
disabled={disabled || disableRelationEdition}
value={value}
options={RELATION_TYPE_OPTIONS}
onChange={onChange}
@@ -145,7 +147,7 @@ export const SettingsDataModelFieldRelationForm = ({
label={t`Object destination`}
dropdownId="object-destination-select"
fullWidth
disabled={disableRelationEdition}
disabled={disabled || disableRelationEdition}
value={value}
options={activeObjectMetadataItems
.filter(isObjectMetadataAvailableForRelation)
@@ -175,7 +177,7 @@ export const SettingsDataModelFieldRelationForm = ({
defaultValue={initialRelationFieldMetadataItem.icon}
render={({ field: { onChange, value } }) => (
<IconPicker
disabled={disableFieldEdition}
disabled={disabled || disableFieldEdition}
dropdownId="field-destination-icon-picker"
selectedIconKey={value ?? undefined}
onChange={({ iconKey }) => onChange(iconKey)}
@@ -190,7 +192,7 @@ export const SettingsDataModelFieldRelationForm = ({
render={({ field: { onChange, value } }) => (
<SettingsTextInput
instanceId="relation-field-label"
disabled={disableFieldEdition}
disabled={disabled || disableFieldEdition}
placeholder={t`Field name`}
value={value}
onChange={onChange}
@@ -18,11 +18,13 @@ import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data
type SettingsDataModelFieldRelationSettingsFormCardProps = {
existingFieldMetadataId: string;
objectNameSingular: string;
disabled?: boolean;
};
export const SettingsDataModelFieldRelationSettingsFormCard = ({
existingFieldMetadataId,
objectNameSingular,
disabled = false,
}: SettingsDataModelFieldRelationSettingsFormCardProps) => {
const { watch } = useFormContext<
SettingsDataModelFieldRelationFormValues &
@@ -95,6 +97,7 @@ export const SettingsDataModelFieldRelationSettingsFormCard = ({
<SettingsDataModelFieldRelationForm
existingFieldMetadataId={existingFieldMetadataId}
objectMetadataItem={relationObjectMetadataItem}
disabled={disabled}
/>
}
/>
@@ -48,6 +48,7 @@ export type SettingsDataModelFieldSelectFormValues = z.infer<
type SettingsDataModelFieldSelectFormProps = {
fieldType: FieldMetadataType.SELECT | FieldMetadataType.MULTI_SELECT;
existingFieldMetadataId: string;
disabled?: boolean;
};
const StyledContainer = styled(CardContent)`
@@ -112,6 +113,7 @@ const StyledButton = styled(LightButton)`
export const SettingsDataModelFieldSelectForm = ({
existingFieldMetadataId,
fieldType,
disabled = false,
}: SettingsDataModelFieldSelectFormProps) => {
const { initialDefaultValue, initialOptions } =
useSelectSettingsFormInitialValues({
@@ -281,7 +283,11 @@ export const SettingsDataModelFieldSelectForm = ({
</StyledOptionsLabel>
</StyledLabelContainer>
<DraggableList
onDragEnd={(result) => handleDragEnd(options, result, onChange)}
onDragEnd={(result) =>
!disabled
? handleDragEnd(options, result, onChange)
: undefined
}
draggableItems={
<>
{options.map((option, index) => (
@@ -297,6 +303,9 @@ export const SettingsDataModelFieldSelectForm = ({
option={option}
isNewRow={index === options.length - 1}
onChange={(nextOption) => {
if (disabled) {
return;
}
const nextOptions = toSpliced(
options,
index,
@@ -315,6 +324,9 @@ export const SettingsDataModelFieldSelectForm = ({
}
}}
onRemove={() => {
if (disabled) {
return;
}
const nextOptions = toSpliced(
options,
index,
@@ -326,13 +338,24 @@ export const SettingsDataModelFieldSelectForm = ({
onChange(nextOptions);
}}
isDefault={isOptionDefaultValue(option.value)}
onSetAsDefault={() =>
handleSetOptionAsDefault(option.value)
}
onRemoveAsDefault={() =>
handleRemoveOptionAsDefault(option.value)
}
onInputEnter={handleInputEnter}
onSetAsDefault={() => {
if (disabled) {
return;
}
handleSetOptionAsDefault(option.value);
}}
onRemoveAsDefault={() => {
if (disabled) {
return;
}
handleRemoveOptionAsDefault(option.value);
}}
onInputEnter={() => {
if (disabled) {
return;
}
handleInputEnter();
}}
/>
}
/>
@@ -341,13 +364,15 @@ export const SettingsDataModelFieldSelectForm = ({
}
/>
</StyledContainer>
<StyledFooter>
<StyledButton
title={t`Add option`}
Icon={IconPlus}
onClick={handleAddOption}
/>
</StyledFooter>
{!disabled && (
<StyledFooter>
<StyledButton
title={t`Add option`}
Icon={IconPlus}
onClick={handleAddOption}
/>
</StyledFooter>
)}
</>
)}
/>
@@ -20,12 +20,14 @@ type SettingsDataModelFieldSelectSettingsFormCardProps = {
objectNameSingular: string;
fieldType: FieldMetadataType.SELECT | FieldMetadataType.MULTI_SELECT;
existingFieldMetadataId: string;
disabled?: boolean;
};
export const SettingsDataModelFieldSelectSettingsFormCard = ({
objectNameSingular,
fieldType,
existingFieldMetadataId,
disabled = false,
}: SettingsDataModelFieldSelectSettingsFormCardProps) => {
const { watch: watchFormValue } = useFormContext<
SettingsDataModelFieldSelectOrMultiSelectFormValues &
@@ -50,6 +52,7 @@ export const SettingsDataModelFieldSelectSettingsFormCard = ({
<SettingsDataModelFieldSelectForm
fieldType={fieldType}
existingFieldMetadataId={existingFieldMetadataId}
disabled={disabled}
/>
}
/>
@@ -6,8 +6,6 @@ import { Link } from 'react-router-dom';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { ObjectFieldRow } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewField';
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag';
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { ObjectFieldRowWithoutRelation } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewFieldWithoutRelation';
@@ -16,6 +14,7 @@ import { useState } from 'react';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconChevronDown, IconChevronUp, useIcons } from 'twenty-ui/display';
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
type SettingsDataModelOverviewObjectNode = Node<ObjectMetadataItem, 'object'>;
type SettingsDataModelOverviewObjectProps =
@@ -134,9 +133,7 @@ export const SettingsDataModelOverviewObject = ({
</StyledObjectLink>
<StyledObjectInstanceCount> · {totalCount}</StyledObjectInstanceCount>
</StyledObjectName>
<SettingsDataModelObjectTypeTag
objectTypeLabel={getObjectTypeLabel(objectMetadataItem)}
></SettingsDataModelObjectTypeTag>
<SettingsItemTypeTag item={objectMetadataItem} />
</StyledHeader>
<StyledInnerCard>
@@ -20,10 +20,12 @@ type SettingsObjectFieldActiveActionDropdownProps = {
onEdit: () => void;
onSetAsLabelIdentifier?: () => void;
fieldMetadataItemId: string;
readonly?: boolean;
};
export const SettingsObjectFieldActiveActionDropdown = ({
isCustomField,
readonly = false,
onDeactivate,
onEdit,
onSetAsLabelIdentifier,
@@ -62,18 +64,18 @@ export const SettingsObjectFieldActiveActionDropdown = ({
<DropdownContent widthInPixels={GenericDropdownContentWidth.Narrow}>
<DropdownMenuItemsContainer>
<MenuItem
text={isCustomField ? 'Edit' : 'View'}
text={isCustomField && !readonly ? 'Edit' : 'View'}
LeftIcon={isCustomField ? IconPencil : IconEye}
onClick={handleEdit}
/>
{isDefined(onSetAsLabelIdentifier) && (
{isDefined(onSetAsLabelIdentifier) && !readonly && (
<MenuItem
text="Set as record text"
LeftIcon={IconTextSize}
onClick={handleSetAsLabelIdentifier}
/>
)}
{isDefined(onDeactivate) && (
{isDefined(onDeactivate) && !readonly && (
<MenuItem
text="Deactivate"
LeftIcon={IconArchive}
@@ -22,10 +22,12 @@ type SettingsObjectFieldInactiveActionDropdownProps = {
onEdit: () => void;
onDelete: () => void;
fieldMetadataItemId: string;
readonly?: boolean;
};
export const SettingsObjectFieldInactiveActionDropdown = ({
onActivate,
readonly = false,
fieldMetadataItemId,
onDelete,
onEdit,
@@ -66,16 +68,18 @@ export const SettingsObjectFieldInactiveActionDropdown = ({
<DropdownContent widthInPixels={GenericDropdownContentWidth.Narrow}>
<DropdownMenuItemsContainer>
<MenuItem
text={isCustomField ? t`Edit` : t`View`}
text={isCustomField && !readonly ? t`Edit` : t`View`}
LeftIcon={isCustomField ? IconPencil : IconEye}
onClick={handleEdit}
/>
<MenuItem
text={t`Activate`}
LeftIcon={IconArchiveOff}
onClick={handleActivate}
/>
{isDeletable && (
{!readonly && (
<MenuItem
text={t`Activate`}
LeftIcon={IconArchiveOff}
onClick={handleActivate}
/>
)}
{isDeletable && !readonly && (
<MenuItem
text={t`Delete`}
accent="danger"
@@ -30,6 +30,7 @@ import { type SettingsObjectDetailTableItem } from '~/pages/settings/data-model/
import { RELATION_TYPES } from '../../constants/RelationTypes';
import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
type SettingsObjectFieldItemTableRowProps = {
settingsObjectDetailTableItem: SettingsObjectDetailTableItem;
@@ -65,6 +66,10 @@ export const SettingsObjectFieldItemTableRow = ({
const { fieldMetadataItem, identifierType, objectMetadataItem } =
settingsObjectDetailTableItem;
const readonly = isObjectMetadataReadOnly({
objectMetadataItem,
});
const isRemoteObjectField = objectMetadataItem.isRemote;
const variant = objectMetadataItem.isCustom ? 'identifier' : 'field-type';
@@ -118,6 +123,10 @@ export const SettingsObjectFieldItemTableRow = ({
const handleDisableField = async (
activeFieldMetadatItem: FieldMetadataItem,
) => {
if (readonly) {
return;
}
await deactivateMetadataField(
activeFieldMetadatItem.id,
objectMetadataItem.id,
@@ -146,13 +155,17 @@ export const SettingsObjectFieldItemTableRow = ({
const handleSetLabelIdentifierField = (
activeFieldMetadatItem: FieldMetadataItem,
) =>
) => {
if (readonly) {
return;
}
updateOneObjectMetadataItem({
idToUpdate: objectMetadataItem.id,
updatePayload: {
labelIdentifierFieldMetadataId: activeFieldMetadatItem.id,
},
});
};
const [, setActiveSettingsObjectFields] = useRecoilState(
settingsObjectFieldsFamilyState({
@@ -254,6 +267,7 @@ export const SettingsObjectFieldItemTableRow = ({
mode === 'view' ? (
<SettingsObjectFieldActiveActionDropdown
isCustomField={fieldMetadataItem.isCustom === true}
readonly={readonly}
fieldMetadataItemId={fieldMetadataItem.id}
onEdit={() =>
navigate(SettingsPath.ObjectFieldEdit, {
@@ -284,6 +298,7 @@ export const SettingsObjectFieldItemTableRow = ({
) : mode === 'view' ? (
<SettingsObjectFieldInactiveActionDropdown
isCustomField={fieldMetadataItem.isCustom === true}
readonly={readonly}
fieldMetadataItemId={fieldMetadataItem.id}
onEdit={() =>
navigate(SettingsPath.ObjectFieldEdit, {
@@ -3,11 +3,10 @@ import styled from '@emotion/styled';
import { type ReactNode } from 'react';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag';
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useIcons } from 'twenty-ui/display';
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
export type SettingsObjectMetadataItemTableRowProps = {
action: ReactNode;
@@ -46,7 +45,6 @@ export const SettingsObjectMetadataItemTableRow = ({
const { getIcon } = useIcons();
const Icon = getIcon(objectMetadataItem.icon);
const objectTypeLabel = getObjectTypeLabel(objectMetadataItem);
return (
<StyledObjectTableRow key={objectMetadataItem.namePlural} to={link}>
@@ -63,7 +61,7 @@ export const SettingsObjectMetadataItemTableRow = ({
</StyledNameLabel>
</StyledNameTableCell>
<TableCell>
<SettingsDataModelObjectTypeTag objectTypeLabel={objectTypeLabel} />
<SettingsItemTypeTag item={objectMetadataItem} />
</TableCell>
<TableCell align="right">
{objectMetadataItem.fields.filter((field) => !field.isSystem).length}
@@ -14,6 +14,7 @@ import { SettingsPath } from 'twenty-shared/types';
import { ZodError } from 'zod';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { updatedObjectNamePluralState } from '~/pages/settings/data-model/states/updatedObjectNamePluralState';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
type SettingsUpdateDataModelObjectAboutFormProps = {
objectMetadataItem: ObjectMetadataItem;
@@ -22,6 +23,7 @@ type SettingsUpdateDataModelObjectAboutFormProps = {
export const SettingsUpdateDataModelObjectAboutForm = ({
objectMetadataItem,
}: SettingsUpdateDataModelObjectAboutFormProps) => {
const readonly = isObjectMetadataReadOnly({ objectMetadataItem });
const navigate = useNavigateSettings();
const { enqueueErrorSnackBar } = useSnackBar();
const setUpdatedObjectNamePlural = useSetRecoilState(
@@ -54,6 +56,10 @@ export const SettingsUpdateDataModelObjectAboutForm = ({
const handleSave = async (
formValues: SettingsDataModelObjectAboutFormValues,
) => {
if (readonly) {
return;
}
if (!(Object.keys(formConfig.formState.dirtyFields).length > 0)) {
return;
}
@@ -71,10 +77,10 @@ export const SettingsUpdateDataModelObjectAboutForm = ({
description,
icon: icon ?? undefined,
isLabelSyncedWithName: formValues.isLabelSyncedWithName,
labelPlural: updatedObject.data?.updateOneObject.labelPlural,
labelSingular: updatedObject.data?.updateOneObject.labelSingular,
namePlural: updatedObject.data?.updateOneObject.namePlural,
nameSingular: updatedObject.data?.updateOneObject.nameSingular,
labelPlural: updatedObject?.data?.updateOneObject.labelPlural,
labelSingular: updatedObject?.data?.updateOneObject.labelSingular,
namePlural: updatedObject?.data?.updateOneObject.namePlural,
nameSingular: updatedObject?.data?.updateOneObject.nameSingular,
});
} else {
formConfig.reset(undefined, { keepValues: true });
@@ -91,6 +97,10 @@ export const SettingsUpdateDataModelObjectAboutForm = ({
const updateObjectMetadata = async (
formValues: SettingsDataModelObjectAboutFormValues,
) => {
if (readonly) {
return;
}
const updatePayload = { ...formValues };
if (!objectMetadataItem.isCustom) {
@@ -139,7 +149,7 @@ export const SettingsUpdateDataModelObjectAboutForm = ({
<FormProvider {...formConfig}>
<SettingsDataModelObjectAboutForm
onNewDirtyField={() => formConfig.handleSubmit(handleSave)()}
disableEdition={!objectMetadataItem.isCustom}
disableEdition={!objectMetadataItem.isCustom || readonly}
objectMetadataItem={objectMetadataItem}
/>
</FormProvider>
@@ -9,6 +9,7 @@ import { H2Title, IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
const StyledDiv = styled.div`
display: flex;
@@ -21,7 +22,9 @@ type ObjectFieldsProps = {
};
export const ObjectFields = ({ objectMetadataItem }: ObjectFieldsProps) => {
const shouldDisplayAddFieldButton = !objectMetadataItem.isRemote;
const readonly = isObjectMetadataReadOnly({
objectMetadataItem,
});
const { t } = useLingui();
const objectLabelSingular = objectMetadataItem.labelSingular;
@@ -36,7 +39,7 @@ export const ObjectFields = ({ objectMetadataItem }: ObjectFieldsProps) => {
objectMetadataItem={objectMetadataItem}
mode="view"
/>
{shouldDisplayAddFieldButton && (
{!readonly && (
<StyledDiv>
<UndecoratedLink
to={getSettingsPath(SettingsPath.ObjectNewFieldSelect, {
@@ -10,6 +10,7 @@ import { H2Title, IconArchive } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
type ObjectSettingsProps = {
objectMetadataItem: ObjectMetadataItem;
@@ -27,6 +28,7 @@ const StyledFormSection = styled(Section)`
export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
const { t } = useLingui();
const readonly = isObjectMetadataReadOnly({ objectMetadataItem });
const navigate = useNavigateSettings();
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
const handleDisable = async () => {
@@ -59,17 +61,22 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
/>
</Section>
</StyledFormSection>
<StyledFormSection>
<Section>
<H2Title title={t`Danger zone`} description={t`Deactivate object`} />
<Button
Icon={IconArchive}
title={t`Deactivate`}
size="small"
onClick={handleDisable}
/>
</Section>
</StyledFormSection>
{!readonly && (
<StyledFormSection>
<Section>
<H2Title
title={t`Danger zone`}
description={t`Deactivate object`}
/>
<Button
Icon={IconArchive}
title={t`Deactivate`}
size="small"
onClick={handleDisable}
/>
</Section>
</StyledFormSection>
)}
</StyledContentContainer>
);
};
@@ -2,13 +2,12 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag';
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
import {
IconBox,
OverflowingTextWithTooltip,
useIcons,
} from 'twenty-ui/display';
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
export type SettingsDataModelObjectPreviewProps = {
className?: string;
@@ -69,7 +68,6 @@ const SettingsDataModelObjectPreviewItem = ({
const theme = useTheme();
const { getIcon } = useIcons();
const ObjectIcon = getIcon(objectMetadataItem.icon);
const objectTypeLabel = getObjectTypeLabel(objectMetadataItem);
return (
<>
@@ -90,7 +88,7 @@ const SettingsDataModelObjectPreviewItem = ({
}
/>
</StyledObjectName>
<SettingsDataModelObjectTypeTag objectTypeLabel={objectTypeLabel} />
<SettingsItemTypeTag item={objectMetadataItem} />
</StyledObjectPreview>
</>
);
@@ -1,21 +0,0 @@
import { type ObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
import { Tag } from 'twenty-ui/components';
type SettingsDataModelObjectTypeTagProps = {
objectTypeLabel: ObjectTypeLabel;
className?: string;
};
export const SettingsDataModelObjectTypeTag = ({
className,
objectTypeLabel,
}: SettingsDataModelObjectTypeTagProps) => {
return (
<Tag
className={className}
color={objectTypeLabel.labelColor}
text={objectTypeLabel.labelText}
weight="medium"
/>
);
};
@@ -139,7 +139,11 @@ export const SettingsDataModelObjectAboutForm = ({
render={({ field: { onChange, value } }) => (
<IconPicker
selectedIconKey={value}
disabled={disableEdition}
onChange={({ iconKey }) => {
if (disableEdition) {
return;
}
onChange(iconKey);
onNewDirtyField?.();
}}
@@ -170,9 +174,10 @@ export const SettingsDataModelObjectAboutForm = ({
}}
onBlur={() => onNewDirtyField?.()}
disabled={
objectMetadataItem &&
!objectMetadataItem?.isCustom &&
isLabelSyncedWithName
disableEdition ||
(objectMetadataItem &&
!objectMetadataItem?.isCustom &&
isLabelSyncedWithName)
}
fullWidth
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
@@ -201,9 +206,10 @@ export const SettingsDataModelObjectAboutForm = ({
}}
onBlur={() => onNewDirtyField?.()}
disabled={
objectMetadataItem &&
!objectMetadataItem?.isCustom &&
isLabelSyncedWithName
disableEdition ||
(objectMetadataItem &&
!objectMetadataItem?.isCustom &&
isLabelSyncedWithName)
}
fullWidth
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
@@ -222,6 +228,7 @@ export const SettingsDataModelObjectAboutForm = ({
value={value ?? undefined}
onChange={(nextValue) => onChange(nextValue ?? null)}
onBlur={() => onNewDirtyField?.()}
disabled={disableEdition}
/>
)}
/>
@@ -326,6 +333,7 @@ export const SettingsDataModelObjectAboutForm = ({
description={t`Should changing an object's label also change the API?`}
checked={value ?? true}
advancedMode
disabled={disableEdition}
onChange={(value) => {
onChange(value);
const isCustomObject =
@@ -16,6 +16,7 @@ import { useNavigate } from 'react-router-dom';
import { isLabelIdentifierFieldMetadataTypes } from 'twenty-shared/utils';
import { IconCircleOff, IconPlus, useIcons } from 'twenty-ui/display';
import { type SelectOption } from 'twenty-ui/input';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
export const settingsDataModelObjectIdentifiersFormSchema =
objectMetadataItemSchema.pick({
@@ -44,6 +45,7 @@ const StyledContainer = styled.div`
export const SettingsDataModelObjectIdentifiersForm = ({
objectMetadataItem,
}: SettingsDataModelObjectIdentifiersFormProps) => {
const readonly = isObjectMetadataReadOnly({ objectMetadataItem });
const formConfig = useForm<SettingsDataModelObjectIdentifiersFormValues>({
mode: 'onTouched',
resolver: zodResolver(settingsDataModelObjectIdentifiersFormSchema),
@@ -134,7 +136,7 @@ export const SettingsDataModelObjectIdentifiersForm = ({
options={options}
value={value}
withSearchInput={label === t`Record label`}
disabled={!objectMetadataItem.isCustom}
disabled={!objectMetadataItem.isCustom || readonly}
callToActionButton={
label === t`Record label`
? {
@@ -0,0 +1,51 @@
import { isDefined } from 'twenty-shared/utils';
export type ItemTagInfo =
| StandardItemTagInfo
| CustomItemTagInfo
| RemoteItemTagInfo
| ManagedItemTagInfo;
type StandardItemTagInfo = {
labelText: 'Standard';
labelColor: 'blue';
};
type CustomItemTagInfo = {
labelText: 'Custom';
labelColor: 'orange';
};
type RemoteItemTagInfo = {
labelText: 'Remote';
labelColor: 'green';
};
type ManagedItemTagInfo = {
labelText: 'Managed';
labelColor: 'sky';
};
export const getItemTagInfo = ({
isCustom,
isRemote,
applicationId,
}: {
isCustom?: boolean;
isRemote?: boolean;
applicationId?: string | null;
}): ItemTagInfo => {
if (isDefined(applicationId)) {
return { labelText: 'Managed', labelColor: 'sky' };
}
if (isCustom!!) {
return { labelText: 'Custom', labelColor: 'orange' };
}
if (isRemote!!) {
return { labelText: 'Remote', labelColor: 'green' };
}
return { labelText: 'Standard', labelColor: 'blue' };
};
@@ -1,39 +0,0 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export type ObjectTypeLabel =
| StandardObjectTypeLabel
| CustomObjectTypeLabel
| RemoteObjectTypeLabel;
type StandardObjectTypeLabel = {
labelText: 'Standard';
labelColor: 'blue';
};
type CustomObjectTypeLabel = {
labelText: 'Custom';
labelColor: 'orange';
};
type RemoteObjectTypeLabel = {
labelText: 'Remote';
labelColor: 'green';
};
export const getObjectTypeLabel = (
objectMetadataItem: Pick<ObjectMetadataItem, 'isCustom' | 'isRemote'>,
): ObjectTypeLabel =>
objectMetadataItem.isCustom
? {
labelText: 'Custom',
labelColor: 'orange',
}
: objectMetadataItem.isRemote
? {
labelText: 'Remote',
labelColor: 'green',
}
: {
labelText: 'Standard',
labelColor: 'blue',
};
@@ -1,19 +1,15 @@
import { Controller, FormProvider } from 'react-hook-form';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { type WebhookFormMode } from '@/settings/developers/constants/WebhookFormMode';
import { useWebhookForm } from '@/settings/developers/hooks/useWebhookForm';
import { Select } from '@/ui/input/components/Select';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { TextArea } from '@/ui/input/components/TextArea';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import { SettingsPath } from 'twenty-shared/types';
import {
@@ -22,38 +18,11 @@ import {
isDefined,
isValidUrl,
} from 'twenty-shared/utils';
import {
H2Title,
IconBox,
IconNorthStar,
IconPlus,
IconTrash,
useIcons,
} from 'twenty-ui/display';
import { Button, IconButton, type SelectOption } from 'twenty-ui/input';
import { H2Title, IconTrash } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const OBJECT_DROPDOWN_WIDTH = 340;
const ACTION_DROPDOWN_WIDTH = 140;
const OBJECT_MOBILE_WIDTH = 150;
const ACTION_MOBILE_WIDTH = 140;
const StyledFilterRow = styled.div<{ isMobile: boolean }>`
display: grid;
grid-template-columns: ${({ isMobile }) =>
isMobile
? `${OBJECT_MOBILE_WIDTH}px ${ACTION_MOBILE_WIDTH}px auto`
: `${OBJECT_DROPDOWN_WIDTH}px ${ACTION_DROPDOWN_WIDTH}px auto`};
gap: ${({ theme }) => theme.spacing(2)};
margin-bottom: ${({ theme }) => theme.spacing(2)};
align-items: center;
`;
const StyledPlaceholder = styled.div`
height: ${({ theme }) => theme.spacing(8)};
width: ${({ theme }) => theme.spacing(8)};
`;
import { SettingsDatabaseEventsForm } from '@/settings/components/SettingsDatabaseEventsForm';
const DELETE_WEBHOOK_MODAL_ID = 'delete-webhook-modal';
@@ -68,9 +37,6 @@ export const SettingsDevelopersWebhookForm = ({
}: SettingsDevelopersWebhookFormProps) => {
const { t } = useLingui();
const navigate = useNavigateSettings();
const { objectMetadataItems } = useObjectMetadataItems();
const isMobile = useIsMobile();
const { getIcon } = useIcons();
const { openModal } = useModal();
const {
formConfig,
@@ -99,22 +65,6 @@ export const SettingsDevelopersWebhookForm = ({
return <SettingsSkeletonLoader />;
}
const objectOptions: SelectOption<string>[] = [
{ label: 'All Objects', value: '*', Icon: IconNorthStar },
...objectMetadataItems.map((item) => ({
label: item.labelPlural,
value: item.nameSingular,
Icon: getIcon(item.icon),
})),
];
const actionOptions: SelectOption<string>[] = [
{ label: 'All', value: '*', Icon: IconNorthStar },
{ label: 'Created', value: 'created', Icon: IconPlus },
{ label: 'Updated', value: 'updated', Icon: IconBox },
{ label: 'Deleted', value: 'deleted', Icon: IconTrash },
];
const descriptionTextAreaId = `${webhookId}-description`;
const targetUrlTextInputId = `${webhookId}-target-url`;
const secretTextInputId = `${webhookId}-secret`;
@@ -200,41 +150,11 @@ export const SettingsDevelopersWebhookForm = ({
name="operations"
control={formConfig.control}
render={({ field: { value } }) => (
<>
{value.map((operation, index) => (
<StyledFilterRow key={index} isMobile={isMobile}>
<Select
dropdownId={`object-webhook-type-select-${index}`}
value={operation.object}
options={objectOptions}
onChange={(newValue) =>
updateOperation(index, 'object', newValue)
}
fullWidth
emptyOption={{ label: 'Object', value: null }}
/>
<Select
dropdownId={`operation-webhook-type-select-${index}`}
value={operation.action}
options={actionOptions}
onChange={(newValue) =>
updateOperation(index, 'action', newValue)
}
fullWidth
/>
{isDefined(operation.object) ? (
<IconButton
Icon={IconTrash}
variant="tertiary"
size="medium"
onClick={() => removeOperation(index)}
/>
) : (
<StyledPlaceholder />
)}
</StyledFilterRow>
))}
</>
<SettingsDatabaseEventsForm
events={value}
updateOperation={updateOperation}
removeOperation={removeOperation}
/>
)}
/>
</Section>
@@ -17,7 +17,6 @@ import {
type IconComponent,
IconCurrencyDollar,
IconDoorEnter,
IconFunction,
IconHierarchy2,
IconKey,
IconLock,
@@ -26,6 +25,7 @@ import {
IconServer,
IconSettings,
IconSparkles,
IconPuzzle2,
IconUserCircle,
IconUsers,
IconWorld,
@@ -56,13 +56,15 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
const billing = useRecoilValue(billingState);
const { signOut } = useAuth();
const isFunctionSettingsEnabled = false;
const isBillingEnabled = billing?.isBillingEnabled ?? false;
const currentUser = useRecoilValue(currentUserState);
const isAdminEnabled =
(currentUser?.canImpersonate || currentUser?.canAccessFullAdminPanel) ??
false;
const isAIEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const isApplicationEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_APPLICATION_ENABLED,
);
const permissionMap = usePermissionFlagMap();
return [
@@ -152,6 +154,15 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
Icon: IconApps,
isHidden: !permissionMap[PermissionFlagType.API_KEYS_AND_WEBHOOKS],
},
{
label: t`Applications`,
path: SettingsPath.Applications,
Icon: IconPuzzle2,
isHidden:
!isApplicationEnabled ||
!permissionMap[PermissionFlagType.WORKSPACE],
isNew: true,
},
{
label: t`AI`,
path: SettingsPath.AI,
@@ -160,13 +171,6 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
!isAIEnabled || !permissionMap[PermissionFlagType.WORKSPACE],
isNew: true,
},
{
label: t`Functions`,
path: SettingsPath.ServerlessFunctions,
Icon: IconFunction,
isHidden: !isFunctionSettingsEnabled,
isAdvanced: true,
},
{
label: t`Security`,
path: SettingsPath.Security,
@@ -1,7 +1,7 @@
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
import { type EditorProps, type Monaco } from '@monaco-editor/react';
import dotenv from 'dotenv';
import { type editor, MarkerSeverity } from 'monaco-editor';
import { type editor } from 'monaco-editor';
import { AutoTypings } from 'monaco-editor-auto-typings';
import { useParams } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
@@ -20,14 +20,12 @@ type SettingsServerlessFunctionCodeEditorProps = Omit<
currentFilePath: string;
files: File[];
onChange: (value: string) => void;
setIsCodeValid: (isCodeValid: boolean) => void;
};
export const SettingsServerlessFunctionCodeEditor = ({
currentFilePath,
files,
onChange,
setIsCodeValid,
height = 450,
options = undefined,
}: SettingsServerlessFunctionCodeEditorProps) => {
@@ -106,16 +104,6 @@ export const SettingsServerlessFunctionCodeEditor = ({
}
};
const handleEditorValidation = (markers: editor.IMarker[]) => {
for (const marker of markers) {
if (marker.severity === MarkerSeverity.Error) {
setIsCodeValid?.(false);
return;
}
}
setIsCodeValid?.(true);
};
return (
isDefined(currentFile) &&
isDefined(availablePackages) && (
@@ -125,7 +113,6 @@ export const SettingsServerlessFunctionCodeEditor = ({
language={currentFile.language}
onMount={handleEditorDidMount}
onChange={onChange}
onValidate={handleEditorValidation}
options={options}
variant="with-header"
/>
@@ -14,9 +14,11 @@ const StyledInputsContainer = styled.div`
export const SettingsServerlessFunctionNewForm = ({
formValues,
onChange,
readonly = false,
}: {
formValues: ServerlessFunctionNewFormValues;
onChange: (key: string) => (value: string) => void;
readonly?: boolean;
}) => {
const descriptionTextAreaId = `${formValues.name}-description`;
const nameTextInputId = `${formValues.name}-name`;
@@ -32,6 +34,7 @@ export const SettingsServerlessFunctionNewForm = ({
autoFocusOnMount
value={formValues.name}
onChange={onChange('name')}
readOnly={readonly}
/>
<TextArea
textAreaId={descriptionTextAreaId}
@@ -39,6 +42,7 @@ export const SettingsServerlessFunctionNewForm = ({
minRows={4}
value={formValues.description}
onChange={onChange('description')}
readOnly={readonly}
/>
</StyledInputsContainer>
</Section>
@@ -1,7 +1,4 @@
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsServerlessFunctionsFieldItemTableRow } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsFieldItemTableRow';
import { SettingsServerlessFunctionsTableEmpty } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty';
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
@@ -10,6 +7,8 @@ import styled from '@emotion/styled';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { type ServerlessFunction } from '~/generated-metadata/graphql';
import { useLingui } from '@lingui/react/macro';
import { useParams } from 'react-router-dom';
const StyledTableRow = styled(TableRow)`
grid-template-columns: 312px 132px 68px;
@@ -19,37 +18,41 @@ const StyledTableBody = styled(TableBody)`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
`;
export const SettingsServerlessFunctionsTable = () => {
const { serverlessFunctions } = useGetManyServerlessFunctions();
export const SettingsServerlessFunctionsTable = ({
serverlessFunctions,
}: {
serverlessFunctions: ServerlessFunction[];
}) => {
const { applicationId = '' } = useParams();
const { t } = useLingui();
if (serverlessFunctions.length === 0) {
return null;
}
return (
<>
{serverlessFunctions.length ? (
<SettingsPageContainer>
<Table>
<StyledTableRow>
<TableHeader>Name</TableHeader>
<TableHeader>Runtime</TableHeader>
<TableHeader></TableHeader>
</StyledTableRow>
<StyledTableBody>
{serverlessFunctions.map(
(serverlessFunction: ServerlessFunction) => (
<SettingsServerlessFunctionsFieldItemTableRow
key={serverlessFunction.id}
serverlessFunction={serverlessFunction}
to={getSettingsPath(SettingsPath.ServerlessFunctions, {
id: serverlessFunction.id,
})}
/>
),
)}
</StyledTableBody>
</Table>
</SettingsPageContainer>
) : (
<SettingsServerlessFunctionsTableEmpty />
)}
</>
<Table>
<StyledTableRow>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>Runtime</TableHeader>
<TableHeader></TableHeader>
</StyledTableRow>
<StyledTableBody>
{serverlessFunctions.map((serverlessFunction: ServerlessFunction) => (
<SettingsServerlessFunctionsFieldItemTableRow
key={serverlessFunction.id}
serverlessFunction={serverlessFunction}
to={getSettingsPath(
SettingsPath.ApplicationServerlessFunctionDetail,
{
applicationId,
serverlessFunctionId: serverlessFunction.id,
},
)}
/>
))}
</StyledTableBody>
</Table>
);
};
@@ -1,43 +0,0 @@
import styled from '@emotion/styled';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
AnimatedPlaceholderEmptySubTitle,
AnimatedPlaceholderEmptyTextContainer,
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout';
const StyledEmptyFunctionsContainer = styled.div`
height: 60vh;
`;
export const SettingsServerlessFunctionsTableEmpty = () => {
return (
<StyledEmptyFunctionsContainer>
<AnimatedPlaceholderEmptyContainer
// eslint-disable-next-line react/jsx-props-no-spreading
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
>
<AnimatedPlaceholder type="emptyFunctions" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
Add your first Function
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
Add your first Function to get started
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
<Button
Icon={IconPlus}
title="New function"
to={getSettingsPath(SettingsPath.NewServerlessFunction)}
/>
</AnimatedPlaceholderEmptyContainer>
</StyledEmptyFunctionsContainer>
);
};
@@ -7,12 +7,7 @@ import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import {
H2Title,
IconGitCommit,
IconPlayerPlay,
IconRestore,
} from 'twenty-ui/display';
import { H2Title, IconPlayerPlay } from 'twenty-ui/display';
import { Button, CoreEditorHeader } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
@@ -23,21 +18,13 @@ const StyledTabList = styled(TabList)`
export const SettingsServerlessFunctionCodeEditorTab = ({
files,
handleExecute,
handlePublish,
handleReset,
resetDisabled,
publishDisabled,
onChange,
setIsCodeValid,
isTesting = false,
}: {
files: File[];
handleExecute: () => void;
handlePublish: () => void;
handleReset: () => void;
resetDisabled: boolean;
publishDisabled: boolean;
onChange: (filePath: string, value: string) => void;
setIsCodeValid: (isCodeValid: boolean) => void;
isTesting?: boolean;
}) => {
const activeTabId = useRecoilComponentValue(
activeTabIdComponentState,
@@ -50,29 +37,10 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
accent="blue"
size="small"
Icon={IconPlayerPlay}
disabled={isTesting}
onClick={handleExecute}
/>
);
const PublishButton = (
<Button
title="Publish"
variant="secondary"
size="small"
Icon={IconGitCommit}
onClick={handlePublish}
disabled={publishDisabled}
/>
);
const ResetButton = (
<Button
title="Reset"
variant="secondary"
size="small"
Icon={IconRestore}
onClick={handleReset}
disabled={resetDisabled}
/>
);
const HeaderTabList = (
<StyledTabList
@@ -91,16 +59,18 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
title="Code your function"
description="Write your function (in typescript) below"
/>
<CoreEditorHeader
leftNodes={[HeaderTabList]}
rightNodes={[ResetButton, PublishButton, TestButton]}
/>
<CoreEditorHeader leftNodes={[HeaderTabList]} rightNodes={[TestButton]} />
{activeTabId && (
<SettingsServerlessFunctionCodeEditor
files={files}
currentFilePath={activeTabId}
onChange={(newCodeValue) => onChange(activeTabId, newCodeValue)}
setIsCodeValid={setIsCodeValid}
options={{
readOnly: true,
readOnlyMessage: {
value: 'Managed serverless functions are not editable',
},
}}
/>
)}
</Section>
@@ -1,70 +1,28 @@
import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm';
import { SettingsServerlessFunctionTabEnvironmentVariablesSection } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection';
import { useDeleteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useDeleteOneServerlessFunction';
import { type ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SettingsPath } from 'twenty-shared/types';
import { H2Title } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const DELETE_FUNCTION_MODAL_ID = 'delete-function-modal';
export const SettingsServerlessFunctionSettingsTab = ({
formValues,
serverlessFunctionId,
onChange,
onCodeChange,
serverlessFunctionId,
}: {
formValues: ServerlessFunctionFormValues;
serverlessFunctionId: string;
onChange: (key: string) => (value: string) => void;
onCodeChange: (filePath: string, value: string) => void;
}) => {
const navigate = useNavigateSettings();
const { openModal } = useModal();
const { deleteOneServerlessFunction } = useDeleteOneServerlessFunction();
const deleteFunction = async () => {
await deleteOneServerlessFunction({ id: serverlessFunctionId });
navigate(SettingsPath.ServerlessFunctions);
};
return (
<>
<SettingsServerlessFunctionNewForm
formValues={formValues}
onChange={onChange}
readonly
/>
<SettingsServerlessFunctionTabEnvironmentVariablesSection
formValues={formValues}
onCodeChange={onCodeChange}
/>
<Section>
<H2Title title="Danger zone" description="Delete this function" />
<Button
accent="danger"
onClick={() => openModal(DELETE_FUNCTION_MODAL_ID)}
variant="secondary"
size="small"
title="Delete function"
/>
</Section>
<ConfirmationModal
confirmationValue={formValues.name}
confirmationPlaceholder={formValues.name}
modalId={DELETE_FUNCTION_MODAL_ID}
title="Function Deletion"
subtitle={
<>
This action cannot be undone. This will permanently delete your
function. <br /> Please type in the function name to confirm.
</>
}
onConfirmClick={deleteFunction}
confirmButtonText="Delete function"
serverlessFunctionId={serverlessFunctionId}
/>
</>
);
@@ -1,18 +1,18 @@
import { SettingsServerlessFunctionTabEnvironmentVariableTableRow } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariableTableRow';
import { type ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import dotenv from 'dotenv';
import { useMemo, useState } from 'react';
import { H2Title, IconPlus, IconSearch } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
import { v4 } from 'uuid';
import { serverlessFunctionEnvVarFamilyState } from '@/settings/serverless-functions/states/serverlessFunctionEnvVarFamilyState';
import { useRecoilState } from 'recoil';
const StyledSearchInput = styled(SettingsTextInput)`
padding-bottom: ${({ theme }) => theme.spacing(2)};
@@ -39,24 +39,18 @@ const StyledTableRow = styled(TableRow)`
export type EnvironmentVariable = { id: string; key: string; value: string };
export const SettingsServerlessFunctionTabEnvironmentVariablesSection = ({
formValues,
onCodeChange,
serverlessFunctionId,
}: {
formValues: ServerlessFunctionFormValues;
serverlessFunctionId: string;
onCodeChange: (filePath: string, value: string) => void;
}) => {
const environmentVariables = formValues.code?.['.env']
? dotenv.parse(formValues.code['.env'])
: {};
const environmentVariablesList = Object.entries(environmentVariables).map(
([key, value]) => ({ id: v4(), key, value }),
);
const [searchTerm, setSearchTerm] = useState('');
const [newEnvVarAdded, setNewEnvVarAdded] = useState(false);
const [envVariables, setEnvVariables] = useState<EnvironmentVariable[]>(
environmentVariablesList,
const [envVariables, setEnvVariables] = useRecoilState(
serverlessFunctionEnvVarFamilyState(serverlessFunctionId),
);
const filteredEnvVariable = useMemo(() => {
return envVariables.filter(
({ key, value }) =>
@@ -68,11 +62,28 @@ export const SettingsServerlessFunctionTabEnvironmentVariablesSection = ({
const getFormattedEnvironmentVariables = (
newEnvVariables: EnvironmentVariable[],
) => {
return newEnvVariables.reduce(
(acc, { key, value }) =>
key.length > 0 && value.length > 0 ? `${acc}\n${key}=${value}` : acc,
'',
);
return [...newEnvVariables]
.reverse()
.reduce(
(acc, { key, value }) =>
key.length > 0 && value.length > 0 ? `${key}=${value}\n${acc}` : acc,
'',
);
};
const onEnvVarChange = (newEnvVariable: EnvironmentVariable) => {
const newEnvVariables: EnvironmentVariable[] = [];
for (const envVariable of envVariables) {
if (envVariable.id === newEnvVariable.id) {
newEnvVariables.push(newEnvVariable);
} else if (envVariable.key !== newEnvVariable.key) {
newEnvVariables.push(envVariable);
}
}
setEnvVariables(newEnvVariables);
onCodeChange('.env', getFormattedEnvironmentVariables(newEnvVariables));
};
return (
@@ -101,24 +112,7 @@ export const SettingsServerlessFunctionTabEnvironmentVariablesSection = ({
key={envVariable.id}
envVariable={envVariable}
initialEditMode={newEnvVarAdded && envVariable.value === ''}
onChange={(newEnvVariable) => {
const newEnvVariables = envVariables.reduce(
(acc, { id, key }) => {
if (id === newEnvVariable.id) {
acc.push(newEnvVariable);
} else if (key !== newEnvVariable.key) {
acc.push(envVariable);
}
return acc;
},
[] as EnvironmentVariable[],
);
setEnvVariables(newEnvVariables);
onCodeChange(
'.env',
getFormattedEnvironmentVariables(newEnvVariables),
);
}}
onChange={onEnvVarChange}
onDelete={() => {
const newEnvVariables = envVariables.filter(
({ id }) => id !== envVariable.id,
@@ -21,9 +21,11 @@ const StyledCodeEditorContainer = styled.div`
export const SettingsServerlessFunctionTestTab = ({
handleExecute,
serverlessFunctionId,
isTesting = false,
}: {
handleExecute: () => void;
serverlessFunctionId: string;
isTesting?: boolean;
}) => {
const { t } = useLingui();
const [serverlessFunctionTestData, setServerlessFunctionTestData] =
@@ -54,6 +56,7 @@ export const SettingsServerlessFunctionTestTab = ({
size="small"
Icon={IconPlayerPlay}
onClick={handleExecute}
disabled={isTesting}
/>,
]}
/>
@@ -67,6 +70,7 @@ export const SettingsServerlessFunctionTestTab = ({
</StyledCodeEditorContainer>
<ServerlessFunctionExecutionResult
serverlessFunctionTestData={serverlessFunctionTestData}
isTesting={isTesting}
/>
</StyledInputsContainer>
</Section>
@@ -0,0 +1,113 @@
import { H2Title, OverflowingTextWithTooltip } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { type ServerlessFunction } from '~/generated/graphql';
import { useLingui } from '@lingui/react/macro';
import { SettingsDatabaseEventsForm } from '@/settings/components/SettingsDatabaseEventsForm';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { Table } from '@/ui/layout/table/components/Table';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { Tag } from 'twenty-ui/components';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
export const StyledRouteTriggerTableRow = styled(TableRow)`
grid-template-columns: 1fr 120px 120px;
`;
const StyledTableCell = styled(TableCell)`
color: ${({ theme }) => theme.font.color.tertiary};
gap: ${({ theme }) => theme.spacing(2)};
min-width: 0;
overflow: hidden;
`;
const StyledRouteTriggerTableHeaderRow = styled(StyledRouteTriggerTableRow)`
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsServerlessFunctionTriggersTab = ({
serverlessFunction,
}: {
serverlessFunction: ServerlessFunction;
}) => {
const { t } = useLingui();
const databaseEventTriggers = serverlessFunction.databaseEventTriggers ?? [];
const cronTriggers = serverlessFunction.cronTriggers ?? [];
const routeTriggers = serverlessFunction.routeTriggers ?? [];
const databaseEvents = databaseEventTriggers?.map((event) => {
const [object, action]: [string, string] =
event.settings.eventName.split('.');
return { object, action };
});
return (
<>
{databaseEvents.length > 0 && (
<Section>
<H2Title
title={t`Database event`}
description={t`Select the events that should trigger the function`}
/>
<SettingsDatabaseEventsForm events={databaseEvents} disabled />
</Section>
)}
{cronTriggers.length > 0 && (
<Section>
<H2Title
title={t`Cron`}
description={t`Triggers the function at regular intervals`}
/>
{cronTriggers.map((cronTrigger, index) => (
<FormTextFieldInput
key={index}
label={t`Expression`}
placeholder="0 */1 * * *"
hint={t`Format: [Minute] [Hour] [Day of Month] [Month] [Day of Week]`}
onChange={() => {}}
readonly
defaultValue={cronTrigger.settings.pattern}
/>
))}
</Section>
)}
{routeTriggers.length > 0 && (
<Section>
<H2Title
title={t`Http`}
description={t`Triggers the function with Http request`}
/>
<Table>
<StyledRouteTriggerTableHeaderRow>
<TableHeader>{t`Path`}</TableHeader>
<TableHeader>{t`Method`}</TableHeader>
<TableHeader>{t`Auth Required`}</TableHeader>
</StyledRouteTriggerTableHeaderRow>
{routeTriggers.map((routeTrigger, index) => (
<StyledRouteTriggerTableRow key={index}>
<StyledTableCell>
<OverflowingTextWithTooltip text={routeTrigger.path} />
</StyledTableCell>
<StyledTableCell>{routeTrigger.httpMethod}</StyledTableCell>
<StyledTableCell>
<Tag
text={routeTrigger.isAuthRequired ? 'True' : 'False'}
color={routeTrigger.isAuthRequired ? 'green' : 'orange'}
weight="medium"
/>
</StyledTableCell>
</StyledRouteTriggerTableRow>
))}
</Table>
</Section>
)}
</>
);
};
@@ -9,6 +9,26 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql`
timeoutSeconds
latestVersion
publishedVersions
cronTriggers {
id
settings
createdAt
updatedAt
}
databaseEventTriggers {
id
settings
createdAt
updatedAt
}
routeTriggers {
id
path
isAuthRequired
httpMethod
createdAt
updatedAt
}
createdAt
updatedAt
}
@@ -11,7 +11,7 @@ export const useGetOneServerlessFunction = (
input: ServerlessFunctionIdInput,
) => {
const apolloMetadataClient = useApolloCoreClient();
const { data } = useQuery<
const { data, loading } = useQuery<
GetOneServerlessFunctionQuery,
GetOneServerlessFunctionQueryVariables
>(FIND_ONE_SERVERLESS_FUNCTION, {
@@ -22,5 +22,6 @@ export const useGetOneServerlessFunction = (
});
return {
serverlessFunction: data?.findOneServerlessFunction || null,
loading,
};
};
@@ -4,9 +4,14 @@ import { useGetOneServerlessFunction } from '@/settings/serverless-functions/hoo
import { useGetOneServerlessFunctionSourceCode } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode';
import { serverlessFunctionTestDataFamilyState } from '@/workflow/workflow-steps/workflow-actions/code-action/states/serverlessFunctionTestDataFamilyState';
import { type Dispatch, type SetStateAction, useState } from 'react';
import { useRecoilState } from 'recoil';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { type FindOneServerlessFunctionSourceCodeQuery } from '~/generated-metadata/graphql';
import { SOURCE_FOLDER_NAME } from '@/serverless-functions/constants/SourceFolderName';
import { type ServerlessFunction } from '~/generated/graphql';
import { type Sources } from '@/serverless-functions/types/sources.type';
import { serverlessFunctionEnvVarFamilyState } from '@/settings/serverless-functions/states/serverlessFunctionEnvVarFamilyState';
import dotenv from 'dotenv';
import { v4 } from 'uuid';
export type ServerlessFunctionNewFormValues = {
name: string;
@@ -14,12 +19,7 @@ export type ServerlessFunctionNewFormValues = {
};
export type ServerlessFunctionFormValues = ServerlessFunctionNewFormValues & {
code: {
src: {
'index.ts': string;
} & { [key: string]: string };
'.env'?: string;
};
code: Sources;
};
type SetServerlessFunctionFormValues = Dispatch<
@@ -34,6 +34,7 @@ export const useServerlessFunctionUpdateFormState = ({
serverlessFunctionVersion?: string;
}): {
formValues: ServerlessFunctionFormValues;
serverlessFunction: ServerlessFunction | null;
setFormValues: SetServerlessFunctionFormValues;
loading: boolean;
} => {
@@ -43,44 +44,69 @@ export const useServerlessFunctionUpdateFormState = ({
code: { src: { 'index.ts': '' } },
});
const setEnvVar = useSetRecoilState(
serverlessFunctionEnvVarFamilyState(serverlessFunctionId),
);
const [serverlessFunctionTestData, setServerlessFunctionTestData] =
useRecoilState(serverlessFunctionTestDataFamilyState(serverlessFunctionId));
const { serverlessFunction } = useGetOneServerlessFunction({
id: serverlessFunctionId,
});
const { serverlessFunction, loading: serverlessFunctionLoading } =
useGetOneServerlessFunction({
id: serverlessFunctionId,
});
const { loading } = useGetOneServerlessFunctionSourceCode({
id: serverlessFunctionId,
version: serverlessFunctionVersion,
onCompleted: async (data: FindOneServerlessFunctionSourceCodeQuery) => {
const newState = {
code: data?.getServerlessFunctionSourceCode || undefined,
name: serverlessFunction?.name || '',
description: serverlessFunction?.description || '',
};
const { loading: serverlessFunctionSourceCodeLoading } =
useGetOneServerlessFunctionSourceCode({
id: serverlessFunctionId,
version: serverlessFunctionVersion,
onCompleted: async (data: FindOneServerlessFunctionSourceCodeQuery) => {
const code = data?.getServerlessFunctionSourceCode;
setFormValues((prevState) => ({
...prevState,
...newState,
}));
const newState = {
code: code || undefined,
name: serverlessFunction?.name || '',
description: serverlessFunction?.description || '',
};
if (serverlessFunctionTestData.shouldInitInput) {
const sourceCode =
data?.getServerlessFunctionSourceCode?.[SOURCE_FOLDER_NAME]?.[
INDEX_FILE_NAME
];
const functionInput = await getFunctionInputFromSourceCode(sourceCode);
setServerlessFunctionTestData((prev) => ({
...prev,
input: functionInput,
shouldInitInput: false,
setFormValues((prevState) => ({
...prevState,
...newState,
}));
}
},
});
return { formValues, setFormValues, loading };
const environmentVariables =
code?.['.env'] && typeof code?.['.env'] === 'string'
? dotenv.parse(code['.env'])
: {};
const environmentVariablesList = Object.entries(
environmentVariables,
).map(([key, value]) => ({ id: v4(), key, value }));
setEnvVar(environmentVariablesList);
if (serverlessFunctionTestData.shouldInitInput) {
const sourceCode =
data?.getServerlessFunctionSourceCode?.[SOURCE_FOLDER_NAME]?.[
INDEX_FILE_NAME
];
const functionInput =
await getFunctionInputFromSourceCode(sourceCode);
setServerlessFunctionTestData((prev) => ({
...prev,
input: functionInput,
shouldInitInput: false,
}));
}
},
});
return {
formValues,
setFormValues,
serverlessFunction,
loading: serverlessFunctionSourceCodeLoading || serverlessFunctionLoading,
};
};
@@ -0,0 +1,10 @@
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
import { type EnvironmentVariable } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection';
export const serverlessFunctionEnvVarFamilyState = createFamilyState<
EnvironmentVariable[],
string
>({
key: 'serverlessFunctionEnvVarFamilyState',
defaultValue: [],
});
@@ -21,6 +21,7 @@ export type TextAreaProps = {
value?: string;
className?: string;
onBlur?: () => void;
readOnly?: boolean;
};
const StyledContainer = styled.div`
@@ -81,6 +82,7 @@ export const TextArea = ({
className,
onChange,
onBlur,
readOnly = false,
}: TextAreaProps) => {
const computedMinRows = Math.min(minRows, maxRows);
@@ -125,6 +127,7 @@ export const TextArea = ({
onBlur={handleBlur}
disabled={disabled}
className={className}
readOnly={readOnly}
/>
</StyledContainer>
);
@@ -51,6 +51,7 @@ import { IconCode, IconPlayerPlay, useIcons } from 'twenty-ui/display';
import { CodeEditor } from 'twenty-ui/input';
import { useIsMobile } from 'twenty-ui/utilities';
import { useDebouncedCallback } from 'use-debounce';
import { computeNewSources } from '@/serverless-functions/utils/computeNewSources';
const CODE_EDITOR_MIN_HEIGHT = 343;
@@ -158,15 +159,16 @@ export const WorkflowEditActionServerlessFunction = ({
if (actionOptions.readonly === true) {
return;
}
setFormValues((prevState) => ({
...prevState,
code: {
...prevState.code,
[SOURCE_FOLDER_NAME]: {
[INDEX_FILE_NAME]: newCode,
},
},
}));
setFormValues((prevState) => {
return {
...prevState,
code: computeNewSources({
previousCode: prevState['code'],
filePath: `${SOURCE_FOLDER_NAME}/${INDEX_FILE_NAME}`,
value: newCode,
}),
};
});
await handleSave();
await handleUpdateFunctionInputSchema(newCode);
};
@@ -376,6 +378,12 @@ export const WorkflowEditActionServerlessFunction = ({
setIsFullScreen(false);
};
const indexFileContent =
typeof formValues.code?.[SOURCE_FOLDER_NAME] !== 'string' &&
typeof formValues.code[SOURCE_FOLDER_NAME][INDEX_FILE_NAME] === 'string'
? formValues.code[SOURCE_FOLDER_NAME][INDEX_FILE_NAME]
: '';
const fullScreenOverlay = renderFullScreenModal(
<div data-globally-prevent-click-outside="true">
<WorkflowEditActionServerlessFunctionFields
@@ -387,7 +395,7 @@ export const WorkflowEditActionServerlessFunction = ({
<StyledFullScreenCodeEditorContainer>
<CodeEditor
height="100%"
value={formValues.code?.[SOURCE_FOLDER_NAME]?.[INDEX_FILE_NAME]}
value={indexFileContent}
language="typescript"
onChange={handleCodeChange}
onMount={handleEditorDidMount}
@@ -434,7 +442,7 @@ export const WorkflowEditActionServerlessFunction = ({
readonly={actionOptions.readonly}
/>
<WorkflowServerlessFunctionCodeEditor
value={formValues.code?.[SOURCE_FOLDER_NAME]?.[INDEX_FILE_NAME]}
value={indexFileContent}
onChange={handleCodeChange}
onMount={handleEditorDidMount}
options={{
@@ -69,6 +69,12 @@ export const WorkflowReadonlyActionServerlessFunction = ({
return null;
}
const indexFileContent =
typeof formValues.code?.[SOURCE_FOLDER_NAME] !== 'string' &&
typeof formValues.code[SOURCE_FOLDER_NAME][INDEX_FILE_NAME] === 'string'
? formValues.code[SOURCE_FOLDER_NAME][INDEX_FILE_NAME]
: '';
return (
<>
<SidePanelHeader
@@ -86,7 +92,7 @@ export const WorkflowReadonlyActionServerlessFunction = ({
<StyledCodeEditorContainer>
<CodeEditor
height={343}
value={formValues.code?.[SOURCE_FOLDER_NAME]?.[INDEX_FILE_NAME]}
value={indexFileContent}
language="typescript"
onMount={handleEditorDidMount}
setMarkers={getWrongExportedFunctionMarkers}
@@ -29,6 +29,7 @@ import { SettingsAgentDetailSkeletonLoader } from './components/SettingsAgentDet
import { SettingsAgentHandoffSection } from './components/SettingsAgentHandoffSection';
import { SettingsAIAgentForm } from './forms/components/SettingsAIAgentForm';
import { useSettingsAgentFormState } from './hooks/useSettingsAgentFormState';
import { useState } from 'react';
const StyledContentContainer = styled.div`
display: flex;
@@ -47,9 +48,12 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
const { enqueueErrorSnackBar } = useSnackBar();
const { openModal } = useModal();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const [isReadonlyMode, setIsReadonlyMode] = useState(false);
const isEditMode = mode === 'edit';
const isCreateMode = mode === 'create';
const {
formValues,
isSubmitting,
@@ -61,10 +65,13 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
const { data, loading } = useFindOneAgentQuery({
variables: { id: agentId },
skip: !isEditMode || !agentId,
skip: isCreateMode || !agentId,
onCompleted: (data) => {
const agent = data?.findOneAgent;
if (isDefined(agent)) {
if (isDefined(agent.applicationId)) {
setIsReadonlyMode(true);
}
resetForm({
name: agent.name,
label: agent.label,
@@ -98,13 +105,17 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
const isAskAIAgent = agent?.id === currentWorkspace?.defaultAgent?.id;
if (isEditMode && !loading && !agent) {
if (!isCreateMode && !loading && !agent) {
return null;
}
const canSave = validateForm() && !isSubmitting;
const canSave = !isReadonlyMode && validateForm() && !isSubmitting;
const handleSave = async () => {
if (isReadonlyMode) {
return;
}
if (!validateForm()) {
return;
}
@@ -112,7 +123,7 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
setIsSubmitting(true);
try {
if (!isEditMode) {
if (isCreateMode) {
const input: CreateAgentInput = {
name: formValues.name,
label: formValues.label,
@@ -161,12 +172,16 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
}
};
const title = isEditMode ? (loading ? t`Agent` : agent?.label) : t`New Agent`;
const pageTitle = isEditMode ? t`Edit Agent` : t`New Agent`;
const pageDescription = isEditMode
const title = !isCreateMode
? loading
? t`Agent`
: agent?.label
: t`New Agent`;
const pageTitle = !isCreateMode ? t`Edit Agent` : t`New Agent`;
const pageDescription = !isCreateMode
? t`Update agent information`
: t`Create a new AI agent`;
const breadcrumbText = isEditMode
const breadcrumbText = !isCreateMode
? loading
? t`Agent`
: agent?.label
@@ -177,7 +192,7 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
<SubMenuTopBarContainer
title={title}
actionButton={
!isEditMode || agent?.isCustom ? (
isCreateMode || (isEditMode && agent?.isCustom) ? (
<SaveAndCancelButtons
onSave={handleSave}
onCancel={() => navigate(SettingsPath.AI)}
@@ -212,23 +227,29 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
<SettingsAIAgentForm
formValues={formValues}
onFieldChange={handleFieldChange}
disabled={isEditMode ? !agent?.isCustom : false}
disabled={
isReadonlyMode ||
(isEditMode ? !agent?.isCustom : false)
}
/>
{isEditMode && agent && formValues.isCustom && (
<Section>
<H2Title
title={t`Danger zone`}
description={t`Delete this agent`}
/>
<Button
accent="danger"
variant="secondary"
title={t`Delete Agent`}
Icon={IconTrash}
onClick={() => openModal(DELETE_AGENT_MODAL_ID)}
/>
</Section>
)}
{!isReadonlyMode &&
isEditMode &&
agent &&
formValues.isCustom && (
<Section>
<H2Title
title={t`Danger zone`}
description={t`Delete this agent`}
/>
<Button
accent="danger"
variant="secondary"
title={t`Delete Agent`}
Icon={IconTrash}
onClick={() => openModal(DELETE_AGENT_MODAL_ID)}
/>
</Section>
)}
</>
)}
</StyledContentContainer>
@@ -4,9 +4,9 @@ import { type ReactNode } from 'react';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { Tag } from 'twenty-ui/components';
import { OverflowingTextWithTooltip, useIcons } from 'twenty-ui/display';
import { type Agent } from '~/generated-metadata/graphql';
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
export type SettingsAIAgentTableRowProps = {
action: ReactNode;
@@ -52,11 +52,7 @@ export const SettingsAIAgentTableRow = ({
<OverflowingTextWithTooltip text={agent.label} />
</StyledNameTableCell>
<TableCell>
<Tag
text={agent.isCustom ? 'Custom' : 'Standard'}
color={agent.isCustom ? 'orange' : 'blue'}
weight="medium"
/>
<SettingsItemTypeTag item={agent} />
</TableCell>
<StyledActionTableCell>{action}</StyledActionTableCell>
</StyledAIAgentTableRow>
@@ -33,7 +33,13 @@ const StyledTableHeaderRow = styled(StyledAIAgentTableRow)`
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsAIAgentsTable = ({ agents }: { agents: Agent[] }) => {
export const SettingsAIAgentsTable = ({
agents,
withSearchBar = false,
}: {
agents: Agent[];
withSearchBar?: boolean;
}) => {
const { t } = useLingui();
const theme = useTheme();
const [searchTerm, setSearchTerm] = useState('');
@@ -50,13 +56,15 @@ export const SettingsAIAgentsTable = ({ agents }: { agents: Agent[] }) => {
return (
<>
<StyledSearchInput
instanceId="settings-ai-agents-search"
LeftIcon={IconSearch}
placeholder={t`Search an agent...`}
value={searchTerm}
onChange={setSearchTerm}
/>
{withSearchBar && (
<StyledSearchInput
instanceId="settings-ai-agents-search"
LeftIcon={IconSearch}
placeholder={t`Search an agent...`}
value={searchTerm}
onChange={setSearchTerm}
/>
)}
<StyledTable>
<StyledTableHeaderRow>
@@ -0,0 +1,114 @@
import { SettingsPath } from 'twenty-shared/types';
import { t } from '@lingui/core/macro';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useParams } from 'react-router-dom';
import { useFindOneApplicationQuery } from '~/generated-metadata/graphql';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { Section } from 'twenty-ui/layout';
import { H2Title } from 'twenty-ui/display';
import { SettingsApplicationDetailSkeletonLoader } from '~/pages/settings/applications/components/SettingsApplicationDetailSkeletonLoader';
import { SettingsServerlessFunctionsTable } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsTable';
import { SettingsAIAgentsTable } from '~/pages/settings/ai/components/SettingsAIAgentsTable';
import { SettingsObjectTable } from '~/pages/settings/data-model/SettingsObjectTable';
import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
export const SettingsApplicationDetails = () => {
const { applicationId = '' } = useParams<{ applicationId: string }>();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { data, loading } = useFindOneApplicationQuery({
variables: { id: applicationId },
skip: !applicationId,
});
if (!isDefined(data?.findOneApplication)) {
return;
}
const {
name: applicationName,
serverlessFunctions,
agents,
objects,
} = data.findOneApplication;
const shouldDisplayServerlessFunctions =
!loading &&
isDefined(serverlessFunctions) &&
serverlessFunctions?.length > 0;
const shouldDisplayAgents =
!loading && isDefined(agents) && agents.length > 0;
const shouldDisplayObjects =
!loading && isDefined(objects) && objects.length > 0;
const objectIds = objects.map((object) => object.id);
const applicationObjectMetadataItems = shouldDisplayObjects
? objectMetadataItems.filter((objectMetadataItem) =>
objectIds.includes(objectMetadataItem.id),
)
: [];
const title = loading
? t`Application details`
: data?.findOneApplication?.name;
return (
<SubMenuTopBarContainer
title={title}
links={[
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: t`Applications`,
href: getSettingsPath(SettingsPath.Applications),
},
{ children: `${applicationName}` },
]}
>
<SettingsPageContainer>
{loading && <SettingsApplicationDetailSkeletonLoader />}
{shouldDisplayServerlessFunctions && (
<Section>
<H2Title
title={t`Application serverless functions`}
description={t`Serverless functions created by application`}
/>
<SettingsServerlessFunctionsTable
serverlessFunctions={serverlessFunctions}
/>
</Section>
)}
{shouldDisplayAgents && (
<Section>
<H2Title
title={t`Application agents`}
description={t`Agents created by application`}
/>
<SettingsAIAgentsTable agents={agents} withSearchBar={false} />
</Section>
)}
{shouldDisplayObjects && (
<Section>
<H2Title
title={t`Application objects`}
description={t`Objects created by application`}
/>
<SettingsObjectTable
activeObjects={applicationObjectMetadataItems}
inactiveObjects={[]}
withSearchBar={false}
/>
</Section>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
@@ -0,0 +1,61 @@
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { t } from '@lingui/core/macro';
import { getSettingsPath } from 'twenty-shared/utils';
import { SettingsPath } from 'twenty-shared/types';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { Section } from 'twenty-ui/layout';
import { SettingsApplicationsTable } from '~/pages/settings/applications/components/SettingsApplicationsTable';
import { useFindManyApplicationsQuery } from '~/generated-metadata/graphql';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import styled from '@emotion/styled';
import { LinkDisplay } from '@/ui/field/display/components/LinkDisplay';
const APPLICATIONS_ID = 'applications';
const StyledNoApplicationContainer = styled.div``;
export const SettingsApplications = () => {
const { data } = useFindManyApplicationsQuery();
const applications = data?.findManyApplications ?? [];
const tabs = [{ id: 'inUsed', title: 'In used' }];
return (
<SubMenuTopBarContainer
title={t`Applications`}
links={[
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.Workspace),
},
{ children: t`Applications` },
]}
>
<SettingsPageContainer>
<Section>
{applications.length > 0 ? (
<>
<TabList
tabs={tabs}
behaveAsLinks={false}
componentInstanceId={APPLICATIONS_ID}
/>
<SettingsApplicationsTable applications={applications} />
</>
) : (
<StyledNoApplicationContainer>
No installed application. Please check our{' '}
<LinkDisplay
value={{
url: 'https://www.npmjs.com/package/twenty-cli',
label: 'twenty-cli',
}}
/>
</StyledNoApplicationContainer>
)}
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
@@ -0,0 +1,84 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
const StyledSkeletonContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(8)};
`;
const StyledFormSection = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledIconNameRow = styled.div`
align-items: flex-start;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledIconContainer = styled.div`
flex-shrink: 0;
`;
const StyledNameContainer = styled.div`
flex: 1;
`;
export const SettingsApplicationDetailSkeletonLoader = () => {
const theme = useTheme();
return (
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<StyledSkeletonContainer>
<StyledFormSection>
<StyledIconNameRow>
<StyledIconContainer>
<Skeleton
width={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
/>
</StyledIconContainer>
<StyledNameContainer>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
width="100%"
/>
</StyledNameContainer>
</StyledIconNameRow>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
width="100%"
/>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
width="100%"
/>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
width="100%"
/>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
width="100%"
/>
<Skeleton height={120} width="100%" />
</StyledFormSection>
</StyledSkeletonContainer>
</SkeletonTheme>
);
};
@@ -0,0 +1,47 @@
import styled from '@emotion/styled';
import { type ReactNode } from 'react';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { type ApplicationWithoutRelation } from '~/pages/settings/applications/types/applicationWithoutRelation';
export type SettingsApplicationTableRowProps = {
action: ReactNode;
application: ApplicationWithoutRelation;
link?: string;
};
export const StyledApplicationTableRow = styled(TableRow)`
grid-template-columns: 1fr 120px 36px;
`;
const StyledNameTableCell = styled(TableCell)`
color: ${({ theme }) => theme.font.color.primary};
gap: ${({ theme }) => theme.spacing(2)};
min-width: 0;
overflow: hidden;
`;
const StyledActionTableCell = styled(TableCell)`
justify-content: flex-end;
padding-right: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsApplicationTableRow = ({
action,
application,
link,
}: SettingsApplicationTableRowProps) => {
return (
<StyledApplicationTableRow key={application.id} to={link}>
<StyledNameTableCell>
<OverflowingTextWithTooltip text={application.name} />
</StyledNameTableCell>
<TableCell>
<OverflowingTextWithTooltip text={application.description} />
</TableCell>
<StyledActionTableCell>{action}</StyledActionTableCell>
</StyledApplicationTableRow>
);
};
@@ -0,0 +1,56 @@
import { IconChevronRight } from 'twenty-ui/display';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { getSettingsPath } from 'twenty-shared/utils';
import { SettingsPath } from 'twenty-shared/types';
import { useLingui } from '@lingui/react/macro';
import { Table } from '@/ui/layout/table/components/Table';
import styled from '@emotion/styled';
import {
SettingsApplicationTableRow,
StyledApplicationTableRow,
} from '~/pages/settings/applications/components/SettingsApplicationTableRow';
import { useTheme } from '@emotion/react';
import { type ApplicationWithoutRelation } from '~/pages/settings/applications/types/applicationWithoutRelation';
const StyledTable = styled(Table)`
margin-top: ${({ theme }) => theme.spacing(3)};
`;
const StyledTableHeaderRow = styled(StyledApplicationTableRow)`
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsApplicationsTable = ({
applications,
}: {
applications: ApplicationWithoutRelation[];
}) => {
const { t } = useLingui();
const theme = useTheme();
return (
<StyledTable>
<StyledTableHeaderRow>
<TableHeader> {t`Name`}</TableHeader>
<TableHeader> {t`Description`}</TableHeader>
<TableHeader />
</StyledTableHeaderRow>
{applications.map((application) => (
<SettingsApplicationTableRow
key={application.id}
application={application}
action={
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
}
link={getSettingsPath(SettingsPath.ApplicationDetail, {
applicationId: application.id,
})}
/>
))}
</StyledTable>
);
};
@@ -0,0 +1,6 @@
import { type Application } from '~/generated/graphql';
export type ApplicationWithoutRelation = Pick<
Application,
'id' | 'name' | 'description'
>;
@@ -6,8 +6,6 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { ObjectFields } from '@/settings/data-model/object-details/components/tabs/ObjectFields';
import { ObjectIndexes } from '@/settings/data-model/object-details/components/tabs/ObjectIndexes';
import { ObjectSettings } from '@/settings/data-model/object-details/components/tabs/ObjectSettings';
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag';
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
@@ -35,6 +33,8 @@ import { FeatureFlagKey } from '~/generated/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { SETTINGS_OBJECT_DETAIL_TABS } from '~/pages/settings/data-model/constants/SettingsObjectDetailTabs';
import { updatedObjectNamePluralState } from '~/pages/settings/data-model/states/updatedObjectNamePluralState';
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
const StyledContentContainer = styled.div`
flex: 1;
@@ -42,7 +42,7 @@ const StyledContentContainer = styled.div`
padding-left: 0;
`;
const StyledObjectTypeTag = styled(SettingsDataModelObjectTypeTag)`
const StyledObjectTypeTag = styled(SettingsItemTypeTag)`
box-sizing: border-box;
height: ${({ theme }) => theme.spacing(5)};
margin-left: ${({ theme }) => theme.spacing(2)};
@@ -67,6 +67,8 @@ export const SettingsObjectDetailPage = () => {
findActiveObjectMetadataItemByNamePlural(objectNamePlural) ??
findActiveObjectMetadataItemByNamePlural(updatedObjectNamePlural);
const readonly = isObjectMetadataReadOnly({ objectMetadataItem });
const activeTabId = useRecoilComponentValue(
activeTabIdComponentState,
SETTINGS_OBJECT_DETAIL_TABS.COMPONENT_INSTANCE_ID,
@@ -132,15 +134,13 @@ export const SettingsObjectDetailPage = () => {
}
};
const objectTypeLabel = getObjectTypeLabel(objectMetadataItem);
return (
<>
<SubMenuTopBarContainer
title={
<StyledTitleContainer>
<H3Title title={objectMetadataItem.labelPlural} />
<StyledObjectTypeTag objectTypeLabel={objectTypeLabel} />
<StyledObjectTypeTag item={objectMetadataItem} />
</StyledTitleContainer>
}
links={[
@@ -154,6 +154,7 @@ export const SettingsObjectDetailPage = () => {
},
]}
actionButton={
!readonly &&
activeTabId === SETTINGS_OBJECT_DETAIL_TABS.TABS_IDS.FIELDS && (
<UndecoratedLink to="./new-field/select">
<Button
@@ -36,6 +36,7 @@ import { Section } from 'twenty-ui/layout';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
//TODO: fix this type
export type SettingsDataModelFieldEditFormValues = z.infer<
@@ -68,6 +69,8 @@ export const SettingsObjectFieldEdit = () => {
const objectMetadataItem =
findObjectMetadataItemByNamePlural(objectNamePlural);
const readonly = isObjectMetadataReadOnly({ objectMetadataItem });
const { deactivateMetadataField, activateMetadataField } =
useFieldMetadataItem();
@@ -118,6 +121,10 @@ export const SettingsObjectFieldEdit = () => {
const handleSave = async (
formValues: SettingsDataModelFieldEditFormValues,
) => {
if (readonly) {
return;
}
const { dirtyFields } = formConfig.formState;
setNewNameDuringSave(formValues.name);
@@ -187,6 +194,10 @@ export const SettingsObjectFieldEdit = () => {
};
const handleDeactivate = async () => {
if (readonly) {
return;
}
await deactivateMetadataField(fieldMetadataItem.id, objectMetadataItem.id);
navigateSettings(SettingsPath.ObjectDetail, {
objectNamePlural,
@@ -194,6 +205,10 @@ export const SettingsObjectFieldEdit = () => {
};
const handleActivate = async () => {
if (readonly) {
return;
}
await activateMetadataField(fieldMetadataItem.id, objectMetadataItem.id);
navigateSettings(SettingsPath.ObjectDetail, {
objectNamePlural,
@@ -228,8 +243,8 @@ export const SettingsObjectFieldEdit = () => {
actionButton={
<SaveAndCancelButtons
isLoading={isSubmitting}
isSaveDisabled={!canSave}
isCancelDisabled={isSubmitting}
isSaveDisabled={!canSave || readonly}
isCancelDisabled={isSubmitting || readonly}
onCancel={handleCancel}
onSave={formConfig.handleSubmit(handleSave)}
/>
@@ -245,6 +260,7 @@ export const SettingsObjectFieldEdit = () => {
fieldMetadataItem={fieldMetadataItem}
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
isCreationMode={false}
readonly={readonly}
/>
</Section>
{
@@ -268,6 +284,7 @@ export const SettingsObjectFieldEdit = () => {
fieldType={fieldMetadataItem.type}
existingFieldMetadataId={fieldMetadataItem.id}
objectNameSingular={objectMetadataItem.nameSingular}
disabled={readonly}
/>
</Section>
</>
@@ -280,10 +297,11 @@ export const SettingsObjectFieldEdit = () => {
/>
<SettingsDataModelFieldDescriptionForm
fieldMetadataItem={fieldMetadataItem}
disabled={readonly}
/>
</Section>
{!isLabelIdentifier && (
{!isLabelIdentifier && !readonly && (
<Section>
<H2Title
title={t`Danger zone`}
@@ -0,0 +1,224 @@
import {
SettingsObjectMetadataItemTableRow,
StyledObjectTableRow,
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
import { GET_SETTINGS_OBJECT_TABLE_METADATA } from '~/pages/settings/data-model/constants/SettingsObjectTableMetadata';
import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { isNonEmptyArray } from '@sniptt/guards';
import { TableSection } from '@/ui/layout/table/components/TableSection';
import { getSettingsPath } from 'twenty-shared/utils';
import { SettingsPath } from 'twenty-shared/types';
import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown';
import { Table } from '@/ui/layout/table/components/Table';
import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
import { useMemo, useState } from 'react';
import { normalizeSearchText } from '~/utils/normalizeSearchText';
import { IconChevronRight, IconSearch } from 'twenty-ui/display';
import styled from '@emotion/styled';
import { getItemTagInfo } from '@/settings/data-model/utils/getItemTagInfo';
import type { SettingsObjectTableItem } from '~/pages/settings/data-model/types/SettingsObjectTableItem';
import { useCombinedGetTotalCount } from '@/object-record/multiple-objects/hooks/useCombinedGetTotalCount';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useLingui } from '@lingui/react/macro';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { useTheme } from '@emotion/react';
const StyledIconChevronRight = styled(IconChevronRight)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
const StyledSearchInput = styled(SettingsTextInput)`
padding-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
export const SettingsObjectTable = ({
activeObjects,
inactiveObjects,
withSearchBar = true,
}: {
activeObjects: ObjectMetadataItem[];
inactiveObjects: ObjectMetadataItem[];
withSearchBar?: boolean;
}) => {
const { t } = useLingui();
const theme = useTheme();
const [searchTerm, setSearchTerm] = useState('');
const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem();
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
const { totalCountByObjectMetadataItemNamePlural } = useCombinedGetTotalCount(
{
objectMetadataItems: [...activeObjects, ...inactiveObjects],
},
);
const activeObjectSettingsArray = useMemo(
() =>
activeObjects.map(
(objectMetadataItem) =>
({
objectMetadataItem,
labelPlural: objectMetadataItem.labelPlural,
objectTypeLabel: getItemTagInfo(objectMetadataItem).labelText,
fieldsCount: objectMetadataItem.fields.filter(
(field) => !field.isSystem,
).length,
totalObjectCount:
totalCountByObjectMetadataItemNamePlural[
objectMetadataItem.namePlural
] ?? 0,
}) satisfies SettingsObjectTableItem,
),
[activeObjects, totalCountByObjectMetadataItemNamePlural],
);
const inactiveObjectSettingsArray = useMemo(
() =>
inactiveObjects.map(
(objectMetadataItem) =>
({
objectMetadataItem,
labelPlural: objectMetadataItem.labelPlural,
objectTypeLabel: getItemTagInfo({
isCustom: objectMetadataItem.isCustom,
isRemote: objectMetadataItem.isRemote,
}).labelText,
fieldsCount: objectMetadataItem.fields.filter(
(field) => !field.isSystem,
).length,
totalObjectCount:
totalCountByObjectMetadataItemNamePlural[
objectMetadataItem.namePlural
] ?? 0,
}) satisfies SettingsObjectTableItem,
),
[inactiveObjects, totalCountByObjectMetadataItemNamePlural],
);
const sortedActiveObjectSettingsItems = useSortedArray(
activeObjectSettingsArray,
GET_SETTINGS_OBJECT_TABLE_METADATA,
);
const sortedInactiveObjectSettingsItems = useSortedArray(
inactiveObjectSettingsArray,
GET_SETTINGS_OBJECT_TABLE_METADATA,
);
const filteredActiveObjectSettingsItems = useMemo(
() =>
sortedActiveObjectSettingsItems.filter((item) => {
const searchNormalized = normalizeSearchText(searchTerm);
return (
normalizeSearchText(item.labelPlural).includes(searchNormalized) ||
normalizeSearchText(item.objectTypeLabel).includes(searchNormalized)
);
}),
[sortedActiveObjectSettingsItems, searchTerm],
);
const filteredInactiveObjectSettingsItems = useMemo(
() =>
sortedInactiveObjectSettingsItems.filter((item) => {
const searchNormalized = normalizeSearchText(searchTerm);
return (
normalizeSearchText(item.labelPlural).includes(searchNormalized) ||
normalizeSearchText(item.objectTypeLabel).includes(searchNormalized)
);
}),
[sortedInactiveObjectSettingsItems, searchTerm],
);
return (
<>
{withSearchBar && (
<StyledSearchInput
instanceId="settings-objects-search"
LeftIcon={IconSearch}
placeholder={t`Search for an object...`}
value={searchTerm}
onChange={setSearchTerm}
/>
)}
<Table>
<StyledObjectTableRow>
{GET_SETTINGS_OBJECT_TABLE_METADATA.fields.map(
(settingsObjectsTableMetadataField) => (
<SortableTableHeader
key={settingsObjectsTableMetadataField.fieldName}
fieldName={settingsObjectsTableMetadataField.fieldName}
label={t(settingsObjectsTableMetadataField.fieldLabel)}
tableId={GET_SETTINGS_OBJECT_TABLE_METADATA.tableId}
align={settingsObjectsTableMetadataField.align}
initialSort={GET_SETTINGS_OBJECT_TABLE_METADATA.initialSort}
/>
),
)}
<TableHeader></TableHeader>
</StyledObjectTableRow>
{isNonEmptyArray(sortedActiveObjectSettingsItems) && (
<TableSection title={t`Active`}>
{filteredActiveObjectSettingsItems.map((objectSettingsItem) => (
<SettingsObjectMetadataItemTableRow
key={objectSettingsItem.objectMetadataItem.namePlural}
objectMetadataItem={objectSettingsItem.objectMetadataItem}
totalObjectCount={objectSettingsItem.totalObjectCount}
action={
<StyledIconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
}
link={getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural:
objectSettingsItem.objectMetadataItem.namePlural,
})}
/>
))}
</TableSection>
)}
{isNonEmptyArray(sortedInactiveObjectSettingsItems) && (
<TableSection title={t`Inactive`}>
{filteredInactiveObjectSettingsItems.map((objectSettingsItem) => (
<SettingsObjectMetadataItemTableRow
key={objectSettingsItem.objectMetadataItem.namePlural}
objectMetadataItem={objectSettingsItem.objectMetadataItem}
totalObjectCount={objectSettingsItem.totalObjectCount}
action={
<SettingsObjectInactiveMenuDropDown
isCustomObject={
objectSettingsItem.objectMetadataItem.isCustom
}
objectMetadataItemNamePlural={
objectSettingsItem.objectMetadataItem.namePlural
}
onActivate={() =>
updateOneObjectMetadataItem({
idToUpdate: objectSettingsItem.objectMetadataItem.id,
updatePayload: { isActive: true },
})
}
onDelete={() =>
deleteOneObjectMetadataItem(
objectSettingsItem.objectMetadataItem.id,
)
}
/>
}
/>
))}
</TableSection>
)}
</Table>
</>
);
};
@@ -1,152 +1,25 @@
import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { useCombinedGetTotalCount } from '@/object-record/multiple-objects/hooks/useCombinedGetTotalCount';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import {
SettingsObjectMetadataItemTableRow,
StyledObjectTableRow,
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
import { SettingsObjectCoverImage } from '@/settings/data-model/objects/components/SettingsObjectCoverImage';
import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown';
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader';
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableSection } from '@/ui/layout/table/components/TableSection';
import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import { isNonEmptyArray } from '@sniptt/guards';
import { useMemo, useState } from 'react';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import {
H2Title,
IconChevronRight,
IconPlus,
IconSearch,
} from 'twenty-ui/display';
import { H2Title, IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { GET_SETTINGS_OBJECT_TABLE_METADATA } from '~/pages/settings/data-model/constants/SettingsObjectTableMetadata';
import { type SettingsObjectTableItem } from '~/pages/settings/data-model/types/SettingsObjectTableItem';
import { normalizeSearchText } from '~/utils/normalizeSearchText';
const StyledIconChevronRight = styled(IconChevronRight)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
const StyledSearchInput = styled(SettingsTextInput)`
padding-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
import { SettingsObjectTable } from '~/pages/settings/data-model/SettingsObjectTable';
export const SettingsObjects = () => {
const { t } = useLingui();
const theme = useTheme();
const [searchTerm, setSearchTerm] = useState('');
const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem();
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
const {
activeNonSystemObjectMetadataItems,
inactiveNonSystemObjectMetadataItems,
} = useFilteredObjectMetadataItems();
const { totalCountByObjectMetadataItemNamePlural } = useCombinedGetTotalCount(
{
objectMetadataItems: [
...activeNonSystemObjectMetadataItems,
...inactiveNonSystemObjectMetadataItems,
],
},
);
const activeObjectSettingsArray = useMemo(
() =>
activeNonSystemObjectMetadataItems.map(
(objectMetadataItem) =>
({
objectMetadataItem,
labelPlural: objectMetadataItem.labelPlural,
objectTypeLabel: getObjectTypeLabel(objectMetadataItem).labelText,
fieldsCount: objectMetadataItem.fields.filter(
(field) => !field.isSystem,
).length,
totalObjectCount:
totalCountByObjectMetadataItemNamePlural[
objectMetadataItem.namePlural
] ?? 0,
}) satisfies SettingsObjectTableItem,
),
[
activeNonSystemObjectMetadataItems,
totalCountByObjectMetadataItemNamePlural,
],
);
const inactiveObjectSettingsArray = useMemo(
() =>
inactiveNonSystemObjectMetadataItems.map(
(objectMetadataItem) =>
({
objectMetadataItem,
labelPlural: objectMetadataItem.labelPlural,
objectTypeLabel: getObjectTypeLabel(objectMetadataItem).labelText,
fieldsCount: objectMetadataItem.fields.filter(
(field) => !field.isSystem,
).length,
totalObjectCount:
totalCountByObjectMetadataItemNamePlural[
objectMetadataItem.namePlural
] ?? 0,
}) satisfies SettingsObjectTableItem,
),
[
inactiveNonSystemObjectMetadataItems,
totalCountByObjectMetadataItemNamePlural,
],
);
const sortedActiveObjectSettingsItems = useSortedArray(
activeObjectSettingsArray,
GET_SETTINGS_OBJECT_TABLE_METADATA,
);
const sortedInactiveObjectSettingsItems = useSortedArray(
inactiveObjectSettingsArray,
GET_SETTINGS_OBJECT_TABLE_METADATA,
);
const filteredActiveObjectSettingsItems = useMemo(
() =>
sortedActiveObjectSettingsItems.filter((item) => {
const searchNormalized = normalizeSearchText(searchTerm);
return (
normalizeSearchText(item.labelPlural).includes(searchNormalized) ||
normalizeSearchText(item.objectTypeLabel).includes(searchNormalized)
);
}),
[sortedActiveObjectSettingsItems, searchTerm],
);
const filteredInactiveObjectSettingsItems = useMemo(
() =>
sortedInactiveObjectSettingsItems.filter((item) => {
const searchNormalized = normalizeSearchText(searchTerm);
return (
normalizeSearchText(item.labelPlural).includes(searchNormalized) ||
normalizeSearchText(item.objectTypeLabel).includes(searchNormalized)
);
}),
[sortedInactiveObjectSettingsItems, searchTerm],
);
return (
<SubMenuTopBarContainer
title={t`Data model`}
@@ -174,95 +47,10 @@ export const SettingsObjects = () => {
<Section>
<H2Title title={t`Existing objects`} />
<StyledSearchInput
instanceId="settings-objects-search"
LeftIcon={IconSearch}
placeholder={t`Search for an object...`}
value={searchTerm}
onChange={setSearchTerm}
<SettingsObjectTable
activeObjects={activeNonSystemObjectMetadataItems}
inactiveObjects={inactiveNonSystemObjectMetadataItems}
/>
<Table>
<StyledObjectTableRow>
{GET_SETTINGS_OBJECT_TABLE_METADATA.fields.map(
(settingsObjectsTableMetadataField) => (
<SortableTableHeader
key={settingsObjectsTableMetadataField.fieldName}
fieldName={settingsObjectsTableMetadataField.fieldName}
label={t(settingsObjectsTableMetadataField.fieldLabel)}
tableId={GET_SETTINGS_OBJECT_TABLE_METADATA.tableId}
align={settingsObjectsTableMetadataField.align}
initialSort={
GET_SETTINGS_OBJECT_TABLE_METADATA.initialSort
}
/>
),
)}
<TableHeader></TableHeader>
</StyledObjectTableRow>
{isNonEmptyArray(sortedActiveObjectSettingsItems) && (
<TableSection title={t`Active`}>
{filteredActiveObjectSettingsItems.map(
(objectSettingsItem) => (
<SettingsObjectMetadataItemTableRow
key={objectSettingsItem.objectMetadataItem.namePlural}
objectMetadataItem={
objectSettingsItem.objectMetadataItem
}
totalObjectCount={objectSettingsItem.totalObjectCount}
action={
<StyledIconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
}
link={getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural:
objectSettingsItem.objectMetadataItem.namePlural,
})}
/>
),
)}
</TableSection>
)}
{isNonEmptyArray(inactiveNonSystemObjectMetadataItems) && (
<TableSection title={t`Inactive`}>
{filteredInactiveObjectSettingsItems.map(
(objectSettingsItem) => (
<SettingsObjectMetadataItemTableRow
key={objectSettingsItem.objectMetadataItem.namePlural}
objectMetadataItem={
objectSettingsItem.objectMetadataItem
}
totalObjectCount={objectSettingsItem.totalObjectCount}
action={
<SettingsObjectInactiveMenuDropDown
isCustomObject={
objectSettingsItem.objectMetadataItem.isCustom
}
objectMetadataItemNamePlural={
objectSettingsItem.objectMetadataItem.namePlural
}
onActivate={() =>
updateOneObjectMetadataItem({
idToUpdate:
objectSettingsItem.objectMetadataItem.id,
updatePayload: { isActive: true },
})
}
onDelete={() =>
deleteOneObjectMetadataItem(
objectSettingsItem.objectMetadataItem.id,
)
}
/>
}
/>
),
)}
</TableSection>
)}
</Table>
</Section>
</>
</SettingsPageContainer>
@@ -3,48 +3,53 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { SettingsServerlessFunctionCodeEditorTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab';
import { SettingsServerlessFunctionSettingsTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab';
import { SettingsServerlessFunctionTestTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab';
import { useGetOneServerlessFunctionSourceCode } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode';
import { usePublishOneServerlessFunction } from '@/settings/serverless-functions/hooks/usePublishOneServerlessFunction';
import { SettingsServerlessFunctionTriggersTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTriggersTab';
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { ApolloError } from '@apollo/client';
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { IconCode, IconSettings, IconTestPipe } from 'twenty-ui/display';
import { getSettingsPath } from 'twenty-shared/utils';
import {
IconCode,
IconSettings,
IconTestPipe,
IconBolt,
} from 'twenty-ui/display';
import { useDebouncedCallback } from 'use-debounce';
import { getErrorMessageFromApolloError } from '~/utils/get-error-message-from-apollo-error.util';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { SOURCE_FOLDER_NAME } from '@/serverless-functions/constants/SourceFolderName';
import { t } from '@lingui/core/macro';
import { useFindOneApplicationQuery } from '~/generated-metadata/graphql';
import { computeNewSources } from '@/serverless-functions/utils/computeNewSources';
import { flattenSources } from '@/serverless-functions/utils/flattenSources';
const SERVERLESS_FUNCTION_DETAIL_ID = 'serverless-function-detail';
export const SettingsServerlessFunctionDetail = () => {
const { serverlessFunctionId = '' } = useParams();
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
const { serverlessFunctionId = '', applicationId = '' } = useParams();
const { data } = useFindOneApplicationQuery({
variables: { id: applicationId },
skip: !applicationId,
});
const applicationName = data?.findOneApplication?.name;
const [activeTabId, setActiveTabId] = useRecoilComponentState(
activeTabIdComponentState,
SERVERLESS_FUNCTION_DETAIL_ID,
);
const [isCodeValid, setIsCodeValid] = useState(true);
const { updateOneServerlessFunction } =
useUpdateOneServerlessFunction(serverlessFunctionId);
const { publishOneServerlessFunction } = usePublishOneServerlessFunction();
const { formValues, setFormValues, loading } =
const { formValues, setFormValues, serverlessFunction, loading } =
useServerlessFunctionUpdateFormState({ serverlessFunctionId });
const { testServerlessFunction } = useTestServerlessFunction({
const { testServerlessFunction, isTesting } = useTestServerlessFunction({
serverlessFunctionId,
});
const { code: latestVersionCode } = useGetOneServerlessFunctionSourceCode({
id: serverlessFunctionId,
version: 'latest',
});
const handleSave = useDebouncedCallback(async () => {
await updateOneServerlessFunction({
@@ -52,7 +57,7 @@ export const SettingsServerlessFunctionDetail = () => {
description: formValues.description,
code: formValues.code,
});
}, 1_000);
}, 500);
const onChange = (key: string) => {
return async (value: string) => {
@@ -65,52 +70,17 @@ export const SettingsServerlessFunctionDetail = () => {
};
const onCodeChange = async (filePath: string, value: string) => {
setFormValues((prevState) => ({
...prevState,
code: { ...prevState.code, [filePath]: value },
}));
await handleSave();
};
const resetDisabled =
!isDefined(latestVersionCode) ||
isDeeplyEqual(latestVersionCode, formValues.code);
const publishDisabled =
!isCodeValid || isDeeplyEqual(latestVersionCode, formValues.code);
const handleReset = async () => {
try {
const newState = {
code: latestVersionCode || {},
};
setFormValues((prevState) => ({
setFormValues((prevState) => {
return {
...prevState,
...newState,
}));
await handleSave();
} catch (err) {
enqueueErrorSnackBar({
apolloError: err instanceof ApolloError ? err : undefined,
});
}
};
const handlePublish = async () => {
try {
await publishOneServerlessFunction({
id: serverlessFunctionId,
});
enqueueSuccessSnackBar({
message: `New function version has been published`,
});
} catch (err) {
enqueueErrorSnackBar({
message:
err instanceof ApolloError
? getErrorMessageFromApolloError(err)
: 'An error occurred while publishing new version',
});
}
code: computeNewSources({
previousCode: prevState['code'],
filePath,
value,
}),
};
});
await handleSave();
};
const handleTestFunction = async () => {
@@ -120,26 +90,24 @@ export const SettingsServerlessFunctionDetail = () => {
const tabs = [
{ id: 'editor', title: 'Editor', Icon: IconCode },
{ id: 'triggers', title: 'Triggers', Icon: IconBolt },
{ id: 'test', title: 'Test', Icon: IconTestPipe },
{ id: 'settings', title: 'Settings', Icon: IconSettings },
];
const files = formValues.code
? [
{
path: '.env',
language: 'ini',
content: formValues.code?.['.env'] || '',
},
...Object.keys(formValues.code?.[SOURCE_FOLDER_NAME]).map((key) => {
return {
path: key,
language: 'typescript',
content: formValues.code?.[SOURCE_FOLDER_NAME]?.[key] || '',
};
}),
].reverse()
: [];
const flattenedCode = flattenSources(formValues.code);
const files = flattenedCode
.map((file) => {
const language = file.path === '.env' ? 'ini' : 'typescript';
return {
path: file.path,
language,
content: file.content,
};
})
.reverse();
const renderActiveTabContent = () => {
switch (activeTabId) {
@@ -148,19 +116,22 @@ export const SettingsServerlessFunctionDetail = () => {
<SettingsServerlessFunctionCodeEditorTab
files={files}
handleExecute={handleTestFunction}
handlePublish={handlePublish}
handleReset={handleReset}
resetDisabled={resetDisabled}
publishDisabled={publishDisabled}
onChange={onCodeChange}
setIsCodeValid={setIsCodeValid}
isTesting={isTesting}
/>
);
case 'triggers':
return serverlessFunction ? (
<SettingsServerlessFunctionTriggersTab
serverlessFunction={serverlessFunction}
/>
) : null;
case 'test':
return (
<SettingsServerlessFunctionTestTab
serverlessFunctionId={serverlessFunctionId}
handleExecute={handleTestFunction}
isTesting={isTesting}
/>
);
case 'settings':
@@ -187,16 +158,21 @@ export const SettingsServerlessFunctionDetail = () => {
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: 'Functions',
href: getSettingsPath(SettingsPath.ServerlessFunctions),
children: t`Applications`,
href: getSettingsPath(SettingsPath.Applications),
},
{ children: `${formValues.name}` },
{
children: `${applicationName}`,
href: getSettingsPath(SettingsPath.ApplicationDetail, {
applicationId,
}),
},
{ children: `${serverlessFunction?.name}` },
]}
>
<SettingsPageContainer>
<TabList
tabs={tabs}
behaveAsLinks={false}
componentInstanceId={SERVERLESS_FUNCTION_DETAIL_ID}
/>
{renderActiveTabContent()}
@@ -1,41 +0,0 @@
import { SettingsServerlessFunctionsTable } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsTable';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { UndecoratedLink } from 'twenty-ui/navigation';
export const SettingsServerlessFunctions = () => {
return (
<SubMenuTopBarContainer
title="Functions"
actionButton={
<UndecoratedLink
to={getSettingsPath(SettingsPath.NewServerlessFunction)}
>
<Button
Icon={IconPlus}
title="New Function"
accent="blue"
size="small"
/>
</UndecoratedLink>
}
links={[
{
children: 'Workspace',
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: 'Functions',
},
]}
>
<Section>
<SettingsServerlessFunctionsTable />
</Section>
</SubMenuTopBarContainer>
);
};
@@ -1,82 +0,0 @@
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm';
import { useCreateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useCreateOneServerlessFunction';
import { type ServerlessFunctionNewFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
import { useState } from 'react';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
export const SettingsServerlessFunctionsNew = () => {
const navigate = useNavigateSettings();
const [formValues, setFormValues] = useState<ServerlessFunctionNewFormValues>(
{
name: '',
description: '',
},
);
const { createOneServerlessFunction } = useCreateOneServerlessFunction();
const handleSave = async () => {
const newServerlessFunction = await createOneServerlessFunction({
name: formValues.name,
description: formValues.description,
});
if (!isDefined(newServerlessFunction?.data)) {
return;
}
navigate(SettingsPath.ServerlessFunctions, {
id: newServerlessFunction.data.createOneServerlessFunction.id,
});
};
const onChange = (key: string) => {
return (value: string) => {
setFormValues((prevState) => ({
...prevState,
[key]: value,
}));
};
};
const canSave = !!formValues.name && createOneServerlessFunction;
return (
<SubMenuTopBarContainer
title="New Function"
links={[
{
children: 'Workspace',
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: 'Functions',
href: getSettingsPath(SettingsPath.ServerlessFunctions),
},
{ children: 'New' },
]}
actionButton={
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => {
navigate(SettingsPath.ServerlessFunctions);
}}
onSave={handleSave}
/>
}
>
<SettingsPageContainer>
<SettingsServerlessFunctionNewForm
formValues={formValues}
onChange={onChange}
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
export default SettingsServerlessFunctionsNew;
@@ -1,31 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { SettingsServerlessFunctions } from '~/pages/settings/serverless-functions/SettingsServerlessFunctions';
import {
PageDecorator,
type PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { sleep } from '~/utils/sleep';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/ServerlessFunctions/SettingsServerlessFunctions',
component: SettingsServerlessFunctions,
decorators: [PageDecorator],
args: { routePath: '/settings/functions' },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsServerlessFunctions>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await sleep(100);
await canvas.findByText('Add your first Function');
},
};
@@ -1,36 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { SettingsServerlessFunctionsNew } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionsNew';
import {
PageDecorator,
type PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/ServerlessFunctions/SettingsServerlessFunctionsNew',
component: SettingsServerlessFunctionsNew,
decorators: [PageDecorator],
args: { routePath: '/settings/functions/new' },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsServerlessFunctionsNew>;
// TEMP_DISABLED_TEST: Temporarily commented out due to test failure
// export const Default: Story = {
// play: async ({ canvasElement }) => {
// const canvas = within(canvasElement);
// await sleep(100);
// await canvas.findByText('Functions');
// await canvas.findByText('New');
// const input = await canvas.findByPlaceholderText('Name');
// await userEvent.type(input, 'Function Name');
// const saveButton = await canvas.findByText('Save');
// await userEvent.click(saveButton);
// },
// };
@@ -0,0 +1,25 @@
import { Catch, ExceptionFilter } from '@nestjs/common';
import { assertUnreachable } from 'twenty-shared/utils';
import {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@Catch(ApplicationException)
export class ApplicationExceptionFilter implements ExceptionFilter {
catch(exception: ApplicationException) {
switch (exception.code) {
case ApplicationExceptionCode.OBJECT_NOT_FOUND:
case ApplicationExceptionCode.ENTITY_NOT_FOUND:
case ApplicationExceptionCode.APPLICATION_NOT_FOUND:
case ApplicationExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND:
throw new NotFoundError(exception);
default: {
assertUnreachable(exception.code);
}
}
}
}
@@ -6,4 +6,5 @@ export enum ApplicationExceptionCode {
OBJECT_NOT_FOUND = 'OBJECT_NOT_FOUND',
SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND',
ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND',
APPLICATION_NOT_FOUND = 'APPLICATION_NOT_FOUND',
}
@@ -1,5 +1,5 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
@@ -8,15 +8,36 @@ import { ApplicationSyncService } from 'src/engine/core-modules/application/appl
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
import { DeleteApplicationInput } from 'src/engine/core-modules/application/dtos/deleteApplication.input';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { RequireFeatureFlag } from 'src/engine/guards/feature-flag.guard';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
@UseGuards(WorkspaceAuthGuard)
@Resolver()
@UseFilters(ApplicationExceptionFilter)
export class ApplicationResolver {
constructor(
private readonly applicationSyncService: ApplicationSyncService,
private readonly applicationService: ApplicationService,
) {}
@Query(() => [ApplicationDTO])
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async findManyApplications(@AuthWorkspace() { id: workspaceId }: Workspace) {
return this.applicationService.findManyApplications(workspaceId);
}
@Query(() => ApplicationDTO)
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async findOneApplication(
@Args('id', { type: () => UUIDScalarType }) id: string,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return await this.applicationService.findOneApplication(id, workspaceId);
}
@Mutation(() => Boolean)
async syncApplication(
@Args() { manifest, packageJson, yarnLock }: ApplicationInput,
@@ -7,6 +7,10 @@ import { Repository } from 'typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { PackageJson } from 'src/engine/core-modules/application/types/application.types';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
@Injectable()
export class ApplicationService {
@@ -16,6 +20,34 @@ export class ApplicationService {
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
async findManyApplications(
workspaceId: string,
): Promise<ApplicationEntity[]> {
return this.applicationRepository.find({
where: { workspaceId },
relations: ['serverlessFunctions', 'agents', 'objects'],
});
}
async findOneApplication(
applicationId: string,
workspaceId: string,
): Promise<ApplicationEntity> {
const application = await this.applicationRepository.findOne({
where: { workspaceId, id: applicationId },
relations: ['serverlessFunctions', 'agents', 'objects'],
});
if (!isDefined(application)) {
throw new ApplicationException(
`Application with id ${applicationId} not found`,
ApplicationExceptionCode.APPLICATION_NOT_FOUND,
);
}
return application;
}
async findById(id: string): Promise<ApplicationEntity | null> {
return this.applicationRepository.findOne({
where: { id },
@@ -0,0 +1,33 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
import { AgentDTO } from 'src/engine/metadata-modules/agent/dtos/agent.dto';
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
@ObjectType('Application')
export class ApplicationDTO {
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
id: string;
@IsString()
@Field()
name: string;
@IsString()
@Field()
description: string;
@Field(() => [AgentDTO])
agents: AgentDTO[];
@Field(() => [ServerlessFunctionDTO])
serverlessFunctions: ServerlessFunctionDTO[];
@Field(() => [ObjectMetadataDTO])
objects: ObjectMetadataDTO[];
}
@@ -5,6 +5,7 @@ export enum FeatureFlagKey {
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_AI_ENABLED = 'IS_AI_ENABLED',
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
@@ -244,6 +244,7 @@ export class WorkspaceResolver {
return {
...agent,
roleId: agent.roleId ?? undefined,
applicationId: agent.applicationId ?? undefined,
};
} catch {
// If agent is not found, return null instead of throwing
@@ -1,8 +1,5 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -16,7 +13,6 @@ import { CreateAgentHandoffInput } from 'src/engine/metadata-modules/agent/dtos/
import { RemoveAgentHandoffInput } from 'src/engine/metadata-modules/agent/dtos/remove-agent-handoff.input';
import { AgentHandoffService } from './agent-handoff.service';
import { AgentEntity } from './agent.entity';
import { AgentService } from './agent.service';
import { AgentHandoffDTO } from './dtos/agent-handoff.dto';
@@ -29,8 +25,6 @@ import { UpdateAgentInput } from './dtos/update-agent.input';
@Resolver()
export class AgentResolver {
constructor(
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
private readonly agentService: AgentService,
private readonly agentHandoffService: AgentHandoffService,
) {}
@@ -60,6 +60,9 @@ export class AgentDTO {
@HideField()
workspaceId: string;
@Field(() => UUIDScalarType, { nullable: true })
applicationId?: string;
@IsDateString()
@Field()
createdAt: Date;
@@ -1,4 +1,4 @@
import { Field, HideField, ObjectType } from '@nestjs/graphql';
import { Field, ObjectType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { IsDateString, IsNotEmpty, IsObject, IsUUID } from 'class-validator';
@@ -18,13 +18,6 @@ export class CronTriggerDTO {
@Field(() => GraphQLJSON)
settings: CronTriggerSettings;
@HideField()
workspaceId: string;
@IsUUID()
@Field()
serverlessFunctionId: string;
@IsDateString()
@Field()
createdAt: Date;
@@ -1,4 +1,4 @@
import { Field, HideField, ObjectType } from '@nestjs/graphql';
import { Field, ObjectType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { IsDateString, IsNotEmpty, IsObject, IsUUID } from 'class-validator';
@@ -18,13 +18,6 @@ export class DatabaseEventTriggerDTO {
@Field(() => GraphQLJSON)
settings: DatabaseEventTriggerSettings;
@HideField()
workspaceId: string;
@IsUUID()
@Field()
serverlessFunctionId: string;
@IsDateString()
@Field()
createdAt: Date;
@@ -12,6 +12,7 @@ export const fromObjectMetadataEntityToObjectMetadataDto = (
standardOverrides,
shortcut,
duplicateCriteria,
applicationId,
...rest
} = objectMetadataEntity;
@@ -24,5 +25,6 @@ export const fromObjectMetadataEntityToObjectMetadataDto = (
standardOverrides: standardOverrides ?? undefined,
shortcut: shortcut ?? undefined,
duplicateCriteria: duplicateCriteria ?? undefined,
applicationId: applicationId ?? undefined,
};
};
@@ -66,7 +66,9 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = {
},
serverlessFunction: {
propertiesToCompare: [
...FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES,
...FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES.filter(
(property) => property !== 'code',
),
'deletedAt',
],
propertiesToStringify: [],
@@ -80,6 +80,9 @@ export class ObjectMetadataDTO {
@HideField()
workspaceId: string;
@Field(() => UUIDScalarType, { nullable: true })
applicationId?: string;
@Field()
createdAt: Date;
@@ -261,7 +261,10 @@ export class RoleResolver {
workspace.id,
);
return agents;
return agents.map((agent) => ({
...agent,
applicationId: agent.applicationId ?? undefined,
}));
}
@ResolveField('apiKeys', () => [ApiKeyForRoleDTO])

Some files were not shown because too many files have changed in this diff Show More