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:
@@ -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__
|
||||
|
||||
@@ -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={
|
||||
|
||||
+24
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
+11
@@ -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 {
|
||||
|
||||
+24
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+11
-5
@@ -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 };
|
||||
+146
@@ -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);
|
||||
});
|
||||
});
|
||||
+58
@@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
+1
@@ -9,6 +9,7 @@ type SettingsDataModelPreviewFormCardProps = {
|
||||
className?: string;
|
||||
preview: ReactNode;
|
||||
form?: ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const StyledPreviewContainer = styled(CardContent)`
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+3
@@ -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}
|
||||
/>
|
||||
}
|
||||
|
||||
+6
-1
@@ -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);
|
||||
|
||||
+3
-1
@@ -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>
|
||||
</>
|
||||
|
||||
+14
@@ -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}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
+1
@@ -42,6 +42,7 @@ export const SettingsDataModelFieldTextSettingsFormCard = ({
|
||||
fieldType={FieldMetadataType.TEXT}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
+1
@@ -55,6 +55,7 @@ export const SettingsDataModelFieldDateSettingsFormCard = ({
|
||||
fieldType={fieldType}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
+3
-1
@@ -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}
|
||||
|
||||
+3
@@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
+1
@@ -42,6 +42,7 @@ export const SettingsDataModelFieldNumberSettingsFormCard = ({
|
||||
fieldType={FieldMetadataType.NUMBER}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
+1
@@ -58,6 +58,7 @@ export const SettingsDataModelFieldPhonesSettingsFormCard = ({
|
||||
fieldType={FieldMetadataType.PHONES}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
+6
-4
@@ -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}
|
||||
|
||||
+3
@@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
+40
-15
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
+3
@@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
+2
-5
@@ -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>
|
||||
|
||||
+5
-3
@@ -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}
|
||||
|
||||
+11
-7
@@ -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"
|
||||
|
||||
+16
-1
@@ -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, {
|
||||
|
||||
+2
-4
@@ -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}
|
||||
|
||||
+15
-5
@@ -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>
|
||||
|
||||
+5
-2
@@ -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, {
|
||||
|
||||
+18
-11
@@ -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
-4
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
-21
@@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
+14
-6
@@ -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 =
|
||||
|
||||
+3
-1
@@ -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',
|
||||
};
|
||||
+8
-88
@@ -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
-14
@@ -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"
|
||||
/>
|
||||
|
||||
+4
@@ -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>
|
||||
|
||||
+36
-33
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
-43
@@ -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>
|
||||
);
|
||||
};
|
||||
+11
-41
@@ -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>
|
||||
|
||||
+3
-45
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+30
-36
@@ -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,
|
||||
|
||||
+4
@@ -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>
|
||||
|
||||
+113
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+20
@@ -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
|
||||
}
|
||||
|
||||
+2
-1
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+65
-39
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+10
@@ -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>
|
||||
);
|
||||
|
||||
+19
-11
@@ -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={{
|
||||
|
||||
+7
-1
@@ -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>
|
||||
);
|
||||
};
|
||||
+84
@@ -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>
|
||||
);
|
||||
};
|
||||
+47
@@ -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>
|
||||
);
|
||||
};
|
||||
+56
@@ -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>
|
||||
);
|
||||
};
|
||||
+6
@@ -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>
|
||||
|
||||
+67
-91
@@ -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()}
|
||||
|
||||
-41
@@ -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>
|
||||
);
|
||||
};
|
||||
-82
@@ -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;
|
||||
-31
@@ -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');
|
||||
},
|
||||
};
|
||||
-36
@@ -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);
|
||||
// },
|
||||
// };
|
||||
+25
@@ -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[];
|
||||
}
|
||||
+1
@@ -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
-8
@@ -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
-8
@@ -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;
|
||||
|
||||
+2
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+3
-1
@@ -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: [],
|
||||
|
||||
+3
@@ -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
Reference in New Issue
Block a user