Compare commits
172
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ba147b631 | ||
|
|
4439029782 | ||
|
|
70de1d929d | ||
|
|
430f3cb7c6 | ||
|
|
4d17b1179f | ||
|
|
eb213069d2 | ||
|
|
5732c1de2a | ||
|
|
3d491ed370 | ||
|
|
641df0a271 | ||
|
|
fcab418fba | ||
|
|
702e02e590 | ||
|
|
74e12df09c | ||
|
|
3980094e02 | ||
|
|
295d924db8 | ||
|
|
39837b9f0f | ||
|
|
d94d69e6a7 | ||
|
|
93b87a5362 | ||
|
|
925bb9596c | ||
|
|
9a1735c4e3 | ||
|
|
18dc6fb313 | ||
|
|
30154aec47 | ||
|
|
b184f5c25f | ||
|
|
287c9c8a67 | ||
|
|
bcf633986f | ||
|
|
0accf11b39 | ||
|
|
b62c941595 | ||
|
|
07b660e2d7 | ||
|
|
af6c31470a | ||
|
|
77755da8fc | ||
|
|
b9cc5589b8 | ||
|
|
59ef653626 | ||
|
|
6a908b7876 | ||
|
|
445c6fe9f6 | ||
|
|
1ba7a3fa54 | ||
|
|
7d7f32b243 | ||
|
|
5f0096c464 | ||
|
|
6ac797a69c | ||
|
|
3d6bcc3102 | ||
|
|
75df1f3997 | ||
|
|
d6b3527552 | ||
|
|
989b45db15 | ||
|
|
d86e827563 | ||
|
|
6ad6fcce0f | ||
|
|
ad47b2972c | ||
|
|
ba493e5a23 | ||
|
|
627b488556 | ||
|
|
b9e5ff2065 | ||
|
|
381ca32055 | ||
|
|
6029847491 | ||
|
|
66afd5a1de | ||
|
|
da5e1152bb | ||
|
|
b338a7a1d2 | ||
|
|
6e00a122c6 | ||
|
|
4dff30f676 | ||
|
|
71c377484e | ||
|
|
26906951b3 | ||
|
|
51202d5a32 | ||
|
|
f4380f89a8 | ||
|
|
e430e4ea0a | ||
|
|
b027e4bdb1 | ||
|
|
fc90b4ba8b | ||
|
|
88b77cb699 | ||
|
|
0ed2e9d82d | ||
|
|
643cfe9b13 | ||
|
|
bc1b7f6fdf | ||
|
|
57118a868f | ||
|
|
93f848fd2f | ||
|
|
667cb95730 | ||
|
|
4e5d47168c | ||
|
|
10c0bed462 | ||
|
|
08b1c5738d | ||
|
|
c2df39405c | ||
|
|
3e2c50c6cf | ||
|
|
f67db8d8b2 | ||
|
|
3cd2458fdf | ||
|
|
13f09d8946 | ||
|
|
961745e9ba | ||
|
|
41832c8d82 | ||
|
|
3041ed3b6e | ||
|
|
6d550611d2 | ||
|
|
3c97d9648b | ||
|
|
3afdabb93e | ||
|
|
25b0e0d091 | ||
|
|
a43e5c3fb3 | ||
|
|
996cdaf3ff | ||
|
|
1d84695fb0 | ||
|
|
64e0b76d00 | ||
|
|
8a74ea8829 | ||
|
|
f4ead89956 | ||
|
|
865ca697ca | ||
|
|
ebfaca5b3d | ||
|
|
9b54200d8c | ||
|
|
bd6811d060 | ||
|
|
b32249877a | ||
|
|
6fb6ef4e7a | ||
|
|
bb4e28904f | ||
|
|
c3d1af89ae | ||
|
|
e6221c5f0e | ||
|
|
6566a918af | ||
|
|
6ea637d6c5 | ||
|
|
531410f64a | ||
|
|
182960051f | ||
|
|
7d9f9605a2 | ||
|
|
7065441972 | ||
|
|
6e2cf4cbc0 | ||
|
|
de7daaa81a | ||
|
|
c0cbe67bcd | ||
|
|
c5606212f2 | ||
|
|
0702e72e3f | ||
|
|
0503aa7982 | ||
|
|
f98c5d0609 | ||
|
|
4797d2f270 | ||
|
|
c8b9dace72 | ||
|
|
fbae66de8a | ||
|
|
46e7f23df1 | ||
|
|
6f3541fd7c | ||
|
|
863f3f29a2 | ||
|
|
9264480824 | ||
|
|
5ff8c3b219 | ||
|
|
ff28547a36 | ||
|
|
dde6df7a26 | ||
|
|
92b0e07617 | ||
|
|
951ead5a1e | ||
|
|
e79f43813b | ||
|
|
73ab5de46c | ||
|
|
986b9dcb3d | ||
|
|
ae7db23bdd | ||
|
|
2f358a1775 | ||
|
|
fc3004b2f6 | ||
|
|
086b79e5b9 | ||
|
|
8cb88cabee | ||
|
|
ac89d2ff56 | ||
|
|
423faa6153 | ||
|
|
a051490ec9 | ||
|
|
34db7ac8b4 | ||
|
|
4a82cddad6 | ||
|
|
9d6c5b7d58 | ||
|
|
a2db0b6932 | ||
|
|
5eb79e7797 | ||
|
|
f19647617a | ||
|
|
424c6737a1 | ||
|
|
be8c25dfc2 | ||
|
|
b8de469f37 | ||
|
|
c74337978b | ||
|
|
059e75e532 | ||
|
|
bb5c2bd00c | ||
|
|
a2673da164 | ||
|
|
08682fb3f5 | ||
|
|
53d4e92dda | ||
|
|
53392f9a16 | ||
|
|
7e7bb81586 | ||
|
|
3703b3e2f6 | ||
|
|
e72d10f550 | ||
|
|
7ca9081efa | ||
|
|
015dca95fc | ||
|
|
076c05cbd0 | ||
|
|
82c565f7dc | ||
|
|
44bae39538 | ||
|
|
1188ea9cd5 | ||
|
|
f721420705 | ||
|
|
96d247e94d | ||
|
|
90f711361c | ||
|
|
98d47d0dd0 | ||
|
|
f613886511 | ||
|
|
a3e41ec267 | ||
|
|
d602f35cbd | ||
|
|
69d89f8cfc | ||
|
|
be39702fd2 | ||
|
|
50c8834e21 | ||
|
|
d54caf10c1 | ||
|
|
0b38b4ffc4 | ||
|
|
b8b115f4e3 |
@@ -80,10 +80,20 @@ jobs:
|
||||
with:
|
||||
name: storybook-twenty-front-component-renderer
|
||||
path: packages/twenty-front-component-renderer/storybook-static
|
||||
- name: Resolve Playwright version
|
||||
id: playwright-version
|
||||
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
|
||||
- name: Cache Playwright browsers
|
||||
id: playwright-cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: v4-playwright-browsers-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
|
||||
- name: Install Playwright
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd packages/twenty-front-component-renderer
|
||||
npx playwright install
|
||||
npx playwright install chromium
|
||||
- name: Serve storybook & run tests
|
||||
run: |
|
||||
npx http-server packages/twenty-front-component-renderer/storybook-static --port 6008 --silent &
|
||||
|
||||
@@ -96,10 +96,20 @@ jobs:
|
||||
with:
|
||||
name: storybook-static
|
||||
path: packages/twenty-front/storybook-static
|
||||
- name: Resolve Playwright version
|
||||
id: playwright-version
|
||||
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
|
||||
- name: Cache Playwright browsers
|
||||
id: playwright-cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: v4-playwright-browsers-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
|
||||
- name: Install Playwright
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd packages/twenty-front
|
||||
npx playwright install
|
||||
npx playwright install chromium
|
||||
- name: Front / Write .env
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Serve storybook & run tests
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
+1
-1
@@ -28,7 +28,7 @@
|
||||
"resolutions": {
|
||||
"graphql": "16.8.1",
|
||||
"type-fest": "4.10.1",
|
||||
"typescript": "5.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"nodemailer": "8.0.4",
|
||||
"graphql-redis-subscriptions/ioredis": "^5.6.0",
|
||||
"@lingui/core": "5.1.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "2.7.0",
|
||||
"version": "2.9.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
@@ -50,7 +50,7 @@
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-node": "^29.4.1",
|
||||
"twenty-shared": "workspace:*",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
|
||||
@@ -80,8 +80,8 @@ export async function teardown() {
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
throw new Error(
|
||||
`App uninstall failed during teardown: ${JSON.stringify(uninstallResult.error, null, 2)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineIndex } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from '../objects/post-card.object';
|
||||
|
||||
export default defineIndex({
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
|
||||
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
],
|
||||
});
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-partners",
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
@@ -16,8 +16,13 @@
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"seed": "tsx src/scripts/seed.ts",
|
||||
"seed:prod": "ENV_FILE=.env.prod tsx src/scripts/seed.ts",
|
||||
"purge": "tsx src/scripts/purge-soft-deleted.ts",
|
||||
"purge:prod": "ENV_FILE=.env.prod tsx src/scripts/purge-soft-deleted.ts",
|
||||
"import:dryrun": "tsx src/scripts/import-from-tft.ts",
|
||||
"import:apply": "IMPORT_APPLY=1 tsx src/scripts/import-from-tft.ts"
|
||||
"import:dryrun:prod": "ENV_FILE=.env.prod tsx src/scripts/import-from-tft.ts",
|
||||
"import:apply": "IMPORT_APPLY=1 tsx src/scripts/import-from-tft.ts",
|
||||
"import:apply:prod": "ENV_FILE=.env.prod IMPORT_APPLY=1 tsx src/scripts/import-from-tft.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-client-sdk": "2.4.0",
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@ export const APP_DESCRIPTION = '';
|
||||
export const APPLICATION_UNIVERSAL_IDENTIFIER = 'e662fc1f-02c1-41ff-b8ba-c95a447b3965';
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'ee18c3f3-ebe7-4c56-ad6d-aad555cc32db';
|
||||
export const PARTNER_OBJECT_UNIVERSAL_IDENTIFIER = '39101b39-1c16-4148-9e82-45dc271bb90d';
|
||||
export const PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER = '65172140-d377-41c1-a2ae-190e96fb79dd';
|
||||
export const PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER = '65172140-d377-41c1-a2ae-190e96fb79dd';
|
||||
export const ALL_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER = '379b11d5-44d5-476b-ba7d-31d5f515c9b4';
|
||||
export const ALL_MATCHED_DEALS_VIEW_UNIVERSAL_IDENTIFIER = '7a34da39-a8e1-44c7-88b4-91ceaa064920';
|
||||
export const PARTNERS_NAV_UNIVERSAL_IDENTIFIER = '3fe15ab5-e38b-4914-af17-2270b210aeb2';
|
||||
@@ -35,7 +35,7 @@ export const ALL_OPPORTUNITIES_NAV_UNIVERSAL_IDENTIFIER = '37944f52-cbe5-4814-a1
|
||||
// Partner views + nav (harmonization)
|
||||
export const PARTNER_APPLICATIONS_VIEW_UNIVERSAL_IDENTIFIER = 'b57a84ed-d7c1-420d-b0eb-348db0dac612';
|
||||
export const VALIDATED_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER = '13cca6a7-b9f1-4103-b011-ea2e39430899';
|
||||
export const PARTNER_QUOTES_VIEW_UNIVERSAL_IDENTIFIER = 'd9db705c-795a-4a14-b891-6201149510b3';
|
||||
export const PARTNER_CONTENT_VIEW_UNIVERSAL_IDENTIFIER = 'd9db705c-795a-4a14-b891-6201149510b3';
|
||||
export const PARTNER_APPLICATIONS_NAV_UNIVERSAL_IDENTIFIER = '13e2334a-6b1e-4080-8c74-d11109990cc1';
|
||||
export const VALIDATED_PARTNERS_NAV_UNIVERSAL_IDENTIFIER = '6aed30c6-d80f-4ac6-aab0-db5bc59e5c4b';
|
||||
export const PARTNER_QUOTES_NAV_UNIVERSAL_IDENTIFIER = '3543723d-80c1-466a-ac35-86f7b284917b';
|
||||
export const PARTNER_CONTENT_NAV_UNIVERSAL_IDENTIFIER = '3543723d-80c1-466a-ac35-86f7b284917b';
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { FieldType, OnDeleteAction, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export const PARTNER_CONTENT_CUSTOMER_COMPANY_FIELD_ID = 'd448f6be-533f-4c43-a70f-55dc361dcdd7';
|
||||
export const PARTNER_CONTENTS_ON_COMPANY_FIELD_ID = '941e7707-1b3d-47d4-b13a-45c6628c1b3d';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_CONTENT_CUSTOMER_COMPANY_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'customerCompany',
|
||||
label: 'Customer Company',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_CONTENTS_ON_COMPANY_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'customerCompanyId',
|
||||
},
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { FieldType, OnDeleteAction, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export const PARTNER_CONTENT_CUSTOMER_PERSON_FIELD_ID = 'f17547be-76aa-4231-bd1a-3f57bb1ae323';
|
||||
export const PARTNER_CONTENTS_ON_PERSON_FIELD_ID = 'fae42f4e-b054-4267-a761-81a6792f7c12';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_CONTENT_CUSTOMER_PERSON_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'customerPerson',
|
||||
label: 'Customer Person',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_CONTENTS_ON_PERSON_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'customerPersonId',
|
||||
},
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { FieldType, OnDeleteAction, RelationType, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export const PARTNER_CONTENT_PARTNER_FIELD_ID = '1774738e-96a1-43e2-aca4-d3c7cc794c50';
|
||||
export const PARTNER_CONTENTS_ON_PARTNER_FIELD_ID = 'ac7995f1-7ccf-4607-827b-0788eeacaee0';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_CONTENT_PARTNER_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partner',
|
||||
label: 'Partner',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_CONTENTS_ON_PARTNER_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'partnerId',
|
||||
},
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { FieldType, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { PARTNER_CONTENTS_ON_COMPANY_FIELD_ID, PARTNER_CONTENT_CUSTOMER_COMPANY_FIELD_ID } from './partner-content-customer-company.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_CONTENTS_ON_COMPANY_FIELD_ID,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partnerContents',
|
||||
label: 'Partner Content',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_CONTENT_CUSTOMER_COMPANY_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { FieldType, RelationType, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { PARTNER_CONTENTS_ON_PARTNER_FIELD_ID, PARTNER_CONTENT_PARTNER_FIELD_ID } from './partner-content-partner.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_CONTENTS_ON_PARTNER_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partnerContents',
|
||||
label: 'Partner Content',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_CONTENT_PARTNER_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { FieldType, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { PARTNER_CONTENTS_ON_PERSON_FIELD_ID, PARTNER_CONTENT_CUSTOMER_PERSON_FIELD_ID } from './partner-content-customer-person.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_CONTENTS_ON_PERSON_FIELD_ID,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partnerContents',
|
||||
label: 'Partner Content',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_CONTENT_CUSTOMER_PERSON_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
import { FieldType, OnDeleteAction, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export const PARTNER_QUOTE_OPPORTUNITY_FIELD_ID = '36684f3e-f01a-4270-8f34-78966747dd64';
|
||||
export const PARTNER_QUOTES_ON_OPPORTUNITY_FIELD_ID = '4857f339-a051-4cdb-bd8a-6219b933d1ce';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_QUOTE_OPPORTUNITY_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'opportunity',
|
||||
label: 'Opportunity',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_QUOTES_ON_OPPORTUNITY_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'opportunityId',
|
||||
},
|
||||
});
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
import { FieldType, OnDeleteAction, RelationType, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export const PARTNER_QUOTE_PARTNER_FIELD_ID = '1774738e-96a1-43e2-aca4-d3c7cc794c50';
|
||||
export const PARTNER_QUOTES_ON_PARTNER_FIELD_ID = 'ac7995f1-7ccf-4607-827b-0788eeacaee0';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_QUOTE_PARTNER_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partner',
|
||||
label: 'Partner',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_QUOTES_ON_PARTNER_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'partnerId',
|
||||
},
|
||||
});
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { FieldType, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { PARTNER_QUOTES_ON_OPPORTUNITY_FIELD_ID, PARTNER_QUOTE_OPPORTUNITY_FIELD_ID } from './partner-quote-opportunity.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_QUOTES_ON_OPPORTUNITY_FIELD_ID,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partnerQuotes',
|
||||
label: 'Partner Quotes',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_QUOTE_OPPORTUNITY_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { FieldType, RelationType, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { PARTNER_QUOTES_ON_PARTNER_FIELD_ID, PARTNER_QUOTE_PARTNER_FIELD_ID } from './partner-quote-partner.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_QUOTES_ON_PARTNER_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partnerQuotes',
|
||||
label: 'Partner Quotes',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_QUOTE_PARTNER_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
+5
-5
@@ -1,15 +1,15 @@
|
||||
import { NavigationMenuItemType, defineNavigationMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
PARTNER_QUOTES_NAV_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_QUOTES_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_CONTENT_NAV_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_CONTENT_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: PARTNER_QUOTES_NAV_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: PARTNER_CONTENT_NAV_UNIVERSAL_IDENTIFIER,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
icon: 'IconFileDollar',
|
||||
icon: 'IconQuote',
|
||||
position: 3,
|
||||
folderUniversalIdentifier: '857be3b5-82c6-45f7-b546-e20a8a97be8d',
|
||||
viewUniversalIdentifier: PARTNER_QUOTES_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
viewUniversalIdentifier: PARTNER_CONTENT_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
+30
-16
@@ -1,15 +1,15 @@
|
||||
import { FieldType, defineObject } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'partnerQuote',
|
||||
namePlural: 'partnerQuotes',
|
||||
labelSingular: 'Partner Quote',
|
||||
labelPlural: 'Partner Quotes',
|
||||
description: 'A quote a partner submitted for a customer deal',
|
||||
icon: 'IconFileDollar',
|
||||
universalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'partnerContent',
|
||||
namePlural: 'partnerContents',
|
||||
labelSingular: 'Partner Content',
|
||||
labelPlural: 'Partner Content',
|
||||
description: 'Marketing content involving a partner: quotes, case studies, logos',
|
||||
icon: 'IconQuote',
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: '9e688624-83d2-4715-8b18-80492a6de2b6',
|
||||
fields: [
|
||||
@@ -21,6 +21,20 @@ export default defineObject({
|
||||
icon: 'IconTag',
|
||||
defaultValue: "''",
|
||||
},
|
||||
{
|
||||
universalIdentifier: '1d926e6e-6ac1-4d60-ab3d-a73114005692',
|
||||
type: FieldType.MULTI_SELECT,
|
||||
name: 'contentType',
|
||||
label: 'Content Type',
|
||||
icon: 'IconCategory',
|
||||
isNullable: true,
|
||||
options: [
|
||||
{ id: '07f85e5b-0d70-416b-9884-256e469ed532', value: 'CUSTOMER_QUOTE', label: 'Customer quote', position: 0, color: 'blue' },
|
||||
{ id: '108b2358-d04d-4fdc-83df-a5978d39f66f', value: 'CASE_STUDY', label: 'Case study', position: 1, color: 'green' },
|
||||
{ id: 'eb45f371-f93c-4c45-9c8a-f29e1a58b7e4', value: 'PARTNER_QUOTE', label: 'Partner quote', position: 2, color: 'orange' },
|
||||
{ id: '3356c8a0-41cd-47c3-a293-5862138abc1a', value: 'LOGO', label: 'Logo', position: 3, color: 'purple' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a0fe09c4-c1f4-4b96-93c6-d7ec38f1166a',
|
||||
type: FieldType.SELECT,
|
||||
@@ -45,21 +59,21 @@ export default defineObject({
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '2cbe67e3-24ec-421b-bab5-50f3306c2391',
|
||||
type: FieldType.CURRENCY,
|
||||
name: 'amount',
|
||||
label: 'Amount',
|
||||
icon: 'IconCoin',
|
||||
universalIdentifier: 'da7e9094-e2c3-47d3-924f-a1d4d3c717ed',
|
||||
type: FieldType.LINKS,
|
||||
name: 'interview',
|
||||
label: 'Interview',
|
||||
icon: 'IconMicrophone',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'f303369e-288c-4a48-9920-c1de0ad9a159',
|
||||
type: FieldType.FILES,
|
||||
name: 'quoteFile',
|
||||
label: 'Quote File',
|
||||
name: 'documents',
|
||||
label: 'Documents',
|
||||
icon: 'IconPaperclip',
|
||||
isNullable: true,
|
||||
universalSettings: { maxNumberOfValues: 1 },
|
||||
universalSettings: { maxNumberOfValues: 10 },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -23,7 +23,7 @@
|
||||
// tsx src/scripts/import-from-tft.ts
|
||||
|
||||
import { config } from 'dotenv';
|
||||
config({ path: '.env.local' });
|
||||
config({ path: process.env.ENV_FILE ?? '.env.local' });
|
||||
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
@@ -76,6 +76,17 @@ const PARTNER_STAGE_TO_VALIDATION: Record<string, string> = {
|
||||
REJECTED: 'REJECTED',
|
||||
};
|
||||
|
||||
// TFT person.partnerTimezone -> our Partner.region (MULTI_SELECT). TFT's value is a
|
||||
// coarse timezone band, not a geography, so each maps to every region it plausibly
|
||||
// covers. OTHER carries no signal -> no region. Region stays empty if unmapped.
|
||||
const TIMEZONE_TO_REGION: Record<string, string[]> = {
|
||||
AMERICAS: ['US', 'LATAM'],
|
||||
EMEA: ['EUROPE', 'MENA', 'AFRICA'],
|
||||
WEST_ASIA: ['MENA', 'APAC'],
|
||||
EAST_ASIA_OCEANIA: ['APAC'],
|
||||
OTHER: [],
|
||||
};
|
||||
|
||||
// Local SELECT option sets, for preflight coverage checks (a TFT value not in
|
||||
// these would fail a real write). Keep in sync with src/objects + src/fields.
|
||||
const LOCAL_OPTIONS: Record<string, Set<string>> = {
|
||||
@@ -101,6 +112,30 @@ const edges = (result: any, key: string): any[] =>
|
||||
const uniq = (values: (string | undefined | null)[]): string[] =>
|
||||
[...new Set(values.filter((v): v is string => !!v))];
|
||||
|
||||
// Normalize a domain for dedup. Twenty's company domain is a unique key but is
|
||||
// stored with an https:// prefix, while TFT values vary, so compare on a canonical
|
||||
// form (no protocol, no www, no trailing slash, lowercased).
|
||||
const normDomain = (d?: string | null): string | undefined =>
|
||||
d
|
||||
? d.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/\/+$/, '') || undefined
|
||||
: undefined;
|
||||
|
||||
// Distinct non-empty values across rows, for the preflight report. Flattens array
|
||||
// fields (e.g. partnerScope, typeCustom) and stringifies, so scalar and
|
||||
// multi-select fields share one path. Derived from the already-fetched source
|
||||
// rows — no per-loop bookkeeping needed.
|
||||
const distinct = <TRow>(rows: TRow[], pick: (row: TRow) => unknown): string[] =>
|
||||
[
|
||||
...new Set(
|
||||
rows.flatMap((row) => {
|
||||
const value = pick(row);
|
||||
return Array.isArray(value) ? value : value != null ? [value] : [];
|
||||
}),
|
||||
),
|
||||
]
|
||||
.map(String)
|
||||
.sort();
|
||||
|
||||
async function main() {
|
||||
console.log(`[import] mode: ${APPLY ? 'APPLY (writing to local)' : 'DRY-RUN (no writes)'}`);
|
||||
const local = new CoreApiClient({
|
||||
@@ -114,26 +149,10 @@ async function main() {
|
||||
if (APPLY && writes++ > 0) await new Promise((r) => setTimeout(r, 750));
|
||||
};
|
||||
|
||||
// Distinct TFT values seen, for the preflight coverage report.
|
||||
const seen: Record<string, Set<string>> = {
|
||||
partnerStage: new Set(),
|
||||
partnerTier: new Set(),
|
||||
partnerScope: new Set(),
|
||||
typeOfTeam: new Set(),
|
||||
oppStage: new Set(),
|
||||
hostingType: new Set(),
|
||||
subscriptionType: new Set(),
|
||||
subscriptionFrequency: new Set(),
|
||||
quoteStatus: new Set(),
|
||||
typeCustom: new Set(),
|
||||
};
|
||||
const note = (bucket: string, value?: string | null) => {
|
||||
if (value) seen[bucket].add(value);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// 1. Read all three TFT datasets (raw fetch; not rate-limited by local).
|
||||
// ---------------------------------------------------------------------
|
||||
console.log('[import] fetching TFT people...');
|
||||
const tftPeople = edges(
|
||||
await tftQuery(`query {
|
||||
people(first: 500, filter: { partnerStage: { in: ["APPLICATION","POTENTIAL_PARTNER","PARTNER","FORMER_PARTNER","REJECTED"] } }) {
|
||||
@@ -143,7 +162,7 @@ async function main() {
|
||||
emails { primaryEmail }
|
||||
city jobTitle
|
||||
linkedinLink { primaryLinkUrl }
|
||||
partnerStage partnerTier partnerScope partnerTypeOfTeam partnerIsAvailable partnerSkills
|
||||
partnerStage partnerTier partnerScope partnerTypeOfTeam partnerTimezone partnerIsAvailable partnerSkills
|
||||
partnerBudgetMinimum { amountMicros currencyCode }
|
||||
partnerBudgetAverage { amountMicros currencyCode }
|
||||
company { id name domainName { primaryLinkUrl } }
|
||||
@@ -152,7 +171,8 @@ async function main() {
|
||||
}`),
|
||||
'people',
|
||||
);
|
||||
const tftOpps = edges(
|
||||
console.log('[import] fetching TFT opportunities...');
|
||||
const tftOppsAll = edges(
|
||||
await tftQuery(`query {
|
||||
opportunities(first: 500) {
|
||||
edges { node {
|
||||
@@ -166,16 +186,32 @@ async function main() {
|
||||
}`),
|
||||
'opportunities',
|
||||
);
|
||||
// Only import opportunities linked to a partner. The rest is TFT's general sales
|
||||
// pipeline (mostly LOST/IDENTIFIED) — noise for a partners app. Every partner
|
||||
// stage (INTRODUCED/WORKING) only ever appears on partner-linked opps anyway.
|
||||
const tftOpps = tftOppsAll.filter((o: any) => o.partner?.id);
|
||||
console.log(`[import] opportunities: ${tftOpps.length} partner-linked of ${tftOppsAll.length} total (skipping ${tftOppsAll.length - tftOpps.length} unlinked)`);
|
||||
console.log('[import] fetching TFT partner content...');
|
||||
const tftContent = edges(
|
||||
await tftQuery(`query {
|
||||
customerContents(first: 500) {
|
||||
edges { node { id name status approvalDate typeCustom partnerPerson { id } } }
|
||||
edges { node {
|
||||
id name status approvalDate typeCustom
|
||||
interview { primaryLinkUrl }
|
||||
partnerPerson { id }
|
||||
customerCompany { id name domainName { primaryLinkUrl } }
|
||||
customerPerson { id }
|
||||
} }
|
||||
}
|
||||
}`),
|
||||
'customerContents',
|
||||
);
|
||||
const quotes = tftContent.filter((c: any) => Array.isArray(c.typeCustom) && c.typeCustom.includes('PARTNER_QUOTE'));
|
||||
console.log(`[import] TFT: ${tftPeople.length} partner-people, ${tftOpps.length} opportunities, ${quotes.length} partner quotes`);
|
||||
// Import all content TYPES (quotes, case studies, logos) but only records that
|
||||
// involve a partner. Customer-only content (no partnerPerson) is noise for the
|
||||
// partners app; a partner-linked case study/quote should show on the partner.
|
||||
const contentRecords = tftContent.filter((c: any) => c.partnerPerson?.id);
|
||||
console.log(`[import] TFT: ${tftPeople.length} partner-people, ${tftOpps.length} opportunities, ${tftContent.length} content records`);
|
||||
console.log(`[import] partner content: ${contentRecords.length} partner-linked of ${tftContent.length} total (skipping ${tftContent.length - contentRecords.length} customer-only)`);
|
||||
|
||||
const personSlug = (p: any): string =>
|
||||
slugify([p.name?.firstName, p.name?.lastName].filter(Boolean).join(' ').trim() || 'Unknown partner') || p.id;
|
||||
@@ -183,6 +219,7 @@ async function main() {
|
||||
// ---------------------------------------------------------------------
|
||||
// 2. Batched existence lookups against local (one `in` query per object).
|
||||
// ---------------------------------------------------------------------
|
||||
console.log('[import] checking existing records in target workspace...');
|
||||
const partnerSlugs = uniq(tftPeople.map(personSlug));
|
||||
const partnerIdBySlug = new Map<string, string>(
|
||||
partnerSlugs.length
|
||||
@@ -195,17 +232,33 @@ async function main() {
|
||||
: [],
|
||||
);
|
||||
|
||||
const companyNames = uniq([...tftPeople.map((p: any) => p.company?.name), ...tftOpps.map((o: any) => o.company?.name)]);
|
||||
const companyIdByName = new Map<string, string>(
|
||||
companyNames.length
|
||||
? edges(
|
||||
await local.query({
|
||||
companies: { __args: { filter: { name: { in: companyNames } }, first: 500 }, edges: { node: { id: true, name: true } } },
|
||||
} as any),
|
||||
'companies',
|
||||
).map((n: any) => [n.name, n.id])
|
||||
: [],
|
||||
);
|
||||
// Fetch existing companies and index by BOTH name and normalized domain. Twenty
|
||||
// enforces uniqueness on domain, so dedup must be domain-aware: the same company
|
||||
// can arrive under different names (e.g. "Acme" vs "acme.com") across TFT people,
|
||||
// opps and content, and creating a second one collides on the domain constraint.
|
||||
// Page through ALL companies (not just the first 500): these dedupe maps must
|
||||
// be complete, or upsertCompany would create domain-colliding duplicates for
|
||||
// companies that live beyond the first page in a larger workspace.
|
||||
const existingCompanies: any[] = [];
|
||||
let companiesCursor: string | undefined;
|
||||
for (;;) {
|
||||
const page: any = await local.query({
|
||||
companies: {
|
||||
__args: { filter: {}, first: 200, ...(companiesCursor ? { after: companiesCursor } : {}) },
|
||||
edges: { node: { id: true, name: true, domainName: { primaryLinkUrl: true } } },
|
||||
pageInfo: { hasNextPage: true, endCursor: true },
|
||||
},
|
||||
} as any);
|
||||
existingCompanies.push(...edges(page, 'companies'));
|
||||
if (!page?.companies?.pageInfo?.hasNextPage) break;
|
||||
companiesCursor = page.companies.pageInfo.endCursor;
|
||||
}
|
||||
const companyIdByName = new Map<string, string>(existingCompanies.map((n: any) => [n.name, n.id]));
|
||||
const companyIdByDomain = new Map<string, string>();
|
||||
for (const c of existingCompanies) {
|
||||
const nd = normDomain(c.domainName?.primaryLinkUrl);
|
||||
if (nd && !companyIdByDomain.has(nd)) companyIdByDomain.set(nd, c.id);
|
||||
}
|
||||
|
||||
const oppTftIds = uniq(tftOpps.filter((o: any) => o.name).map((o: any) => o.id));
|
||||
const oppIdByTftId = new Map<string, string>(
|
||||
@@ -219,14 +272,18 @@ async function main() {
|
||||
: [],
|
||||
);
|
||||
|
||||
const quoteNames = uniq(quotes.map((q: any) => q.name || 'Partner quote'));
|
||||
const quoteIdByName = new Map<string, string>(
|
||||
quoteNames.length
|
||||
// Unnamed content upserts by name, so a constant fallback would make every
|
||||
// unnamed record collide on one key (collapsing them on re-run). Key the
|
||||
// fallback on the TFT id so each unnamed record stays distinct.
|
||||
const contentName = (c: any): string => c.name || `Partner content ${c.id}`;
|
||||
const contentNames = uniq(contentRecords.map(contentName));
|
||||
const contentIdByName = new Map<string, string>(
|
||||
contentNames.length
|
||||
? edges(
|
||||
await local.query({
|
||||
partnerQuotes: { __args: { filter: { name: { in: quoteNames } }, first: 500 }, edges: { node: { id: true, name: true } } },
|
||||
partnerContents: { __args: { filter: { name: { in: contentNames } }, first: 500 }, edges: { node: { id: true, name: true } } },
|
||||
} as any),
|
||||
'partnerQuotes',
|
||||
'partnerContents',
|
||||
).map((n: any) => [n.name, n.id])
|
||||
: [],
|
||||
);
|
||||
@@ -238,43 +295,102 @@ async function main() {
|
||||
const upsertCompany = async (name?: string, domain?: string): Promise<string | undefined> => {
|
||||
if (!name) return undefined;
|
||||
if (companyIdByName.has(name)) return companyIdByName.get(name);
|
||||
const nd = normDomain(domain);
|
||||
// Same company under a different name but same domain — reuse it.
|
||||
if (nd && companyIdByDomain.has(nd)) {
|
||||
const existingId = companyIdByDomain.get(nd) as string;
|
||||
companyIdByName.set(name, existingId);
|
||||
return existingId;
|
||||
}
|
||||
let id = `dry:company:${name}`;
|
||||
if (APPLY) {
|
||||
await pace();
|
||||
const created: any = await local.mutation({
|
||||
createCompany: {
|
||||
__args: { data: { name, ...(domain ? { domainName: { primaryLinkUrl: domain } } : {}) } },
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
id = created.createCompany.id;
|
||||
try {
|
||||
const created: any = await local.mutation({
|
||||
createCompany: {
|
||||
__args: { data: { name, ...(domain ? { domainName: { primaryLinkUrl: domain } } : {}) } },
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
id = created.createCompany.id;
|
||||
} catch (err) {
|
||||
// Fallback: createCompany failed, almost certainly because the domain
|
||||
// already exists (Twenty enforces a unique domain) on a company we
|
||||
// didn't index. Re-find it and reuse instead of failing the import.
|
||||
// `ilike` is a substring match — and the stored value carries an
|
||||
// https:// prefix so we can't `eq` the normalized form — so it can
|
||||
// return the wrong company ("acme.com" also matches "notacme.com" or
|
||||
// "acme.com.br"). Confirm an exact normalized-domain match before reuse.
|
||||
if (!nd) throw err;
|
||||
await pace();
|
||||
const candidates = edges(
|
||||
await local.query({
|
||||
companies: { __args: { filter: { domainName: { primaryLinkUrl: { ilike: `%${nd}%` } } }, first: 20 }, edges: { node: { id: true, domainName: { primaryLinkUrl: true } } } },
|
||||
} as any),
|
||||
'companies',
|
||||
);
|
||||
const match = candidates.find((c: any) => normDomain(c.domainName?.primaryLinkUrl) === nd);
|
||||
if (!match?.id) throw err;
|
||||
id = match.id;
|
||||
console.log(`[import] company "${name}" reused existing by domain ${nd}`);
|
||||
}
|
||||
}
|
||||
companyIdByName.set(name, id);
|
||||
if (nd) companyIdByDomain.set(nd, id);
|
||||
return id;
|
||||
};
|
||||
|
||||
const budgetCurrency = (amount: any) =>
|
||||
amount?.amountMicros != null ? { amountMicros: amount.amountMicros, currencyCode: amount.currencyCode ?? 'USD' } : undefined;
|
||||
|
||||
// Generic create/update dispatch shared by partners, opportunities and content.
|
||||
// Owns pacing + APPLY-gating + the create-vs-update branch, so each loop below
|
||||
// only builds its `data`. genql keys a mutation by the object name, so the
|
||||
// create<Object>/update<Object> names are derived from one argument. Companies
|
||||
// keep their own upsert (above) because of the domain-collision fallback.
|
||||
// Returns the row id: the real id on APPLY, a synthetic dry id otherwise so the
|
||||
// relation mapping in dry-run still resolves to a stable placeholder.
|
||||
const upsert = async (
|
||||
object: 'Partner' | 'Opportunity' | 'PartnerContent',
|
||||
existingId: string | undefined,
|
||||
data: Record<string, unknown>,
|
||||
dryKey: string,
|
||||
): Promise<string> => {
|
||||
if (!APPLY) return existingId ?? `dry:${object}:${dryKey}`;
|
||||
await pace();
|
||||
if (existingId) {
|
||||
await local.mutation({ [`update${object}`]: { __args: { id: existingId, data }, id: true } } as any);
|
||||
return existingId;
|
||||
}
|
||||
const created: any = await local.mutation({ [`create${object}`]: { __args: { data }, id: true } } as any);
|
||||
return created[`create${object}`].id;
|
||||
};
|
||||
|
||||
// -- Partners (upsert by slug) --
|
||||
console.log(`[import] upserting ${tftPeople.length} partners...`);
|
||||
const localPartnerIdByTftPersonId = new Map<string, string>();
|
||||
let partnersCreated = 0;
|
||||
let partnersUpdated = 0;
|
||||
let partnersDone = 0;
|
||||
for (const p of tftPeople) {
|
||||
note('partnerStage', p.partnerStage);
|
||||
note('partnerTier', p.partnerTier);
|
||||
(Array.isArray(p.partnerScope) ? p.partnerScope : []).forEach((s: string) => note('partnerScope', s));
|
||||
note('typeOfTeam', p.partnerTypeOfTeam);
|
||||
|
||||
const slug = personSlug(p);
|
||||
const companyId = await upsertCompany(p.company?.name, p.company?.domainName?.primaryLinkUrl);
|
||||
// Timezone band -> geographic region(s). Unmapped/OTHER -> no region.
|
||||
const region = TIMEZONE_TO_REGION[p.partnerTimezone] ?? [];
|
||||
// A partner scoped for hosting is, by definition, a self-host expert.
|
||||
const scope = Array.isArray(p.partnerScope) ? p.partnerScope : [];
|
||||
const deploymentExpertise = scope.includes('HOSTING_ENVIRONMENT') ? ['SELF_HOST'] : [];
|
||||
const data: Record<string, unknown> = {
|
||||
name: [p.name?.firstName, p.name?.lastName].filter(Boolean).join(' ').trim() || 'Unknown partner',
|
||||
slug,
|
||||
validationStage: PARTNER_STAGE_TO_VALIDATION[p.partnerStage] ?? 'APPLICATION',
|
||||
availability: p.partnerIsAvailable ? 'AVAILABLE' : 'UNAVAILABLE',
|
||||
// TFT has no language data; default everyone to English.
|
||||
languagesSpoken: ['ENGLISH'],
|
||||
...(p.partnerTier ? { partnerTier: p.partnerTier } : {}),
|
||||
...(Array.isArray(p.partnerScope) && p.partnerScope.length ? { partnerScope: p.partnerScope } : {}),
|
||||
...(scope.length ? { partnerScope: scope } : {}),
|
||||
...(region.length ? { region } : {}),
|
||||
...(deploymentExpertise.length ? { deploymentExpertise } : {}),
|
||||
...(p.partnerTypeOfTeam ? { typeOfTeam: p.partnerTypeOfTeam } : {}),
|
||||
...(Array.isArray(p.partnerSkills) && p.partnerSkills.length ? { skills: p.partnerSkills } : {}),
|
||||
...(p.city ? { city: p.city } : {}),
|
||||
@@ -284,39 +400,29 @@ async function main() {
|
||||
...(companyId && APPLY ? { companyId } : {}),
|
||||
};
|
||||
|
||||
let partnerId = partnerIdBySlug.get(slug);
|
||||
if (partnerId) {
|
||||
if (APPLY) {
|
||||
await pace();
|
||||
await local.mutation({ updatePartner: { __args: { id: partnerId, data }, id: true } } as any);
|
||||
}
|
||||
const existingId = partnerIdBySlug.get(slug);
|
||||
const partnerId = await upsert('Partner', existingId, data, slug);
|
||||
if (existingId) {
|
||||
partnersUpdated++;
|
||||
} else {
|
||||
if (APPLY) {
|
||||
await pace();
|
||||
const created: any = await local.mutation({ createPartner: { __args: { data }, id: true } } as any);
|
||||
partnerId = created.createPartner.id;
|
||||
} else {
|
||||
partnerId = `dry:partner:${slug}`;
|
||||
}
|
||||
partnerIdBySlug.set(slug, partnerId as string);
|
||||
partnerIdBySlug.set(slug, partnerId);
|
||||
partnersCreated++;
|
||||
}
|
||||
localPartnerIdByTftPersonId.set(p.id, partnerId as string);
|
||||
localPartnerIdByTftPersonId.set(p.id, partnerId);
|
||||
partnersDone++;
|
||||
if (partnersDone % 10 === 0 || partnersDone === tftPeople.length)
|
||||
console.log(`[import] partners ${partnersDone}/${tftPeople.length} (created=${partnersCreated} updated=${partnersUpdated})`);
|
||||
}
|
||||
console.log(`[import] partners created=${partnersCreated} updated=${partnersUpdated}`);
|
||||
console.log(`[import] partners done: created=${partnersCreated} updated=${partnersUpdated}`);
|
||||
|
||||
// -- Opportunities (upsert by tftOpportunityId) --
|
||||
console.log(`[import] upserting ${tftOpps.length} opportunities...`);
|
||||
let oppsCreated = 0;
|
||||
let oppsUpdated = 0;
|
||||
let oppsPartnerLinked = 0;
|
||||
let oppsDone = 0;
|
||||
for (const o of tftOpps) {
|
||||
if (!o.name) continue;
|
||||
note('oppStage', o.stage);
|
||||
note('hostingType', o.hostingType);
|
||||
note('subscriptionType', o.subscriptionType);
|
||||
note('subscriptionFrequency', o.subscriptionFrequence);
|
||||
|
||||
const companyId = await upsertCompany(o.company?.name, o.company?.domainName?.primaryLinkUrl);
|
||||
const partnerId = o.partner?.id ? localPartnerIdByTftPersonId.get(o.partner.id) : undefined;
|
||||
if (partnerId) oppsPartnerLinked++;
|
||||
@@ -338,58 +444,44 @@ async function main() {
|
||||
};
|
||||
|
||||
const existingId = oppIdByTftId.get(o.id);
|
||||
if (existingId) {
|
||||
if (APPLY) {
|
||||
await pace();
|
||||
await local.mutation({ updateOpportunity: { __args: { id: existingId, data }, id: true } } as any);
|
||||
}
|
||||
oppsUpdated++;
|
||||
} else {
|
||||
if (APPLY) {
|
||||
await pace();
|
||||
await local.mutation({ createOpportunity: { __args: { data }, id: true } } as any);
|
||||
}
|
||||
oppsCreated++;
|
||||
}
|
||||
await upsert('Opportunity', existingId, data, o.id);
|
||||
if (existingId) oppsUpdated++;
|
||||
else oppsCreated++;
|
||||
oppsDone++;
|
||||
if (oppsDone % 20 === 0 || oppsDone === tftOpps.length)
|
||||
console.log(`[import] opportunities ${oppsDone}/${tftOpps.length} (created=${oppsCreated} updated=${oppsUpdated})`);
|
||||
}
|
||||
console.log(`[import] opportunities created=${oppsCreated} updated=${oppsUpdated} (partner-linked=${oppsPartnerLinked})`);
|
||||
console.log(`[import] opportunities done: created=${oppsCreated} updated=${oppsUpdated} (partner-linked=${oppsPartnerLinked})`);
|
||||
|
||||
// -- Partner quotes (upsert by name) --
|
||||
let quotesCreated = 0;
|
||||
let quotesUpdated = 0;
|
||||
tftContent.forEach((c: any) => (Array.isArray(c.typeCustom) ? c.typeCustom : []).forEach((t: string) => note('typeCustom', t)));
|
||||
for (const q of quotes) {
|
||||
note('quoteStatus', q.status);
|
||||
const name = q.name || 'Partner quote';
|
||||
const partnerId = q.partnerPerson?.id ? localPartnerIdByTftPersonId.get(q.partnerPerson.id) : undefined;
|
||||
// -- Partner content (upsert by name) --
|
||||
console.log(`[import] upserting ${contentRecords.length} content records...`);
|
||||
let contentCreated = 0;
|
||||
let contentUpdated = 0;
|
||||
for (const c of contentRecords) {
|
||||
const name = contentName(c);
|
||||
const partnerId = c.partnerPerson?.id ? localPartnerIdByTftPersonId.get(c.partnerPerson.id) : undefined;
|
||||
const customerCompanyId = await upsertCompany(c.customerCompany?.name, c.customerCompany?.domainName?.primaryLinkUrl);
|
||||
const data: Record<string, unknown> = {
|
||||
name,
|
||||
...(q.status ? { status: q.status } : {}),
|
||||
...(q.approvalDate ? { approvalDate: q.approvalDate } : {}),
|
||||
...(Array.isArray(c.typeCustom) && c.typeCustom.length ? { contentType: c.typeCustom } : {}),
|
||||
...(c.status ? { status: c.status } : {}),
|
||||
...(c.approvalDate ? { approvalDate: c.approvalDate } : {}),
|
||||
...(c.interview?.primaryLinkUrl ? { interview: { primaryLinkUrl: c.interview.primaryLinkUrl } } : {}),
|
||||
...(partnerId && APPLY ? { partnerId } : {}),
|
||||
...(customerCompanyId && APPLY ? { customerCompanyId } : {}),
|
||||
};
|
||||
const existingId = quoteIdByName.get(name);
|
||||
if (existingId) {
|
||||
if (APPLY) {
|
||||
await pace();
|
||||
await local.mutation({ updatePartnerQuote: { __args: { id: existingId, data }, id: true } } as any);
|
||||
}
|
||||
quotesUpdated++;
|
||||
} else {
|
||||
if (APPLY) {
|
||||
await pace();
|
||||
await local.mutation({ createPartnerQuote: { __args: { data }, id: true } } as any);
|
||||
}
|
||||
quotesCreated++;
|
||||
}
|
||||
const existingId = contentIdByName.get(name);
|
||||
await upsert('PartnerContent', existingId, data, name);
|
||||
if (existingId) contentUpdated++;
|
||||
else contentCreated++;
|
||||
}
|
||||
console.log(`[import] partner quotes created=${quotesCreated} updated=${quotesUpdated}`);
|
||||
console.log(`[import] partner content created=${contentCreated} updated=${contentUpdated}`);
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// 4. Preflight: distinct TFT values vs local option coverage.
|
||||
// 4. Preflight: distinct TFT values vs local option coverage. Derived
|
||||
// directly from the fetched source rows (no per-loop bookkeeping).
|
||||
// ---------------------------------------------------------------------
|
||||
const report = (label: string, bucket: string, optionKey?: string) => {
|
||||
const values = [...seen[bucket]].sort();
|
||||
const report = (label: string, values: string[], optionKey?: string) => {
|
||||
const allowed = optionKey ? LOCAL_OPTIONS[optionKey] : undefined;
|
||||
const uncovered = allowed ? values.filter((v) => !allowed.has(v)) : [];
|
||||
console.log(
|
||||
@@ -397,21 +489,27 @@ async function main() {
|
||||
(uncovered.length ? ` ⚠️ NOT IN LOCAL OPTIONS: ${uncovered.join(', ')}` : ''),
|
||||
);
|
||||
};
|
||||
const partnerStages = distinct(tftPeople, (p: any) => p.partnerStage);
|
||||
const oppStages = distinct(tftOpps, (o: any) => o.stage);
|
||||
const timezones = distinct(tftPeople, (p: any) => p.partnerTimezone);
|
||||
console.log('--- preflight: distinct TFT values seen ---');
|
||||
report('partnerStage (-> validationStage map)', 'partnerStage');
|
||||
report('partnerTier', 'partnerTier', 'partnerTier');
|
||||
report('partnerScope', 'partnerScope', 'partnerScope');
|
||||
report('typeOfTeam', 'typeOfTeam', 'typeOfTeam');
|
||||
report('opp stage (-> matchStatus map)', 'oppStage');
|
||||
report('hostingType', 'hostingType', 'hostingType');
|
||||
report('subscriptionType', 'subscriptionType', 'subscriptionType');
|
||||
report('subscriptionFrequency', 'subscriptionFrequency', 'subscriptionFrequency');
|
||||
report('quote status', 'quoteStatus', 'quoteStatus');
|
||||
report('customerContent typeCustom', 'typeCustom');
|
||||
const unmappedStages = [...seen.partnerStage].filter((s) => !(s in PARTNER_STAGE_TO_VALIDATION));
|
||||
const unmappedOpps = [...seen.oppStage].filter((s) => !(s in STAGE_TO_MATCH_STATUS));
|
||||
report('partnerStage (-> validationStage map)', partnerStages);
|
||||
report('partnerTier', distinct(tftPeople, (p: any) => p.partnerTier), 'partnerTier');
|
||||
report('partnerScope', distinct(tftPeople, (p: any) => p.partnerScope), 'partnerScope');
|
||||
report('typeOfTeam', distinct(tftPeople, (p: any) => p.partnerTypeOfTeam), 'typeOfTeam');
|
||||
report('partnerTimezone (-> region map)', timezones);
|
||||
report('opp stage (-> matchStatus map)', oppStages);
|
||||
report('hostingType', distinct(tftOpps, (o: any) => o.hostingType), 'hostingType');
|
||||
report('subscriptionType', distinct(tftOpps, (o: any) => o.subscriptionType), 'subscriptionType');
|
||||
report('subscriptionFrequency', distinct(tftOpps, (o: any) => o.subscriptionFrequence), 'subscriptionFrequency');
|
||||
report('quote status', distinct(contentRecords, (c: any) => c.status), 'quoteStatus');
|
||||
report('customerContent typeCustom', distinct(contentRecords, (c: any) => c.typeCustom));
|
||||
const unmappedStages = partnerStages.filter((s) => !(s in PARTNER_STAGE_TO_VALIDATION));
|
||||
const unmappedOpps = oppStages.filter((s) => !(s in STAGE_TO_MATCH_STATUS));
|
||||
const unmappedTz = timezones.filter((t) => !(t in TIMEZONE_TO_REGION));
|
||||
if (unmappedStages.length) console.log(`[preflight] ⚠️ partnerStage not mapped: ${unmappedStages.join(', ')}`);
|
||||
if (unmappedOpps.length) console.log(`[preflight] ⚠️ opp stage not mapped: ${unmappedOpps.join(', ')}`);
|
||||
if (unmappedTz.length) console.log(`[preflight] ⚠️ partnerTimezone not mapped: ${unmappedTz.join(', ')}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Hard-destroy soft-deleted records that block re-imports.
|
||||
//
|
||||
// Twenty SOFT-deletes (sets deletedAt); the row stays in the DB and keeps holding
|
||||
// unique constraints (e.g. company domain, partner slug). But normal queries —
|
||||
// including the import's existence checks — exclude soft-deleted rows. So after a
|
||||
// UI "delete" or a partial import that got rolled back, re-running the import hits
|
||||
// "A duplicate entry was detected" on records it cannot see. This purges those
|
||||
// ghosts permanently so idempotent upserts work again.
|
||||
//
|
||||
// Only touches soft-deleted rows (deletedAt IS NOT NULL); active/default data is
|
||||
// left untouched. One bulk destroy per object, so it is not rate-limited.
|
||||
//
|
||||
// yarn purge # against .env.local
|
||||
// yarn purge:prod # against .env.prod
|
||||
//
|
||||
import { config } from 'dotenv';
|
||||
config({ path: process.env.ENV_FILE ?? '.env.local' });
|
||||
|
||||
const requireEnv = (name: string): string => {
|
||||
const value = process.env[name];
|
||||
if (!value) throw new Error(`Missing ${name} env var`);
|
||||
return value;
|
||||
};
|
||||
|
||||
// Objects the import writes to. partners + partnerContents are app custom objects;
|
||||
// companies + opportunities are standard but populated by the import.
|
||||
const OBJECTS = ['companies', 'partners', 'opportunities', 'partnerContents'] as const;
|
||||
|
||||
const gql = async (url: string, key: string, query: string): Promise<any> => {
|
||||
const response = await fetch(`${url.replace(/\/$/, '')}/graphql`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query }),
|
||||
});
|
||||
const json: any = await response.json();
|
||||
if (json.errors?.length) throw new Error(JSON.stringify(json.errors));
|
||||
return json.data;
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const url = requireEnv('TWENTY_PARTNERS_API_URL');
|
||||
const key = requireEnv('TWENTY_PARTNERS_API_KEY');
|
||||
console.log(`[purge] target: ${url} — destroying soft-deleted rows only`);
|
||||
|
||||
for (const obj of OBJECTS) {
|
||||
// destroy<Object>s is the bulk variant; capitalise + already-plural names.
|
||||
const mutationName = `destroy${obj.charAt(0).toUpperCase()}${obj.slice(1)}`;
|
||||
const data = await gql(url, key, `mutation { ${mutationName}(filter: { deletedAt: { is: NOT_NULL } }) { id } }`);
|
||||
const destroyed = data[mutationName]?.length ?? 0;
|
||||
console.log(`[purge] ${obj}: destroyed ${destroyed} soft-deleted`);
|
||||
}
|
||||
|
||||
console.log('[purge] done');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -11,7 +11,7 @@
|
||||
// tsx src/scripts/seed.ts
|
||||
|
||||
import { config } from 'dotenv';
|
||||
config({ path: '.env.local' });
|
||||
config({ path: process.env.ENV_FILE ?? '.env.local' });
|
||||
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
@@ -89,13 +89,13 @@ const OPPORTUNITIES: Opp[] = [
|
||||
{ name: 'Sunrise — vendor onboarding', companyName: 'Sunrise Logistics', matchStatus: 'LOST', numberOfSeats: 10, hostingType: 'CLOUD', subscriptionType: 'PRO', subscriptionFrequency: 'MONTHLY' },
|
||||
];
|
||||
|
||||
type Quote = { name: string; status: string; partnerSlug: string; oppName: string };
|
||||
type Quote = { name: string; status: string; partnerSlug: string; contentType: string[] };
|
||||
const QUOTES: Quote[] = [
|
||||
{ name: 'Sunrise APAC fleet — Nine Dots quote', status: 'WIP', partnerSlug: 'nine-dots-ventures', oppName: 'Sunrise — APAC fleet CRM' },
|
||||
{ name: 'Helix clinical — NetZero quote', status: 'INTERVIEW_SCHEDULED', partnerSlug: 'netzero-systems', oppName: 'Helix Bio — clinical trials CRM' },
|
||||
{ name: 'Acme rollout — Elevate quote', status: 'UNDER_CUSTOMER_PARTNER_REVIEW', partnerSlug: 'elevate-consulting', oppName: 'Acme RE — CRM rollout' },
|
||||
{ name: 'Sunrise LATAM — Nine Dots quote', status: 'APPROVED', partnerSlug: 'nine-dots-ventures', oppName: 'Sunrise — LATAM expansion' },
|
||||
{ name: 'Helix self-host — Meridian quote', status: 'REJECTED', partnerSlug: 'meridian-craft', oppName: 'Helix Bio — self-host evaluation' },
|
||||
{ name: 'Sunrise APAC fleet — Nine Dots quote', status: 'WIP', partnerSlug: 'nine-dots-ventures', contentType: ['PARTNER_QUOTE'] },
|
||||
{ name: 'Helix clinical — NetZero quote', status: 'INTERVIEW_SCHEDULED', partnerSlug: 'netzero-systems', contentType: ['PARTNER_QUOTE'] },
|
||||
{ name: 'Acme rollout — Elevate quote', status: 'UNDER_CUSTOMER_PARTNER_REVIEW', partnerSlug: 'elevate-consulting', contentType: ['PARTNER_QUOTE'] },
|
||||
{ name: 'Sunrise LATAM — Nine Dots quote', status: 'APPROVED', partnerSlug: 'nine-dots-ventures', contentType: ['PARTNER_QUOTE'] },
|
||||
{ name: 'Helix self-host — Meridian quote', status: 'REJECTED', partnerSlug: 'meridian-craft', contentType: ['CASE_STUDY'] },
|
||||
];
|
||||
|
||||
const nodes = (r: any, key: string): any[] => (r?.[key]?.edges ?? []).map((e: any) => e.node);
|
||||
@@ -177,12 +177,12 @@ async function main() {
|
||||
// -- Partner quotes (upsert by name) --
|
||||
let quoteCount = 0;
|
||||
for (const q of QUOTES) {
|
||||
const data = { name: q.name, status: q.status, partnerId: partnerIdBySlug.get(q.partnerSlug), opportunityId: oppIdByName.get(q.oppName) };
|
||||
const existing = nodes(await client.query({ partnerQuotes: { __args: { filter: { name: { eq: q.name } }, first: 1 }, edges: { node: { id: true } } } } as any), 'partnerQuotes');
|
||||
const data = { name: q.name, status: q.status, contentType: q.contentType, partnerId: partnerIdBySlug.get(q.partnerSlug) };
|
||||
const existing = nodes(await client.query({ partnerContents: { __args: { filter: { name: { eq: q.name } }, first: 1 }, edges: { node: { id: true } } } } as any), 'partnerContents');
|
||||
if (existing[0]?.id) {
|
||||
await client.mutation({ updatePartnerQuote: { __args: { id: existing[0].id, data }, id: true } } as any);
|
||||
await client.mutation({ updatePartnerContent: { __args: { id: existing[0].id, data }, id: true } } as any);
|
||||
} else {
|
||||
await client.mutation({ createPartnerQuote: { __args: { data }, id: true } } as any);
|
||||
await client.mutation({ createPartnerContent: { __args: { data }, id: true } } as any);
|
||||
}
|
||||
quoteCount++;
|
||||
}
|
||||
|
||||
+9
-9
@@ -1,21 +1,21 @@
|
||||
import { ViewType, defineView } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
PARTNER_QUOTES_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_CONTENT_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
// Index view for partner quotes.
|
||||
// Index view for partner content.
|
||||
export default defineView({
|
||||
universalIdentifier: PARTNER_QUOTES_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Partner quotes',
|
||||
icon: 'IconFileDollar',
|
||||
objectUniversalIdentifier: PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: PARTNER_CONTENT_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Partner content',
|
||||
icon: 'IconQuote',
|
||||
objectUniversalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: ViewType.TABLE,
|
||||
fields: [
|
||||
{ universalIdentifier: 'ad7b3702-b552-4355-afec-1e1e96d9f3df', fieldMetadataUniversalIdentifier: '9e688624-83d2-4715-8b18-80492a6de2b6', position: 0, isVisible: true },
|
||||
{ universalIdentifier: '426e0c2d-449d-4a06-860b-0cfe0ed501e6', fieldMetadataUniversalIdentifier: 'a0fe09c4-c1f4-4b96-93c6-d7ec38f1166a', position: 1, isVisible: true },
|
||||
{ universalIdentifier: '62bb1536-539c-4fb0-95bf-440c5c5da89f', fieldMetadataUniversalIdentifier: '2cbe67e3-24ec-421b-bab5-50f3306c2391', position: 2, isVisible: true },
|
||||
{ universalIdentifier: 'a9bf3eaa-ec27-4a0a-8df2-e18c8f4239a7', fieldMetadataUniversalIdentifier: '1d926e6e-6ac1-4d60-ab3d-a73114005692', position: 1, isVisible: true },
|
||||
{ universalIdentifier: '426e0c2d-449d-4a06-860b-0cfe0ed501e6', fieldMetadataUniversalIdentifier: 'a0fe09c4-c1f4-4b96-93c6-d7ec38f1166a', position: 2, isVisible: true },
|
||||
{ universalIdentifier: 'fbd1f953-1dd2-4d0f-a239-148a0688fbff', fieldMetadataUniversalIdentifier: 'b52d263e-423e-40b0-b82c-29214597c005', position: 3, isVisible: true },
|
||||
],
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"version": "2.7.0",
|
||||
"version": "2.9.0",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@@ -42,14 +42,14 @@
|
||||
"dependencies": {
|
||||
"@genql/cli": "^3.0.3",
|
||||
"@genql/runtime": "^2.10.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild": "^0.28.0",
|
||||
"graphql": "^16.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"twenty-shared": "workspace:*",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
|
||||
@@ -48,9 +48,9 @@ type ApplicationRegistration {
|
||||
isListed: Boolean!
|
||||
isFeatured: Boolean!
|
||||
isPreInstalled: Boolean!
|
||||
logoUrl: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
logo: FileOutput
|
||||
isConfigured: Boolean!
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ type FieldPermission {
|
||||
type RolePermissionFlag {
|
||||
id: UUID!
|
||||
roleId: UUID!
|
||||
flag: PermissionFlagType!
|
||||
flag: String!
|
||||
}
|
||||
|
||||
type ApiKeyForRole {
|
||||
@@ -283,18 +283,11 @@ type Role {
|
||||
rowLevelPermissionPredicateGroups: [RowLevelPermissionPredicateGroup!]
|
||||
}
|
||||
|
||||
type FileOutput {
|
||||
fileId: UUID
|
||||
label: String
|
||||
extension: String
|
||||
url: String!
|
||||
}
|
||||
|
||||
type ApplicationRegistrationSummary {
|
||||
id: UUID!
|
||||
latestAvailableVersion: String
|
||||
sourceType: ApplicationRegistrationSourceType!
|
||||
logo: FileOutput
|
||||
logoUrl: String
|
||||
}
|
||||
|
||||
type ApplicationVariable {
|
||||
@@ -405,6 +398,7 @@ enum EngineComponentKey {
|
||||
FRONT_COMPONENT_RENDERER
|
||||
REPLY_TO_EMAIL_THREAD
|
||||
COMPOSE_EMAIL
|
||||
COMPOSE_CAMPAIGN
|
||||
GO_TO_PEOPLE
|
||||
GO_TO_COMPANIES
|
||||
GO_TO_DASHBOARDS
|
||||
@@ -530,6 +524,7 @@ type IndexField {
|
||||
id: UUID!
|
||||
fieldMetadataId: UUID!
|
||||
order: Float!
|
||||
subFieldName: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
@@ -698,6 +693,7 @@ type Application {
|
||||
id: UUID!
|
||||
name: String!
|
||||
description: String
|
||||
logo: String
|
||||
version: String
|
||||
universalIdentifier: String!
|
||||
packageJsonChecksum: String
|
||||
@@ -717,7 +713,6 @@ type Application {
|
||||
objects: [Object!]!
|
||||
applicationVariables: [ApplicationVariable!]!
|
||||
applicationRegistration: ApplicationRegistrationSummary
|
||||
logo: FileOutput
|
||||
}
|
||||
|
||||
type ViewField {
|
||||
@@ -1106,7 +1101,7 @@ type PageLayoutWidgetCanvasPosition {
|
||||
layoutMode: PageLayoutTabLayoutMode!
|
||||
}
|
||||
|
||||
union WidgetConfiguration = AggregateChartConfiguration | StandaloneRichTextConfiguration | PieChartConfiguration | LineChartConfiguration | IframeConfiguration | GaugeChartConfiguration | BarChartConfiguration | CalendarConfiguration | FrontComponentConfiguration | EmailsConfiguration | EmailThreadConfiguration | FieldConfiguration | FieldRichTextConfiguration | FieldsConfiguration | FilesConfiguration | NotesConfiguration | TasksConfiguration | TimelineConfiguration | ViewConfiguration | RecordTableConfiguration | WorkflowConfiguration | WorkflowRunConfiguration | WorkflowVersionConfiguration
|
||||
union WidgetConfiguration = AggregateChartConfiguration | StandaloneRichTextConfiguration | PieChartConfiguration | LineChartConfiguration | IframeConfiguration | BarChartConfiguration | CalendarConfiguration | FrontComponentConfiguration | EmailsConfiguration | EmailThreadConfiguration | FieldConfiguration | FieldRichTextConfiguration | FieldsConfiguration | FilesConfiguration | NotesConfiguration | TasksConfiguration | TimelineConfiguration | ViewConfiguration | RecordTableConfiguration | WorkflowConfiguration | WorkflowRunConfiguration | WorkflowVersionConfiguration
|
||||
|
||||
type AggregateChartConfiguration {
|
||||
configurationType: WidgetConfigurationType!
|
||||
@@ -1126,7 +1121,6 @@ type AggregateChartConfiguration {
|
||||
|
||||
enum WidgetConfigurationType {
|
||||
AGGREGATE_CHART
|
||||
GAUGE_CHART
|
||||
PIE_CHART
|
||||
BAR_CHART
|
||||
LINE_CHART
|
||||
@@ -1245,18 +1239,6 @@ type IframeConfiguration {
|
||||
url: String
|
||||
}
|
||||
|
||||
type GaugeChartConfiguration {
|
||||
configurationType: WidgetConfigurationType!
|
||||
aggregateFieldMetadataId: UUID!
|
||||
aggregateOperation: AggregateOperations!
|
||||
displayDataLabel: Boolean
|
||||
color: String
|
||||
description: String
|
||||
filter: JSON
|
||||
timezone: String
|
||||
firstDayOfTheWeek: Int
|
||||
}
|
||||
|
||||
type BarChartConfiguration {
|
||||
configurationType: WidgetConfigurationType!
|
||||
aggregateFieldMetadataId: UUID!
|
||||
@@ -1321,6 +1303,7 @@ type FieldConfiguration {
|
||||
configurationType: WidgetConfigurationType!
|
||||
fieldMetadataId: String!
|
||||
fieldDisplayMode: FieldDisplayMode!
|
||||
viewId: String
|
||||
}
|
||||
|
||||
"""Display mode for field configuration widgets"""
|
||||
@@ -1329,6 +1312,7 @@ enum FieldDisplayMode {
|
||||
EDITOR
|
||||
FIELD
|
||||
VIEW
|
||||
TABLE
|
||||
}
|
||||
|
||||
type FieldRichTextConfiguration {
|
||||
@@ -1450,6 +1434,45 @@ type Analytics {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type VerificationRecord {
|
||||
type: String!
|
||||
key: String!
|
||||
value: String!
|
||||
priority: Float
|
||||
}
|
||||
|
||||
type EmailingDomain {
|
||||
id: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
domain: String!
|
||||
driver: EmailingDomainDriver!
|
||||
status: EmailingDomainStatus!
|
||||
verificationRecords: [VerificationRecord!]
|
||||
verifiedAt: DateTime
|
||||
}
|
||||
|
||||
enum EmailingDomainDriver {
|
||||
AWS_SES
|
||||
}
|
||||
|
||||
enum EmailingDomainStatus {
|
||||
PENDING
|
||||
VERIFIED
|
||||
FAILED
|
||||
TEMPORARY_FAILURE
|
||||
}
|
||||
|
||||
type SendMessageBroadcastOutputDTO {
|
||||
broadcastId: String!
|
||||
sentCount: Int!
|
||||
failedCount: Int!
|
||||
}
|
||||
|
||||
type SendEmailViaDomainOutput {
|
||||
messageId: String!
|
||||
}
|
||||
|
||||
type ApprovedAccessDomain {
|
||||
id: UUID!
|
||||
domain: String!
|
||||
@@ -1760,10 +1783,10 @@ enum FeatureFlagKey {
|
||||
IS_JSON_FILTER_ENABLED
|
||||
IS_MARKETPLACE_SETTING_TAB_VISIBLE
|
||||
IS_PUBLIC_DOMAIN_ENABLED
|
||||
IS_EMAILING_DOMAIN_ENABLED
|
||||
IS_EMAIL_GROUP_ENABLED
|
||||
IS_JUNCTION_RELATIONS_ENABLED
|
||||
IS_REST_METADATA_API_NEW_FORMAT_DIRECT
|
||||
IS_SETTINGS_DISCOVERY_HERO_ENABLED
|
||||
}
|
||||
|
||||
type WorkspaceUrls {
|
||||
@@ -1978,7 +2001,7 @@ type CreateApplicationRegistration {
|
||||
type PublicApplicationRegistration {
|
||||
id: UUID!
|
||||
name: String!
|
||||
logo: FileOutput
|
||||
logoUrl: String
|
||||
websiteUrl: String
|
||||
oAuthScopes: [String!]!
|
||||
}
|
||||
@@ -2364,7 +2387,6 @@ type MarketplaceAppDetail {
|
||||
sourceType: ApplicationRegistrationSourceType!
|
||||
sourcePackage: String
|
||||
latestAvailableVersion: String
|
||||
logo: FileOutput
|
||||
isListed: Boolean!
|
||||
isFeatured: Boolean!
|
||||
manifest: JSON
|
||||
@@ -2378,35 +2400,6 @@ type PublicDomain {
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type VerificationRecord {
|
||||
type: String!
|
||||
key: String!
|
||||
value: String!
|
||||
priority: Float
|
||||
}
|
||||
|
||||
type EmailingDomain {
|
||||
id: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
domain: String!
|
||||
driver: EmailingDomainDriver!
|
||||
status: EmailingDomainStatus!
|
||||
verificationRecords: [VerificationRecord!]
|
||||
verifiedAt: DateTime
|
||||
}
|
||||
|
||||
enum EmailingDomainDriver {
|
||||
AWS_SES
|
||||
}
|
||||
|
||||
enum EmailingDomainStatus {
|
||||
PENDING
|
||||
VERIFIED
|
||||
FAILED
|
||||
TEMPORARY_FAILURE
|
||||
}
|
||||
|
||||
type AutocompleteResult {
|
||||
text: String!
|
||||
placeId: String!
|
||||
@@ -2714,6 +2707,12 @@ type AgentTurn {
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type WorkspaceAiStats {
|
||||
conversationsCount: Int!
|
||||
skillsCount: Int!
|
||||
toolsCount: Int!
|
||||
}
|
||||
|
||||
type CalendarChannel {
|
||||
id: UUID!
|
||||
handle: String!
|
||||
@@ -2951,6 +2950,7 @@ type Query {
|
||||
getPageLayoutTab(id: String!): PageLayoutTab!
|
||||
getPageLayouts(objectMetadataId: String, pageLayoutType: PageLayoutType): [PageLayout!]!
|
||||
getPageLayout(id: String!): PageLayout
|
||||
getEmailingDomains: [EmailingDomain!]!
|
||||
applicationConnectionProviders(applicationId: UUID!): [ApplicationConnectionProvider!]!
|
||||
getPageLayoutWidgets(pageLayoutTabId: String!): [PageLayoutWidget!]!
|
||||
getPageLayoutWidget(id: String!): PageLayoutWidget!
|
||||
@@ -3010,6 +3010,7 @@ type Query {
|
||||
myConnectedAccounts: [ConnectedAccountPublicDTO!]!
|
||||
myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
|
||||
minimalMetadata: MinimalMetadata!
|
||||
findWorkspaceAiStats: WorkspaceAiStats!
|
||||
chatThreads: [AgentChatThread!]!
|
||||
chatThread(id: UUID!): AgentChatThread!
|
||||
chatMessages(threadId: UUID!): [AgentMessage!]!
|
||||
@@ -3045,7 +3046,6 @@ type Query {
|
||||
getAddressDetails(placeId: String!, token: String!): PlaceDetailsResult!
|
||||
getUsageAnalytics(input: UsageAnalyticsInput): UsageAnalytics!
|
||||
findManyPublicDomains: [PublicDomain!]!
|
||||
getEmailingDomains: [EmailingDomain!]!
|
||||
findManyMarketplaceApps: [MarketplaceApp!]!
|
||||
findMarketplaceAppDetail(universalIdentifier: String!): MarketplaceAppDetail!
|
||||
}
|
||||
@@ -3200,6 +3200,11 @@ type Mutation {
|
||||
resetPageLayoutToDefault(id: String!): PageLayout!
|
||||
resetPageLayoutWidgetToDefault(id: String!): PageLayoutWidget!
|
||||
resetPageLayoutTabToDefault(id: String!): PageLayoutTab!
|
||||
createEmailingDomain(domain: String!, driver: EmailingDomainDriver!): EmailingDomain!
|
||||
deleteEmailingDomain(id: String!): Boolean!
|
||||
verifyEmailingDomain(id: String!): EmailingDomain!
|
||||
sendEmailViaEmailingDomain(input: SendEmailViaDomainInput!): SendEmailViaDomainOutput!
|
||||
sendMessageBroadcast(input: SendMessageBroadcastInput!): SendMessageBroadcastOutputDTO!
|
||||
updateOneApplicationVariable(key: String!, value: String!, applicationId: UUID!): Boolean!
|
||||
createPageLayoutWidget(input: CreatePageLayoutWidgetInput!): PageLayoutWidget!
|
||||
updatePageLayoutWidget(id: String!, input: UpdatePageLayoutWidgetInput!): PageLayoutWidget!
|
||||
@@ -3217,6 +3222,8 @@ type Mutation {
|
||||
createOneObject(input: CreateOneObjectInput!): Object!
|
||||
deleteOneObject(input: DeleteOneObjectInput!): Object!
|
||||
updateOneObject(input: UpdateOneObjectInput!): Object!
|
||||
createOneIndex(input: CreateOneIndexInput!): Index!
|
||||
deleteOneIndex(input: DeleteOneIndexInput!): Index!
|
||||
createOneAgent(input: CreateAgentInput!): Agent!
|
||||
updateOneAgent(input: UpdateAgentInput!): Agent!
|
||||
deleteOneAgent(input: AgentIdInput!): Agent!
|
||||
@@ -3278,6 +3285,7 @@ type Mutation {
|
||||
authorizeApp(clientId: String!, codeChallenge: String, redirectUrl: String!, state: String, scope: String): AuthorizeApp!
|
||||
renewToken(appToken: String!): AuthTokens!
|
||||
generateApiKeyToken(apiKeyId: UUID!, expiresAt: String!): ApiKeyToken!
|
||||
generatePlaygroundToken: AuthToken!
|
||||
emailPasswordResetLink(email: String!, workspaceId: UUID): EmailPasswordResetLink!
|
||||
updatePasswordViaResetToken(passwordResetToken: String!, newPassword: String!): InvalidatePassword!
|
||||
createApplicationRegistration(input: CreateApplicationRegistrationInput!): CreateApplicationRegistration!
|
||||
@@ -3318,9 +3326,6 @@ type Mutation {
|
||||
updatePublicDomain(domain: String!, applicationId: String): PublicDomain!
|
||||
deletePublicDomain(domain: String!): Boolean!
|
||||
checkPublicDomainValidRecords(domain: String!): DomainValidRecords
|
||||
createEmailingDomain(domain: String!, driver: EmailingDomainDriver!): EmailingDomain!
|
||||
deleteEmailingDomain(id: String!): Boolean!
|
||||
verifyEmailingDomain(id: String!): EmailingDomain!
|
||||
createOneAppToken(input: CreateOneAppTokenInput!): AppToken!
|
||||
installMarketplaceApp(universalIdentifier: String!, version: String): Boolean! @deprecated(reason: "Use installApplication instead")
|
||||
installApplication(universalIdentifier: String!, version: String): Application!
|
||||
@@ -3764,6 +3769,27 @@ input GridPositionInput {
|
||||
columnSpan: Float!
|
||||
}
|
||||
|
||||
input SendEmailViaDomainInput {
|
||||
emailingDomainId: String!
|
||||
to: [String!]!
|
||||
cc: [String!]
|
||||
bcc: [String!]
|
||||
subject: String!
|
||||
text: String!
|
||||
html: String
|
||||
from: String!
|
||||
replyTo: [String!]
|
||||
}
|
||||
|
||||
input SendMessageBroadcastInput {
|
||||
messageTopicId: String!
|
||||
recipientViewId: String
|
||||
segmentId: String
|
||||
subject: String!
|
||||
body: String!
|
||||
fromAddress: String!
|
||||
}
|
||||
|
||||
input CreatePageLayoutWidgetInput {
|
||||
pageLayoutTabId: UUID!
|
||||
title: String!
|
||||
@@ -3933,6 +3959,27 @@ input UpdateObjectPayload {
|
||||
isSearchable: Boolean
|
||||
}
|
||||
|
||||
input CreateOneIndexInput {
|
||||
"""The custom index to create"""
|
||||
index: CreateIndexInput!
|
||||
}
|
||||
|
||||
input CreateIndexInput {
|
||||
objectMetadataId: UUID!
|
||||
fields: [CreateIndexFieldInput!]!
|
||||
indexType: IndexType! = BTREE
|
||||
}
|
||||
|
||||
input CreateIndexFieldInput {
|
||||
fieldMetadataId: UUID!
|
||||
subFieldName: String
|
||||
}
|
||||
|
||||
input DeleteOneIndexInput {
|
||||
"""The id of the custom index to delete."""
|
||||
id: UUID!
|
||||
}
|
||||
|
||||
input CreateAgentInput {
|
||||
name: String
|
||||
label: String!
|
||||
@@ -4013,7 +4060,7 @@ input ObjectPermissionInput {
|
||||
|
||||
input UpsertPermissionFlagsInput {
|
||||
roleId: UUID!
|
||||
permissionFlagKeys: [PermissionFlagType!]!
|
||||
permissionFlagKeys: [String!]!
|
||||
}
|
||||
|
||||
input UpsertFieldPermissionsInput {
|
||||
|
||||
@@ -52,9 +52,9 @@ export interface ApplicationRegistration {
|
||||
isListed: Scalars['Boolean']
|
||||
isFeatured: Scalars['Boolean']
|
||||
isPreInstalled: Scalars['Boolean']
|
||||
logoUrl?: Scalars['String']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
logo?: FileOutput
|
||||
isConfigured: Scalars['Boolean']
|
||||
__typename: 'ApplicationRegistration'
|
||||
}
|
||||
@@ -193,7 +193,7 @@ export interface FieldPermission {
|
||||
export interface RolePermissionFlag {
|
||||
id: Scalars['UUID']
|
||||
roleId: Scalars['UUID']
|
||||
flag: PermissionFlagType
|
||||
flag: Scalars['String']
|
||||
__typename: 'RolePermissionFlag'
|
||||
}
|
||||
|
||||
@@ -232,19 +232,11 @@ export interface Role {
|
||||
__typename: 'Role'
|
||||
}
|
||||
|
||||
export interface FileOutput {
|
||||
fileId?: Scalars['UUID']
|
||||
label?: Scalars['String']
|
||||
extension?: Scalars['String']
|
||||
url: Scalars['String']
|
||||
__typename: 'FileOutput'
|
||||
}
|
||||
|
||||
export interface ApplicationRegistrationSummary {
|
||||
id: Scalars['UUID']
|
||||
latestAvailableVersion?: Scalars['String']
|
||||
sourceType: ApplicationRegistrationSourceType
|
||||
logo?: FileOutput
|
||||
logoUrl?: Scalars['String']
|
||||
__typename: 'ApplicationRegistrationSummary'
|
||||
}
|
||||
|
||||
@@ -312,7 +304,7 @@ export interface CommandMenuItem {
|
||||
__typename: 'CommandMenuItem'
|
||||
}
|
||||
|
||||
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS'
|
||||
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'COMPOSE_CAMPAIGN' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS'
|
||||
|
||||
export type CommandMenuItemAvailabilityType = 'GLOBAL' | 'GLOBAL_OBJECT_CONTEXT' | 'RECORD_SELECTION' | 'FALLBACK'
|
||||
|
||||
@@ -394,6 +386,7 @@ export interface IndexField {
|
||||
id: Scalars['UUID']
|
||||
fieldMetadataId: Scalars['UUID']
|
||||
order: Scalars['Float']
|
||||
subFieldName?: Scalars['String']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'IndexField'
|
||||
@@ -464,6 +457,7 @@ export interface Application {
|
||||
id: Scalars['UUID']
|
||||
name: Scalars['String']
|
||||
description?: Scalars['String']
|
||||
logo?: Scalars['String']
|
||||
version?: Scalars['String']
|
||||
universalIdentifier: Scalars['String']
|
||||
packageJsonChecksum?: Scalars['String']
|
||||
@@ -483,7 +477,6 @@ export interface Application {
|
||||
objects: Object[]
|
||||
applicationVariables: ApplicationVariable[]
|
||||
applicationRegistration?: ApplicationRegistrationSummary
|
||||
logo?: FileOutput
|
||||
__typename: 'Application'
|
||||
}
|
||||
|
||||
@@ -797,7 +790,7 @@ export interface PageLayoutWidgetCanvasPosition {
|
||||
__typename: 'PageLayoutWidgetCanvasPosition'
|
||||
}
|
||||
|
||||
export type WidgetConfiguration = (AggregateChartConfiguration | StandaloneRichTextConfiguration | PieChartConfiguration | LineChartConfiguration | IframeConfiguration | GaugeChartConfiguration | BarChartConfiguration | CalendarConfiguration | FrontComponentConfiguration | EmailsConfiguration | EmailThreadConfiguration | FieldConfiguration | FieldRichTextConfiguration | FieldsConfiguration | FilesConfiguration | NotesConfiguration | TasksConfiguration | TimelineConfiguration | ViewConfiguration | RecordTableConfiguration | WorkflowConfiguration | WorkflowRunConfiguration | WorkflowVersionConfiguration) & { __isUnion?: true }
|
||||
export type WidgetConfiguration = (AggregateChartConfiguration | StandaloneRichTextConfiguration | PieChartConfiguration | LineChartConfiguration | IframeConfiguration | BarChartConfiguration | CalendarConfiguration | FrontComponentConfiguration | EmailsConfiguration | EmailThreadConfiguration | FieldConfiguration | FieldRichTextConfiguration | FieldsConfiguration | FilesConfiguration | NotesConfiguration | TasksConfiguration | TimelineConfiguration | ViewConfiguration | RecordTableConfiguration | WorkflowConfiguration | WorkflowRunConfiguration | WorkflowVersionConfiguration) & { __isUnion?: true }
|
||||
|
||||
export interface AggregateChartConfiguration {
|
||||
configurationType: WidgetConfigurationType
|
||||
@@ -816,7 +809,7 @@ export interface AggregateChartConfiguration {
|
||||
__typename: 'AggregateChartConfiguration'
|
||||
}
|
||||
|
||||
export type WidgetConfigurationType = 'AGGREGATE_CHART' | 'GAUGE_CHART' | 'PIE_CHART' | 'BAR_CHART' | 'LINE_CHART' | 'IFRAME' | 'STANDALONE_RICH_TEXT' | 'VIEW' | 'FIELD' | 'FIELDS' | 'TIMELINE' | 'TASKS' | 'NOTES' | 'FILES' | 'EMAILS' | 'CALENDAR' | 'FIELD_RICH_TEXT' | 'WORKFLOW' | 'WORKFLOW_VERSION' | 'WORKFLOW_RUN' | 'FRONT_COMPONENT' | 'RECORD_TABLE' | 'EMAIL_THREAD'
|
||||
export type WidgetConfigurationType = 'AGGREGATE_CHART' | 'PIE_CHART' | 'BAR_CHART' | 'LINE_CHART' | 'IFRAME' | 'STANDALONE_RICH_TEXT' | 'VIEW' | 'FIELD' | 'FIELDS' | 'TIMELINE' | 'TASKS' | 'NOTES' | 'FILES' | 'EMAILS' | 'CALENDAR' | 'FIELD_RICH_TEXT' | 'WORKFLOW' | 'WORKFLOW_VERSION' | 'WORKFLOW_RUN' | 'FRONT_COMPONENT' | 'RECORD_TABLE' | 'EMAIL_THREAD'
|
||||
|
||||
export interface StandaloneRichTextConfiguration {
|
||||
configurationType: WidgetConfigurationType
|
||||
@@ -895,19 +888,6 @@ export interface IframeConfiguration {
|
||||
__typename: 'IframeConfiguration'
|
||||
}
|
||||
|
||||
export interface GaugeChartConfiguration {
|
||||
configurationType: WidgetConfigurationType
|
||||
aggregateFieldMetadataId: Scalars['UUID']
|
||||
aggregateOperation: AggregateOperations
|
||||
displayDataLabel?: Scalars['Boolean']
|
||||
color?: Scalars['String']
|
||||
description?: Scalars['String']
|
||||
filter?: Scalars['JSON']
|
||||
timezone?: Scalars['String']
|
||||
firstDayOfTheWeek?: Scalars['Int']
|
||||
__typename: 'GaugeChartConfiguration'
|
||||
}
|
||||
|
||||
export interface BarChartConfiguration {
|
||||
configurationType: WidgetConfigurationType
|
||||
aggregateFieldMetadataId: Scalars['UUID']
|
||||
@@ -973,12 +953,13 @@ export interface FieldConfiguration {
|
||||
configurationType: WidgetConfigurationType
|
||||
fieldMetadataId: Scalars['String']
|
||||
fieldDisplayMode: FieldDisplayMode
|
||||
viewId?: Scalars['String']
|
||||
__typename: 'FieldConfiguration'
|
||||
}
|
||||
|
||||
|
||||
/** Display mode for field configuration widgets */
|
||||
export type FieldDisplayMode = 'CARD' | 'EDITOR' | 'FIELD' | 'VIEW'
|
||||
export type FieldDisplayMode = 'CARD' | 'EDITOR' | 'FIELD' | 'VIEW' | 'TABLE'
|
||||
|
||||
export interface FieldRichTextConfiguration {
|
||||
configurationType: WidgetConfigurationType
|
||||
@@ -1113,6 +1094,42 @@ export interface Analytics {
|
||||
__typename: 'Analytics'
|
||||
}
|
||||
|
||||
export interface VerificationRecord {
|
||||
type: Scalars['String']
|
||||
key: Scalars['String']
|
||||
value: Scalars['String']
|
||||
priority?: Scalars['Float']
|
||||
__typename: 'VerificationRecord'
|
||||
}
|
||||
|
||||
export interface EmailingDomain {
|
||||
id: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
domain: Scalars['String']
|
||||
driver: EmailingDomainDriver
|
||||
status: EmailingDomainStatus
|
||||
verificationRecords?: VerificationRecord[]
|
||||
verifiedAt?: Scalars['DateTime']
|
||||
__typename: 'EmailingDomain'
|
||||
}
|
||||
|
||||
export type EmailingDomainDriver = 'AWS_SES'
|
||||
|
||||
export type EmailingDomainStatus = 'PENDING' | 'VERIFIED' | 'FAILED' | 'TEMPORARY_FAILURE'
|
||||
|
||||
export interface SendMessageBroadcastOutputDTO {
|
||||
broadcastId: Scalars['String']
|
||||
sentCount: Scalars['Int']
|
||||
failedCount: Scalars['Int']
|
||||
__typename: 'SendMessageBroadcastOutputDTO'
|
||||
}
|
||||
|
||||
export interface SendEmailViaDomainOutput {
|
||||
messageId: Scalars['String']
|
||||
__typename: 'SendEmailViaDomainOutput'
|
||||
}
|
||||
|
||||
export interface ApprovedAccessDomain {
|
||||
id: Scalars['UUID']
|
||||
domain: Scalars['String']
|
||||
@@ -1401,7 +1418,7 @@ export interface FeatureFlag {
|
||||
__typename: 'FeatureFlag'
|
||||
}
|
||||
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT'
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED'
|
||||
|
||||
export interface WorkspaceUrls {
|
||||
customUrl?: Scalars['String']
|
||||
@@ -1619,7 +1636,7 @@ export interface CreateApplicationRegistration {
|
||||
export interface PublicApplicationRegistration {
|
||||
id: Scalars['UUID']
|
||||
name: Scalars['String']
|
||||
logo?: FileOutput
|
||||
logoUrl?: Scalars['String']
|
||||
websiteUrl?: Scalars['String']
|
||||
oAuthScopes: Scalars['String'][]
|
||||
__typename: 'PublicApplicationRegistration'
|
||||
@@ -2042,7 +2059,6 @@ export interface MarketplaceAppDetail {
|
||||
sourceType: ApplicationRegistrationSourceType
|
||||
sourcePackage?: Scalars['String']
|
||||
latestAvailableVersion?: Scalars['String']
|
||||
logo?: FileOutput
|
||||
isListed: Scalars['Boolean']
|
||||
isFeatured: Scalars['Boolean']
|
||||
manifest?: Scalars['JSON']
|
||||
@@ -2058,30 +2074,6 @@ export interface PublicDomain {
|
||||
__typename: 'PublicDomain'
|
||||
}
|
||||
|
||||
export interface VerificationRecord {
|
||||
type: Scalars['String']
|
||||
key: Scalars['String']
|
||||
value: Scalars['String']
|
||||
priority?: Scalars['Float']
|
||||
__typename: 'VerificationRecord'
|
||||
}
|
||||
|
||||
export interface EmailingDomain {
|
||||
id: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
domain: Scalars['String']
|
||||
driver: EmailingDomainDriver
|
||||
status: EmailingDomainStatus
|
||||
verificationRecords?: VerificationRecord[]
|
||||
verifiedAt?: Scalars['DateTime']
|
||||
__typename: 'EmailingDomain'
|
||||
}
|
||||
|
||||
export type EmailingDomainDriver = 'AWS_SES'
|
||||
|
||||
export type EmailingDomainStatus = 'PENDING' | 'VERIFIED' | 'FAILED' | 'TEMPORARY_FAILURE'
|
||||
|
||||
export interface AutocompleteResult {
|
||||
text: Scalars['String']
|
||||
placeId: Scalars['String']
|
||||
@@ -2425,6 +2417,13 @@ export interface AgentTurn {
|
||||
__typename: 'AgentTurn'
|
||||
}
|
||||
|
||||
export interface WorkspaceAiStats {
|
||||
conversationsCount: Scalars['Int']
|
||||
skillsCount: Scalars['Int']
|
||||
toolsCount: Scalars['Int']
|
||||
__typename: 'WorkspaceAiStats'
|
||||
}
|
||||
|
||||
export interface CalendarChannel {
|
||||
id: Scalars['UUID']
|
||||
handle: Scalars['String']
|
||||
@@ -2579,6 +2578,7 @@ export interface Query {
|
||||
getPageLayoutTab: PageLayoutTab
|
||||
getPageLayouts: PageLayout[]
|
||||
getPageLayout?: PageLayout
|
||||
getEmailingDomains: EmailingDomain[]
|
||||
applicationConnectionProviders: ApplicationConnectionProvider[]
|
||||
getPageLayoutWidgets: PageLayoutWidget[]
|
||||
getPageLayoutWidget: PageLayoutWidget
|
||||
@@ -2611,6 +2611,7 @@ export interface Query {
|
||||
myConnectedAccounts: ConnectedAccountPublicDTO[]
|
||||
myCalendarChannels: CalendarChannel[]
|
||||
minimalMetadata: MinimalMetadata
|
||||
findWorkspaceAiStats: WorkspaceAiStats
|
||||
chatThreads: AgentChatThread[]
|
||||
chatThread: AgentChatThread
|
||||
chatMessages: AgentMessage[]
|
||||
@@ -2646,7 +2647,6 @@ export interface Query {
|
||||
getAddressDetails: PlaceDetailsResult
|
||||
getUsageAnalytics: UsageAnalytics
|
||||
findManyPublicDomains: PublicDomain[]
|
||||
getEmailingDomains: EmailingDomain[]
|
||||
findManyMarketplaceApps: MarketplaceApp[]
|
||||
findMarketplaceAppDetail: MarketplaceAppDetail
|
||||
__typename: 'Query'
|
||||
@@ -2734,6 +2734,11 @@ export interface Mutation {
|
||||
resetPageLayoutToDefault: PageLayout
|
||||
resetPageLayoutWidgetToDefault: PageLayoutWidget
|
||||
resetPageLayoutTabToDefault: PageLayoutTab
|
||||
createEmailingDomain: EmailingDomain
|
||||
deleteEmailingDomain: Scalars['Boolean']
|
||||
verifyEmailingDomain: EmailingDomain
|
||||
sendEmailViaEmailingDomain: SendEmailViaDomainOutput
|
||||
sendMessageBroadcast: SendMessageBroadcastOutputDTO
|
||||
updateOneApplicationVariable: Scalars['Boolean']
|
||||
createPageLayoutWidget: PageLayoutWidget
|
||||
updatePageLayoutWidget: PageLayoutWidget
|
||||
@@ -2751,6 +2756,8 @@ export interface Mutation {
|
||||
createOneObject: Object
|
||||
deleteOneObject: Object
|
||||
updateOneObject: Object
|
||||
createOneIndex: Index
|
||||
deleteOneIndex: Index
|
||||
createOneAgent: Agent
|
||||
updateOneAgent: Agent
|
||||
deleteOneAgent: Agent
|
||||
@@ -2812,6 +2819,7 @@ export interface Mutation {
|
||||
authorizeApp: AuthorizeApp
|
||||
renewToken: AuthTokens
|
||||
generateApiKeyToken: ApiKeyToken
|
||||
generatePlaygroundToken: AuthToken
|
||||
emailPasswordResetLink: EmailPasswordResetLink
|
||||
updatePasswordViaResetToken: InvalidatePassword
|
||||
createApplicationRegistration: CreateApplicationRegistration
|
||||
@@ -2852,9 +2860,6 @@ export interface Mutation {
|
||||
updatePublicDomain: PublicDomain
|
||||
deletePublicDomain: Scalars['Boolean']
|
||||
checkPublicDomainValidRecords?: DomainValidRecords
|
||||
createEmailingDomain: EmailingDomain
|
||||
deleteEmailingDomain: Scalars['Boolean']
|
||||
verifyEmailingDomain: EmailingDomain
|
||||
createOneAppToken: AppToken
|
||||
/** @deprecated Use installApplication instead */
|
||||
installMarketplaceApp: Scalars['Boolean']
|
||||
@@ -2932,9 +2937,9 @@ export interface ApplicationRegistrationGenqlSelection{
|
||||
isListed?: boolean | number
|
||||
isFeatured?: boolean | number
|
||||
isPreInstalled?: boolean | number
|
||||
logoUrl?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
logo?: FileOutputGenqlSelection
|
||||
isConfigured?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
@@ -3105,20 +3110,11 @@ export interface RoleGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface FileOutputGenqlSelection{
|
||||
fileId?: boolean | number
|
||||
label?: boolean | number
|
||||
extension?: boolean | number
|
||||
url?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ApplicationRegistrationSummaryGenqlSelection{
|
||||
id?: boolean | number
|
||||
latestAvailableVersion?: boolean | number
|
||||
sourceType?: boolean | number
|
||||
logo?: FileOutputGenqlSelection
|
||||
logoUrl?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
@@ -3275,6 +3271,7 @@ export interface IndexFieldGenqlSelection{
|
||||
id?: boolean | number
|
||||
fieldMetadataId?: boolean | number
|
||||
order?: boolean | number
|
||||
subFieldName?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
@@ -3383,6 +3380,7 @@ export interface ApplicationGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
logo?: boolean | number
|
||||
version?: boolean | number
|
||||
universalIdentifier?: boolean | number
|
||||
packageJsonChecksum?: boolean | number
|
||||
@@ -3402,7 +3400,6 @@ export interface ApplicationGenqlSelection{
|
||||
objects?: ObjectGenqlSelection
|
||||
applicationVariables?: ApplicationVariableGenqlSelection
|
||||
applicationRegistration?: ApplicationRegistrationSummaryGenqlSelection
|
||||
logo?: FileOutputGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
@@ -3717,7 +3714,6 @@ export interface WidgetConfigurationGenqlSelection{
|
||||
on_PieChartConfiguration?:PieChartConfigurationGenqlSelection,
|
||||
on_LineChartConfiguration?:LineChartConfigurationGenqlSelection,
|
||||
on_IframeConfiguration?:IframeConfigurationGenqlSelection,
|
||||
on_GaugeChartConfiguration?:GaugeChartConfigurationGenqlSelection,
|
||||
on_BarChartConfiguration?:BarChartConfigurationGenqlSelection,
|
||||
on_CalendarConfiguration?:CalendarConfigurationGenqlSelection,
|
||||
on_FrontComponentConfiguration?:FrontComponentConfigurationGenqlSelection,
|
||||
@@ -3825,20 +3821,6 @@ export interface IframeConfigurationGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface GaugeChartConfigurationGenqlSelection{
|
||||
configurationType?: boolean | number
|
||||
aggregateFieldMetadataId?: boolean | number
|
||||
aggregateOperation?: boolean | number
|
||||
displayDataLabel?: boolean | number
|
||||
color?: boolean | number
|
||||
description?: boolean | number
|
||||
filter?: boolean | number
|
||||
timezone?: boolean | number
|
||||
firstDayOfTheWeek?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface BarChartConfigurationGenqlSelection{
|
||||
configurationType?: boolean | number
|
||||
aggregateFieldMetadataId?: boolean | number
|
||||
@@ -3901,6 +3883,7 @@ export interface FieldConfigurationGenqlSelection{
|
||||
configurationType?: boolean | number
|
||||
fieldMetadataId?: boolean | number
|
||||
fieldDisplayMode?: boolean | number
|
||||
viewId?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
@@ -4054,6 +4037,42 @@ export interface AnalyticsGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface VerificationRecordGenqlSelection{
|
||||
type?: boolean | number
|
||||
key?: boolean | number
|
||||
value?: boolean | number
|
||||
priority?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EmailingDomainGenqlSelection{
|
||||
id?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
domain?: boolean | number
|
||||
driver?: boolean | number
|
||||
status?: boolean | number
|
||||
verificationRecords?: VerificationRecordGenqlSelection
|
||||
verifiedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface SendMessageBroadcastOutputDTOGenqlSelection{
|
||||
broadcastId?: boolean | number
|
||||
sentCount?: boolean | number
|
||||
failedCount?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface SendEmailViaDomainOutputGenqlSelection{
|
||||
messageId?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ApprovedAccessDomainGenqlSelection{
|
||||
id?: boolean | number
|
||||
domain?: boolean | number
|
||||
@@ -4573,7 +4592,7 @@ export interface CreateApplicationRegistrationGenqlSelection{
|
||||
export interface PublicApplicationRegistrationGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
logo?: FileOutputGenqlSelection
|
||||
logoUrl?: boolean | number
|
||||
websiteUrl?: boolean | number
|
||||
oAuthScopes?: boolean | number
|
||||
__typename?: boolean | number
|
||||
@@ -5048,7 +5067,6 @@ export interface MarketplaceAppDetailGenqlSelection{
|
||||
sourceType?: boolean | number
|
||||
sourcePackage?: boolean | number
|
||||
latestAvailableVersion?: boolean | number
|
||||
logo?: FileOutputGenqlSelection
|
||||
isListed?: boolean | number
|
||||
isFeatured?: boolean | number
|
||||
manifest?: boolean | number
|
||||
@@ -5066,28 +5084,6 @@ export interface PublicDomainGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface VerificationRecordGenqlSelection{
|
||||
type?: boolean | number
|
||||
key?: boolean | number
|
||||
value?: boolean | number
|
||||
priority?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EmailingDomainGenqlSelection{
|
||||
id?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
domain?: boolean | number
|
||||
driver?: boolean | number
|
||||
status?: boolean | number
|
||||
verificationRecords?: VerificationRecordGenqlSelection
|
||||
verifiedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AutocompleteResultGenqlSelection{
|
||||
text?: boolean | number
|
||||
placeId?: boolean | number
|
||||
@@ -5467,6 +5463,14 @@ export interface AgentTurnGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface WorkspaceAiStatsGenqlSelection{
|
||||
conversationsCount?: boolean | number
|
||||
skillsCount?: boolean | number
|
||||
toolsCount?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface CalendarChannelGenqlSelection{
|
||||
id?: boolean | number
|
||||
handle?: boolean | number
|
||||
@@ -5603,6 +5607,7 @@ export interface QueryGenqlSelection{
|
||||
getPageLayoutTab?: (PageLayoutTabGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
getPageLayouts?: (PageLayoutGenqlSelection & { __args?: {objectMetadataId?: (Scalars['String'] | null), pageLayoutType?: (PageLayoutType | null)} })
|
||||
getPageLayout?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
getEmailingDomains?: EmailingDomainGenqlSelection
|
||||
applicationConnectionProviders?: (ApplicationConnectionProviderGenqlSelection & { __args: {applicationId: Scalars['UUID']} })
|
||||
getPageLayoutWidgets?: (PageLayoutWidgetGenqlSelection & { __args: {pageLayoutTabId: Scalars['String']} })
|
||||
getPageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
@@ -5653,6 +5658,7 @@ export interface QueryGenqlSelection{
|
||||
myConnectedAccounts?: ConnectedAccountPublicDTOGenqlSelection
|
||||
myCalendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
minimalMetadata?: MinimalMetadataGenqlSelection
|
||||
findWorkspaceAiStats?: WorkspaceAiStatsGenqlSelection
|
||||
chatThreads?: AgentChatThreadGenqlSelection
|
||||
chatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
chatMessages?: (AgentMessageGenqlSelection & { __args: {threadId: Scalars['UUID']} })
|
||||
@@ -5688,7 +5694,6 @@ export interface QueryGenqlSelection{
|
||||
getAddressDetails?: (PlaceDetailsResultGenqlSelection & { __args: {placeId: Scalars['String'], token: Scalars['String']} })
|
||||
getUsageAnalytics?: (UsageAnalyticsGenqlSelection & { __args?: {input?: (UsageAnalyticsInput | null)} })
|
||||
findManyPublicDomains?: PublicDomainGenqlSelection
|
||||
getEmailingDomains?: EmailingDomainGenqlSelection
|
||||
findManyMarketplaceApps?: MarketplaceAppGenqlSelection
|
||||
findMarketplaceAppDetail?: (MarketplaceAppDetailGenqlSelection & { __args: {universalIdentifier: Scalars['String']} })
|
||||
__typename?: boolean | number
|
||||
@@ -5797,6 +5802,11 @@ export interface MutationGenqlSelection{
|
||||
resetPageLayoutToDefault?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
resetPageLayoutWidgetToDefault?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
resetPageLayoutTabToDefault?: (PageLayoutTabGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
createEmailingDomain?: (EmailingDomainGenqlSelection & { __args: {domain: Scalars['String'], driver: EmailingDomainDriver} })
|
||||
deleteEmailingDomain?: { __args: {id: Scalars['String']} }
|
||||
verifyEmailingDomain?: (EmailingDomainGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
sendEmailViaEmailingDomain?: (SendEmailViaDomainOutputGenqlSelection & { __args: {input: SendEmailViaDomainInput} })
|
||||
sendMessageBroadcast?: (SendMessageBroadcastOutputDTOGenqlSelection & { __args: {input: SendMessageBroadcastInput} })
|
||||
updateOneApplicationVariable?: { __args: {key: Scalars['String'], value: Scalars['String'], applicationId: Scalars['UUID']} }
|
||||
createPageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {input: CreatePageLayoutWidgetInput} })
|
||||
updatePageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutWidgetInput} })
|
||||
@@ -5814,6 +5824,8 @@ export interface MutationGenqlSelection{
|
||||
createOneObject?: (ObjectGenqlSelection & { __args: {input: CreateOneObjectInput} })
|
||||
deleteOneObject?: (ObjectGenqlSelection & { __args: {input: DeleteOneObjectInput} })
|
||||
updateOneObject?: (ObjectGenqlSelection & { __args: {input: UpdateOneObjectInput} })
|
||||
createOneIndex?: (IndexGenqlSelection & { __args: {input: CreateOneIndexInput} })
|
||||
deleteOneIndex?: (IndexGenqlSelection & { __args: {input: DeleteOneIndexInput} })
|
||||
createOneAgent?: (AgentGenqlSelection & { __args: {input: CreateAgentInput} })
|
||||
updateOneAgent?: (AgentGenqlSelection & { __args: {input: UpdateAgentInput} })
|
||||
deleteOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} })
|
||||
@@ -5875,6 +5887,7 @@ export interface MutationGenqlSelection{
|
||||
authorizeApp?: (AuthorizeAppGenqlSelection & { __args: {clientId: Scalars['String'], codeChallenge?: (Scalars['String'] | null), redirectUrl: Scalars['String'], state?: (Scalars['String'] | null), scope?: (Scalars['String'] | null)} })
|
||||
renewToken?: (AuthTokensGenqlSelection & { __args: {appToken: Scalars['String']} })
|
||||
generateApiKeyToken?: (ApiKeyTokenGenqlSelection & { __args: {apiKeyId: Scalars['UUID'], expiresAt: Scalars['String']} })
|
||||
generatePlaygroundToken?: AuthTokenGenqlSelection
|
||||
emailPasswordResetLink?: (EmailPasswordResetLinkGenqlSelection & { __args: {email: Scalars['String'], workspaceId?: (Scalars['UUID'] | null)} })
|
||||
updatePasswordViaResetToken?: (InvalidatePasswordGenqlSelection & { __args: {passwordResetToken: Scalars['String'], newPassword: Scalars['String']} })
|
||||
createApplicationRegistration?: (CreateApplicationRegistrationGenqlSelection & { __args: {input: CreateApplicationRegistrationInput} })
|
||||
@@ -5915,9 +5928,6 @@ export interface MutationGenqlSelection{
|
||||
updatePublicDomain?: (PublicDomainGenqlSelection & { __args: {domain: Scalars['String'], applicationId?: (Scalars['String'] | null)} })
|
||||
deletePublicDomain?: { __args: {domain: Scalars['String']} }
|
||||
checkPublicDomainValidRecords?: (DomainValidRecordsGenqlSelection & { __args: {domain: Scalars['String']} })
|
||||
createEmailingDomain?: (EmailingDomainGenqlSelection & { __args: {domain: Scalars['String'], driver: EmailingDomainDriver} })
|
||||
deleteEmailingDomain?: { __args: {id: Scalars['String']} }
|
||||
verifyEmailingDomain?: (EmailingDomainGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
createOneAppToken?: (AppTokenGenqlSelection & { __args: {input: CreateOneAppTokenInput} })
|
||||
/** @deprecated Use installApplication instead */
|
||||
installMarketplaceApp?: { __args: {universalIdentifier: Scalars['String'], version?: (Scalars['String'] | null)} }
|
||||
@@ -6095,6 +6105,10 @@ export interface UpdatePageLayoutWidgetWithIdInput {id: Scalars['UUID'],pageLayo
|
||||
|
||||
export interface GridPositionInput {row: Scalars['Float'],column: Scalars['Float'],rowSpan: Scalars['Float'],columnSpan: Scalars['Float']}
|
||||
|
||||
export interface SendEmailViaDomainInput {emailingDomainId: Scalars['String'],to: Scalars['String'][],cc?: (Scalars['String'][] | null),bcc?: (Scalars['String'][] | null),subject: Scalars['String'],text: Scalars['String'],html?: (Scalars['String'] | null),from: Scalars['String'],replyTo?: (Scalars['String'][] | null)}
|
||||
|
||||
export interface SendMessageBroadcastInput {messageTopicId: Scalars['String'],recipientViewId?: (Scalars['String'] | null),segmentId?: (Scalars['String'] | null),subject: Scalars['String'],body: Scalars['String'],fromAddress: Scalars['String']}
|
||||
|
||||
export interface CreatePageLayoutWidgetInput {pageLayoutTabId: Scalars['UUID'],title: Scalars['String'],type: WidgetType,objectMetadataId?: (Scalars['UUID'] | null),gridPosition: GridPositionInput,position?: (Scalars['JSON'] | null),configuration: Scalars['JSON']}
|
||||
|
||||
export interface UpdatePageLayoutWidgetInput {pageLayoutTabId?: (Scalars['UUID'] | null),title?: (Scalars['String'] | null),type?: (WidgetType | null),objectMetadataId?: (Scalars['UUID'] | null),gridPosition?: (GridPositionInput | null),position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null)}
|
||||
@@ -6145,6 +6159,18 @@ id: Scalars['UUID']}
|
||||
|
||||
export interface UpdateObjectPayload {labelSingular?: (Scalars['String'] | null),labelPlural?: (Scalars['String'] | null),nameSingular?: (Scalars['String'] | null),namePlural?: (Scalars['String'] | null),description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortcut?: (Scalars['String'] | null),color?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null),labelIdentifierFieldMetadataId?: (Scalars['UUID'] | null),imageIdentifierFieldMetadataId?: (Scalars['UUID'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null),isSearchable?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface CreateOneIndexInput {
|
||||
/** The custom index to create */
|
||||
index: CreateIndexInput}
|
||||
|
||||
export interface CreateIndexInput {objectMetadataId: Scalars['UUID'],fields: CreateIndexFieldInput[],indexType: IndexType}
|
||||
|
||||
export interface CreateIndexFieldInput {fieldMetadataId: Scalars['UUID'],subFieldName?: (Scalars['String'] | null)}
|
||||
|
||||
export interface DeleteOneIndexInput {
|
||||
/** The id of the custom index to delete. */
|
||||
id: Scalars['UUID']}
|
||||
|
||||
export interface CreateAgentInput {name?: (Scalars['String'] | null),label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt: Scalars['String'],modelId: Scalars['String'],roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)}
|
||||
|
||||
export interface UpdateAgentInput {id: Scalars['UUID'],name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt?: (Scalars['String'] | null),modelId?: (Scalars['String'] | null),roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)}
|
||||
@@ -6161,7 +6187,7 @@ export interface UpsertObjectPermissionsInput {roleId: Scalars['UUID'],objectPer
|
||||
|
||||
export interface ObjectPermissionInput {objectMetadataId: Scalars['UUID'],canReadObjectRecords?: (Scalars['Boolean'] | null),canUpdateObjectRecords?: (Scalars['Boolean'] | null),canSoftDeleteObjectRecords?: (Scalars['Boolean'] | null),canDestroyObjectRecords?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface UpsertPermissionFlagsInput {roleId: Scalars['UUID'],permissionFlagKeys: PermissionFlagType[]}
|
||||
export interface UpsertPermissionFlagsInput {roleId: Scalars['UUID'],permissionFlagKeys: Scalars['String'][]}
|
||||
|
||||
export interface UpsertFieldPermissionsInput {roleId: Scalars['UUID'],fieldPermissions: FieldPermissionInput[]}
|
||||
|
||||
@@ -6428,14 +6454,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const FileOutput_possibleTypes: string[] = ['FileOutput']
|
||||
export const isFileOutput = (obj?: { __typename?: any } | null): obj is FileOutput => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isFileOutput"')
|
||||
return FileOutput_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ApplicationRegistrationSummary_possibleTypes: string[] = ['ApplicationRegistrationSummary']
|
||||
export const isApplicationRegistrationSummary = (obj?: { __typename?: any } | null): obj is ApplicationRegistrationSummary => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isApplicationRegistrationSummary"')
|
||||
@@ -6716,7 +6734,7 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const WidgetConfiguration_possibleTypes: string[] = ['AggregateChartConfiguration','StandaloneRichTextConfiguration','PieChartConfiguration','LineChartConfiguration','IframeConfiguration','GaugeChartConfiguration','BarChartConfiguration','CalendarConfiguration','FrontComponentConfiguration','EmailsConfiguration','EmailThreadConfiguration','FieldConfiguration','FieldRichTextConfiguration','FieldsConfiguration','FilesConfiguration','NotesConfiguration','TasksConfiguration','TimelineConfiguration','ViewConfiguration','RecordTableConfiguration','WorkflowConfiguration','WorkflowRunConfiguration','WorkflowVersionConfiguration']
|
||||
const WidgetConfiguration_possibleTypes: string[] = ['AggregateChartConfiguration','StandaloneRichTextConfiguration','PieChartConfiguration','LineChartConfiguration','IframeConfiguration','BarChartConfiguration','CalendarConfiguration','FrontComponentConfiguration','EmailsConfiguration','EmailThreadConfiguration','FieldConfiguration','FieldRichTextConfiguration','FieldsConfiguration','FilesConfiguration','NotesConfiguration','TasksConfiguration','TimelineConfiguration','ViewConfiguration','RecordTableConfiguration','WorkflowConfiguration','WorkflowRunConfiguration','WorkflowVersionConfiguration']
|
||||
export const isWidgetConfiguration = (obj?: { __typename?: any } | null): obj is WidgetConfiguration => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isWidgetConfiguration"')
|
||||
return WidgetConfiguration_possibleTypes.includes(obj.__typename)
|
||||
@@ -6764,14 +6782,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const GaugeChartConfiguration_possibleTypes: string[] = ['GaugeChartConfiguration']
|
||||
export const isGaugeChartConfiguration = (obj?: { __typename?: any } | null): obj is GaugeChartConfiguration => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isGaugeChartConfiguration"')
|
||||
return GaugeChartConfiguration_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const BarChartConfiguration_possibleTypes: string[] = ['BarChartConfiguration']
|
||||
export const isBarChartConfiguration = (obj?: { __typename?: any } | null): obj is BarChartConfiguration => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isBarChartConfiguration"')
|
||||
@@ -6964,6 +6974,38 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const VerificationRecord_possibleTypes: string[] = ['VerificationRecord']
|
||||
export const isVerificationRecord = (obj?: { __typename?: any } | null): obj is VerificationRecord => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isVerificationRecord"')
|
||||
return VerificationRecord_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EmailingDomain_possibleTypes: string[] = ['EmailingDomain']
|
||||
export const isEmailingDomain = (obj?: { __typename?: any } | null): obj is EmailingDomain => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEmailingDomain"')
|
||||
return EmailingDomain_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const SendMessageBroadcastOutputDTO_possibleTypes: string[] = ['SendMessageBroadcastOutputDTO']
|
||||
export const isSendMessageBroadcastOutputDTO = (obj?: { __typename?: any } | null): obj is SendMessageBroadcastOutputDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isSendMessageBroadcastOutputDTO"')
|
||||
return SendMessageBroadcastOutputDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const SendEmailViaDomainOutput_possibleTypes: string[] = ['SendEmailViaDomainOutput']
|
||||
export const isSendEmailViaDomainOutput = (obj?: { __typename?: any } | null): obj is SendEmailViaDomainOutput => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isSendEmailViaDomainOutput"')
|
||||
return SendEmailViaDomainOutput_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ApprovedAccessDomain_possibleTypes: string[] = ['ApprovedAccessDomain']
|
||||
export const isApprovedAccessDomain = (obj?: { __typename?: any } | null): obj is ApprovedAccessDomain => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isApprovedAccessDomain"')
|
||||
@@ -7868,22 +7910,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const VerificationRecord_possibleTypes: string[] = ['VerificationRecord']
|
||||
export const isVerificationRecord = (obj?: { __typename?: any } | null): obj is VerificationRecord => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isVerificationRecord"')
|
||||
return VerificationRecord_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EmailingDomain_possibleTypes: string[] = ['EmailingDomain']
|
||||
export const isEmailingDomain = (obj?: { __typename?: any } | null): obj is EmailingDomain => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEmailingDomain"')
|
||||
return EmailingDomain_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const AutocompleteResult_possibleTypes: string[] = ['AutocompleteResult']
|
||||
export const isAutocompleteResult = (obj?: { __typename?: any } | null): obj is AutocompleteResult => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAutocompleteResult"')
|
||||
@@ -8172,6 +8198,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const WorkspaceAiStats_possibleTypes: string[] = ['WorkspaceAiStats']
|
||||
export const isWorkspaceAiStats = (obj?: { __typename?: any } | null): obj is WorkspaceAiStats => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isWorkspaceAiStats"')
|
||||
return WorkspaceAiStats_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const CalendarChannel_possibleTypes: string[] = ['CalendarChannel']
|
||||
export const isCalendarChannel = (obj?: { __typename?: any } | null): obj is CalendarChannel => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isCalendarChannel"')
|
||||
@@ -8388,6 +8422,7 @@ export const enumEngineComponentKey = {
|
||||
FRONT_COMPONENT_RENDERER: 'FRONT_COMPONENT_RENDERER' as const,
|
||||
REPLY_TO_EMAIL_THREAD: 'REPLY_TO_EMAIL_THREAD' as const,
|
||||
COMPOSE_EMAIL: 'COMPOSE_EMAIL' as const,
|
||||
COMPOSE_CAMPAIGN: 'COMPOSE_CAMPAIGN' as const,
|
||||
GO_TO_PEOPLE: 'GO_TO_PEOPLE' as const,
|
||||
GO_TO_COMPANIES: 'GO_TO_COMPANIES' as const,
|
||||
GO_TO_DASHBOARDS: 'GO_TO_DASHBOARDS' as const,
|
||||
@@ -8569,7 +8604,6 @@ export const enumPageLayoutTabLayoutMode = {
|
||||
|
||||
export const enumWidgetConfigurationType = {
|
||||
AGGREGATE_CHART: 'AGGREGATE_CHART' as const,
|
||||
GAUGE_CHART: 'GAUGE_CHART' as const,
|
||||
PIE_CHART: 'PIE_CHART' as const,
|
||||
BAR_CHART: 'BAR_CHART' as const,
|
||||
LINE_CHART: 'LINE_CHART' as const,
|
||||
@@ -8636,7 +8670,8 @@ export const enumFieldDisplayMode = {
|
||||
CARD: 'CARD' as const,
|
||||
EDITOR: 'EDITOR' as const,
|
||||
FIELD: 'FIELD' as const,
|
||||
VIEW: 'VIEW' as const
|
||||
VIEW: 'VIEW' as const,
|
||||
TABLE: 'TABLE' as const
|
||||
}
|
||||
|
||||
export const enumPageLayoutType = {
|
||||
@@ -8646,6 +8681,17 @@ export const enumPageLayoutType = {
|
||||
STANDALONE_PAGE: 'STANDALONE_PAGE' as const
|
||||
}
|
||||
|
||||
export const enumEmailingDomainDriver = {
|
||||
AWS_SES: 'AWS_SES' as const
|
||||
}
|
||||
|
||||
export const enumEmailingDomainStatus = {
|
||||
PENDING: 'PENDING' as const,
|
||||
VERIFIED: 'VERIFIED' as const,
|
||||
FAILED: 'FAILED' as const,
|
||||
TEMPORARY_FAILURE: 'TEMPORARY_FAILURE' as const
|
||||
}
|
||||
|
||||
export const enumBillingPlanKey = {
|
||||
PRO: 'PRO' as const,
|
||||
ENTERPRISE: 'ENTERPRISE' as const
|
||||
@@ -8712,10 +8758,10 @@ export const enumFeatureFlagKey = {
|
||||
IS_JSON_FILTER_ENABLED: 'IS_JSON_FILTER_ENABLED' as const,
|
||||
IS_MARKETPLACE_SETTING_TAB_VISIBLE: 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' as const,
|
||||
IS_PUBLIC_DOMAIN_ENABLED: 'IS_PUBLIC_DOMAIN_ENABLED' as const,
|
||||
IS_EMAILING_DOMAIN_ENABLED: 'IS_EMAILING_DOMAIN_ENABLED' as const,
|
||||
IS_EMAIL_GROUP_ENABLED: 'IS_EMAIL_GROUP_ENABLED' as const,
|
||||
IS_JUNCTION_RELATIONS_ENABLED: 'IS_JUNCTION_RELATIONS_ENABLED' as const,
|
||||
IS_REST_METADATA_API_NEW_FORMAT_DIRECT: 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' as const
|
||||
IS_REST_METADATA_API_NEW_FORMAT_DIRECT: 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' as const,
|
||||
IS_SETTINGS_DISCOVERY_HERO_ENABLED: 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' as const
|
||||
}
|
||||
|
||||
export const enumIdentityProviderType = {
|
||||
@@ -8759,17 +8805,6 @@ export const enumBillingEntitlementKey = {
|
||||
AUDIT_LOGS: 'AUDIT_LOGS' as const
|
||||
}
|
||||
|
||||
export const enumEmailingDomainDriver = {
|
||||
AWS_SES: 'AWS_SES' as const
|
||||
}
|
||||
|
||||
export const enumEmailingDomainStatus = {
|
||||
PENDING: 'PENDING' as const,
|
||||
VERIFIED: 'VERIFIED' as const,
|
||||
FAILED: 'FAILED' as const,
|
||||
TEMPORARY_FAILURE: 'TEMPORARY_FAILURE' as const
|
||||
}
|
||||
|
||||
export const enumCalendarChannelSyncStatus = {
|
||||
NOT_SYNCED: 'NOT_SYNCED' as const,
|
||||
ONGOING: 'ONGOING' as const,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@ export default defineField({
|
||||
- When defining fields **inline inside `defineObject()`**, you do **not** need `objectUniversalIdentifier` — it's inherited from the parent object.
|
||||
- `defineField()` is the only way to add fields to objects you didn't create with `defineObject()`.
|
||||
- File location is up to you. The convention is `src/fields/<name>.field.ts`, but the SDK detects fields anywhere in `src/`.
|
||||
- To add a tab to a standard page layout (e.g. the Task or Company detail page), use [`definePageLayoutTab`](/developers/extend/apps/layout/page-layouts#definepagelayouttab) with `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` from `twenty-sdk/define`.
|
||||
|
||||
## Adding a relation to an existing object
|
||||
|
||||
|
||||
@@ -44,8 +44,53 @@ A Twenty app's **data layer** is the data your app *adds* to a workspace — the
|
||||
| **Object** | A new custom record type (e.g. PostCard, Invoice) with its own fields | `defineObject()` |
|
||||
| **Field** | A column on an object. Standalone fields can extend objects you didn't create (e.g. add `loyaltyTier` to Company) | `defineField()` |
|
||||
| **Relation** | A bidirectional link between two objects — both sides declared as fields | `defineField()` with `FieldType.RELATION` |
|
||||
| **Index** | A database index to speed up a recurring query on one of your objects | `defineIndex()` |
|
||||
|
||||
The SDK detects these via AST analysis at build time, so file organization is up to you — the convention is `src/objects/` and `src/fields/`. Stable `universalIdentifier` UUIDs tie everything together across deploys.
|
||||
The SDK detects these via AST analysis at build time, so file organization is up to you — the convention is `src/objects/`, `src/fields/`, and `src/indexes/`. Stable `universalIdentifier` UUIDs tie everything together across deploys.
|
||||
|
||||
## Indexes (optional)
|
||||
|
||||
Apps can ship indexes alongside their objects to keep recurring queries fast. The most common case is a status or foreign-key column that you read frequently.
|
||||
|
||||
```ts src/indexes/post-card-status.index.ts
|
||||
import { defineIndex } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from '../objects/post-card.object';
|
||||
|
||||
export default defineIndex({
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
|
||||
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Unique indexes
|
||||
|
||||
`defineIndex` accepts `isUnique: true` for both single- and multi-column uniqueness. This is the recommended primitive — `defineField({ isUnique: true })` is deprecated and will be removed in a future release.
|
||||
|
||||
```ts
|
||||
defineIndex({
|
||||
universalIdentifier: '…',
|
||||
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
|
||||
isUnique: true,
|
||||
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
|
||||
});
|
||||
```
|
||||
|
||||
### Other constraints
|
||||
|
||||
- Partial `WHERE` clauses stay under admin control — apps can't declare them.
|
||||
- Each object is capped at 10 custom indexes (the framework's own indexes don't count).
|
||||
|
||||
Order the `fields` array the way Postgres should use it — leftmost column first, like a phone book. Indexes are not free: every write to the table updates them. Add one only when you have a query that needs it.
|
||||
|
||||
<Note>
|
||||
Looking for **Application Config** or **Roles & Permissions**? Those describe the app itself rather than the data it adds — they live under [Config](/developers/extend/apps/config/overview). Looking for **Connections** (Linear, GitHub, Slack OAuth)? Those exist to be called *from* logic functions and live under [Logic](/developers/extend/apps/logic/connections).
|
||||
|
||||
@@ -103,6 +103,10 @@ export default defineCommandMenuItem({
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`RECORD_SELECTION` already implies a non-empty selection — use `numberOfSelectedRecords` only for specific counts (e.g. `>= 2`).
|
||||
</Note>
|
||||
|
||||
### Context variables
|
||||
|
||||
These represent the current state of the page:
|
||||
|
||||
@@ -196,6 +196,94 @@ export default defineFrontComponent({
|
||||
});
|
||||
```
|
||||
|
||||
## Calling a logic function
|
||||
|
||||
Front components run browser-side in a sandboxed Web Worker, while [logic functions](/developers/extend/apps/logic/logic-functions) run server-side. There is no direct in-process call between the two — instead, a front component reaches a logic function over HTTP.
|
||||
|
||||
A logic function declared with `httpRouteTriggerSettings` is exposed under the `/s/` endpoint at `${TWENTY_API_URL}/s<path>`. Your front component calls that route with `fetch`, authenticating with the `TWENTY_APP_ACCESS_TOKEN` that Twenty injects into the worker.
|
||||
|
||||
A small reusable helper keeps the call sites clean:
|
||||
|
||||
```ts src/shared/call-app-route.ts
|
||||
export async function callAppRoute(
|
||||
path: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const apiUrl = process.env.TWENTY_API_URL ?? '';
|
||||
const token = process.env.TWENTY_APP_ACCESS_TOKEN;
|
||||
|
||||
const res = await fetch(`${apiUrl}/s${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Logic function failed (${res.status})`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
```
|
||||
|
||||
A headless front component can run the call on mount via the `Command` component, then unmount automatically:
|
||||
|
||||
```tsx src/front-components/sync-prs.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { callAppRoute } from 'src/shared/call-app-route';
|
||||
|
||||
const SyncPrs = () => {
|
||||
const execute = async () => {
|
||||
await callAppRoute('/github/fetch-prs', {
|
||||
owner: 'twentyhq',
|
||||
repo: 'twenty',
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-prs',
|
||||
description: 'Triggers the fetch-prs logic function',
|
||||
isHeadless: true,
|
||||
component: SyncPrs,
|
||||
});
|
||||
```
|
||||
|
||||
The `path` passed to `callAppRoute` must match the logic function's `httpRouteTriggerSettings.path` (the `/s` prefix is added by the helper):
|
||||
|
||||
```ts src/logic-functions/fetch-prs.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
|
||||
// ...fetch from GitHub and persist records...
|
||||
return { ok: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-prs',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/github/fetch-prs',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`TWENTY_API_URL` and `TWENTY_APP_ACCESS_TOKEN` are injected automatically — see [Application variables](#application-variables). Because secret application variables are never exposed to front components, keep API keys and other sensitive logic in the logic function, not in the front component.
|
||||
</Note>
|
||||
|
||||
## Accessing runtime context
|
||||
|
||||
Inside your component, use SDK hooks to access the current user, record, and component instance:
|
||||
|
||||
@@ -65,16 +65,15 @@ Use this when you only want to **add** a tab to an existing layout — for examp
|
||||
import {
|
||||
definePageLayoutTab,
|
||||
PageLayoutTabLayoutMode,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
|
||||
'20202020-ab01-4001-8001-c0aba11c0100';
|
||||
|
||||
export default definePageLayoutTab({
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
|
||||
pageLayoutUniversalIdentifier:
|
||||
COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
|
||||
.universalIdentifier,
|
||||
title: 'Hello World',
|
||||
position: 1000,
|
||||
icon: 'IconWorld',
|
||||
@@ -97,6 +96,34 @@ export default definePageLayoutTab({
|
||||
### Key points
|
||||
|
||||
- `pageLayoutUniversalIdentifier` is **required** and must point to a page layout that already exists at install time — either a standard Twenty layout or one defined by your own app. Cross-app references to layouts owned by another installed app are not supported today. When the parent layout is missing, installation fails with a clear validation error.
|
||||
- For standard Twenty layouts, import identifiers from `twenty-sdk/define`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||||
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
|
||||
// …
|
||||
```
|
||||
|
||||
Each layout entry also exposes its `tabs` and their `widgets`, so you can reference any level:
|
||||
|
||||
```ts
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.universalIdentifier
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets.fields.universalIdentifier
|
||||
```
|
||||
|
||||
A short alias `STANDARD_PAGE_LAYOUT` is also available:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
|
||||
|
||||
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
|
||||
```
|
||||
|
||||
- `widgets` are scoped to this tab only — they reference [front components](/developers/extend/apps/layout/front-components), views, etc. exactly like widgets defined inline in `definePageLayout`.
|
||||
- `position` controls ordering against existing tabs on the targeted layout. Pick a value that places your tab where you want it relative to built-in tabs.
|
||||
- Use this instead of `definePageLayout` when you only want to add to an existing layout. Use `definePageLayout` when you own the entire layout.
|
||||
|
||||
@@ -70,7 +70,7 @@ filters: [
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS` |
|
||||
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Field types with similar names can use entirely different operands — `SELECT` and `MULTI_SELECT` being a common case.
|
||||
|
||||
@@ -53,6 +53,10 @@ export default defineLogicFunction({
|
||||
Available trigger types:
|
||||
- **httpRoute**: Exposes your function on an HTTP path and method **under the `/s/` endpoint**:
|
||||
> e.g. `path: '/post-card/create'` is callable at `https://your-twenty-server.com/s/post-card/create`
|
||||
|
||||
<Note>
|
||||
To invoke a route-triggered logic function from a (headless) front component, see [Calling a logic function](/developers/extend/apps/layout/front-components#calling-a-logic-function).
|
||||
</Note>
|
||||
- **cron**: Runs your function on a schedule using a CRON expression.
|
||||
- **databaseEvent**: Runs on workspace object lifecycle events. When the event operation is `updated`, specific fields to listen to can be specified in the `updatedFields` array. If left undefined or empty, any update will trigger the function.
|
||||
> e.g. `person.updated`, `*.created`, `company.*`
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
title: Other methods
|
||||
icon: "cloud"
|
||||
---
|
||||
|
||||
<Warning>
|
||||
This document is maintained by the community. It might contain issues.
|
||||
</Warning>
|
||||
|
||||
## Kubernetes via Terraform and Manifests
|
||||
|
||||
Community-led documentation for Kubernetes deployment is available [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s)
|
||||
|
||||
### Coolify
|
||||
|
||||
Deploy Twenty on servers using Coolify. (official image on Coolify will be available soon)
|
||||
|
||||
[Coolify documentation](https://coolify.io/docs/get-started/introduction)
|
||||
|
||||
|
||||
### EasyPanel
|
||||
|
||||
Deploy Twenty on EasyPanel with the community maintained template below.
|
||||
|
||||
[Deploy on EasyPanel](https://easypanel.io/docs/templates/twenty)
|
||||
|
||||
### Elest.io
|
||||
|
||||
Deploy Twenty on servers with Elest.io using link below.
|
||||
|
||||
[Deploy on Elest.io](https://elest.io/open-source/twenty)
|
||||
|
||||
### Twenty on Railway
|
||||
|
||||
Deploy Twenty on Railway with the community maintained template below.
|
||||
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
|
||||
### Twenty on Sealos
|
||||
|
||||
Deploy Twenty on Sealos with the community maintained template below.
|
||||
|
||||
[](https://sealos.io/products/app-store/twenty)
|
||||
|
||||
## Others
|
||||
|
||||
Please feel free to Open a PR to add more Cloud Provider options.
|
||||
|
||||
|
||||
@@ -21,8 +21,5 @@ Twenty can be self-hosted on your own infrastructure, giving you full control ov
|
||||
<Card title="Docker Compose" icon="docker" href="/developers/self-host/capabilities/docker-compose">
|
||||
Quick setup with Docker
|
||||
</Card>
|
||||
<Card title="Cloud Providers" icon="cloud" href="/developers/self-host/capabilities/cloud-providers">
|
||||
Deploy on AWS, GCP, or Azure
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
|
||||
@@ -449,7 +449,6 @@
|
||||
"developers/self-host/capabilities/docker-compose",
|
||||
"developers/self-host/capabilities/setup",
|
||||
"developers/self-host/capabilities/upgrade-guide",
|
||||
"developers/self-host/capabilities/cloud-providers",
|
||||
"developers/self-host/capabilities/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -882,7 +881,6 @@
|
||||
"l/fr/developers/self-host/capabilities/docker-compose",
|
||||
"l/fr/developers/self-host/capabilities/setup",
|
||||
"l/fr/developers/self-host/capabilities/upgrade-guide",
|
||||
"l/fr/developers/self-host/capabilities/cloud-providers",
|
||||
"l/fr/developers/self-host/capabilities/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -1315,7 +1313,6 @@
|
||||
"l/ar/developers/self-host/capabilities/docker-compose",
|
||||
"l/ar/developers/self-host/capabilities/setup",
|
||||
"l/ar/developers/self-host/capabilities/upgrade-guide",
|
||||
"l/ar/developers/self-host/capabilities/cloud-providers",
|
||||
"l/ar/developers/self-host/capabilities/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -1748,7 +1745,6 @@
|
||||
"l/cs/developers/self-host/capabilities/docker-compose",
|
||||
"l/cs/developers/self-host/capabilities/setup",
|
||||
"l/cs/developers/self-host/capabilities/upgrade-guide",
|
||||
"l/cs/developers/self-host/capabilities/cloud-providers",
|
||||
"l/cs/developers/self-host/capabilities/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -2181,7 +2177,6 @@
|
||||
"l/de/developers/self-host/capabilities/docker-compose",
|
||||
"l/de/developers/self-host/capabilities/setup",
|
||||
"l/de/developers/self-host/capabilities/upgrade-guide",
|
||||
"l/de/developers/self-host/capabilities/cloud-providers",
|
||||
"l/de/developers/self-host/capabilities/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -2614,7 +2609,6 @@
|
||||
"l/es/developers/self-host/capabilities/docker-compose",
|
||||
"l/es/developers/self-host/capabilities/setup",
|
||||
"l/es/developers/self-host/capabilities/upgrade-guide",
|
||||
"l/es/developers/self-host/capabilities/cloud-providers",
|
||||
"l/es/developers/self-host/capabilities/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -3047,7 +3041,6 @@
|
||||
"l/it/developers/self-host/capabilities/docker-compose",
|
||||
"l/it/developers/self-host/capabilities/setup",
|
||||
"l/it/developers/self-host/capabilities/upgrade-guide",
|
||||
"l/it/developers/self-host/capabilities/cloud-providers",
|
||||
"l/it/developers/self-host/capabilities/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -3480,7 +3473,6 @@
|
||||
"l/ja/developers/self-host/capabilities/docker-compose",
|
||||
"l/ja/developers/self-host/capabilities/setup",
|
||||
"l/ja/developers/self-host/capabilities/upgrade-guide",
|
||||
"l/ja/developers/self-host/capabilities/cloud-providers",
|
||||
"l/ja/developers/self-host/capabilities/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -3913,7 +3905,6 @@
|
||||
"l/ko/developers/self-host/capabilities/docker-compose",
|
||||
"l/ko/developers/self-host/capabilities/setup",
|
||||
"l/ko/developers/self-host/capabilities/upgrade-guide",
|
||||
"l/ko/developers/self-host/capabilities/cloud-providers",
|
||||
"l/ko/developers/self-host/capabilities/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -4346,7 +4337,6 @@
|
||||
"l/pt/developers/self-host/capabilities/docker-compose",
|
||||
"l/pt/developers/self-host/capabilities/setup",
|
||||
"l/pt/developers/self-host/capabilities/upgrade-guide",
|
||||
"l/pt/developers/self-host/capabilities/cloud-providers",
|
||||
"l/pt/developers/self-host/capabilities/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -4779,7 +4769,6 @@
|
||||
"l/ro/developers/self-host/capabilities/docker-compose",
|
||||
"l/ro/developers/self-host/capabilities/setup",
|
||||
"l/ro/developers/self-host/capabilities/upgrade-guide",
|
||||
"l/ro/developers/self-host/capabilities/cloud-providers",
|
||||
"l/ro/developers/self-host/capabilities/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -5212,7 +5201,6 @@
|
||||
"l/ru/developers/self-host/capabilities/docker-compose",
|
||||
"l/ru/developers/self-host/capabilities/setup",
|
||||
"l/ru/developers/self-host/capabilities/upgrade-guide",
|
||||
"l/ru/developers/self-host/capabilities/cloud-providers",
|
||||
"l/ru/developers/self-host/capabilities/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -5645,7 +5633,6 @@
|
||||
"l/tr/developers/self-host/capabilities/docker-compose",
|
||||
"l/tr/developers/self-host/capabilities/setup",
|
||||
"l/tr/developers/self-host/capabilities/upgrade-guide",
|
||||
"l/tr/developers/self-host/capabilities/cloud-providers",
|
||||
"l/tr/developers/self-host/capabilities/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -6078,7 +6065,6 @@
|
||||
"l/zh/developers/self-host/capabilities/docker-compose",
|
||||
"l/zh/developers/self-host/capabilities/setup",
|
||||
"l/zh/developers/self-host/capabilities/upgrade-guide",
|
||||
"l/zh/developers/self-host/capabilities/cloud-providers",
|
||||
"l/zh/developers/self-host/capabilities/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -6196,10 +6182,6 @@
|
||||
"source": "/developers/self-hosting/upgrade-guide",
|
||||
"destination": "/developers/self-host/capabilities/upgrade-guide"
|
||||
},
|
||||
{
|
||||
"source": "/developers/self-hosting/cloud-providers",
|
||||
"destination": "/developers/self-host/capabilities/cloud-providers"
|
||||
},
|
||||
{
|
||||
"source": "/developers/self-hosting/troubleshooting",
|
||||
"destination": "/developers/self-host/capabilities/troubleshooting"
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
title: طرق أخرى
|
||||
icon: cloud
|
||||
---
|
||||
|
||||
<Warning>
|
||||
هذا المستند يُحافظ عليه من قبل المجتمع. قد يحتوي على مشكلات.
|
||||
</Warning>
|
||||
|
||||
## Kubernetes عبر Terraform والمخططات
|
||||
|
||||
يتوفر توثيق يقوده المجتمع لعملية نشر Kubernetes [هنا](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s)
|
||||
|
||||
### Coolify
|
||||
|
||||
نشر Twenty على الخوادم باستخدام Coolify. (الصورة الرسمية على Coolify ستكون متاحة قريبًا)
|
||||
|
||||
[توثيق Coolify](https://coolify.io/docs/get-started/introduction)
|
||||
|
||||
### EasyPanel
|
||||
|
||||
نشر Twenty على EasyPanel مع القالب الذي يُحافظ عليه المجتمع أدناه.
|
||||
|
||||
[نشر على EasyPanel](https://easypanel.io/docs/templates/twenty)
|
||||
|
||||
### Elest.io
|
||||
|
||||
نشر Twenty على الخوادم باستخدام Elest.io عبر الرابط التالي.
|
||||
|
||||
[نشر على Elest.io](https://elest.io/open-source/twenty)
|
||||
|
||||
### Twenty على Railway
|
||||
|
||||
نشر Twenty على Railway مع القالب الذي يُحافظ عليه المجتمع أدناه.
|
||||
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
|
||||
### Twenty على Sealos
|
||||
|
||||
انشر Twenty على Sealos باستخدام القالب الذي تتم صيانته من قِبل المجتمع أدناه.
|
||||
|
||||
[](https://sealos.io/products/app-store/twenty)
|
||||
|
||||
## أخرى
|
||||
|
||||
لا تتردد في فتح طلب سحب لإضافة المزيد من خيارات موفّري السحابة.
|
||||
@@ -23,7 +23,4 @@ description: قم بنشر Twenty وإدارته على البنية التحت
|
||||
<Card title="Docker Compose" icon="docker" href="/l/ar/developers/self-host/capabilities/docker-compose">
|
||||
إعداد سريع باستخدام Docker
|
||||
</Card>
|
||||
<Card title="مزودو الخدمات السحابية" icon="cloud" href="/l/ar/developers/self-host/capabilities/cloud-providers">
|
||||
انشر على AWS أو GCP أو Azure
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
title: Další metody
|
||||
icon: cloud
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Tento dokument je udržován komunitou. Může obsahovat problémy.
|
||||
</Warning>
|
||||
|
||||
## Kubernetes pomocí Terraform a Manifestů
|
||||
|
||||
Dokumentace vedená komunitou k nasazení Kubernetes je k dispozici [zde](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s)
|
||||
|
||||
### Coolify
|
||||
|
||||
Nasazení Twenty na serverech pomocí Coolify. (oficiální obrázek na Coolify bude brzy k dispozici)
|
||||
|
||||
[Dokumentace Coolify](https://coolify.io/docs/get-started/introduction)
|
||||
|
||||
### EasyPanel
|
||||
|
||||
Nasazení Twenty na EasyPanel s komunitně udržovanou šablonou níže.
|
||||
|
||||
[Nasadit na EasyPanel](https://easypanel.io/docs/templates/twenty)
|
||||
|
||||
### Elest.io
|
||||
|
||||
Nasazení Twenty na serverech pomocí Elest.io s odkazem níže.
|
||||
|
||||
[Nasadit na Elest.io](https://elest.io/open-source/twenty)
|
||||
|
||||
### Twenty na Railway
|
||||
|
||||
Nasazení Twenty na Railway s komunitně udržovanou šablonou níže.
|
||||
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
|
||||
### Ostatní
|
||||
|
||||
Nasazení Twenty na Sealos s komunitně udržovanou šablonou níže.
|
||||
|
||||
[](https://sealos.io/products/app-store/twenty)
|
||||
|
||||
## Ostatní
|
||||
|
||||
Neváhejte otevřít PR pro přidání dalších možností poskytovatele cloudových služeb.
|
||||
@@ -23,7 +23,4 @@ Twenty můžete samostatně hostovat na vlastní infrastruktuře, což vám dáv
|
||||
<Card title="Docker Compose" icon="docker" href="/l/cs/developers/self-host/capabilities/docker-compose">
|
||||
Rychlé nastavení s Dockerem
|
||||
</Card>
|
||||
<Card title="Poskytovatelé cloudu" icon="cloud" href="/l/cs/developers/self-host/capabilities/cloud-providers">
|
||||
Nasaďte na AWS, GCP nebo Azure
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -43,6 +43,8 @@ export default defineField({
|
||||
|
||||
* Der Speicherort der Datei liegt bei Ihnen. Die Konvention ist `src/fields/\<name>.field.ts`, aber das SDK erkennt Felder überall in `src/`.
|
||||
|
||||
* Um eine Registerkarte zu einem Standard-Seitenlayout hinzuzufügen (z. B. der Aufgaben- oder Unternehmensdetailseite), verwenden Sie [`definePageLayoutTab`](/l/de/developers/extend/apps/layout/page-layouts#definepagelayouttab) mit `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` aus `twenty-sdk/define`.
|
||||
|
||||
## Hinzufügen einer Relation zu einem bestehenden Objekt
|
||||
|
||||
Um ein Relationsfeld hinzuzufügen (z. B. zur Verknüpfung Ihres benutzerdefinierten Objekts mit einer Standard-`Person`), verwenden Sie `defineField()` mit `FieldType.RELATION`. Das Muster ist dasselbe wie bei Inline-Relationen, jedoch mit explizit gesetztem `objectUniversalIdentifier`. Siehe [Relations](/l/de/developers/extend/apps/data/relations) für das bidirektionale Muster.
|
||||
|
||||
@@ -44,8 +44,53 @@ Die **Datenebene** einer Twenty-App umfasst die Daten, die Ihre App zu einem Wor
|
||||
| **Objekt** | Ein neuer benutzerdefinierter Datensatztyp (z. B. PostCard, Invoice) mit eigenen Feldern | `defineObject()` |
|
||||
| **Feld** | Eine Spalte in einem Objekt. Eigenständige Felder können Objekte erweitern, die Sie nicht erstellt haben (z. B. `loyaltyTier` zu Company hinzufügen) | `defineField()` |
|
||||
| **Beziehung** | Eine bidirektionale Verknüpfung zwischen zwei Objekten – beide Seiten werden als Felder deklariert | `defineField()` mit `FieldType.RELATION` |
|
||||
| **Indizes** | Ein Datenbankindex, um eine wiederkehrende Abfrage für eines Ihrer Objekte zu beschleunigen | `defineIndex()` |
|
||||
|
||||
Das SDK erkennt diese zur Build-Zeit über eine AST-Analyse, sodass die Dateiorganisation Ihnen überlassen ist – die Konvention ist `src/objects/` und `src/fields/`. Stabile `universalIdentifier`-UUIDs verknüpfen alles über Deploys hinweg.
|
||||
Das SDK erkennt diese zur Build-Zeit über eine AST-Analyse, sodass die Dateiorganisation Ihnen überlassen ist – die Konvention ist `src/objects/`, `src/fields/` und `src/indexes/`. Stabile `universalIdentifier`-UUIDs verknüpfen alles über Deploys hinweg.
|
||||
|
||||
## Indizes (optional)
|
||||
|
||||
Apps können Indizes gemeinsam mit ihren Objekten ausliefern, um wiederkehrende Abfragen schnell zu halten. Der häufigste Fall ist eine Status- oder Fremdschlüsselspalte, die Sie häufig lesen.
|
||||
|
||||
```ts src/indexes/post-card-status.index.ts
|
||||
import { defineIndex } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from '../objects/post-card.object';
|
||||
|
||||
export default defineIndex({
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
|
||||
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Eindeutige Indizes
|
||||
|
||||
`defineIndex` akzeptiert `isUnique: true` sowohl für Einspalten- als auch Mehrspalteneindeutigkeit. Dies ist das empfohlene Primitive – `defineField({ isUnique: true })` ist veraltet und wird in einer zukünftigen Version entfernt.
|
||||
|
||||
```ts
|
||||
defineIndex({
|
||||
universalIdentifier: '…',
|
||||
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
|
||||
isUnique: true,
|
||||
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
|
||||
});
|
||||
```
|
||||
|
||||
### Andere Einschränkungen
|
||||
|
||||
* Partielle `WHERE`-Klauseln bleiben unter Kontrolle der Administratoren – Apps können sie nicht deklarieren.
|
||||
* Jedes Objekt ist auf 10 benutzerdefinierte Indizes begrenzt (die Indizes des Frameworks selbst werden nicht mitgezählt).
|
||||
|
||||
Ordnen Sie das `fields`-Array so an, wie Postgres es verwenden soll – die ganz linke Spalte zuerst, wie in einem Telefonbuch. Indizes sind nicht kostenlos: Jeder Schreibvorgang in die Tabelle aktualisiert sie. Fügen Sie einen nur dann hinzu, wenn Sie eine Abfrage haben, die ihn benötigt.
|
||||
|
||||
<Note>
|
||||
Suchen Sie nach **Application Config** oder **Roles & Permissions**? Diese beschreiben die App selbst und nicht die Daten, die sie hinzufügt – sie befinden sich unter [Config](/l/de/developers/extend/apps/config/overview). Suchen Sie nach **Connections** (Linear, GitHub, Slack OAuth)? Diese existieren, um *von* Logikfunktionen aufgerufen zu werden, und befinden sich unter [Logic](/l/de/developers/extend/apps/logic/connections).
|
||||
|
||||
@@ -103,6 +103,10 @@ export default defineCommandMenuItem({
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`RECORD_SELECTION` impliziert bereits eine nichtleere Auswahl — verwende `numberOfSelectedRecords` nur für bestimmte Zählwerte (z. B. `>= 2`).
|
||||
</Note>
|
||||
|
||||
### Kontextvariablen
|
||||
|
||||
Diese repräsentieren den aktuellen Zustand der Seite:
|
||||
|
||||
@@ -196,6 +196,94 @@ export default defineFrontComponent({
|
||||
});
|
||||
```
|
||||
|
||||
## Aufrufen einer Logikfunktion
|
||||
|
||||
Front-Komponenten laufen browserseitig in einem isolierten Web Worker, während [Logikfunktionen](/l/de/developers/extend/apps/logic/logic-functions) serverseitig ausgeführt werden. Es gibt keinen direkten In-Process-Aufruf zwischen beiden – stattdessen ruft eine Front-Komponente eine Logikfunktion über HTTP auf.
|
||||
|
||||
Eine mit `httpRouteTriggerSettings` deklarierte Logikfunktion wird unter dem `/s/`-Endpunkt unter `${TWENTY_API_URL}/s\<path>` bereitgestellt. Ihre Front-Komponente ruft diese Route mit `fetch` auf und authentifiziert sich dabei mit dem `TWENTY_APP_ACCESS_TOKEN`, das Twenty in den Worker injiziert.
|
||||
|
||||
Ein kleiner wiederverwendbarer Helper hält die Aufrufstellen übersichtlich:
|
||||
|
||||
```ts src/shared/call-app-route.ts
|
||||
export async function callAppRoute(
|
||||
path: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const apiUrl = process.env.TWENTY_API_URL ?? '';
|
||||
const token = process.env.TWENTY_APP_ACCESS_TOKEN;
|
||||
|
||||
const res = await fetch(`${apiUrl}/s${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Logic function failed (${res.status})`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
```
|
||||
|
||||
Eine headless Front-Komponente kann den Aufruf beim Mounten über die `Command`-Komponente ausführen und sich anschließend automatisch unmounten:
|
||||
|
||||
```tsx src/front-components/sync-prs.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { callAppRoute } from 'src/shared/call-app-route';
|
||||
|
||||
const SyncPrs = () => {
|
||||
const execute = async () => {
|
||||
await callAppRoute('/github/fetch-prs', {
|
||||
owner: 'twentyhq',
|
||||
repo: 'twenty',
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-prs',
|
||||
description: 'Triggers the fetch-prs logic function',
|
||||
isHeadless: true,
|
||||
component: SyncPrs,
|
||||
});
|
||||
```
|
||||
|
||||
Der an `callAppRoute` übergebene `path` muss dem `httpRouteTriggerSettings.path` der Logikfunktion entsprechen (das `/s`-Präfix wird vom Helper hinzugefügt):
|
||||
|
||||
```ts src/logic-functions/fetch-prs.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
|
||||
// ...fetch from GitHub and persist records...
|
||||
return { ok: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-prs',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/github/fetch-prs',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`TWENTY_API_URL` und `TWENTY_APP_ACCESS_TOKEN` werden automatisch injiziert – siehe [Anwendungsvariablen](#application-variables). Da geheime Anwendungsvariablen niemals in Front-Komponenten offengelegt werden, sollten API-Schlüssel und andere sensible Logik in der Logikfunktion verbleiben und nicht in der Front-Komponente.
|
||||
</Note>
|
||||
|
||||
## Zugriff auf den Laufzeitkontext
|
||||
|
||||
Verwenden Sie innerhalb Ihrer Komponente SDK-Hooks, um auf den aktuellen Benutzer, den Datensatz und die Komponenteninstanz zuzugreifen:
|
||||
|
||||
@@ -65,16 +65,15 @@ Verwenden Sie dies, wenn Sie nur einen Tab zu einem vorhandenen Layout **hinzuf
|
||||
import {
|
||||
definePageLayoutTab,
|
||||
PageLayoutTabLayoutMode,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
|
||||
'20202020-ab01-4001-8001-c0aba11c0100';
|
||||
|
||||
export default definePageLayoutTab({
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
|
||||
pageLayoutUniversalIdentifier:
|
||||
COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
|
||||
.universalIdentifier,
|
||||
title: 'Hello World',
|
||||
position: 1000,
|
||||
icon: 'IconWorld',
|
||||
@@ -97,6 +96,37 @@ export default definePageLayoutTab({
|
||||
### Hauptpunkte
|
||||
|
||||
* `pageLayoutUniversalIdentifier` ist **erforderlich** und muss auf ein Seitenlayout verweisen, das zum Installationszeitpunkt bereits existiert – entweder ein standardmäßiges Twenty-Layout oder eines, das von Ihrer eigenen App definiert wurde. App-übergreifende Verweise auf Layouts, die einer anderen installierten App gehören, werden derzeit nicht unterstützt. Wenn das übergeordnete Layout fehlt, schlägt die Installation mit einem eindeutigen Validierungsfehler fehl.
|
||||
|
||||
* Für Standard-Twenty-Layouts importieren Sie die Bezeichner aus `twenty-sdk/define`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||||
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
|
||||
// …
|
||||
```
|
||||
|
||||
Jeder Layout-Eintrag stellt außerdem seine `tabs` und deren `widgets` zur Verfügung, sodass Sie auf jede Ebene verweisen können:
|
||||
|
||||
```ts
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.universalIdentifier
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets.fields.universalIdentifier
|
||||
```
|
||||
|
||||
Eine kurze Alias-Variable `STANDARD_PAGE_LAYOUT` ist ebenfalls verfügbar:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
|
||||
|
||||
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
|
||||
```
|
||||
|
||||
* `widgets` sind ausschließlich auf diesen Tab beschränkt – sie verweisen auf [Frontend-Komponenten](/l/de/developers/extend/apps/layout/front-components), Ansichten usw., genau wie Widgets, die inline in `definePageLayout` definiert sind.
|
||||
|
||||
* `position` steuert die Reihenfolge im Zielseitenlayout relativ zu den vorhandenen Registerkarten. Wählen Sie einen Wert, der Ihre Registerkarte relativ zu integrierten Registerkarten an die gewünschte Position bringt.
|
||||
|
||||
* Verwenden Sie dies anstelle von `definePageLayout`, wenn Sie einem vorhandenen Layout nur etwas hinzufügen möchten. Verwenden Sie `definePageLayout`, wenn Sie das gesamte Layout besitzen.
|
||||
|
||||
@@ -70,7 +70,7 @@ filters: [
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS` |
|
||||
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Feldtypen mit ähnlichen Namen können völlig unterschiedliche Operanden verwenden – `SELECT` und `MULTI_SELECT` sind ein häufiges Beispiel.
|
||||
|
||||
@@ -53,6 +53,10 @@ export default defineLogicFunction({
|
||||
Verfügbare Trigger-Typen:
|
||||
* **httpRoute**: Stellt Ihre Funktion unter einem HTTP-Pfad und einer Methode **unter dem Endpunkt `/s/`** bereit:
|
||||
> z. B. `path: '/post-card/create'` ist unter `https://your-twenty-server.com/s/post-card/create` aufrufbar
|
||||
|
||||
<Note>
|
||||
Um eine routenausgelöste Logikfunktion von einer (headless) Front-Komponente aus aufzurufen, siehe [Aufrufen einer Logikfunktion](/l/de/developers/extend/apps/layout/front-components#calling-a-logic-function).
|
||||
</Note>
|
||||
* **cron**: Führt Ihre Funktion nach Zeitplan mithilfe eines CRON-Ausdrucks aus.
|
||||
* **databaseEvent**: Wird bei Lebenszyklusereignissen von Workspace-Objekten ausgeführt. Wenn die Ereignisoperation `updated` ist, können bestimmte zu überwachende Felder im Array `updatedFields` angegeben werden. Wenn das Array undefiniert oder leer ist, löst jede Aktualisierung die Funktion aus.
|
||||
> z. B. `person.updated`, `*.created`, `company.*`
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
title: Weitere Methoden
|
||||
icon: cloud
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Dieses Dokument wird von der Community gepflegt. Es könnte Probleme enthalten.
|
||||
</Warning>
|
||||
|
||||
## Kubernetes über Terraform und Manifeste
|
||||
|
||||
Die von der Community gepflegte Dokumentation zur Kubernetes-Bereitstellung ist [hier](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s) verfügbar.
|
||||
|
||||
### Coolify
|
||||
|
||||
Stellen Sie Twenty auf Servern mit Coolify bereit. (offizielles Bild auf Coolify wird bald verfügbar sein)
|
||||
|
||||
[Coolify-Dokumentation](https://coolify.io/docs/get-started/introduction)
|
||||
|
||||
### EasyPanel
|
||||
|
||||
Stellen Sie Twenty auf EasyPanel mit der untenstehenden, von der Community gepflegten Vorlage bereit.
|
||||
|
||||
[Bereitstellung auf EasyPanel](https://easypanel.io/docs/templates/twenty)
|
||||
|
||||
### Elest.io
|
||||
|
||||
Stellen Sie Twenty auf Servern mit Elest.io über den untenstehenden Link bereit.
|
||||
|
||||
[Bereitstellung auf Elest.io](https://elest.io/open-source/twenty)
|
||||
|
||||
### Twenty auf Railway
|
||||
|
||||
Stellen Sie Twenty auf Railway mit der untenstehenden, von der Community gepflegten Vorlage bereit.
|
||||
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
|
||||
### Twenty auf Sealos
|
||||
|
||||
Stellen Sie Twenty auf Sealos mit der untenstehenden, von der Community gepflegten Vorlage bereit.
|
||||
|
||||
[](https://sealos.io/products/app-store/twenty)
|
||||
|
||||
## Andere
|
||||
|
||||
Bitte zögern Sie nicht, einen PR zu öffnen, um mehr Optionen für Cloud-Anbieter hinzuzufügen.
|
||||
@@ -23,7 +23,4 @@ Twenty kann auf Ihrer eigenen Infrastruktur im Selbsthosting betrieben werden, s
|
||||
<Card title="Docker Compose" icon="docker" href="/l/de/developers/self-host/capabilities/docker-compose">
|
||||
Schnelle Einrichtung mit Docker
|
||||
</Card>
|
||||
<Card title="Cloud-Anbieter" icon="cloud" href="/l/de/developers/self-host/capabilities/cloud-providers">
|
||||
Auf AWS, GCP oder Azure bereitstellen
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -101,6 +101,10 @@ Machen Sie ein Feld einzigartig, um sicherzustellen, dass sich keine verschieden
|
||||
|
||||
Wenn beim Einstellen der Einzigartigkeit ein Fehler auftritt, überprüfen Sie auf doppelte Werte in Ihren Daten (einschließlich gelöschter Datensätze).
|
||||
|
||||
## Indizes (Erweitert)
|
||||
|
||||
Datenbankindizes werden automatisch verwaltet – eigene hinzuzufügen ist selten erforderlich und kann leicht schiefgehen. Wenn der Erweiterte Modus aktiviert ist, hat jedes Objekt unter `Einstellungen → Datenmodell → <object>` einen Abschnitt **Indizes** für die Fälle, in denen du weißt, dass du einen brauchst.
|
||||
|
||||
## Beste Praktiken zur Feldkonfiguration
|
||||
|
||||
### Benennungskonventionen und Einschränkungen
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
title: Otros métodos
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Este documento es mantenido por la comunidad. Podría contener problemas.
|
||||
</Warning>
|
||||
|
||||
## Kubernetes vía Terraform y Manifests
|
||||
|
||||
Documentación comunitaria para la implementación de Kubernetes está disponible [aquí](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s)
|
||||
|
||||
### Coolify
|
||||
|
||||
Despliega Twenty en servidores usando Coolify. (la imagen oficial en Coolify estará disponible pronto)
|
||||
|
||||
[Documentación de Coolify](https://coolify.io/docs/get-started/introduction)
|
||||
|
||||
### EasyPanel
|
||||
|
||||
Despliega Twenty en EasyPanel con la plantilla mantenida por la comunidad a continuación.
|
||||
|
||||
[Desplegar en EasyPanel](https://easypanel.io/docs/templates/twenty)
|
||||
|
||||
### Elest.io
|
||||
|
||||
Despliega Twenty en servidores con Elest.io utilizando el enlace a continuación.
|
||||
|
||||
[Desplegar en Elest.io](https://elest.io/open-source/twenty)
|
||||
|
||||
### Twenty en Railway
|
||||
|
||||
Despliega Twenty en Railway con la plantilla mantenida por la comunidad a continuación.
|
||||
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
|
||||
### Twenty en Sealos
|
||||
|
||||
Despliega Twenty en Sealos con la plantilla mantenida por la comunidad a continuación.
|
||||
|
||||
[](https://sealos.io/products/app-store/twenty)
|
||||
|
||||
## Otros
|
||||
|
||||
No dudes en abrir un PR para añadir más opciones de proveedores de la nube.
|
||||
@@ -23,8 +23,4 @@ Puedes autoalojar Twenty en tu propia infraestructura, lo que te brinda control
|
||||
<Card title="Docker Compose" icon="docker" href="/l/es/developers/self-host/capabilities/docker-compose">
|
||||
Configuración rápida con Docker
|
||||
</Card>
|
||||
|
||||
<Card title="Proveedores de la nube" icon="cloud" href="/l/es/developers/self-host/capabilities/cloud-providers">
|
||||
Despliega en AWS, GCP o Azure
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
title: Autres méthodes
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Ce document est maintenu par la communauté. Il pourrait contenir des problèmes.
|
||||
</Warning>
|
||||
|
||||
## Kubernetes via Terraform et Manifests
|
||||
|
||||
La documentation communautaire pour le déploiement de Kubernetes est disponible [ici](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s)
|
||||
|
||||
### Coolify
|
||||
|
||||
Déployer Twenty sur les serveurs avec Coolify. (l'image officielle sur Coolify sera bientôt disponible)
|
||||
|
||||
[Documentation Coolify](https://coolify.io/docs/get-started/introduction)
|
||||
|
||||
### EasyPanel
|
||||
|
||||
Déployez Twenty sur EasyPanel avec le modèle maintenu par la communauté ci-dessous.
|
||||
|
||||
[Déployer sur EasyPanel](https://easypanel.io/docs/templates/twenty)
|
||||
|
||||
### Elest.io
|
||||
|
||||
Déployez Twenty sur les serveurs avec Elest.io en utilisant le lien ci-dessous.
|
||||
|
||||
[Déployer sur Elest.io](https://elest.io/open-source/twenty)
|
||||
|
||||
### Twenty sur Railway
|
||||
|
||||
Déployez Twenty sur Railway avec le modèle maintenu par la communauté ci-dessous.
|
||||
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
|
||||
### Twenty sur Sealos
|
||||
|
||||
Déployez Twenty sur Sealos avec le modèle maintenu par la communauté ci-dessous.
|
||||
|
||||
[](https://sealos.io/products/app-store/twenty)
|
||||
|
||||
## Autres
|
||||
|
||||
N'hésitez pas à ouvrir une PR pour ajouter d'autres options de fournisseur cloud.
|
||||
@@ -23,8 +23,4 @@ Twenty peut être auto-hébergé sur votre propre infrastructure, vous offrant u
|
||||
<Card title="Docker Compose" icon="docker" href="/l/fr/developers/self-host/capabilities/docker-compose">
|
||||
Configuration rapide avec Docker
|
||||
</Card>
|
||||
|
||||
<Card title="Fournisseurs cloud" icon="cloud" href="/l/fr/developers/self-host/capabilities/cloud-providers">
|
||||
Déployez sur AWS, GCP ou Azure
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
title: Altri metodi
|
||||
icon: cloud
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Questo documento è mantenuto dalla comunità. Potrebbe contenere problemi.
|
||||
</Warning>
|
||||
|
||||
## Kubernetes tramite Terraform e Manifest
|
||||
|
||||
La documentazione guidata dalla comunità per la distribuzione su Kubernetes è disponibile [qui](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s)
|
||||
|
||||
### Coolify
|
||||
|
||||
Distribuisci Twenty sui server utilizzando Coolify. (L'immagine ufficiale su Coolify sarà disponibile a breve)
|
||||
|
||||
[Documentazione di Coolify](https://coolify.io/docs/get-started/introduction)
|
||||
|
||||
### EasyPanel
|
||||
|
||||
Distribuisci Twenty su EasyPanel con il modello mantenuto dalla comunità di seguito.
|
||||
|
||||
[Distribuisci su EasyPanel](https://easypanel.io/docs/templates/twenty)
|
||||
|
||||
### Elest.io
|
||||
|
||||
Distribuisci Twenty sui server con Elest.io utilizzando il link sottostante.
|
||||
|
||||
[Distribuisci su Elest.io](https://elest.io/open-source/twenty)
|
||||
|
||||
### Twenty su Railway
|
||||
|
||||
Distribuisci Twenty su Railway con il modello mantenuto dalla comunità di seguito.
|
||||
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
|
||||
### Twenty su Sealos
|
||||
|
||||
Distribuisci Twenty su Sealos con il modello mantenuto dalla comunità di seguito.
|
||||
|
||||
[](https://sealos.io/products/app-store/twenty)
|
||||
|
||||
## Altro
|
||||
|
||||
Sentiti libero di aprire un PR per aggiungere più opzioni di provider Cloud.
|
||||
@@ -23,7 +23,4 @@ Twenty può essere auto-ospitato sulla tua infrastruttura, offrendoti il pieno c
|
||||
<Card title="Docker Compose" icon="docker" href="/l/it/developers/self-host/capabilities/docker-compose">
|
||||
Configurazione rapida con Docker
|
||||
</Card>
|
||||
<Card title="Provider cloud" icon="cloud" href="/l/it/developers/self-host/capabilities/cloud-providers">
|
||||
Distribuisci su AWS, GCP o Azure
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
title: その他の方法
|
||||
---
|
||||
|
||||
<Warning>
|
||||
このドキュメントはコミュニティによって管理されています。 問題を含む可能性があります。
|
||||
問題を含む可能性があります。
|
||||
</Warning>
|
||||
|
||||
## Terraformとマニフェストを通したKubernetes
|
||||
|
||||
Kubernetesデプロイメントに関するコミュニティ主導のドキュメントは[こちら](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s)です。
|
||||
|
||||
### Coolify
|
||||
|
||||
Coolifyを使用してサーバーにTwentyをデプロイします。 (Coolify上の公式イメージは近日公開予定です) (Coolify上の公式イメージは近日公開予定です)
|
||||
|
||||
[Coolifyのドキュメント](https://coolify.io/docs/get-started/introduction)
|
||||
|
||||
### EasyPanel
|
||||
|
||||
以下のコミュニティ維持テンプレートを使用して、EasyPanelにTwentyをデプロイします。
|
||||
|
||||
以下のコミュニティ維持テンプレートを使用して、EasyPanelにTwentyをデプロイします。
|
||||
|
||||
### Elest.io
|
||||
|
||||
以下のリンクを使用して、Elest.ioにTwentyをサーバー上にデプロイします。
|
||||
|
||||
[Elest.ioにデプロイする](https://elest.io/open-source/twenty)
|
||||
|
||||
### Railway上のTwenty
|
||||
|
||||
以下のコミュニティ維持テンプレートを使用して、RailwayにTwentyをデプロイします。
|
||||
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
|
||||
### Sealos上のTwenty
|
||||
|
||||
以下のコミュニティがメンテナンスしているテンプレートを使用して、SealosにTwentyをデプロイします。
|
||||
|
||||
[](https://sealos.io/products/app-store/twenty)
|
||||
|
||||
## その他
|
||||
|
||||
もっと多くのクラウドプロバイダーオプションを追加するために、PRを自由に作成してください。
|
||||
@@ -23,8 +23,4 @@ Twenty は独自のインフラでセルフホストでき、データとデプ
|
||||
<Card title="Docker Compose" icon="docker" href="/l/ja/developers/self-host/capabilities/docker-compose">
|
||||
Docker でのクイックセットアップ
|
||||
</Card>
|
||||
|
||||
<Card title="クラウドプロバイダー" icon="cloud" href="/l/ja/developers/self-host/capabilities/cloud-providers">
|
||||
AWS、GCP、または Azure にデプロイ
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
title: 기타 방법
|
||||
---
|
||||
|
||||
<Warning>
|
||||
이 문서는 커뮤니티에서 관리됩니다. 문제점이 있을 수 있습니다.
|
||||
</Warning>
|
||||
|
||||
## Terraform 및 매니페스트를 통한 Kubernetes
|
||||
|
||||
Kubernetes 배포에 대한 커뮤니티 주도 문서는 [여기](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s)에서 확인할 수 있습니다.
|
||||
|
||||
### Coolify
|
||||
|
||||
Coolify를 사용하여 서버에 Twenty를 배포하세요. (Coolify의 공식 이미지가 곧 제공될 예정입니다)
|
||||
|
||||
[Coolify 문서](https://coolify.io/docs/get-started/introduction)
|
||||
|
||||
### EasyPanel
|
||||
|
||||
아래의 커뮤니티가 유지 관리하는 템플릿으로 EasyPanel에 Twenty를 배포하세요.
|
||||
|
||||
[EasyPanel에 배포](https://easypanel.io/docs/templates/twenty)
|
||||
|
||||
### Elest.io
|
||||
|
||||
아래 링크를 통해 Elest.io에서 서버에 Twenty를 배포하세요.
|
||||
|
||||
[Elest.io에 배포](https://elest.io/open-source/twenty)
|
||||
|
||||
### Railway에서의 Twenty
|
||||
|
||||
아래의 커뮤니티가 유지 관리하는 템플릿으로 Railway에 Twenty를 배포하세요.
|
||||
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
|
||||
### Sealos에서의 Twenty
|
||||
|
||||
아래의 커뮤니티가 유지 관리하는 템플릿으로 Sealos에 Twenty를 배포하세요.
|
||||
|
||||
[](https://sealos.io/products/app-store/twenty)
|
||||
|
||||
## 기타
|
||||
|
||||
더 많은 클라우드 제공자 옵션을 추가하기 위해 PR을 자유롭게 열어주세요.
|
||||
@@ -23,8 +23,4 @@ Twenty는 자체 인프라에서 셀프 호스팅할 수 있어 데이터와 배
|
||||
<Card title="Docker Compose" icon="docker" href="/l/ko/developers/self-host/capabilities/docker-compose">
|
||||
Docker로 빠르게 설정
|
||||
</Card>
|
||||
|
||||
<Card title="클라우드 제공자" icon="cloud" href="/l/ko/developers/self-host/capabilities/cloud-providers">
|
||||
AWS, GCP 또는 Azure에 배포
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -43,6 +43,8 @@ export default defineField({
|
||||
|
||||
* A localização do arquivo fica a seu critério. A convenção é `src/fields/\<name>.field.ts`, mas o SDK detecta campos em qualquer lugar dentro de `src/`.
|
||||
|
||||
* Para adicionar uma aba a um layout de página padrão (por exemplo, a página de detalhes de Task ou Company), use [`definePageLayoutTab`](/l/pt/developers/extend/apps/layout/page-layouts#definepagelayouttab) com `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` de `twenty-sdk/define`.
|
||||
|
||||
## Adicionando uma relação a um objeto existente
|
||||
|
||||
Para adicionar um campo de relação (por exemplo, vinculando seu objeto personalizado a um `Person` padrão), use `defineField()` com `FieldType.RELATION`. O padrão é o mesmo que para relações inline, mas com `objectUniversalIdentifier` definido explicitamente. Veja [Relações](/l/pt/developers/extend/apps/data/relations) para o padrão bidirecional.
|
||||
|
||||
@@ -44,8 +44,53 @@ A **camada de dados** de um app Twenty é o conjunto de dados que seu app *adici
|
||||
| **Objeto** | Um novo tipo de registro personalizado (por exemplo, PostCard, Invoice) com seus próprios campos | `defineObject()` |
|
||||
| **Campo** | Uma coluna em um objeto. Campos independentes podem estender objetos que você não criou (por exemplo, adicionar `loyaltyTier` ao objeto Company) | `defineField()` |
|
||||
| **Relação** | Um vínculo bidirecional entre dois objetos — ambos os lados declarados como campos | `defineField()` com `FieldType.RELATION` |
|
||||
| **Índice** | Um índice de banco de dados para acelerar uma consulta recorrente em um dos seus objetos | `defineIndex()` |
|
||||
|
||||
O SDK detecta esses elementos por meio de análise de AST em tempo de build, então a organização dos arquivos fica a seu critério — a convenção é `src/objects/` e `src/fields/`. UUIDs `universalIdentifier` estáveis conectam tudo em implantações diferentes.
|
||||
O SDK detecta esses elementos por meio de análise de AST em tempo de build, então a organização dos arquivos fica a seu critério — a convenção é `src/objects/`, `src/fields/` e `src/indexes/`. UUIDs `universalIdentifier` estáveis conectam tudo em implantações diferentes.
|
||||
|
||||
## Índices (Opcional)
|
||||
|
||||
Os apps podem incluir índices junto com seus objetos para manter rápidas as consultas recorrentes. O caso mais comum é uma coluna de status ou de chave estrangeira que você lê com frequência.
|
||||
|
||||
```ts src/indexes/post-card-status.index.ts
|
||||
import { defineIndex } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from '../objects/post-card.object';
|
||||
|
||||
export default defineIndex({
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
|
||||
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Índices únicos
|
||||
|
||||
`defineIndex` aceita `isUnique: true` tanto para unicidade de uma única coluna quanto de múltiplas colunas. Este é o recurso recomendado — `defineField({ isUnique: true })` está obsoleto e será removido em uma versão futura.
|
||||
|
||||
```ts
|
||||
defineIndex({
|
||||
universalIdentifier: '…',
|
||||
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
|
||||
isUnique: true,
|
||||
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
|
||||
});
|
||||
```
|
||||
|
||||
### Outras restrições
|
||||
|
||||
* Cláusulas `WHERE` parciais permanecem sob controle do administrador — os apps não podem declará-las.
|
||||
* Cada objeto é limitado a 10 índices personalizados (os índices do próprio framework não contam).
|
||||
|
||||
Ordene o array `fields` da forma como o Postgres deve usá-lo — coluna mais à esquerda primeiro, como em uma lista telefônica. Índices não são gratuitos: cada gravação na tabela os atualiza. Adicione um apenas quando você tiver uma consulta que precise dele.
|
||||
|
||||
<Note>
|
||||
Procurando por **Application Config** ou **Roles & Permissions**? Esses descrevem o próprio app em vez dos dados que ele adiciona — eles ficam em [Config](/l/pt/developers/extend/apps/config/overview). Procurando por **Connections** (Linear, GitHub, Slack OAuth)? Essas existem para serem chamadas *a partir de* funções de lógica e ficam em [Logic](/l/pt/developers/extend/apps/logic/connections).
|
||||
|
||||
@@ -103,6 +103,10 @@ export default defineCommandMenuItem({
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`RECORD_SELECTION` já implica uma seleção não vazia — use `numberOfSelectedRecords` apenas para contagens específicas (por exemplo, `>= 2`).
|
||||
</Note>
|
||||
|
||||
### Variáveis de contexto
|
||||
|
||||
Elas representam o estado atual da página:
|
||||
|
||||
@@ -196,6 +196,94 @@ export default defineFrontComponent({
|
||||
});
|
||||
```
|
||||
|
||||
## Chamando uma função lógica
|
||||
|
||||
Os componentes de front são executados no navegador em um Web Worker isolado, enquanto as [funções lógicas](/l/pt/developers/extend/apps/logic/logic-functions) são executadas no servidor. Não há chamada direta no mesmo processo entre os dois — em vez disso, um componente de front acessa uma função lógica via HTTP.
|
||||
|
||||
Uma função lógica declarada com `httpRouteTriggerSettings` é exposta sob o endpoint `/s/` em `${TWENTY_API_URL}/s\<path>`. Seu componente de front chama essa rota com `fetch`, autenticando com o `TWENTY_APP_ACCESS_TOKEN` que Twenty injeta no worker.
|
||||
|
||||
Um pequeno helper reutilizável mantém os locais de chamada limpos:
|
||||
|
||||
```ts src/shared/call-app-route.ts
|
||||
export async function callAppRoute(
|
||||
path: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const apiUrl = process.env.TWENTY_API_URL ?? '';
|
||||
const token = process.env.TWENTY_APP_ACCESS_TOKEN;
|
||||
|
||||
const res = await fetch(`${apiUrl}/s${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Logic function failed (${res.status})`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
```
|
||||
|
||||
Um componente de front headless pode executar a chamada ao montar via o componente `Command` e, em seguida, desmontar automaticamente:
|
||||
|
||||
```tsx src/front-components/sync-prs.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { callAppRoute } from 'src/shared/call-app-route';
|
||||
|
||||
const SyncPrs = () => {
|
||||
const execute = async () => {
|
||||
await callAppRoute('/github/fetch-prs', {
|
||||
owner: 'twentyhq',
|
||||
repo: 'twenty',
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-prs',
|
||||
description: 'Triggers the fetch-prs logic function',
|
||||
isHeadless: true,
|
||||
component: SyncPrs,
|
||||
});
|
||||
```
|
||||
|
||||
O `path` passado para `callAppRoute` deve corresponder ao `httpRouteTriggerSettings.path` da função lógica (o prefixo `/s` é adicionado pelo helper):
|
||||
|
||||
```ts src/logic-functions/fetch-prs.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
|
||||
// ...fetch from GitHub and persist records...
|
||||
return { ok: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-prs',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/github/fetch-prs',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`TWENTY_API_URL` e `TWENTY_APP_ACCESS_TOKEN` são injetados automaticamente — consulte [Variáveis de aplicação](#application-variables). Como as variáveis de aplicação secretas nunca são expostas aos componentes de front, mantenha as chaves de API e outra lógica sensível na função lógica, não no componente de front.
|
||||
</Note>
|
||||
|
||||
## Acessando o contexto de execução
|
||||
|
||||
Dentro do seu componente, use hooks do SDK para acessar o usuário atual, o registro e a instância do componente:
|
||||
|
||||
@@ -65,16 +65,15 @@ Use isto quando você quiser apenas **adicionar** uma aba a um layout existente
|
||||
import {
|
||||
definePageLayoutTab,
|
||||
PageLayoutTabLayoutMode,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
|
||||
'20202020-ab01-4001-8001-c0aba11c0100';
|
||||
|
||||
export default definePageLayoutTab({
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
|
||||
pageLayoutUniversalIdentifier:
|
||||
COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
|
||||
.universalIdentifier,
|
||||
title: 'Hello World',
|
||||
position: 1000,
|
||||
icon: 'IconWorld',
|
||||
@@ -97,6 +96,37 @@ export default definePageLayoutTab({
|
||||
### Pontos-chave
|
||||
|
||||
* `pageLayoutUniversalIdentifier` é **obrigatório** e deve apontar para um layout de página que já exista no momento da instalação — seja um layout padrão da Twenty ou um definido pelo seu próprio aplicativo. Referências entre aplicativos para layouts pertencentes a outro aplicativo instalado não são compatíveis atualmente. Quando o layout pai estiver ausente, a instalação falha com um erro de validação claro.
|
||||
|
||||
* Para layouts padrão do Twenty, importe identificadores de `twenty-sdk/define`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||||
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
|
||||
// …
|
||||
```
|
||||
|
||||
Cada entrada de layout também expõe suas `tabs` e seus `widgets`, para que você possa fazer referência a qualquer nível:
|
||||
|
||||
```ts
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.universalIdentifier
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets.fields.universalIdentifier
|
||||
```
|
||||
|
||||
Um alias abreviado `STANDARD_PAGE_LAYOUT` também está disponível:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
|
||||
|
||||
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
|
||||
```
|
||||
|
||||
* `widgets` têm escopo apenas para esta aba — eles referenciam [front components](/l/pt/developers/extend/apps/layout/front-components), visualizações etc., exatamente como widgets definidos inline em `definePageLayout`.
|
||||
|
||||
* `position` controla a ordenação em relação às abas existentes no layout de destino. Escolha um valor que posicione sua aba onde você deseja em relação às abas nativas.
|
||||
|
||||
* Use isto em vez de `definePageLayout` quando você quiser apenas adicionar a um layout existente. Use `definePageLayout` quando você possuir todo o layout.
|
||||
|
||||
@@ -70,7 +70,7 @@ filters: [
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS` |
|
||||
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Tipos de campos com nomes semelhantes podem usar operandos completamente diferentes — `SELECT` e `MULTI_SELECT` sendo um caso comum.
|
||||
|
||||
@@ -53,6 +53,10 @@ export default defineLogicFunction({
|
||||
Tipos de gatilho disponíveis:
|
||||
* **httpRoute**: Expõe sua função em um caminho e método HTTP **no endpoint `/s/`**:
|
||||
> por exemplo, `path: '/post-card/create'` é acessível em `https://your-twenty-server.com/s/post-card/create`
|
||||
|
||||
<Note>
|
||||
Para invocar uma função de lógica acionada por rota a partir de um componente de front-end (headless), consulte [Chamando uma função de lógica](/l/pt/developers/extend/apps/layout/front-components#calling-a-logic-function).
|
||||
</Note>
|
||||
* **cron**: Executa sua função em um agendamento usando uma expressão CRON.
|
||||
* **databaseEvent**: Executa em eventos do ciclo de vida de objetos do espaço de trabalho. Quando a operação do evento é `updated`, campos específicos a serem observados podem ser especificados no array `updatedFields`. Se deixar indefinido ou vazio, qualquer atualização acionará a função.
|
||||
> por exemplo, `person.updated`, `*.created`, `company.*`
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
title: Outros métodos
|
||||
icon: cloud
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Este documento é mantido pela comunidade. Pode conter problemas.
|
||||
</Warning>
|
||||
|
||||
## Kubernetes via Terraform e Manifests
|
||||
|
||||
A documentação liderada pela comunidade para a implantação do Kubernetes está disponível [aqui](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s)
|
||||
|
||||
### Coolify
|
||||
|
||||
Implante o Twenty em servidores usando o Coolify. (imagem oficial no Coolify estará disponível em breve)
|
||||
|
||||
[Documentação Coolify](https://coolify.io/docs/get-started/introduction)
|
||||
|
||||
### EasyPanel
|
||||
|
||||
Implante o Twenty no EasyPanel com o modelo mantido pela comunidade abaixo.
|
||||
|
||||
[Implante no EasyPanel](https://easypanel.io/docs/templates/twenty)
|
||||
|
||||
### Elest.io
|
||||
|
||||
Implante o Twenty em servidores com Elest.io usando o link abaixo.
|
||||
|
||||
[Implante no Elest.io](https://elest.io/open-source/twenty)
|
||||
|
||||
### Twenty no Railway
|
||||
|
||||
Implante o Twenty no Railway com o modelo mantido pela comunidade abaixo.
|
||||
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
|
||||
### Twenty no Sealos
|
||||
|
||||
Implante o Twenty no Sealos com o modelo mantido pela comunidade abaixo.
|
||||
|
||||
[](https://sealos.io/products/app-store/twenty)
|
||||
|
||||
## Outros
|
||||
|
||||
Sinta-se à vontade para abrir um PR para adicionar mais opções de Provedor de Nuvem.
|
||||
@@ -23,7 +23,4 @@ O Twenty pode ser auto-hospedado na sua própria infraestrutura, oferecendo cont
|
||||
<Card title="Docker Compose" icon="docker" href="/l/pt/developers/self-host/capabilities/docker-compose">
|
||||
Configuração rápida com Docker
|
||||
</Card>
|
||||
<Card title="Provedores de nuvem" icon="cloud" href="/l/pt/developers/self-host/capabilities/cloud-providers">
|
||||
Implante na AWS, GCP ou Azure
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -101,6 +101,10 @@ Torne um campo único para garantir que registros distintos não possam ter o me
|
||||
|
||||
Se você receber um erro ao definir exclusividade, verifique se há valores duplicados nos seus dados (incluindo registros excluídos).
|
||||
|
||||
## Índices (Avançado)
|
||||
|
||||
Os índices do banco de dados são gerenciados automaticamente — adicionar os seus próprios raramente é necessário e é fácil cometer erros. Com o modo Avançado ativado, cada objeto tem uma seção **Índices** em `Settings → Data Model → <object>` para os casos em que você sabe que precisa de um índice.
|
||||
|
||||
## Melhores Práticas de Configuração de Campos
|
||||
|
||||
### Convenções de Nomeação e Limitações
|
||||
|
||||
@@ -43,6 +43,8 @@ export default defineField({
|
||||
|
||||
* Locația fișierului depinde de dumneavoastră. Convenția este `src/fields/\<name>.field.ts`, dar SDK-ul detectează câmpuri oriunde în `src/`.
|
||||
|
||||
* Pentru a adăuga o filă într-un layout standard de pagină (de ex. pagina de detalii pentru Task sau Company), folosește [`definePageLayoutTab`](/l/ro/developers/extend/apps/layout/page-layouts#definepagelayouttab) cu `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` din `twenty-sdk/define`.
|
||||
|
||||
## Adăugarea unei relații la un obiect existent
|
||||
|
||||
Pentru a adăuga un câmp de tip relație (de ex. pentru a lega obiectul personalizat de un `Person` standard), folosiți `defineField()` cu `FieldType.RELATION`. Modelul este același ca pentru relațiile inline, dar cu `objectUniversalIdentifier` setat explicit. Consultați [Relații](/l/ro/developers/extend/apps/data/relations) pentru modelul bidirecțional.
|
||||
|
||||
@@ -44,8 +44,53 @@ Stratul de **date** al unei aplicații Twenty reprezintă datele pe care aplica
|
||||
| **Obiect** | Un nou tip de înregistrare personalizat (de ex. PostCard, Invoice) cu propriile sale câmpuri | `defineObject()` |
|
||||
| **Câmp** | O coloană pe un obiect. Câmpurile independente pot extinde obiecte pe care nu le-ați creat (de ex. adăugați `loyaltyTier` la Company) | `defineField()` |
|
||||
| **Relație** | O legătură bidirecțională între două obiecte — ambele părți declarate ca câmpuri | `defineField()` cu `FieldType.RELATION` |
|
||||
| **Indice** | Un indice de bază de date pentru a accelera o interogare recurentă asupra unuia dintre obiectele tale | `defineIndex()` |
|
||||
|
||||
SDK-ul detectează acestea prin analiza AST la momentul build-ului, astfel încât organizarea fișierelor ține de dumneavoastră — convenția este `src/objects/` și `src/fields/`. UUID-urile stabile `universalIdentifier` leagă totul în toate implementările.
|
||||
SDK-ul detectează acestea prin analiza AST la momentul build-ului, astfel încât organizarea fișierelor ține de tine — convenția este `src/objects/`, `src/fields/` și `src/indexes/`. UUID-urile stabile `universalIdentifier` leagă totul în toate implementările.
|
||||
|
||||
## Indici (opțional)
|
||||
|
||||
Aplicațiile pot livra indici împreună cu obiectele lor pentru a menține rapide interogările recurente. Cel mai comun caz este o coloană de status sau o coloană cu cheie străină pe care o citești frecvent.
|
||||
|
||||
```ts src/indexes/post-card-status.index.ts
|
||||
import { defineIndex } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from '../objects/post-card.object';
|
||||
|
||||
export default defineIndex({
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
|
||||
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Indici unici
|
||||
|
||||
`defineIndex` acceptă `isUnique: true` atât pentru unicitatea pe o singură coloană, cât și pe mai multe coloane. Aceasta este primitiva recomandată — `defineField({ isUnique: true })` este învechită (deprecated) și va fi eliminată într-o versiune viitoare.
|
||||
|
||||
```ts
|
||||
defineIndex({
|
||||
universalIdentifier: '…',
|
||||
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
|
||||
isUnique: true,
|
||||
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
|
||||
});
|
||||
```
|
||||
|
||||
### Alte constrângeri
|
||||
|
||||
* Clauzele `WHERE` parțiale rămân sub controlul administratorului — aplicațiile nu le pot declara.
|
||||
* Fiecare obiect este limitat la 10 indici personalizați (indicii proprii ai framework-ului nu se pun la socoteală).
|
||||
|
||||
Ordonează array-ul `fields` în modul în care Postgres ar trebui să îl folosească — coloana din stânga prima, ca într-o agendă telefonică. Indicii nu sunt gratuiți: fiecare scriere în tabel îi actualizează. Adaugă unul doar atunci când ai o interogare care are nevoie de el.
|
||||
|
||||
<Note>
|
||||
Căutați **Application Config** sau **Roles & Permissions**? Acestea descriu aplicația în sine, mai degrabă decât datele pe care le adaugă — se află la [Config](/l/ro/developers/extend/apps/config/overview). Căutați **Connections** (Linear, GitHub, Slack OAuth)? Acestea există pentru a fi apelate *din* funcții de logică și se află la [Logic](/l/ro/developers/extend/apps/logic/connections).
|
||||
|
||||
@@ -103,6 +103,10 @@ export default defineCommandMenuItem({
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`RECORD_SELECTION` implică deja o selecție care nu este goală — folosește `numberOfSelectedRecords` doar pentru valori specifice (de ex. `>= 2`).
|
||||
</Note>
|
||||
|
||||
### Variabile de context
|
||||
|
||||
Acestea reprezintă starea curentă a paginii:
|
||||
|
||||
@@ -196,6 +196,94 @@ export default defineFrontComponent({
|
||||
});
|
||||
```
|
||||
|
||||
## Apelarea unei funcții logice
|
||||
|
||||
Componentele de front rulează în browser într-un Web Worker izolat, în timp ce [funcțiile logice](/l/ro/developers/extend/apps/logic/logic-functions) rulează pe server. Nu există un apel direct în același proces între cele două — în schimb, o componentă de front apelează o funcție logică prin HTTP.
|
||||
|
||||
O funcție logică declarată cu `httpRouteTriggerSettings` este expusă sub endpoint-ul `/s/` la `${TWENTY_API_URL}/s\<path>`. Componenta ta de front apelează acea rută cu `fetch`, autentificându-se cu `TWENTY_APP_ACCESS_TOKEN` pe care Twenty îl injectează în worker.
|
||||
|
||||
Un mic utilitar reutilizabil menține locurile de apel curate:
|
||||
|
||||
```ts src/shared/call-app-route.ts
|
||||
export async function callAppRoute(
|
||||
path: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const apiUrl = process.env.TWENTY_API_URL ?? '';
|
||||
const token = process.env.TWENTY_APP_ACCESS_TOKEN;
|
||||
|
||||
const res = await fetch(`${apiUrl}/s${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Logic function failed (${res.status})`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
```
|
||||
|
||||
O componentă de front headless poate efectua apelul la montare prin componenta `Command`, apoi se demontează automat:
|
||||
|
||||
```tsx src/front-components/sync-prs.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { callAppRoute } from 'src/shared/call-app-route';
|
||||
|
||||
const SyncPrs = () => {
|
||||
const execute = async () => {
|
||||
await callAppRoute('/github/fetch-prs', {
|
||||
owner: 'twentyhq',
|
||||
repo: 'twenty',
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-prs',
|
||||
description: 'Triggers the fetch-prs logic function',
|
||||
isHeadless: true,
|
||||
component: SyncPrs,
|
||||
});
|
||||
```
|
||||
|
||||
`path` transmis către `callAppRoute` trebuie să corespundă cu `httpRouteTriggerSettings.path` al funcției logice (prefixul `/s` este adăugat de utilitar):
|
||||
|
||||
```ts src/logic-functions/fetch-prs.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
|
||||
// ...fetch from GitHub and persist records...
|
||||
return { ok: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-prs',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/github/fetch-prs',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`TWENTY_API_URL` și `TWENTY_APP_ACCESS_TOKEN` sunt injectate automat — vezi [Application variables](#application-variables). Deoarece variabilele de aplicație secrete nu sunt niciodată expuse componentelor de front, păstrează cheile API și altă logică sensibilă în funcția logică, nu în componenta de front.
|
||||
</Note>
|
||||
|
||||
## Accesarea contextului de rulare
|
||||
|
||||
În interiorul componentei, folosiți hook-urile SDK pentru a accesa utilizatorul curent, înregistrarea curentă și instanța componentei:
|
||||
|
||||
@@ -65,16 +65,15 @@ Folosește aceasta atunci când vrei doar să **adaugi** o filă la un layout ex
|
||||
import {
|
||||
definePageLayoutTab,
|
||||
PageLayoutTabLayoutMode,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
|
||||
'20202020-ab01-4001-8001-c0aba11c0100';
|
||||
|
||||
export default definePageLayoutTab({
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
|
||||
pageLayoutUniversalIdentifier:
|
||||
COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
|
||||
.universalIdentifier,
|
||||
title: 'Hello World',
|
||||
position: 1000,
|
||||
icon: 'IconWorld',
|
||||
@@ -97,6 +96,37 @@ export default definePageLayoutTab({
|
||||
### Puncte cheie
|
||||
|
||||
* `pageLayoutUniversalIdentifier` este **obligatoriu** și trebuie să indice către un layout de pagină care există deja la momentul instalării — fie un layout standard Twenty, fie unul definit de propria ta aplicație. Referințele cross-app către layouturi deținute de o altă aplicație instalată nu sunt acceptate în prezent. Când lipsește layoutul părinte, instalarea eșuează cu o eroare clară de validare.
|
||||
|
||||
* Pentru layout-urile standard Twenty, importați identificatorii din `twenty-sdk/define`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||||
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
|
||||
// …
|
||||
```
|
||||
|
||||
Fiecare intrare de layout își expune, de asemenea, `tabs` și `widgets`, astfel încât puteți face referire la orice nivel:
|
||||
|
||||
```ts
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.universalIdentifier
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets.fields.universalIdentifier
|
||||
```
|
||||
|
||||
Este disponibil și un alias scurt `STANDARD_PAGE_LAYOUT`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
|
||||
|
||||
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
|
||||
```
|
||||
|
||||
* `widgets` sunt limitate doar la această filă — fac referire la [front components](/l/ro/developers/extend/apps/layout/front-components), vizualizări etc., exact ca widgeturile definite inline în `definePageLayout`.
|
||||
|
||||
* `position` controlează ordonarea în raport cu filele existente din layoutul țintă. Alege o valoare care să plaseze fila ta acolo unde dorești, relativ la filele predefinite.
|
||||
|
||||
* Folosește aceasta în loc de `definePageLayout` atunci când vrei doar să adaugi la un layout existent. Folosește `definePageLayout` atunci când deții întregul layout.
|
||||
|
||||
@@ -70,7 +70,7 @@ filters: [
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS` |
|
||||
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Tipurile de câmp cu nume similare pot folosi operanzi complet diferiți — `SELECT` și `MULTI_SELECT` fiind un caz comun.
|
||||
|
||||
@@ -53,6 +53,10 @@ export default defineLogicFunction({
|
||||
Tipuri de declanșatoare disponibile:
|
||||
* **httpRoute**: Expune funcția pe o cale și metodă HTTP **sub endpoint-ul `/s/`**:
|
||||
> de ex. `path: '/post-card/create'` este apelabil la `https://your-twenty-server.com/s/post-card/create`
|
||||
|
||||
<Note>
|
||||
Pentru a apela o funcție logică declanșată de o rută dintr-o componentă front-end (headless), consultă [Apelarea unei funcții logice](/l/ro/developers/extend/apps/layout/front-components#calling-a-logic-function).
|
||||
</Note>
|
||||
* **cron**: Rulează funcția pe un program folosind o expresie CRON.
|
||||
* **databaseEvent**: Rulează la evenimentele ciclului de viață ale obiectelor din spațiul de lucru. Când operațiunea evenimentului este `updated`, câmpurile specifice de urmărit pot fi specificate în array-ul `updatedFields`. Dacă este lăsat nedefinit sau gol, orice actualizare va declanșa funcția.
|
||||
> de ex. `person.updated`, `*.created`, `company.*`
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
title: Alte metode
|
||||
icon: cloud
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Acest document este întreținut de comunitate. Ar putea conține probleme.
|
||||
</Warning>
|
||||
|
||||
## Kubernetes prin Terraform și Manifeste
|
||||
|
||||
Documentația realizată de comunitate pentru implementarea pe Kubernetes este disponibilă [aici](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s)
|
||||
|
||||
### Coolify
|
||||
|
||||
Deplasați Twenty pe servere folosind Coolify. (imaginea oficială pe Coolify va fi disponibilă curând)
|
||||
|
||||
[Documentație Coolify](https://coolify.io/docs/get-started/introduction)
|
||||
|
||||
### EasyPanel
|
||||
|
||||
Deplasați Twenty pe EasyPanel cu șablonul întreținut de comunitate mai jos.
|
||||
|
||||
[Deplasați pe EasyPanel](https://easypanel.io/docs/templates/twenty)
|
||||
|
||||
### Elest.io
|
||||
|
||||
Deplasați Twenty pe servere cu Elest.io folosind link-ul mai jos.
|
||||
|
||||
[Deplasați pe Elest.io](https://elest.io/open-source/twenty)
|
||||
|
||||
### Twenty pe Railway
|
||||
|
||||
Deplasați Twenty pe Railway cu șablonul întreținut de comunitate mai jos.
|
||||
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
|
||||
### Twenty pe Sealos
|
||||
|
||||
Deplasați Twenty pe Sealos cu șablonul întreținut de comunitate mai jos.
|
||||
|
||||
[](https://sealos.io/products/app-store/twenty)
|
||||
|
||||
## Altele
|
||||
|
||||
Vă rugăm să deschideți un PR pentru a adăuga mai multe opțiuni de furnizori de cloud.
|
||||
@@ -23,7 +23,4 @@ Twenty poate fi găzduit în regim propriu pe propria infrastructură, oferindu-
|
||||
<Card title="Docker Compose" icon="docker" href="/l/ro/developers/self-host/capabilities/docker-compose">
|
||||
Configurare rapidă cu Docker
|
||||
</Card>
|
||||
<Card title="Furnizori cloud" icon="cloud" href="/l/ro/developers/self-host/capabilities/cloud-providers">
|
||||
Implementați pe AWS, GCP sau Azure
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -101,6 +101,10 @@ Faceți un câmp unic pentru a vă asigura că înregistrările distincte nu pot
|
||||
|
||||
Dacă primiți o eroare când setați unicitatea, verificați prezența valorilor duplicate în datele dumneavoastră (inclusiv în cele șterse).
|
||||
|
||||
## Indexuri (Avansat)
|
||||
|
||||
Indexurile bazei de date sunt gestionate automat — adăugarea unor indexuri proprii este rareori necesară și se greșește ușor. Cu modul Avansat activat, fiecare obiect are o secțiune **Indexuri** sub `Settings → Data Model → <object>` pentru cazurile în care știi că ai nevoie de unul.
|
||||
|
||||
## Cele mai bune practici pentru configurarea câmpurilor
|
||||
|
||||
### Convenții de denumire și limitări
|
||||
|
||||
@@ -43,6 +43,8 @@ export default defineField({
|
||||
|
||||
* Расположение файла зависит от вас. Принятое соглашение — `src/fields/\<name>.field.ts`, но SDK обнаруживает поля в любом месте внутри `src/`.
|
||||
|
||||
* Чтобы добавить вкладку в стандартную компоновку страницы (например, на страницу сведений о задаче или компании), используйте [`definePageLayoutTab`](/l/ru/developers/extend/apps/layout/page-layouts#definepagelayouttab) с `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` из `twenty-sdk/define`.
|
||||
|
||||
## Добавление связи к существующему объекту
|
||||
|
||||
Чтобы добавить поле связи (например, связать ваш пользовательский объект со стандартным `Person`), используйте `defineField()` с `FieldType.RELATION`. Шаблон тот же, что и для встроенных связей, но с явным указанием `objectUniversalIdentifier`. Смотрите раздел [Relations](/l/ru/developers/extend/apps/data/relations) для двунаправленного шаблона.
|
||||
|
||||
@@ -44,8 +44,53 @@ icon: database
|
||||
| **Объект** | Новый пользовательский тип записей (например, PostCard, Invoice) с собственными полями | `defineObject()` |
|
||||
| **Поле** | Столбец в объекте. Отдельные поля могут расширять объекты, которые вы не создавали (например, добавьте `loyaltyTier` к объекту Company) | `defineField()` |
|
||||
| **Связь** | Двусторонняя связь между двумя объектами — обе стороны объявлены как поля | `defineField()` с `FieldType.RELATION` |
|
||||
| **Индекс** | Индекс базы данных для ускорения повторяющегося запроса к одному из ваших объектов | `defineIndex()` |
|
||||
|
||||
SDK обнаруживает их с помощью анализа AST во время сборки, поэтому организация файлов остается на ваше усмотрение — по соглашению используются `src/objects/` и `src/fields/`. Стабильные UUID `universalIdentifier` связывают все воедино между развертываниями.
|
||||
SDK обнаруживает их с помощью анализа AST во время сборки, поэтому организация файлов остается на ваше усмотрение — по соглашению используются `src/objects/`, `src/fields/` и `src/indexes/`. Стабильные UUID `universalIdentifier` связывают все воедино между развертываниями.
|
||||
|
||||
## Индексы (необязательно)
|
||||
|
||||
Приложения могут поставлять индексы вместе со своими объектами, чтобы повторяющиеся запросы выполнялись быстро. Наиболее распространенный случай — столбец статуса или внешнего ключа, к которому вы часто обращаетесь при чтении.
|
||||
|
||||
```ts src/indexes/post-card-status.index.ts
|
||||
import { defineIndex } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from '../objects/post-card.object';
|
||||
|
||||
export default defineIndex({
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
|
||||
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Уникальные индексы
|
||||
|
||||
`defineIndex` принимает `isUnique: true` как для уникальности по одному столбцу, так и по нескольким столбцам. Это рекомендуемый примитив — `defineField({ isUnique: true })` устарел и будет удален в одном из будущих релизов.
|
||||
|
||||
```ts
|
||||
defineIndex({
|
||||
universalIdentifier: '…',
|
||||
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
|
||||
isUnique: true,
|
||||
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
|
||||
});
|
||||
```
|
||||
|
||||
### Другие ограничения
|
||||
|
||||
* Частичные предложения `WHERE` остаются под контролем администратора — приложения не могут объявлять их.
|
||||
* Для каждого объекта допускается не более 10 пользовательских индексов (индексы самого фреймворка не учитываются).
|
||||
|
||||
Упорядочьте массив `fields` в том порядке, в котором Postgres должен его использовать — сначала самый левый столбец, как в телефонной книге. Индексы не бесплатны: при каждой записи в таблицу они обновляются. Добавляйте индекс только тогда, когда у вас есть запрос, которому он действительно нужен.
|
||||
|
||||
<Note>
|
||||
Ищете **Application Config** или **Roles & Permissions**? Они описывают само приложение, а не данные, которые оно добавляет, — их можно найти в разделе [Config](/l/ru/developers/extend/apps/config/overview). Ищете **Connections** (Linear, GitHub, Slack OAuth)? Они существуют для вызова *из* логических функций и находятся в разделе [Logic](/l/ru/developers/extend/apps/logic/connections).
|
||||
|
||||
@@ -103,6 +103,10 @@ export default defineCommandMenuItem({
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`RECORD_SELECTION` уже подразумевает, что есть выбранные записи — используйте `numberOfSelectedRecords` только для конкретных количеств (например, `>= 2`).
|
||||
</Note>
|
||||
|
||||
### Переменные контекста
|
||||
|
||||
Они представляют текущее состояние страницы:
|
||||
|
||||
@@ -196,6 +196,94 @@ export default defineFrontComponent({
|
||||
});
|
||||
```
|
||||
|
||||
## Вызов логической функции
|
||||
|
||||
Front-компоненты выполняются в браузере в изолированном Web Worker, в то время как [логические функции](/l/ru/developers/extend/apps/logic/logic-functions) выполняются на стороне сервера. Между ними нет прямого внутрипроцессного вызова — вместо этого front-компонент обращается к логической функции по HTTP.
|
||||
|
||||
Логическая функция, объявленная с `httpRouteTriggerSettings`, доступна по эндпоинту `/s/` по адресу `${TWENTY_API_URL}/s\<path>`. Ваш front-компонент вызывает этот маршрут с помощью `fetch`, аутентифицируясь с использованием `TWENTY_APP_ACCESS_TOKEN`, который Twenty внедряет в worker.
|
||||
|
||||
Небольшой переиспользуемый хелпер делает места вызова чище:
|
||||
|
||||
```ts src/shared/call-app-route.ts
|
||||
export async function callAppRoute(
|
||||
path: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const apiUrl = process.env.TWENTY_API_URL ?? '';
|
||||
const token = process.env.TWENTY_APP_ACCESS_TOKEN;
|
||||
|
||||
const res = await fetch(`${apiUrl}/s${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Logic function failed (${res.status})`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
```
|
||||
|
||||
Безголовый front-компонент может выполнить вызов при монтировании через компонент `Command`, а затем автоматически размонтироваться:
|
||||
|
||||
```tsx src/front-components/sync-prs.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { callAppRoute } from 'src/shared/call-app-route';
|
||||
|
||||
const SyncPrs = () => {
|
||||
const execute = async () => {
|
||||
await callAppRoute('/github/fetch-prs', {
|
||||
owner: 'twentyhq',
|
||||
repo: 'twenty',
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-prs',
|
||||
description: 'Triggers the fetch-prs logic function',
|
||||
isHeadless: true,
|
||||
component: SyncPrs,
|
||||
});
|
||||
```
|
||||
|
||||
`path`, переданный в `callAppRoute`, должен совпадать с `httpRouteTriggerSettings.path` логической функции (префикс `/s` добавляется хелпером):
|
||||
|
||||
```ts src/logic-functions/fetch-prs.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
|
||||
// ...fetch from GitHub and persist records...
|
||||
return { ok: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-prs',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/github/fetch-prs',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`TWENTY_API_URL` и `TWENTY_APP_ACCESS_TOKEN` внедряются автоматически — см. [переменные приложения](#application-variables). Поскольку секретные переменные приложения никогда не раскрываются front-компонентам, храните ключи API и другую конфиденциальную логику в логической функции, а не во front-компоненте.
|
||||
</Note>
|
||||
|
||||
## Доступ к контексту времени выполнения
|
||||
|
||||
Внутри вашего компонента используйте хуки SDK для доступа к текущему пользователю, записи и экземпляру компонента:
|
||||
|
||||
@@ -65,16 +65,15 @@ export default definePageLayout({
|
||||
import {
|
||||
definePageLayoutTab,
|
||||
PageLayoutTabLayoutMode,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
|
||||
'20202020-ab01-4001-8001-c0aba11c0100';
|
||||
|
||||
export default definePageLayoutTab({
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
|
||||
pageLayoutUniversalIdentifier:
|
||||
COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
|
||||
.universalIdentifier,
|
||||
title: 'Hello World',
|
||||
position: 1000,
|
||||
icon: 'IconWorld',
|
||||
@@ -97,6 +96,37 @@ export default definePageLayoutTab({
|
||||
### Основные моменты
|
||||
|
||||
* `pageLayoutUniversalIdentifier` является **обязательным** и должен указывать на макет страницы, который уже существует на момент установки — либо стандартный макет Twenty, либо определённый вашим собственным приложением. Кросс-приложенческие ссылки на макеты, которыми владеет другое установленное приложение, на данный момент не поддерживаются. Если родительский макет отсутствует, установка завершается с понятной ошибкой проверки.
|
||||
|
||||
* Для стандартных макетов Twenty импортируйте идентификаторы из `twenty-sdk/define`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||||
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
|
||||
// …
|
||||
```
|
||||
|
||||
Каждый элемент макета также предоставляет свои `tabs` и их `widgets`, поэтому вы можете ссылаться на любой уровень:
|
||||
|
||||
```ts
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.universalIdentifier
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets.fields.universalIdentifier
|
||||
```
|
||||
|
||||
Также доступен короткий псевдоним `STANDARD_PAGE_LAYOUT`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
|
||||
|
||||
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
|
||||
```
|
||||
|
||||
* `widgets` ограничены только этой вкладкой — они ссылаются на [front components](/l/ru/developers/extend/apps/layout/front-components), представления и т. п. точно так же, как виджеты, определённые непосредственно в `definePageLayout`.
|
||||
|
||||
* `position` управляет порядком относительно существующих вкладок в целевом макете. Выберите значение, которое поместит вашу вкладку в нужное место относительно встроенных вкладок.
|
||||
|
||||
* Используйте это вместо `definePageLayout`, когда вы хотите только добавить к существующему макету. Используйте `definePageLayout`, когда вы управляете всем макетом.
|
||||
|
||||
@@ -70,7 +70,7 @@ filters: [
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS` |
|
||||
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Типы полей с похожими названиями могут использовать совершенно разные операнды — типичный пример: `SELECT` и `MULTI_SELECT`.
|
||||
|
||||
@@ -53,6 +53,10 @@ export default defineLogicFunction({
|
||||
Доступные типы триггеров:
|
||||
* **httpRoute**: Публикует вашу функцию по HTTP-пути и методу **под конечной точкой `/s/`**:
|
||||
> например, `path: '/post-card/create'` вызывается по адресу `https://your-twenty-server.com/s/post-card/create`
|
||||
|
||||
<Note>
|
||||
Чтобы вызвать логическую функцию, запускаемую маршрутом, из фронтенд-компонента (без интерфейса), см. раздел [Вызов логической функции](/l/ru/developers/extend/apps/layout/front-components#calling-a-logic-function).
|
||||
</Note>
|
||||
* **cron**: Запускает вашу функцию по расписанию с использованием выражения CRON.
|
||||
* **databaseEvent**: Запускается при событиях жизненного цикла объектов рабочего пространства. Когда операция события — `updated`, можно указать конкретные поля для отслеживания в массиве `updatedFields`. Если оставить не заданным или пустым, любое обновление будет вызывать функцию.
|
||||
> например, `person.updated`, `*.created`, `company.*`
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
title: Другие методы
|
||||
icon: cloud
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Этот документ поддерживается сообществом. Он может содержать ошибки.
|
||||
</Warning>
|
||||
|
||||
## Kubernetes через Terraform и Manifests
|
||||
|
||||
Документация, поддерживаемая сообществом для развертывания Kubernetes, доступна [здесь](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s)
|
||||
|
||||
### Coolify
|
||||
|
||||
Разверните Twenty на серверах с использованием Coolify. (официальное изображение на Coolify будет вскоре доступно)
|
||||
|
||||
[Документация Coolify](https://coolify.io/docs/get-started/introduction)
|
||||
|
||||
### EasyPanel
|
||||
|
||||
Разверните Twenty на EasyPanel с использованием шаблона, поддерживаемого сообществом, ниже.
|
||||
|
||||
[Развернуть на EasyPanel](https://easypanel.io/docs/templates/twenty)
|
||||
|
||||
### Elest.io
|
||||
|
||||
Разверните Twenty на серверах с Elest.io, используя ссылку ниже.
|
||||
|
||||
[Развернуть на Elest.io](https://elest.io/open-source/twenty)
|
||||
|
||||
### Twenty на Railway
|
||||
|
||||
Разверните Twenty на Railway с использованием шаблона, поддерживаемого сообществом, ниже.
|
||||
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
|
||||
### Twenty на Sealos
|
||||
|
||||
Разверните Twenty на Sealos с использованием шаблона, поддерживаемого сообществом, ниже.
|
||||
|
||||
[](https://sealos.io/products/app-store/twenty)
|
||||
|
||||
## Другое
|
||||
|
||||
Пожалуйста, не стесняйтесь открыть PR, чтобы добавить больше вариантов облачных провайдеров.
|
||||
@@ -23,7 +23,4 @@ Twenty можно развернуть в собственной инфраст
|
||||
<Card title="Docker Compose" icon="docker" href="/l/ru/developers/self-host/capabilities/docker-compose">
|
||||
Быстрая настройка с Docker
|
||||
</Card>
|
||||
<Card title="Облачные провайдеры" icon="cloud" href="/l/ru/developers/self-host/capabilities/cloud-providers">
|
||||
Разверните на AWS, GCP или Azure
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -101,6 +101,10 @@ Twenty поддерживает различные типы полей:
|
||||
|
||||
Если вы получаете ошибку при установке уникальности, проверьте дублирующиеся значения в ваших данных (включая удаленные записи).
|
||||
|
||||
## Индексы (расширенный режим)
|
||||
|
||||
Индексы базы данных управляются автоматически — добавлять собственные почти никогда не требуется и при этом легко допустить ошибку. При включенном расширенном режиме у каждого объекта есть раздел **Индексы** в `Settings → Data Model → <object>` для случаев, когда вы знаете, что вам нужен индекс.
|
||||
|
||||
## Лучшие практики в настройке полей
|
||||
|
||||
### Именование и ограничения
|
||||
|
||||
@@ -43,6 +43,8 @@ export default defineField({
|
||||
|
||||
* Dosya konumu size bağlıdır. Genel kabul gören yapı `src/fields/\<name>.field.ts` şeklindedir, ancak SDK `src/` içinde herhangi bir yerdeki alanları algılar.
|
||||
|
||||
* Standart bir sayfa yerleşimine (örneğin, Görev veya Şirket detay sayfası) bir sekme eklemek için, `twenty-sdk/define` içindeki `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` ile birlikte [`definePageLayoutTab`](/l/tr/developers/extend/apps/layout/page-layouts#definepagelayouttab) kullanın.
|
||||
|
||||
## Mevcut bir nesneye ilişki ekleme
|
||||
|
||||
Bir ilişki alanı eklemek için (örneğin özel nesnenizi standart bir `Person` nesnesine bağlamak), `FieldType.RELATION` ile `defineField()` kullanın. Desen, satır içi ilişkilerle aynıdır ancak `objectUniversalIdentifier` açıkça ayarlanır. Çift yönlü desen için [Relations](/l/tr/developers/extend/apps/data/relations) bölümüne bakın.
|
||||
|
||||
@@ -44,8 +44,53 @@ Bir Twenty uygulamasının **veri katmanı**, uygulamanızın bir çalışma ala
|
||||
| **Nesne** | Kendi alanlarına sahip yeni bir özel kayıt türü (ör. PostCard, Invoice) | `defineObject()` |
|
||||
| **Alan** | Bir nesne üzerindeki sütun. Bağımsız alanlar, oluşturmadığınız nesneleri genişletebilir (ör. Company nesnesine `loyaltyTier` ekleyin) | `defineField()` |
|
||||
| **İlişki** | İki nesne arasında, her iki tarafı da alan olarak bildirilmiş çift yönlü bir bağlantı | `defineField()` ile `FieldType.RELATION` |
|
||||
| **Dizin** | Nesnelerinizden biri üzerinde yinelenen bir sorguyu hızlandırmak için bir veritabanı dizini | `defineIndex()` |
|
||||
|
||||
SDK bunları derleme zamanında AST analiziyle algılar, bu yüzden dosya organizasyonu size kalmıştır — kullanılan gelenek `src/objects/` ve `src/fields/` dizinleridir. Kararlı `universalIdentifier` UUID’leri, dağıtımlar arasında her şeyi birbirine bağlar.
|
||||
SDK bunları derleme zamanında AST analiziyle algılar, bu yüzden dosya organizasyonu size kalmıştır — kullanılan gelenek `src/objects/`, `src/fields/` ve `src/indexes/` dizinleridir. Kararlı `universalIdentifier` UUID’leri, dağıtımlar arasında her şeyi birbirine bağlar.
|
||||
|
||||
## Dizinler (İsteğe bağlı)
|
||||
|
||||
Uygulamalar, yinelenen sorguları hızlı tutmak için nesneleriyle birlikte dizinler sunabilir. En yaygın durum, sık okuduğunuz bir durum ya da yabancı anahtar sütunudur.
|
||||
|
||||
```ts src/indexes/post-card-status.index.ts
|
||||
import { defineIndex } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from '../objects/post-card.object';
|
||||
|
||||
export default defineIndex({
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
|
||||
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Benzersiz dizinler
|
||||
|
||||
`defineIndex`, hem tek sütunlu hem çok sütunlu benzersizlik için `isUnique: true` kabul eder. Önerilen yöntem budur — `defineField({ isUnique: true })` kullanımdan kaldırılmıştır ve gelecekteki bir sürümde kaldırılacaktır.
|
||||
|
||||
```ts
|
||||
defineIndex({
|
||||
universalIdentifier: '…',
|
||||
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
|
||||
isUnique: true,
|
||||
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
|
||||
});
|
||||
```
|
||||
|
||||
### Diğer kısıtlamalar
|
||||
|
||||
* Kısmi `WHERE` koşulları yönetici kontrolü altında kalır — uygulamalar bunları tanımlayamaz.
|
||||
* Her nesne, 10 özel dizin ile sınırlandırılmıştır (framework'ün kendi dizinleri buna dahil değildir).
|
||||
|
||||
`fields` dizisini, Postgres'in kullanması gereken şekilde sıralayın — en soldaki sütun ilk, bir telefon rehberinde olduğu gibi. Dizinler bedava değildir: tabloya yapılan her yazma işlemi bunları günceller. Bir dizini yalnızca ona ihtiyaç duyan bir sorgunuz olduğunda ekleyin.
|
||||
|
||||
<Note>
|
||||
**Application Config** veya **Roles & Permissions** mı arıyorsunuz? Bunlar, ekledikleri verilerden çok uygulamanın kendisini tanımlar — [Config](/l/tr/developers/extend/apps/config/overview) altında bulunurlar. **Connections** (Linear, GitHub, Slack OAuth) mı arıyorsunuz? Bunlar, mantık fonksiyonları *içinden* çağrılmak için vardır ve [Logic](/l/tr/developers/extend/apps/logic/connections) altında bulunurlar.
|
||||
|
||||
@@ -103,6 +103,10 @@ export default defineCommandMenuItem({
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`RECORD_SELECTION` zaten boş olmayan bir seçim anlamına gelir — `numberOfSelectedRecords` ifadesini yalnızca belirli sayılar için kullanın (örneğin `>= 2`).
|
||||
</Note>
|
||||
|
||||
### Bağlam değişkenleri
|
||||
|
||||
Bunlar sayfanın mevcut durumunu temsil eder:
|
||||
|
||||
@@ -196,6 +196,94 @@ export default defineFrontComponent({
|
||||
});
|
||||
```
|
||||
|
||||
## Bir mantık işlevini çağırma
|
||||
|
||||
Ön bileşenler, tarayıcı tarafında izole bir Web Worker içinde çalışırken, [mantık işlevleri](/l/tr/developers/extend/apps/logic/logic-functions) sunucu tarafında çalışır. İkisi arasında doğrudan, işlem içi bir çağrı yoktur — bunun yerine, bir ön bileşen bir mantık işlevine HTTP üzerinden erişir.
|
||||
|
||||
`httpRouteTriggerSettings` ile bildirilen bir mantık işlevi, `${TWENTY_API_URL}/s\<path>` altındaki `/s/` uç noktasında sunulur. Ön bileşeniniz bu rotayı, Twenty tarafından Worker'a enjekte edilen `TWENTY_APP_ACCESS_TOKEN` ile kimlik doğrulayarak `fetch` ile çağırır.
|
||||
|
||||
Küçük, yeniden kullanılabilir bir yardımcı işlev çağrı noktalarını sade tutar:
|
||||
|
||||
```ts src/shared/call-app-route.ts
|
||||
export async function callAppRoute(
|
||||
path: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const apiUrl = process.env.TWENTY_API_URL ?? '';
|
||||
const token = process.env.TWENTY_APP_ACCESS_TOKEN;
|
||||
|
||||
const res = await fetch(`${apiUrl}/s${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Logic function failed (${res.status})`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
```
|
||||
|
||||
Başsız bir ön bileşen, çağrıyı `Command` bileşeni aracılığıyla mount sırasında çalıştırabilir ve ardından otomatik olarak unmount olabilir:
|
||||
|
||||
```tsx src/front-components/sync-prs.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { callAppRoute } from 'src/shared/call-app-route';
|
||||
|
||||
const SyncPrs = () => {
|
||||
const execute = async () => {
|
||||
await callAppRoute('/github/fetch-prs', {
|
||||
owner: 'twentyhq',
|
||||
repo: 'twenty',
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-prs',
|
||||
description: 'Triggers the fetch-prs logic function',
|
||||
isHeadless: true,
|
||||
component: SyncPrs,
|
||||
});
|
||||
```
|
||||
|
||||
`callAppRoute` fonksiyonuna geçirilen `path`, mantık işlevinin `httpRouteTriggerSettings.path` değeriyle eşleşmelidir (`/s` öneki yardımcı işlev tarafından eklenir):
|
||||
|
||||
```ts src/logic-functions/fetch-prs.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
|
||||
// ...fetch from GitHub and persist records...
|
||||
return { ok: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-prs',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/github/fetch-prs',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`TWENTY_API_URL` ve `TWENTY_APP_ACCESS_TOKEN` otomatik olarak enjekte edilir — bkz. [Uygulama değişkenleri](#application-variables). Gizli uygulama değişkenleri asla ön bileşenlere açığa çıkarılmadığından, API anahtarlarını ve diğer hassas mantığı ön bileşende değil, mantık işlevinin içinde tutun.
|
||||
</Note>
|
||||
|
||||
## Çalışma zamanı bağlamına erişme
|
||||
|
||||
Bileşeninizin içinde, geçerli kullanıcıya, kayda ve bileşen örneğine erişmek için SDK hook'larını kullanın:
|
||||
|
||||
@@ -65,16 +65,15 @@ Bunu yalnızca mevcut bir düzene **sekme eklemek** istediğinizde kullanın —
|
||||
import {
|
||||
definePageLayoutTab,
|
||||
PageLayoutTabLayoutMode,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
|
||||
'20202020-ab01-4001-8001-c0aba11c0100';
|
||||
|
||||
export default definePageLayoutTab({
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
|
||||
pageLayoutUniversalIdentifier:
|
||||
COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
|
||||
.universalIdentifier,
|
||||
title: 'Hello World',
|
||||
position: 1000,
|
||||
icon: 'IconWorld',
|
||||
@@ -97,6 +96,37 @@ export default definePageLayoutTab({
|
||||
### Önemli noktalar
|
||||
|
||||
* `pageLayoutUniversalIdentifier` **zorunludur** ve kurulum anında zaten var olan bir sayfa düzenini işaret etmelidir — standart bir Twenty düzeni veya kendi uygulamanız tarafından tanımlanan bir düzen olabilir. Başka yüklü bir uygulamaya ait düzenlere uygulamalar arası referanslar bugün desteklenmemektedir. Üst düzen eksik olduğunda, kurulum net bir doğrulama hatasıyla başarısız olur.
|
||||
|
||||
* Standart Twenty yerleşimleri için tanımlayıcıları `twenty-sdk/define` içinden içe aktarın:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||||
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
|
||||
// …
|
||||
```
|
||||
|
||||
Her yerleşim girdisi ayrıca `tabs` ve bunların `widgets` ögelerini de açığa çıkarır, böylece herhangi bir düzeye başvurabilirsiniz:
|
||||
|
||||
```ts
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.universalIdentifier
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets.fields.universalIdentifier
|
||||
```
|
||||
|
||||
Kısa bir takma ad `STANDARD_PAGE_LAYOUT` da kullanılabilir:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
|
||||
|
||||
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
|
||||
```
|
||||
|
||||
* `widgets` yalnızca bu sekmeyle sınırlıdır — satır içi olarak `definePageLayout` içinde tanımlanan widget'larda olduğu gibi, [ön uç bileşenlerine](/l/tr/developers/extend/apps/layout/front-components), görünümlere vb. tam olarak aynı şekilde referans verirler.
|
||||
|
||||
* `position`, hedeflenen düzende mevcut sekmelere göre sıralamayı kontrol eder. Yerleşik sekmelere göre sekmenizi istediğiniz konuma yerleştirecek bir değer seçin.
|
||||
|
||||
* Yalnızca mevcut bir düzene ekleme yapmak istediğinizde `definePageLayout` yerine bunu kullanın. Tüm düzene sahip olduğunuzda `definePageLayout` kullanın.
|
||||
|
||||
@@ -70,7 +70,7 @@ filters: [
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS` |
|
||||
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Benzer adlara sahip alan türleri tamamen farklı işleçler kullanabilir — buna `SELECT` ve `MULTI_SELECT` yaygın bir örnektir.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user