feat(applications): add type and options to application variables (#22157)
## Before <img width="1452" height="709" alt="image" src="https://github.com/user-attachments/assets/cd384ffa-cbe6-49d5-a807-ca8d580f55a9" /> <img width="1074" height="452" alt="image" src="https://github.com/user-attachments/assets/720d38db-3495-4032-8831-17d24ec6a7e7" /> ## After <img width="1421" height="865" alt="image" src="https://github.com/user-attachments/assets/2275c996-c895-4800-8324-2aa2ddfddd43" /> <img width="1348" height="870" alt="image" src="https://github.com/user-attachments/assets/3e1a891d-6db0-4cbd-870a-2a5bbde4929d" /> ## Summary Adds typed application variables with optional select **options**. This is the other half of #22059, split out from the custom-settings-tab removal. ## Changes - **Shared types**: `ApplicationVariable` / `ServerVariables` gain an optional `type` (a `FieldMetadataType` subset — `TEXT`, `BOOLEAN`, `NUMBER`, `DATE`, `SELECT`, `MULTI_SELECT`, `RAW_JSON`, `RICH_TEXT`, `ARRAY`, …) and select `options`. New `serializeApplicationVariableValue` / `deserializeApplicationVariableValue` helpers convert typed values to/from the encrypted string storage. - **Server**: `type`/`options` columns on `applicationVariable` and `applicationRegistrationVariable` (entities + DTOs), a fast `2-17` instance command, manifest processing via the serialization helpers, and a `QueryDeepPartialEntity` cast where the manifest JSON column is persisted. - **Frontend**: a polymorphic `SettingsApplicationVariableInput` that renders the native `Form*` field component for each type (boolean, number, date/date-time, select, multi-select, array, raw JSON, rich text, text); fragment/query updates to fetch `type`/`options`. - **SDK**: `defineApplication` validates that `SELECT`/`MULTI_SELECT` variables declare non-empty `options` at build time (since `options` is kept structurally optional for TypeORM/SDK compatibility). Variables default to `TEXT` when no type is given, so existing manifests are unaffected. ## Notes The generated GraphQL artifacts (`type`/`options` on the variable types) are regenerated by codegen; that change accompanies this PR. https://claude.ai/code/session_013Z7UB35V2mvUozh55QHG23 --- _Generated by [Claude Code](https://claude.ai/code/session_013Z7UB35V2mvUozh55QHG23)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22157?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
import { defineApplication, FieldType } from 'twenty-sdk/define';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './src/roles/default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
@@ -12,6 +12,83 @@ export default defineApplication({
|
||||
value: 'Alex Karp',
|
||||
isSecret: false,
|
||||
},
|
||||
|
||||
GREETING_TEXT: {
|
||||
universalIdentifier: 'ad19edc5-4cc5-4003-a996-aef53a5c8de0',
|
||||
description: 'Free text shown on the postcard',
|
||||
type: FieldType.TEXT,
|
||||
value: 'Hello from Rich App',
|
||||
},
|
||||
ENABLE_TRACKING: {
|
||||
universalIdentifier: 'b9c58bb2-58c3-4c6c-9877-498ea6c03fde',
|
||||
description: 'Toggle delivery tracking',
|
||||
type: FieldType.BOOLEAN,
|
||||
value: true,
|
||||
},
|
||||
MAX_POSTCARDS: {
|
||||
universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713',
|
||||
description: 'Maximum postcards per batch',
|
||||
type: FieldType.NUMBER,
|
||||
value: 10,
|
||||
},
|
||||
DISCOUNT_RATE: {
|
||||
universalIdentifier: 'd32f810f-06bb-4d29-b0ee-1dc4228f7cb8',
|
||||
description: 'Bulk discount rate applied at checkout',
|
||||
type: FieldType.NUMERIC,
|
||||
value: 2.5,
|
||||
},
|
||||
CAMPAIGN_START_DATE: {
|
||||
universalIdentifier: '5aa4fcec-e8a3-4bc1-9c7f-762e1f9dfb40',
|
||||
description: 'Date the campaign starts',
|
||||
type: FieldType.DATE,
|
||||
value: '2026-01-01',
|
||||
},
|
||||
CAMPAIGN_START_AT: {
|
||||
universalIdentifier: '273d0cfd-d6ab-4148-8816-c4a5d4b1d600',
|
||||
description: 'Exact moment the campaign starts',
|
||||
type: FieldType.DATE_TIME,
|
||||
value: '2026-01-01T09:00:00.000Z',
|
||||
},
|
||||
DEFAULT_REGION: {
|
||||
universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227',
|
||||
description: 'Default shipping region',
|
||||
type: FieldType.SELECT,
|
||||
options: [
|
||||
{ label: 'Europe', value: 'eu' },
|
||||
{ label: 'United States', value: 'us' },
|
||||
{ label: 'Asia-Pacific', value: 'apac' },
|
||||
],
|
||||
value: 'eu',
|
||||
},
|
||||
ENABLED_CHANNELS: {
|
||||
universalIdentifier: '706a2b08-8284-4715-8bc0-922a99cb26af',
|
||||
description: 'Channels the app is allowed to use',
|
||||
type: FieldType.MULTI_SELECT,
|
||||
options: [
|
||||
{ label: 'Email', value: 'email' },
|
||||
{ label: 'SMS', value: 'sms' },
|
||||
{ label: 'Postcard', value: 'postcard' },
|
||||
],
|
||||
value: ['email', 'postcard'],
|
||||
},
|
||||
ALLOWED_TAGS: {
|
||||
universalIdentifier: 'c1c8a4c9-9130-4ab8-8dce-18d2b50879ed',
|
||||
description: 'Free-form tags applied to recipients',
|
||||
type: FieldType.ARRAY,
|
||||
value: ['vip', 'returning'],
|
||||
},
|
||||
PROVIDER_CONFIG: {
|
||||
universalIdentifier: '183d5285-c70c-4f29-96e0-c68659fbe5ae',
|
||||
description: 'Raw JSON configuration for the printing provider',
|
||||
type: FieldType.RAW_JSON,
|
||||
value: { retries: 3, timeoutMs: 5000 },
|
||||
},
|
||||
WELCOME_MESSAGE: {
|
||||
universalIdentifier: '25a66ff5-8458-498e-9e7e-33ea458a6f3c',
|
||||
description: 'Rich text welcome message',
|
||||
type: FieldType.RICH_TEXT,
|
||||
value: { blocknote: null, markdown: 'Welcome to **Rich App**!' },
|
||||
},
|
||||
},
|
||||
serverVariables: {
|
||||
POSTCARD_API_KEY: {
|
||||
@@ -24,6 +101,75 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_DELIVERY_SPEED: {
|
||||
description: 'Delivery speed requested from the provider',
|
||||
type: FieldType.SELECT,
|
||||
options: [
|
||||
{ label: 'Standard', value: 'standard' },
|
||||
{ label: 'Express', value: 'express' },
|
||||
],
|
||||
isSecret: false,
|
||||
isRequired: true,
|
||||
},
|
||||
POSTCARD_DAILY_LIMIT: {
|
||||
description: 'Maximum postcards the provider will accept per day',
|
||||
type: FieldType.NUMBER,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_UNIT_PRICE: {
|
||||
description: 'Price charged by the provider per postcard',
|
||||
type: FieldType.NUMERIC,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_SANDBOX_MODE: {
|
||||
description: 'Send postcards through the provider sandbox instead of production',
|
||||
type: FieldType.BOOLEAN,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_CONTRACT_START_DATE: {
|
||||
description: 'Date the provider contract starts',
|
||||
type: FieldType.DATE,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_CONTRACT_RENEWAL_AT: {
|
||||
description: 'Exact moment the provider contract renews',
|
||||
type: FieldType.DATE_TIME,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_ENABLED_REGIONS: {
|
||||
description: 'Regions the provider is allowed to ship to',
|
||||
type: FieldType.MULTI_SELECT,
|
||||
options: [
|
||||
{ label: 'Europe', value: 'eu' },
|
||||
{ label: 'United States', value: 'us' },
|
||||
{ label: 'Asia-Pacific', value: 'apac' },
|
||||
],
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_WEBHOOK_EVENTS: {
|
||||
description: 'Provider webhook events the app subscribes to',
|
||||
type: FieldType.ARRAY,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_PROVIDER_CONFIG: {
|
||||
description: 'Raw JSON configuration for the printing provider',
|
||||
type: FieldType.RAW_JSON,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_INVOICE_NOTE: {
|
||||
description: 'Rich text note appended to provider invoices',
|
||||
type: FieldType.RICH_TEXT,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
@@ -29,11 +29,18 @@ type ApplicationRegistrationVariable {
|
||||
description: String!
|
||||
isSecret: Boolean!
|
||||
isRequired: Boolean!
|
||||
type: String!
|
||||
options: JSON
|
||||
isFilled: Boolean!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
"""
|
||||
The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
|
||||
"""
|
||||
scalar JSON
|
||||
|
||||
type ApplicationRegistration {
|
||||
id: UUID!
|
||||
universalIdentifier: String!
|
||||
@@ -114,11 +121,6 @@ enum RowLevelPermissionPredicateOperand {
|
||||
VECTOR_SEARCH
|
||||
}
|
||||
|
||||
"""
|
||||
The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
|
||||
"""
|
||||
scalar JSON
|
||||
|
||||
type ObjectPermission {
|
||||
objectMetadataId: UUID!
|
||||
canReadObjectRecords: Boolean
|
||||
@@ -297,6 +299,8 @@ type ApplicationVariable {
|
||||
value: String!
|
||||
description: String!
|
||||
isSecret: Boolean!
|
||||
type: String!
|
||||
options: JSON
|
||||
}
|
||||
|
||||
type AuthToken {
|
||||
@@ -1763,6 +1767,8 @@ type ApplicationRegistrationVariableDTO {
|
||||
isSecret: Boolean!
|
||||
isRequired: Boolean!
|
||||
isFilled: Boolean!
|
||||
type: String!
|
||||
options: JSON
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ export type Scalars = {
|
||||
UUID: string,
|
||||
DateTime: string,
|
||||
Boolean: boolean,
|
||||
Float: number,
|
||||
JSON: Record<string, unknown>,
|
||||
Float: number,
|
||||
Int: number,
|
||||
ConnectionCursor: any,
|
||||
JSONObject: any,
|
||||
@@ -32,6 +32,8 @@ export interface ApplicationRegistrationVariable {
|
||||
description: Scalars['String']
|
||||
isSecret: Scalars['Boolean']
|
||||
isRequired: Scalars['Boolean']
|
||||
type: Scalars['String']
|
||||
options?: Scalars['JSON']
|
||||
isFilled: Scalars['Boolean']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
@@ -246,6 +248,8 @@ export interface ApplicationVariable {
|
||||
value: Scalars['String']
|
||||
description: Scalars['String']
|
||||
isSecret: Scalars['Boolean']
|
||||
type: Scalars['String']
|
||||
options?: Scalars['JSON']
|
||||
__typename: 'ApplicationVariable'
|
||||
}
|
||||
|
||||
@@ -1399,6 +1403,8 @@ export interface ApplicationRegistrationVariableDTO {
|
||||
isSecret: Scalars['Boolean']
|
||||
isRequired: Scalars['Boolean']
|
||||
isFilled: Scalars['Boolean']
|
||||
type: Scalars['String']
|
||||
options?: Scalars['JSON']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'ApplicationRegistrationVariableDTO'
|
||||
@@ -3029,6 +3035,8 @@ export interface ApplicationRegistrationVariableGenqlSelection{
|
||||
description?: boolean | number
|
||||
isSecret?: boolean | number
|
||||
isRequired?: boolean | number
|
||||
type?: boolean | number
|
||||
options?: boolean | number
|
||||
isFilled?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
@@ -3238,6 +3246,8 @@ export interface ApplicationVariableGenqlSelection{
|
||||
value?: boolean | number
|
||||
description?: boolean | number
|
||||
isSecret?: boolean | number
|
||||
type?: boolean | number
|
||||
options?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
@@ -4441,6 +4451,8 @@ export interface ApplicationRegistrationVariableDTOGenqlSelection{
|
||||
isSecret?: boolean | number
|
||||
isRequired?: boolean | number
|
||||
isFilled?: boolean | number
|
||||
type?: boolean | number
|
||||
options?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,60 @@ Notes:
|
||||
- The default role is detected automatically from the role file marked with [`defineApplicationRole()`](/developers/extend/apps/config/roles) — you do not need to reference it from `defineApplication()`.
|
||||
- Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`.
|
||||
- Passing `defaultRoleUniversalIdentifier` explicitly is still supported for backward compatibility, but is deprecated in favor of `defineApplicationRole()`.
|
||||
- `serverVariables` are instance-scoped configuration and secrets (e.g. API keys). Unlike `applicationVariables`, they declare no value in the manifest — the workspace operator fills them in from the app's settings, and they are injected into logic functions only once set.
|
||||
|
||||
## Variable types
|
||||
|
||||
Both `applicationVariables` and `serverVariables` accept an optional `type` (and, for `SELECT` / `MULTI_SELECT`, an `options` list). Supported types: `TEXT` (default), `BOOLEAN`, `NUMBER`, `NUMERIC`, `DATE`, `DATE_TIME`, `SELECT`, `MULTI_SELECT`, `ARRAY`, `RAW_JSON`, `RICH_TEXT`.
|
||||
|
||||
```ts src/application-config.ts
|
||||
import { defineApplication, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
// ...identity, role...
|
||||
applicationVariables: {
|
||||
MAX_POSTCARDS: {
|
||||
universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713',
|
||||
description: 'Maximum postcards per batch',
|
||||
type: FieldType.NUMBER,
|
||||
value: 10,
|
||||
},
|
||||
DEFAULT_REGION: {
|
||||
universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227',
|
||||
description: 'Default shipping region',
|
||||
type: FieldType.SELECT,
|
||||
options: [
|
||||
{ label: 'Europe', value: 'eu' },
|
||||
{ label: 'United States', value: 'us' },
|
||||
],
|
||||
value: 'eu',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The `type` only affects **presentation and validation** — it selects the matching input in the workspace settings UI (a toggle, number field, dropdown, date picker, JSON editor, …) and lets the build validate your config (for example, `SELECT` / `MULTI_SELECT` must declare non-empty `options`). It does **not** change how the value reaches your code.
|
||||
|
||||
Values are **always injected as strings** — this is inherent to environment variables (`process.env.*` is string-only). When your logic function runs, the executor serializes each value by its declared `type` while building `process.env`, so the string format is consistent no matter how the value was set (manifest default, settings UI, or a previous version):
|
||||
|
||||
| Type | `process.env` string |
|
||||
|------|----------------------|
|
||||
| `TEXT`, `SELECT`, `DATE`, `DATE_TIME` | the raw value (`"eu"`, `"2026-01-01"`) |
|
||||
| `BOOLEAN` | `"true"` / `"false"` |
|
||||
| `NUMBER`, `NUMERIC` | decimal string (`"10"`, `"2.5"`) |
|
||||
| `MULTI_SELECT`, `ARRAY` | JSON array (`'["email","postcard"]'`) |
|
||||
| `RAW_JSON`, `RICH_TEXT` | JSON object (`'{"retries":3}'`) |
|
||||
|
||||
Parse the string back into the type you expect:
|
||||
|
||||
```ts
|
||||
const maxCards = Number(process.env.MAX_POSTCARDS); // "10" -> 10
|
||||
const enabled = process.env.ENABLE_TRACKING === 'true'; // "true" -> true
|
||||
const channels = JSON.parse(process.env.ENABLED_CHANNELS ?? '[]'); // '["email"]' -> ["email"]
|
||||
const config = JSON.parse(process.env.PROVIDER_CONFIG ?? '{}'); // '{"retries":3}' -> { retries: 3 }
|
||||
```
|
||||
|
||||
The same applies to front components reading values via `getApplicationVariable('VARIABLE_NAME')` — the returned value is a string; parse it as needed.
|
||||
|
||||
## Default function role
|
||||
|
||||
|
||||
@@ -377,6 +377,8 @@ export default defineFrontComponent({
|
||||
Secret variables (`isSecret: true`) are **not** exposed to front components. They are only available in [logic functions](/developers/extend/apps/logic/logic-functions), which run server-side. This prevents sensitive values like API keys from being sent to the browser.
|
||||
</Warning>
|
||||
|
||||
`getApplicationVariable` always returns a **string** (or `undefined`), regardless of the variable's declared `type`. The string is serialized consistently by type (booleans as `"true"` / `"false"`, numbers as decimal strings, arrays / objects as JSON), the same format used for logic-function `process.env` — parse it yourself (`Number(...)`, `JSON.parse(...)`, `=== 'true'`). See [Variable types](/developers/extend/apps/config/application#variable-types).
|
||||
|
||||
The following system variables are always available via `process.env`:
|
||||
|
||||
| Variable | Description |
|
||||
|
||||
@@ -232,6 +232,8 @@ export type ApplicationRegistrationVariableDto = {
|
||||
isRequired: Scalars['Boolean']['output'];
|
||||
isSecret: Scalars['Boolean']['output'];
|
||||
key: Scalars['String']['output'];
|
||||
options?: Maybe<Scalars['JSON']['output']>;
|
||||
type: Scalars['String']['output'];
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
value?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
@@ -1092,7 +1094,7 @@ export type FindAdminApplicationRegistrationVariablesQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type FindAdminApplicationRegistrationVariablesQuery = { __typename?: 'Query', findAdminApplicationRegistrationVariables: Array<{ __typename?: 'ApplicationRegistrationVariableDTO', id: string, key: string, value?: string | null, description: string, isSecret: boolean, isRequired: boolean, isFilled: boolean, createdAt: string, updatedAt: string }> };
|
||||
export type FindAdminApplicationRegistrationVariablesQuery = { __typename?: 'Query', findAdminApplicationRegistrationVariables: Array<{ __typename?: 'ApplicationRegistrationVariableDTO', id: string, key: string, value?: string | null, description: string, isSecret: boolean, isRequired: boolean, isFilled: boolean, type: string, options?: any | null, createdAt: string, updatedAt: string }> };
|
||||
|
||||
export type FindAllApplicationRegistrationsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@@ -1339,7 +1341,7 @@ export const UpdateAdminApplicationRegistrationDocument = {"kind":"Document","de
|
||||
export const UpdateAdminApplicationRegistrationVariableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateAdminApplicationRegistrationVariable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateApplicationRegistrationVariableInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateAdminApplicationRegistrationVariable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"isSecret"}},{"kind":"Field","name":{"kind":"Name","value":"isRequired"}},{"kind":"Field","name":{"kind":"Name","value":"isFilled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<UpdateAdminApplicationRegistrationVariableMutation, UpdateAdminApplicationRegistrationVariableMutationVariables>;
|
||||
export const FindAdminApplicationRegistrationInstalledWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAdminApplicationRegistrationInstalledWorkspaces"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FindApplicationRegistrationInstalledWorkspacesInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAdminApplicationRegistrationInstalledWorkspaces"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"hasMore"}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"version"}}]}}]}}]}}]} as unknown as DocumentNode<FindAdminApplicationRegistrationInstalledWorkspacesQuery, FindAdminApplicationRegistrationInstalledWorkspacesQueryVariables>;
|
||||
export const FindAdminApplicationRegistrationStatsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAdminApplicationRegistrationStats"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAdminApplicationRegistrationStats"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeInstalls"}},{"kind":"Field","name":{"kind":"Name","value":"mostInstalledVersion"}},{"kind":"Field","name":{"kind":"Name","value":"versionDistribution"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}}]}}]}}]} as unknown as DocumentNode<FindAdminApplicationRegistrationStatsQuery, FindAdminApplicationRegistrationStatsQueryVariables>;
|
||||
export const FindAdminApplicationRegistrationVariablesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAdminApplicationRegistrationVariables"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAdminApplicationRegistrationVariables"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"isSecret"}},{"kind":"Field","name":{"kind":"Name","value":"isRequired"}},{"kind":"Field","name":{"kind":"Name","value":"isFilled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<FindAdminApplicationRegistrationVariablesQuery, FindAdminApplicationRegistrationVariablesQueryVariables>;
|
||||
export const FindAdminApplicationRegistrationVariablesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAdminApplicationRegistrationVariables"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAdminApplicationRegistrationVariables"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"isSecret"}},{"kind":"Field","name":{"kind":"Name","value":"isRequired"}},{"kind":"Field","name":{"kind":"Name","value":"isFilled"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<FindAdminApplicationRegistrationVariablesQuery, FindAdminApplicationRegistrationVariablesQueryVariables>;
|
||||
export const FindAllApplicationRegistrationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAllApplicationRegistrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAllApplicationRegistrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"isPreInstalled"}},{"kind":"Field","name":{"kind":"Name","value":"isConfigured"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindAllApplicationRegistrationsQuery, FindAllApplicationRegistrationsQueryVariables>;
|
||||
export const CreateDatabaseConfigVariableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateDatabaseConfigVariable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"value"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createDatabaseConfigVariable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}},{"kind":"Argument","name":{"kind":"Name","value":"value"},"value":{"kind":"Variable","name":{"kind":"Name","value":"value"}}}]}]}}]} as unknown as DocumentNode<CreateDatabaseConfigVariableMutation, CreateDatabaseConfigVariableMutationVariables>;
|
||||
export const DeleteDatabaseConfigVariableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteDatabaseConfigVariable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteDatabaseConfigVariable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}]}]}}]} as unknown as DocumentNode<DeleteDatabaseConfigVariableMutation, DeleteDatabaseConfigVariableMutationVariables>;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -264,14 +264,6 @@ const SettingsAdminApplicationRegistrationDetail = lazy(() =>
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAdminApplicationRegistrationConfigVariableDetail = lazy(() =>
|
||||
import('~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationConfigVariableDetail').then(
|
||||
(module) => ({
|
||||
default: module.SettingsAdminApplicationRegistrationConfigVariableDetail,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAvailableApplicationDetails = lazy(() =>
|
||||
import('~/pages/settings/applications/SettingsAvailableApplicationDetails').then(
|
||||
(module) => ({
|
||||
@@ -288,14 +280,6 @@ const SettingsApplicationRegistrationDetails = lazy(() =>
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsApplicationRegistrationConfigVariableDetail = lazy(() =>
|
||||
import('~/pages/settings/applications/components/SettingsApplicationRegistrationConfigVariableDetail').then(
|
||||
(module) => ({
|
||||
default: module.SettingsApplicationRegistrationConfigVariableDetail,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAgentForm = lazy(() =>
|
||||
import('~/pages/settings/ai/SettingsAgentForm').then((module) => ({
|
||||
default: module.SettingsAgentForm,
|
||||
@@ -925,10 +909,6 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
|
||||
path={SettingsPath.ApplicationPageLayoutDetail}
|
||||
element={<SettingsLayoutPageLayoutDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ApplicationRegistrationConfigVariableDetails}
|
||||
element={<SettingsApplicationRegistrationConfigVariableDetail />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
@@ -1016,14 +996,6 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
|
||||
path={SettingsPath.AdminPanelApplicationRegistrationDetail}
|
||||
element={<SettingsAdminApplicationRegistrationDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={
|
||||
SettingsPath.AdminPanelApplicationRegistrationConfigVariableDetails
|
||||
}
|
||||
element={
|
||||
<SettingsAdminApplicationRegistrationConfigVariableDetail />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AdminPanelWorkspaceChatThread}
|
||||
element={<SettingsAdminWorkspaceChatThread />}
|
||||
|
||||
+2
@@ -31,6 +31,8 @@ export const APPLICATION_FRAGMENT = gql`
|
||||
value
|
||||
description
|
||||
isSecret
|
||||
type
|
||||
options
|
||||
}
|
||||
agents {
|
||||
...AgentFields
|
||||
|
||||
+5
@@ -9,6 +9,7 @@ import { UndoRedo } from '@tiptap/extensions/undo-redo';
|
||||
import { Slice } from '@tiptap/pm/model';
|
||||
|
||||
import { type Editor, useEditor } from '@tiptap/react';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined, parseJson } from 'twenty-shared/utils';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
@@ -131,5 +132,9 @@ export const useTextVariableEditor = ({
|
||||
injectCSS: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
editor?.setEditable(!readonly, false);
|
||||
}, [editor, readonly]);
|
||||
|
||||
return editor;
|
||||
};
|
||||
|
||||
+2
@@ -14,6 +14,8 @@ export const FIND_ADMIN_APPLICATION_REGISTRATION_VARIABLES = gql`
|
||||
isSecret
|
||||
isRequired
|
||||
isFilled
|
||||
type
|
||||
options
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { ConfigVariableEdit } from '@/settings/config-variables/components/ConfigVariableEdit';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
type VariableData = {
|
||||
id: string;
|
||||
key: string;
|
||||
value?: string | null;
|
||||
description: string;
|
||||
isFilled: boolean;
|
||||
};
|
||||
|
||||
export const ApplicationRegistrationConfigVariableEditForm = ({
|
||||
variable,
|
||||
onUpdateVariable,
|
||||
}: {
|
||||
variable: VariableData;
|
||||
onUpdateVariable: (
|
||||
id: string,
|
||||
update: { value: string; resetValue?: boolean },
|
||||
) => Promise<void>;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const [value, setValue] = useState<string>('');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const canOpenCancelModal = variable.isFilled && !isNonEmptyString(value);
|
||||
|
||||
const onCancel = () => {
|
||||
setValue('');
|
||||
};
|
||||
|
||||
const onSave = async () => {
|
||||
if (!isNonEmptyString(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await onUpdateVariable(variable.id, { value });
|
||||
} finally {
|
||||
setValue('');
|
||||
}
|
||||
};
|
||||
|
||||
const onConfirmReset = async () => {
|
||||
try {
|
||||
await onUpdateVariable(variable.id, { value: '', resetValue: true });
|
||||
} finally {
|
||||
setValue('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigVariableEdit
|
||||
title={variable.key}
|
||||
description={variable.description}
|
||||
input={
|
||||
<TextInput
|
||||
value={value}
|
||||
placeholder={!isEditing ? (variable.value ?? t`Enter a value`) : ''}
|
||||
onChange={setValue}
|
||||
disabled={!isEditing}
|
||||
fullWidth
|
||||
/>
|
||||
}
|
||||
isEditing={isEditing}
|
||||
setIsEditing={setIsEditing}
|
||||
isSaveDisabled={!isNonEmptyString(value)}
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
canOpenCancelModal={canOpenCancelModal}
|
||||
onConfirmReset={onConfirmReset}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+2
@@ -14,6 +14,8 @@ export const FIND_APPLICATION_REGISTRATION_VARIABLES = gql`
|
||||
isSecret
|
||||
isRequired
|
||||
isFilled
|
||||
type
|
||||
options
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import {
|
||||
FindAdminApplicationRegistrationVariablesDocument,
|
||||
FindOneAdminApplicationRegistrationDocument,
|
||||
UpdateAdminApplicationRegistrationVariableDocument,
|
||||
} from '~/generated-admin/graphql';
|
||||
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
|
||||
import { APPLICATION_REGISTRATION_ADMIN_PATH } from '@/settings/admin-panel/apps/constants/ApplicationRegistrationAdminPath';
|
||||
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
|
||||
import { NotFound } from '~/pages/not-found/NotFound';
|
||||
import { ApplicationRegistrationConfigVariableEditForm } from '@/settings/application-registrations/components/ApplicationRegistrationConfigVariableEditForm';
|
||||
|
||||
export const SettingsAdminApplicationRegistrationConfigVariableDetail = () => {
|
||||
const { t } = useLingui();
|
||||
const apolloAdminClient = useApolloAdminClient();
|
||||
|
||||
const { variableKey, applicationRegistrationId = '' } = useParams<{
|
||||
applicationRegistrationId: string;
|
||||
variableKey: string;
|
||||
}>();
|
||||
|
||||
const { data: applicationRegistrationData, loading: registrationLoading } =
|
||||
useQuery(FindOneAdminApplicationRegistrationDocument, {
|
||||
client: apolloAdminClient,
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
});
|
||||
|
||||
const registration =
|
||||
applicationRegistrationData?.findOneAdminApplicationRegistration;
|
||||
|
||||
const { data: variablesData, loading: variablesLoading } = useQuery(
|
||||
FindAdminApplicationRegistrationVariablesDocument,
|
||||
{
|
||||
client: apolloAdminClient,
|
||||
variables: { applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const variable = (
|
||||
variablesData?.findAdminApplicationRegistrationVariables ?? []
|
||||
).find((variable) => variable.key === variableKey);
|
||||
|
||||
const [updateVariable] = useMutation(
|
||||
UpdateAdminApplicationRegistrationVariableDocument,
|
||||
{
|
||||
client: apolloAdminClient,
|
||||
refetchQueries: [FindAdminApplicationRegistrationVariablesDocument],
|
||||
},
|
||||
);
|
||||
|
||||
if (registrationLoading || variablesLoading) {
|
||||
return <SettingsSkeletonLoader />;
|
||||
}
|
||||
|
||||
if (!variable || !registration) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
const onUpdateVariable = async (
|
||||
id: string,
|
||||
update: { value: string; resetValue?: boolean },
|
||||
) => {
|
||||
await updateVariable({
|
||||
variables: { input: { id, update } },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsPageLayout
|
||||
links={[
|
||||
{
|
||||
children: t`Other`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Admin Panel - Apps`,
|
||||
href: APPLICATION_REGISTRATION_ADMIN_PATH,
|
||||
},
|
||||
{
|
||||
children: t`${registration.name} - Config`,
|
||||
href: getSettingsPath(
|
||||
SettingsPath.AdminPanelApplicationRegistrationDetail,
|
||||
{ applicationRegistrationId },
|
||||
undefined,
|
||||
'config',
|
||||
),
|
||||
},
|
||||
{
|
||||
children: variableKey,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ApplicationRegistrationConfigVariableEditForm
|
||||
variable={variable}
|
||||
onUpdateVariable={onUpdateVariable}
|
||||
/>
|
||||
</SettingsPageLayout>
|
||||
);
|
||||
};
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
FindApplicationRegistrationVariablesDocument,
|
||||
FindOneApplicationRegistrationDocument,
|
||||
UpdateApplicationRegistrationVariableDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
|
||||
import { NotFound } from '~/pages/not-found/NotFound';
|
||||
import { ApplicationRegistrationConfigVariableEditForm } from '@/settings/application-registrations/components/ApplicationRegistrationConfigVariableEditForm';
|
||||
|
||||
export const SettingsApplicationRegistrationConfigVariableDetail = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const { variableKey, applicationRegistrationId = '' } = useParams<{
|
||||
applicationRegistrationId: string;
|
||||
variableKey: string;
|
||||
}>();
|
||||
|
||||
const { data: applicationRegistrationData, loading: registrationLoading } =
|
||||
useQuery(FindOneApplicationRegistrationDocument, {
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
});
|
||||
|
||||
const registration =
|
||||
applicationRegistrationData?.findOneApplicationRegistration;
|
||||
|
||||
const { data: variablesData, loading: variablesLoading } = useQuery(
|
||||
FindApplicationRegistrationVariablesDocument,
|
||||
{
|
||||
variables: { applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const variable = (
|
||||
variablesData?.findApplicationRegistrationVariables ?? []
|
||||
).find((variable) => variable.key === variableKey);
|
||||
|
||||
const [updateVariable] = useMutation(
|
||||
UpdateApplicationRegistrationVariableDocument,
|
||||
{
|
||||
refetchQueries: [FindApplicationRegistrationVariablesDocument],
|
||||
},
|
||||
);
|
||||
|
||||
if (registrationLoading || variablesLoading) {
|
||||
return <SettingsSkeletonLoader />;
|
||||
}
|
||||
|
||||
if (!variable || !registration) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
const onUpdateVariable = async (
|
||||
id: string,
|
||||
update: { value: string; resetValue?: boolean },
|
||||
) => {
|
||||
await updateVariable({
|
||||
variables: { input: { id, update } },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsPageLayout
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.General),
|
||||
},
|
||||
{
|
||||
children: t`Applications - Developer`,
|
||||
href: getSettingsPath(
|
||||
SettingsPath.Applications,
|
||||
undefined,
|
||||
undefined,
|
||||
'developer',
|
||||
),
|
||||
},
|
||||
{
|
||||
children: t`${registration.name} - Config`,
|
||||
href: getSettingsPath(
|
||||
SettingsPath.ApplicationRegistrationDetail,
|
||||
{ applicationRegistrationId },
|
||||
undefined,
|
||||
'config',
|
||||
),
|
||||
},
|
||||
{
|
||||
children: variableKey,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ApplicationRegistrationConfigVariableEditForm
|
||||
variable={variable}
|
||||
onUpdateVariable={onUpdateVariable}
|
||||
/>
|
||||
</SettingsPageLayout>
|
||||
);
|
||||
};
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
import { FormArrayFieldInput } from '@/object-record/record-field/ui/form-types/components/FormArrayFieldInput';
|
||||
import { FormBooleanFieldInput } from '@/object-record/record-field/ui/form-types/components/FormBooleanFieldInput';
|
||||
import { FormDateFieldInput } from '@/object-record/record-field/ui/form-types/components/FormDateFieldInput';
|
||||
import { FormDateTimeFieldInput } from '@/object-record/record-field/ui/form-types/components/FormDateTimeFieldInput';
|
||||
import { FormMultiSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput';
|
||||
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
|
||||
import { FormRawJsonFieldInput } from '@/object-record/record-field/ui/form-types/components/FormRawJsonFieldInput';
|
||||
import { FormRichTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormRichTextFieldInput';
|
||||
import { FormSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormSelectFieldInput';
|
||||
import { type FieldRichTextValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
type ApplicationVariableOption,
|
||||
deserializeApplicationVariableValue,
|
||||
} from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
|
||||
type SettingsApplicationVariableInputProps = {
|
||||
type?: string | null;
|
||||
value: string;
|
||||
options?: ApplicationVariableOption[] | null;
|
||||
onChange: (serializedValue: string) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const toSelectOptions = (
|
||||
options: ApplicationVariableOption[] | null | undefined,
|
||||
): SelectOption[] =>
|
||||
(options ?? []).map((option) => ({
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
}));
|
||||
|
||||
const parseStringArray = (value: string): string[] => {
|
||||
const parsed = deserializeApplicationVariableValue(
|
||||
value,
|
||||
FieldMetadataType.MULTI_SELECT,
|
||||
);
|
||||
|
||||
return Array.isArray(parsed) ? (parsed as string[]) : [];
|
||||
};
|
||||
|
||||
const parseRichTextValue = (value: string): FieldRichTextValue => {
|
||||
try {
|
||||
const parsed = JSON.parse(value) as Partial<FieldRichTextValue>;
|
||||
|
||||
if (isDefined(parsed) && typeof parsed === 'object') {
|
||||
const blocknote =
|
||||
typeof parsed.blocknote === 'string' ? parsed.blocknote : null;
|
||||
const markdown =
|
||||
typeof parsed.markdown === 'string' ? parsed.markdown : null;
|
||||
|
||||
return { blocknote, markdown };
|
||||
}
|
||||
} catch {
|
||||
return { blocknote: null, markdown: value === '' ? null : value };
|
||||
}
|
||||
|
||||
return { blocknote: null, markdown: value === '' ? null : value };
|
||||
};
|
||||
|
||||
export const SettingsApplicationVariableInput = ({
|
||||
type,
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled,
|
||||
}: SettingsApplicationVariableInputProps) => {
|
||||
const fieldType = (type as FieldMetadataType) ?? FieldMetadataType.TEXT;
|
||||
|
||||
switch (fieldType) {
|
||||
case FieldMetadataType.BOOLEAN:
|
||||
return (
|
||||
<FormBooleanFieldInput
|
||||
defaultValue={value === '' ? undefined : value === 'true'}
|
||||
onChange={(newValue) =>
|
||||
onChange(
|
||||
newValue === true ? 'true' : newValue === false ? 'false' : '',
|
||||
)
|
||||
}
|
||||
readonly={disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
case FieldMetadataType.NUMBER:
|
||||
case FieldMetadataType.NUMERIC:
|
||||
return (
|
||||
<FormNumberFieldInput
|
||||
defaultValue={value}
|
||||
onChange={(newValue) =>
|
||||
onChange(isDefined(newValue) ? String(newValue) : '')
|
||||
}
|
||||
placeholder={placeholder}
|
||||
readonly={disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
case FieldMetadataType.DATE:
|
||||
return (
|
||||
<FormDateFieldInput
|
||||
defaultValue={value === '' ? undefined : value}
|
||||
onChange={(newValue) => onChange(newValue ?? '')}
|
||||
placeholder={placeholder}
|
||||
readonly={disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
case FieldMetadataType.DATE_TIME:
|
||||
return (
|
||||
<FormDateTimeFieldInput
|
||||
defaultValue={value === '' ? undefined : value}
|
||||
onChange={(newValue) => onChange(newValue ?? '')}
|
||||
placeholder={placeholder}
|
||||
readonly={disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
case FieldMetadataType.SELECT:
|
||||
return (
|
||||
<FormSelectFieldInput
|
||||
defaultValue={value === '' ? undefined : value}
|
||||
options={toSelectOptions(options)}
|
||||
onChange={(newValue) => onChange(newValue ?? '')}
|
||||
readonly={disabled}
|
||||
isNullable
|
||||
/>
|
||||
);
|
||||
|
||||
case FieldMetadataType.MULTI_SELECT:
|
||||
return (
|
||||
<FormMultiSelectFieldInput
|
||||
defaultValue={parseStringArray(value)}
|
||||
options={toSelectOptions(options)}
|
||||
onChange={(newValue) =>
|
||||
onChange(Array.isArray(newValue) ? JSON.stringify(newValue) : '')
|
||||
}
|
||||
placeholder={placeholder}
|
||||
readonly={disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
case FieldMetadataType.ARRAY:
|
||||
return (
|
||||
<FormArrayFieldInput
|
||||
defaultValue={parseStringArray(value)}
|
||||
onChange={(newValue) =>
|
||||
onChange(Array.isArray(newValue) ? JSON.stringify(newValue) : '')
|
||||
}
|
||||
placeholder={placeholder}
|
||||
readonly={disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
case FieldMetadataType.RAW_JSON:
|
||||
return (
|
||||
<FormRawJsonFieldInput
|
||||
defaultValue={value === '' ? null : value}
|
||||
onChange={(newValue) => onChange(newValue ?? '')}
|
||||
placeholder={placeholder}
|
||||
readonly={disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
return (
|
||||
<FormRichTextFieldInput
|
||||
defaultValue={parseRichTextValue(value)}
|
||||
onChange={(newValue) => onChange(JSON.stringify(newValue))}
|
||||
placeholder={placeholder}
|
||||
readonly={disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<TextInput
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder ?? t`Value`}
|
||||
readOnly={disabled}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
+1
@@ -48,6 +48,7 @@ const buildApplication = (variableValue: string): Application => ({
|
||||
value: variableValue,
|
||||
description: '',
|
||||
isSecret: false,
|
||||
type: 'TEXT',
|
||||
},
|
||||
],
|
||||
agents: [],
|
||||
|
||||
+10
-3
@@ -1,8 +1,8 @@
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useContext, useState } from 'react';
|
||||
import { type ApplicationVariableOption } from 'twenty-shared/application';
|
||||
import { IconInfoCircle } from 'twenty-ui/icon';
|
||||
import { AppTooltip, TooltipDelay } from 'twenty-ui/surfaces';
|
||||
import { H2Title } from 'twenty-ui/typography';
|
||||
@@ -10,6 +10,7 @@ import { Section } from 'twenty-ui/layout';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { type ApplicationVariable } from '~/generated-metadata/graphql';
|
||||
import { SettingsApplicationVariableInput } from '~/pages/settings/applications/components/SettingsApplicationVariableInput';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -80,8 +81,15 @@ export const SettingsApplicationDetailEnvironmentVariablesTable = ({
|
||||
</>
|
||||
)}
|
||||
</StyledLabelRow>
|
||||
<TextInput
|
||||
<SettingsApplicationVariableInput
|
||||
type={editedEnvVariable.type}
|
||||
value={editedEnvVariable.value}
|
||||
options={
|
||||
editedEnvVariable.options as
|
||||
| ApplicationVariableOption[]
|
||||
| null
|
||||
| undefined
|
||||
}
|
||||
onChange={(newValue) => {
|
||||
setEditedEnvVariables((prevState) =>
|
||||
prevState.map((val) => {
|
||||
@@ -94,7 +102,6 @@ export const SettingsApplicationDetailEnvironmentVariablesTable = ({
|
||||
onUpdateDebounced({ ...editedEnvVariable, value: newValue });
|
||||
}}
|
||||
placeholder={t`Value`}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+143
-22
@@ -1,16 +1,90 @@
|
||||
import type { ApplicationRegistrationData } from '~/pages/settings/applications/tabs/types/ApplicationRegistrationData';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { FindApplicationRegistrationVariablesDocument } from '~/generated-metadata/graphql';
|
||||
import { FindAdminApplicationRegistrationVariablesDocument } from '~/generated-admin/graphql';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import {
|
||||
FindApplicationRegistrationVariablesDocument,
|
||||
UpdateApplicationRegistrationVariableDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import {
|
||||
FindAdminApplicationRegistrationVariablesDocument,
|
||||
UpdateAdminApplicationRegistrationVariableDocument,
|
||||
} from '~/generated-admin/graphql';
|
||||
import { styled } from '@linaria/react';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { Status } from 'twenty-ui/data-display';
|
||||
import { H2Title } from 'twenty-ui/typography';
|
||||
import { IconInfoCircle } from 'twenty-ui/icon';
|
||||
import { AppTooltip, TooltipDelay } from 'twenty-ui/surfaces';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { ConfigVariableTable } from '@/settings/config-variables/components/ConfigVariableTable';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useContext, useState } from 'react';
|
||||
import { type ApplicationVariableOption } from 'twenty-shared/application';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { SettingsApplicationVariableInput } from '~/pages/settings/applications/components/SettingsApplicationVariableInput';
|
||||
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
|
||||
|
||||
type ConfigVariable = {
|
||||
id: string;
|
||||
key: string;
|
||||
value?: string | null;
|
||||
type?: string | null;
|
||||
options?: ApplicationVariableOption[] | null;
|
||||
isSecret?: boolean | null;
|
||||
isFilled?: boolean | null;
|
||||
};
|
||||
|
||||
const ConfigVariableInput = ({
|
||||
variable,
|
||||
onUpdate,
|
||||
}: {
|
||||
variable: ConfigVariable;
|
||||
onUpdate: (id: string, value: string) => void;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const isSecretFilled =
|
||||
variable.isSecret === true && variable.isFilled === true;
|
||||
|
||||
const [value, setValue] = useState(
|
||||
isSecretFilled ? '' : (variable.value ?? ''),
|
||||
);
|
||||
|
||||
const onUpdateDebounced = useDebouncedCallback((newValue: string) => {
|
||||
onUpdate(variable.id, newValue);
|
||||
}, 250);
|
||||
|
||||
return (
|
||||
<SettingsApplicationVariableInput
|
||||
type={variable.type}
|
||||
value={value}
|
||||
options={variable.options}
|
||||
onChange={(newValue) => {
|
||||
setValue(newValue);
|
||||
onUpdateDebounced(newValue);
|
||||
}}
|
||||
placeholder={isSecretFilled ? (variable.value ?? undefined) : t`Value`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledLabelRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
margin-bottom: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
font-size: 11px;
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
export const SettingsApplicationRegistrationConfigTab = ({
|
||||
registration,
|
||||
fromAdmin,
|
||||
@@ -19,6 +93,7 @@ export const SettingsApplicationRegistrationConfigTab = ({
|
||||
fromAdmin?: boolean;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const apolloAdminClient = useApolloAdminClient();
|
||||
|
||||
const applicationRegistrationId = registration.id;
|
||||
@@ -40,24 +115,36 @@ export const SettingsApplicationRegistrationConfigTab = ({
|
||||
},
|
||||
);
|
||||
|
||||
const [updateWorkspaceVariable] = useMutation(
|
||||
UpdateApplicationRegistrationVariableDocument,
|
||||
{
|
||||
refetchQueries: [FindApplicationRegistrationVariablesDocument],
|
||||
},
|
||||
);
|
||||
|
||||
const [updateAdminVariable] = useMutation(
|
||||
UpdateAdminApplicationRegistrationVariableDocument,
|
||||
{
|
||||
client: apolloAdminClient,
|
||||
refetchQueries: [FindAdminApplicationRegistrationVariablesDocument],
|
||||
},
|
||||
);
|
||||
|
||||
const variables = fromAdmin
|
||||
? (adminVariablesData?.findAdminApplicationRegistrationVariables ?? [])
|
||||
: (workspaceVariablesData?.findApplicationRegistrationVariables ?? []);
|
||||
|
||||
const configVariables = variables.map((variable) => ({
|
||||
name: variable.key,
|
||||
description: variable.description,
|
||||
value: variable.value ?? <Status color="gray" text={t`Not set`} />,
|
||||
to: getSettingsPath(
|
||||
fromAdmin
|
||||
? SettingsPath.AdminPanelApplicationRegistrationConfigVariableDetails
|
||||
: SettingsPath.ApplicationRegistrationConfigVariableDetails,
|
||||
{
|
||||
applicationRegistrationId,
|
||||
variableKey: variable.key,
|
||||
},
|
||||
),
|
||||
}));
|
||||
const handleUpdate = (id: string, value: string) => {
|
||||
if (fromAdmin === true) {
|
||||
updateAdminVariable({
|
||||
variables: { input: { id, update: { value } } },
|
||||
});
|
||||
} else {
|
||||
updateWorkspaceVariable({
|
||||
variables: { input: { id, update: { value } } },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
variables.length > 0 && (
|
||||
@@ -66,7 +153,41 @@ export const SettingsApplicationRegistrationConfigTab = ({
|
||||
title={t`Server Variables`}
|
||||
description={t`Server variables are applied to all workspace installations.`}
|
||||
/>
|
||||
<ConfigVariableTable configVariables={configVariables} />
|
||||
<StyledContainer>
|
||||
{variables.map((variable) => {
|
||||
const tooltipId = `config-var-desc-${variable.key}`;
|
||||
return (
|
||||
<div key={variable.key}>
|
||||
<StyledLabelRow>
|
||||
<StyledLabel>{variable.key}</StyledLabel>
|
||||
{isNonEmptyString(variable.description) && (
|
||||
<>
|
||||
<IconInfoCircle
|
||||
id={tooltipId}
|
||||
size={theme.icon.size.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
style={{ outline: 'none', cursor: 'pointer' }}
|
||||
/>
|
||||
<AppTooltip
|
||||
anchorSelect={`#${tooltipId}`}
|
||||
content={variable.description}
|
||||
offset={5}
|
||||
noArrow
|
||||
place="bottom"
|
||||
positionStrategy="fixed"
|
||||
delay={TooltipDelay.shortDelay}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</StyledLabelRow>
|
||||
<ConfigVariableInput
|
||||
variable={variable as ConfigVariable}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</StyledContainer>
|
||||
</Section>
|
||||
)
|
||||
);
|
||||
|
||||
+146
@@ -55,6 +55,82 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
value: 'Alex Karp',
|
||||
},
|
||||
GREETING_TEXT: {
|
||||
universalIdentifier: 'ad19edc5-4cc5-4003-a996-aef53a5c8de0',
|
||||
description: 'Free text shown on the postcard',
|
||||
type: FieldMetadataType.TEXT,
|
||||
value: 'Hello from Rich App',
|
||||
},
|
||||
ENABLE_TRACKING: {
|
||||
universalIdentifier: 'b9c58bb2-58c3-4c6c-9877-498ea6c03fde',
|
||||
description: 'Toggle delivery tracking',
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
value: true,
|
||||
},
|
||||
MAX_POSTCARDS: {
|
||||
universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713',
|
||||
description: 'Maximum postcards per batch',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
value: 10,
|
||||
},
|
||||
DISCOUNT_RATE: {
|
||||
universalIdentifier: 'd32f810f-06bb-4d29-b0ee-1dc4228f7cb8',
|
||||
description: 'Bulk discount rate applied at checkout',
|
||||
type: FieldMetadataType.NUMERIC,
|
||||
value: 2.5,
|
||||
},
|
||||
CAMPAIGN_START_DATE: {
|
||||
universalIdentifier: '5aa4fcec-e8a3-4bc1-9c7f-762e1f9dfb40',
|
||||
description: 'Date the campaign starts',
|
||||
type: FieldMetadataType.DATE,
|
||||
value: '2026-01-01',
|
||||
},
|
||||
CAMPAIGN_START_AT: {
|
||||
universalIdentifier: '273d0cfd-d6ab-4148-8816-c4a5d4b1d600',
|
||||
description: 'Exact moment the campaign starts',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
value: '2026-01-01T09:00:00.000Z',
|
||||
},
|
||||
DEFAULT_REGION: {
|
||||
universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227',
|
||||
description: 'Default shipping region',
|
||||
type: FieldMetadataType.SELECT,
|
||||
options: [
|
||||
{ label: 'Europe', value: 'eu' },
|
||||
{ label: 'United States', value: 'us' },
|
||||
{ label: 'Asia-Pacific', value: 'apac' },
|
||||
],
|
||||
value: 'eu',
|
||||
},
|
||||
ENABLED_CHANNELS: {
|
||||
universalIdentifier: '706a2b08-8284-4715-8bc0-922a99cb26af',
|
||||
description: 'Channels the app is allowed to use',
|
||||
type: FieldMetadataType.MULTI_SELECT,
|
||||
options: [
|
||||
{ label: 'Email', value: 'email' },
|
||||
{ label: 'SMS', value: 'sms' },
|
||||
{ label: 'Postcard', value: 'postcard' },
|
||||
],
|
||||
value: ['email', 'postcard'],
|
||||
},
|
||||
ALLOWED_TAGS: {
|
||||
universalIdentifier: 'c1c8a4c9-9130-4ab8-8dce-18d2b50879ed',
|
||||
description: 'Free-form tags applied to recipients',
|
||||
type: FieldMetadataType.ARRAY,
|
||||
value: ['vip', 'returning'],
|
||||
},
|
||||
PROVIDER_CONFIG: {
|
||||
universalIdentifier: '183d5285-c70c-4f29-96e0-c68659fbe5ae',
|
||||
description: 'Raw JSON configuration for the printing provider',
|
||||
type: FieldMetadataType.RAW_JSON,
|
||||
value: { retries: 3, timeoutMs: 5000 },
|
||||
},
|
||||
WELCOME_MESSAGE: {
|
||||
universalIdentifier: '25a66ff5-8458-498e-9e7e-33ea458a6f3c',
|
||||
description: 'Rich text welcome message',
|
||||
type: FieldMetadataType.RICH_TEXT,
|
||||
value: { blocknote: null, markdown: 'Welcome to **Rich App**!' },
|
||||
},
|
||||
},
|
||||
serverVariables: {
|
||||
POSTCARD_API_KEY: {
|
||||
@@ -67,6 +143,76 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_DELIVERY_SPEED: {
|
||||
description: 'Delivery speed requested from the provider',
|
||||
type: FieldMetadataType.SELECT,
|
||||
options: [
|
||||
{ label: 'Standard', value: 'standard' },
|
||||
{ label: 'Express', value: 'express' },
|
||||
],
|
||||
isSecret: false,
|
||||
isRequired: true,
|
||||
},
|
||||
POSTCARD_DAILY_LIMIT: {
|
||||
description: 'Maximum postcards the provider will accept per day',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_UNIT_PRICE: {
|
||||
description: 'Price charged by the provider per postcard',
|
||||
type: FieldMetadataType.NUMERIC,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_SANDBOX_MODE: {
|
||||
description:
|
||||
'Send postcards through the provider sandbox instead of production',
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_CONTRACT_START_DATE: {
|
||||
description: 'Date the provider contract starts',
|
||||
type: FieldMetadataType.DATE,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_CONTRACT_RENEWAL_AT: {
|
||||
description: 'Exact moment the provider contract renews',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_ENABLED_REGIONS: {
|
||||
description: 'Regions the provider is allowed to ship to',
|
||||
type: FieldMetadataType.MULTI_SELECT,
|
||||
options: [
|
||||
{ label: 'Europe', value: 'eu' },
|
||||
{ label: 'United States', value: 'us' },
|
||||
{ label: 'Asia-Pacific', value: 'apac' },
|
||||
],
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_WEBHOOK_EVENTS: {
|
||||
description: 'Provider webhook events the app subscribes to',
|
||||
type: FieldMetadataType.ARRAY,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_PROVIDER_CONFIG: {
|
||||
description: 'Raw JSON configuration for the printing provider',
|
||||
type: FieldMetadataType.RAW_JSON,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
POSTCARD_INVOICE_NOTE: {
|
||||
description: 'Rich text note appended to provider invoices',
|
||||
type: FieldMetadataType.RICH_TEXT,
|
||||
isSecret: false,
|
||||
isRequired: false,
|
||||
},
|
||||
},
|
||||
description: 'A simple rich app',
|
||||
displayName: 'Rich App',
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isNonEmptyArray } from 'twenty-shared/utils';
|
||||
|
||||
import { type ApplicationConfig } from '@/sdk/define/application/application-config';
|
||||
import { type DefineEntity } from '@/sdk/define/common/types/define-entity.type';
|
||||
import { createValidationResult } from '@/sdk/define/common/utils/create-validation-result';
|
||||
@@ -14,6 +17,20 @@ export const defineApplication: DefineEntity<ApplicationConfig> = (config) => {
|
||||
errors.push('Application must have a non empty display name');
|
||||
}
|
||||
|
||||
for (const [variableName, variable] of Object.entries(
|
||||
config.applicationVariables ?? {},
|
||||
)) {
|
||||
const requiresOptions =
|
||||
variable.type === FieldMetadataType.SELECT ||
|
||||
variable.type === FieldMetadataType.MULTI_SELECT;
|
||||
|
||||
if (requiresOptions && !isNonEmptyArray(variable.options)) {
|
||||
errors.push(
|
||||
`Application variable "${variableName}" of type ${variable.type} must define non-empty options`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.defaultRoleUniversalIdentifier) {
|
||||
warnings.push(
|
||||
'`defaultRoleUniversalIdentifier` on defineApplication() is deprecated. Use defineApplicationRole() in your role file instead.',
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.19.0', 1783065514000)
|
||||
export class AddTypeAndOptionsToApplicationVariablesFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationVariable" ADD COLUMN IF NOT EXISTS "type" text NOT NULL DEFAULT 'TEXT'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationVariable" ADD COLUMN IF NOT EXISTS "options" jsonb`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistrationVariable" ADD COLUMN IF NOT EXISTS "type" text NOT NULL DEFAULT 'TEXT'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistrationVariable" ADD COLUMN IF NOT EXISTS "options" jsonb`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistrationVariable" DROP COLUMN IF EXISTS "options"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistrationVariable" DROP COLUMN IF EXISTS "type"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationVariable" DROP COLUMN IF EXISTS "options"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationVariable" DROP COLUMN IF EXISTS "type"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const ADD_TYPE_AND_OPTIONS_TO_APPLICATION_VARIABLES_UPGRADE_COMMAND_NAME =
|
||||
'2.19.0_AddTypeAndOptionsToApplicationVariablesFastInstanceCommand_1783065514000';
|
||||
+2
@@ -44,6 +44,7 @@ import { BackfillTsVectorFieldMetadataIdOnSearchFieldMetadataSlowInstanceCommand
|
||||
import { AddMetadataOverridesColumnFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-instance-command-fast-1782986475000-add-metadata-overrides-column';
|
||||
import { AddLastStreamErrorToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-instance-command-fast-1782996657000-add-last-stream-error-to-agent-chat-thread';
|
||||
import { BackfillMetadataOverridesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-instance-command-slow-1782986476000-backfill-metadata-overrides';
|
||||
import { AddTypeAndOptionsToApplicationVariablesFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-instance-command-fast-1783065514000-add-type-and-options-to-application-variables';
|
||||
import { AddCacheTokensToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777455269302-add-cache-tokens-to-agent-chat-thread';
|
||||
import { AddLogoToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777539664664-add-logo-to-application';
|
||||
import { AddSubFieldNameToViewSortEarlyFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1747234200000-add-sub-field-name-to-view-sort';
|
||||
@@ -182,4 +183,5 @@ export const INSTANCE_COMMANDS = [
|
||||
BackfillMetadataOverridesSlowInstanceCommand,
|
||||
AddLastStreamErrorToAgentChatThreadFastInstanceCommand,
|
||||
DropMetadataStandardOverridesColumnFastInstanceCommand,
|
||||
AddTypeAndOptionsToApplicationVariablesFastInstanceCommand,
|
||||
];
|
||||
|
||||
+12
@@ -1,3 +1,9 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
type ApplicationVariableOption,
|
||||
type ApplicationVariableType,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
import { type EncryptedString } from 'src/engine/core-modules/secret-encryption/branded-strings/encrypted-string.type';
|
||||
import { type UniversalFlatApplicationVariable } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-application-variable.type';
|
||||
|
||||
@@ -8,6 +14,8 @@ export const fromApplicationVariableManifestToUniversalFlatApplicationVariable =
|
||||
description,
|
||||
encryptedValue,
|
||||
isSecret,
|
||||
type,
|
||||
options,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
}: {
|
||||
@@ -16,6 +24,8 @@ export const fromApplicationVariableManifestToUniversalFlatApplicationVariable =
|
||||
description?: string;
|
||||
encryptedValue: EncryptedString | '';
|
||||
isSecret?: boolean;
|
||||
type?: ApplicationVariableType;
|
||||
options?: ApplicationVariableOption[];
|
||||
applicationUniversalIdentifier: string;
|
||||
now: string;
|
||||
}): UniversalFlatApplicationVariable => {
|
||||
@@ -26,6 +36,8 @@ export const fromApplicationVariableManifestToUniversalFlatApplicationVariable =
|
||||
value: encryptedValue,
|
||||
description: description ?? '',
|
||||
isSecret: isSecret ?? false,
|
||||
type: type ?? FieldMetadataType.TEXT,
|
||||
options: options ?? null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
+15
-4
@@ -1,7 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import {
|
||||
type Manifest,
|
||||
serializeApplicationVariableValue,
|
||||
} from 'twenty-shared/application';
|
||||
import { MAX_CUSTOM_INDEXES_PER_OBJECT } from 'twenty-shared/constants';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { fromApplicationVariableManifestToUniversalFlatApplicationVariable } from 'src/engine/core-modules/application/application-manifest/converters/from-application-variable-manifest-to-universal-flat-application-variable.util';
|
||||
@@ -604,13 +608,18 @@ export class ComputeApplicationManifestAllUniversalFlatEntityMapsService {
|
||||
for (const [key, applicationVariableManifest] of Object.entries(
|
||||
manifest.application.applicationVariables ?? {},
|
||||
)) {
|
||||
const type = applicationVariableManifest.type ?? FieldMetadataType.TEXT;
|
||||
|
||||
const plaintextValue =
|
||||
'value' in applicationVariableManifest
|
||||
? applicationVariableManifest.value
|
||||
: undefined;
|
||||
? serializeApplicationVariableValue(
|
||||
applicationVariableManifest.value,
|
||||
type,
|
||||
)
|
||||
: '';
|
||||
|
||||
const isSecret = applicationVariableManifest.isSecret;
|
||||
const rawValue = isSecret ? '' : (plaintextValue ?? '');
|
||||
const rawValue = isSecret ? '' : plaintextValue;
|
||||
|
||||
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
|
||||
universalFlatEntity:
|
||||
@@ -624,6 +633,8 @@ export class ComputeApplicationManifestAllUniversalFlatEntityMapsService {
|
||||
),
|
||||
description: applicationVariableManifest.description,
|
||||
isSecret,
|
||||
type,
|
||||
options: applicationVariableManifest.options,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
}),
|
||||
|
||||
+15
@@ -15,6 +15,13 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
type ApplicationVariableOption,
|
||||
type ApplicationVariableType,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { type EncryptedString } from 'src/engine/core-modules/secret-encryption/branded-strings/encrypted-string.type';
|
||||
@@ -57,6 +64,14 @@ export class ApplicationRegistrationVariableEntity {
|
||||
@Column({ nullable: false, type: 'boolean', default: false })
|
||||
isRequired: boolean;
|
||||
|
||||
@Field(() => String)
|
||||
@Column({ nullable: false, type: 'text', default: FieldMetadataType.TEXT })
|
||||
type: ApplicationVariableType;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@Column({ nullable: true, type: 'jsonb', default: null })
|
||||
options: ApplicationVariableOption[] | null;
|
||||
|
||||
@Field()
|
||||
get isFilled(): boolean {
|
||||
return this.encryptedValue !== '';
|
||||
|
||||
+5
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { type ServerVariables } from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Not, type Repository } from 'typeorm';
|
||||
|
||||
@@ -136,6 +137,8 @@ export class ApplicationRegistrationVariableService {
|
||||
description: schema.description ?? '',
|
||||
isSecret: schema.isSecret ?? true,
|
||||
isRequired: schema.isRequired ?? false,
|
||||
type: schema.type ?? FieldMetadataType.TEXT,
|
||||
options: schema.options ?? null,
|
||||
});
|
||||
} else {
|
||||
await this.variableRepository.save(
|
||||
@@ -146,6 +149,8 @@ export class ApplicationRegistrationVariableService {
|
||||
description: schema.description ?? '',
|
||||
isSecret: schema.isSecret ?? true,
|
||||
isRequired: schema.isRequired ?? false,
|
||||
type: schema.type ?? FieldMetadataType.TEXT,
|
||||
options: schema.options ?? null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
+11
-1
@@ -1,7 +1,9 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { IsBoolean, IsString } from 'class-validator';
|
||||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { type ApplicationVariableOption } from 'twenty-shared/application';
|
||||
|
||||
@ObjectType()
|
||||
export class ApplicationRegistrationVariableDTO {
|
||||
@@ -32,6 +34,14 @@ export class ApplicationRegistrationVariableDTO {
|
||||
@Field()
|
||||
isFilled: boolean;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
type: string;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
options?: ApplicationVariableOption[] | null;
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@ import semver from 'semver';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
@@ -210,7 +211,7 @@ export class ApplicationTarballService {
|
||||
isListed: false,
|
||||
isFeatured: false,
|
||||
ownerWorkspaceId: params.ownerWorkspaceId,
|
||||
});
|
||||
} as QueryDeepPartialEntity<ApplicationRegistrationEntity>);
|
||||
|
||||
if (manifest.application?.serverVariables) {
|
||||
await this.applicationRegistrationVariableService.syncVariableSchemas(
|
||||
|
||||
+22
@@ -10,8 +10,16 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
type ApplicationVariableOption,
|
||||
type ApplicationVariableType,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
import { ADD_TYPE_AND_OPTIONS_TO_APPLICATION_VARIABLES_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-19/add-type-and-options-to-application-variables-upgrade-command-name.constant';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { type EncryptedString } from 'src/engine/core-modules/secret-encryption/branded-strings/encrypted-string.type';
|
||||
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
@Entity({
|
||||
@@ -42,6 +50,20 @@ export class ApplicationVariableEntity extends SyncableEntity {
|
||||
@Column({ nullable: false, type: 'boolean', default: false })
|
||||
isSecret: boolean;
|
||||
|
||||
@WasIntroducedInUpgrade({
|
||||
upgradeCommandName:
|
||||
ADD_TYPE_AND_OPTIONS_TO_APPLICATION_VARIABLES_UPGRADE_COMMAND_NAME,
|
||||
})
|
||||
@Column({ nullable: false, type: 'text', default: FieldMetadataType.TEXT })
|
||||
type: ApplicationVariableType;
|
||||
|
||||
@WasIntroducedInUpgrade({
|
||||
upgradeCommandName:
|
||||
ADD_TYPE_AND_OPTIONS_TO_APPLICATION_VARIABLES_UPGRADE_COMMAND_NAME,
|
||||
})
|
||||
@Column({ nullable: true, type: 'jsonb', default: null })
|
||||
options: ApplicationVariableOption[] | null;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
+11
-1
@@ -1,7 +1,9 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsBoolean, IsString } from 'class-validator';
|
||||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import { type ApplicationVariableOption } from 'twenty-shared/application';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@@ -25,4 +27,12 @@ export class ApplicationVariableEntityDTO {
|
||||
@IsBoolean()
|
||||
@Field()
|
||||
isSecret: boolean;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
type: string;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
options?: ApplicationVariableOption[] | null;
|
||||
}
|
||||
|
||||
+18
@@ -1,3 +1,5 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type EncryptedString } from 'src/engine/core-modules/secret-encryption/branded-strings/encrypted-string.type';
|
||||
import { buildEnvVar } from 'src/engine/core-modules/logic-function/logic-function-executor/utils/build-env-var';
|
||||
import { type SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
|
||||
@@ -37,6 +39,8 @@ describe('buildEnvVar', () => {
|
||||
`enc:v2:deadbeef:https://example.com|${workspaceA}` as EncryptedString,
|
||||
description: 'Public URL',
|
||||
isSecret: false,
|
||||
type: FieldMetadataType.TEXT,
|
||||
options: null,
|
||||
applicationId: 'app-1',
|
||||
workspaceId: workspaceA,
|
||||
universalIdentifier: '00000000-0000-0000-0000-000000000000',
|
||||
@@ -50,6 +54,8 @@ describe('buildEnvVar', () => {
|
||||
value: `enc:v2:deadbeef:secret-123|${workspaceA}` as EncryptedString,
|
||||
description: 'API secret',
|
||||
isSecret: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
options: null,
|
||||
applicationId: 'app-1',
|
||||
workspaceId: workspaceA,
|
||||
universalIdentifier: '00000000-0000-0000-0000-000000000000',
|
||||
@@ -63,6 +69,8 @@ describe('buildEnvVar', () => {
|
||||
value: `enc:v2:deadbeef:true|${workspaceA}` as EncryptedString,
|
||||
description: 'Debug flag',
|
||||
isSecret: false,
|
||||
type: FieldMetadataType.TEXT,
|
||||
options: null,
|
||||
applicationId: 'app-1',
|
||||
workspaceId: workspaceA,
|
||||
universalIdentifier: '00000000-0000-0000-0000-000000000000',
|
||||
@@ -92,6 +100,8 @@ describe('buildEnvVar', () => {
|
||||
value: `enc:v2:deadbeef:value-a|${workspaceA}` as EncryptedString,
|
||||
description: '',
|
||||
isSecret: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
options: null,
|
||||
applicationId: 'app-1',
|
||||
workspaceId: workspaceA,
|
||||
universalIdentifier: '00000000-0000-0000-0000-000000000000',
|
||||
@@ -105,6 +115,8 @@ describe('buildEnvVar', () => {
|
||||
value: `enc:v2:deadbeef:value-b|${workspaceB}` as EncryptedString,
|
||||
description: '',
|
||||
isSecret: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
options: null,
|
||||
applicationId: 'app-1',
|
||||
workspaceId: workspaceB,
|
||||
universalIdentifier: '00000000-0000-0000-0000-000000000000',
|
||||
@@ -136,6 +148,8 @@ describe('buildEnvVar', () => {
|
||||
value: null as unknown as EncryptedString | '',
|
||||
description: '',
|
||||
isSecret: false,
|
||||
type: FieldMetadataType.TEXT,
|
||||
options: null,
|
||||
applicationId: 'app-1',
|
||||
workspaceId: workspaceA,
|
||||
universalIdentifier: '00000000-0000-0000-0000-000000000000',
|
||||
@@ -149,6 +163,8 @@ describe('buildEnvVar', () => {
|
||||
value: undefined as unknown as EncryptedString | '',
|
||||
description: '',
|
||||
isSecret: false,
|
||||
type: FieldMetadataType.TEXT,
|
||||
options: null,
|
||||
applicationId: 'app-1',
|
||||
workspaceId: workspaceA,
|
||||
universalIdentifier: '00000000-0000-0000-0000-000000000000',
|
||||
@@ -174,6 +190,8 @@ describe('buildEnvVar', () => {
|
||||
value: 123 as unknown as EncryptedString | '',
|
||||
description: '',
|
||||
isSecret: false,
|
||||
type: FieldMetadataType.TEXT,
|
||||
options: null,
|
||||
applicationId: 'app-1',
|
||||
workspaceId: workspaceA,
|
||||
universalIdentifier: '00000000-0000-0000-0000-000000000000',
|
||||
|
||||
+5
-1
@@ -25,8 +25,12 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
"key",
|
||||
"description",
|
||||
"isSecret",
|
||||
"type",
|
||||
"options",
|
||||
],
|
||||
"propertiesToStringify": [
|
||||
"options",
|
||||
],
|
||||
"propertiesToStringify": [],
|
||||
},
|
||||
"commandMenuItem": {
|
||||
"propertiesToCompare": [
|
||||
|
||||
+10
@@ -1761,6 +1761,16 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
type: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
options: {
|
||||
toCompare: true,
|
||||
toStringify: true,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
createdAt: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
|
||||
+4
@@ -1,3 +1,5 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type EncryptedString } from 'src/engine/core-modules/secret-encryption/branded-strings/encrypted-string.type';
|
||||
import { type FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type';
|
||||
import { stripSecretFromApplicationVariables } from 'src/engine/metadata-modules/front-component/utils/strip-secret-from-application-variables';
|
||||
@@ -10,6 +12,8 @@ const makeFlatVariable = (
|
||||
value: 'value' as EncryptedString,
|
||||
description: '',
|
||||
isSecret: false,
|
||||
type: FieldMetadataType.TEXT,
|
||||
options: null,
|
||||
applicationId: 'app-1',
|
||||
workspaceId: '00000000-0000-0000-0000-000000000000',
|
||||
universalIdentifier: '00000000-0000-0000-0000-000000000000',
|
||||
|
||||
@@ -1,16 +1,54 @@
|
||||
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
|
||||
import { FieldMetadataType } from '@/types/FieldMetadataType';
|
||||
|
||||
type SecretApplicationVariable = SyncableEntityOptions & {
|
||||
description?: string;
|
||||
isSecret: true;
|
||||
export const APPLICATION_VARIABLE_FIELD_METADATA_TYPES = [
|
||||
FieldMetadataType.TEXT,
|
||||
FieldMetadataType.ARRAY,
|
||||
FieldMetadataType.BOOLEAN,
|
||||
FieldMetadataType.DATE,
|
||||
FieldMetadataType.DATE_TIME,
|
||||
FieldMetadataType.NUMBER,
|
||||
FieldMetadataType.NUMERIC,
|
||||
FieldMetadataType.RAW_JSON,
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.MULTI_SELECT,
|
||||
] as const;
|
||||
|
||||
export type ApplicationVariableType =
|
||||
(typeof APPLICATION_VARIABLE_FIELD_METADATA_TYPES)[number];
|
||||
|
||||
export type ApplicationVariableOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type NonSecretApplicationVariable = SyncableEntityOptions & {
|
||||
value?: string;
|
||||
description?: string;
|
||||
isSecret?: false;
|
||||
export type ApplicationVariableValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| string[]
|
||||
| Record<string, unknown>
|
||||
| null;
|
||||
|
||||
type TypedApplicationVariable = {
|
||||
type?: ApplicationVariableType;
|
||||
options?: ApplicationVariableOption[];
|
||||
};
|
||||
|
||||
type SecretApplicationVariable = SyncableEntityOptions &
|
||||
TypedApplicationVariable & {
|
||||
description?: string;
|
||||
isSecret: true;
|
||||
};
|
||||
|
||||
type NonSecretApplicationVariable = SyncableEntityOptions &
|
||||
TypedApplicationVariable & {
|
||||
value?: ApplicationVariableValue;
|
||||
description?: string;
|
||||
isSecret?: false;
|
||||
};
|
||||
|
||||
export type ApplicationVariable =
|
||||
| SecretApplicationVariable
|
||||
| NonSecretApplicationVariable;
|
||||
|
||||
@@ -11,9 +11,13 @@ export type { AgentManifest } from './agentManifestType';
|
||||
export type { AppConnection } from './appConnectionType';
|
||||
export type { ApplicationManifest } from './applicationType';
|
||||
export type {
|
||||
ApplicationVariableType,
|
||||
ApplicationVariableOption,
|
||||
ApplicationVariableValue,
|
||||
ApplicationVariable,
|
||||
ApplicationVariables,
|
||||
} from './applicationVariablesType';
|
||||
export { APPLICATION_VARIABLE_FIELD_METADATA_TYPES } from './applicationVariablesType';
|
||||
export type { AssetManifest } from './assetManifestType';
|
||||
export type { ConnectionProviderManifest } from './connectionProviderManifestType';
|
||||
export type { ConnectionProviderType } from './connectionProviderType';
|
||||
@@ -122,6 +126,10 @@ export type { SkillManifest } from './skillManifestType';
|
||||
export type { StoredOAuthConnectionProviderConfig } from './storedOAuthConnectionProviderConfigType';
|
||||
export type { SyncableEntityOptions } from './syncableEntityOptionsType';
|
||||
export type { ToolTriggerSettings } from './toolTriggerSettingsType';
|
||||
export {
|
||||
serializeApplicationVariableValue,
|
||||
deserializeApplicationVariableValue,
|
||||
} from './utils/applicationVariableValueSerialization';
|
||||
export type {
|
||||
ViewManifestFilterValue,
|
||||
ViewFieldManifest,
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import {
|
||||
type ApplicationVariableOption,
|
||||
type ApplicationVariableType,
|
||||
} from '@/application/applicationVariablesType';
|
||||
|
||||
type ServerVariableSchema = {
|
||||
description?: string;
|
||||
isSecret?: boolean;
|
||||
isRequired?: boolean;
|
||||
type?: ApplicationVariableType;
|
||||
options?: ApplicationVariableOption[];
|
||||
};
|
||||
|
||||
export type ServerVariables = Record<string, ServerVariableSchema>;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
type ApplicationVariableType,
|
||||
type ApplicationVariableValue,
|
||||
} from '@/application/applicationVariablesType';
|
||||
import { FieldMetadataType } from '@/types/FieldMetadataType';
|
||||
|
||||
export const serializeApplicationVariableValue = (
|
||||
value: ApplicationVariableValue | undefined,
|
||||
type: ApplicationVariableType = FieldMetadataType.TEXT,
|
||||
): string => {
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case FieldMetadataType.BOOLEAN:
|
||||
return String(value) === 'true' ? 'true' : 'false';
|
||||
case FieldMetadataType.NUMBER:
|
||||
case FieldMetadataType.NUMERIC:
|
||||
return String(value);
|
||||
case FieldMetadataType.ARRAY:
|
||||
case FieldMetadataType.MULTI_SELECT:
|
||||
if (Array.isArray(value)) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value) as unknown;
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
return value;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return JSON.stringify([value]);
|
||||
}
|
||||
|
||||
return JSON.stringify(value);
|
||||
case FieldMetadataType.RAW_JSON:
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
return typeof value === 'string' ? value : JSON.stringify(value);
|
||||
default:
|
||||
return typeof value === 'string' ? value : String(value);
|
||||
}
|
||||
};
|
||||
|
||||
export const deserializeApplicationVariableValue = (
|
||||
value: string,
|
||||
type: ApplicationVariableType = FieldMetadataType.TEXT,
|
||||
): ApplicationVariableValue => {
|
||||
if (value === '') {
|
||||
return type === FieldMetadataType.ARRAY ||
|
||||
type === FieldMetadataType.MULTI_SELECT
|
||||
? []
|
||||
: '';
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case FieldMetadataType.BOOLEAN:
|
||||
return value === 'true';
|
||||
case FieldMetadataType.NUMBER:
|
||||
case FieldMetadataType.NUMERIC: {
|
||||
const parsed = Number(value);
|
||||
|
||||
return Number.isNaN(parsed) ? value : parsed;
|
||||
}
|
||||
case FieldMetadataType.ARRAY:
|
||||
case FieldMetadataType.MULTI_SELECT:
|
||||
try {
|
||||
const parsed = JSON.parse(value) as unknown;
|
||||
|
||||
return Array.isArray(parsed) ? (parsed as string[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
case FieldMetadataType.RAW_JSON:
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
try {
|
||||
return JSON.parse(value) as Record<string, unknown>;
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
};
|
||||
@@ -55,7 +55,6 @@ export enum SettingsPath {
|
||||
ApplicationPageLayoutDetail = 'applications/:applicationId/pageLayouts/:pageLayoutUniversalIdentifier',
|
||||
AvailableApplicationDetail = 'applications/available/:availableApplicationId',
|
||||
ApplicationRegistrationDetail = 'applications/registrations/:applicationRegistrationId',
|
||||
ApplicationRegistrationConfigVariableDetails = 'applications/registrations/:applicationRegistrationId/config-variables/:variableKey',
|
||||
LogicFunctions = 'functions',
|
||||
NewLogicFunction = 'functions/new',
|
||||
LogicFunctionDetail = 'functions/:logicFunctionId',
|
||||
@@ -86,7 +85,6 @@ export enum SettingsPath {
|
||||
AdminPanelUserDetail = 'admin-panel/users/:userId',
|
||||
AdminPanelWorkspaceDetail = 'admin-panel/workspaces/:workspaceId',
|
||||
AdminPanelApplicationRegistrationDetail = 'admin-panel/applications/registrations/:applicationRegistrationId',
|
||||
AdminPanelApplicationRegistrationConfigVariableDetails = 'admin-panel/applications/registrations/:applicationRegistrationId/config-variables/:variableKey',
|
||||
AdminPanelWorkspaceChatThread = 'admin-panel/workspaces/:workspaceId/threads/:threadId',
|
||||
|
||||
Roles = 'members/roles',
|
||||
|
||||
Reference in New Issue
Block a user