Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9cb46f42d | ||
|
|
882781a432 | ||
|
|
2267ed1984 | ||
|
|
347ef9ca47 | ||
|
|
9d6c5b7d58 | ||
|
|
a2db0b6932 | ||
|
|
5eb79e7797 | ||
|
|
f19647617a | ||
|
|
424c6737a1 | ||
|
|
be8c25dfc2 | ||
|
|
b8de469f37 | ||
|
|
c74337978b | ||
|
|
059e75e532 | ||
|
|
bb5c2bd00c | ||
|
|
a2673da164 | ||
|
|
08682fb3f5 | ||
|
|
53d4e92dda | ||
|
|
53392f9a16 | ||
|
|
7e7bb81586 | ||
|
|
3703b3e2f6 | ||
|
|
e72d10f550 | ||
|
|
7ca9081efa | ||
|
|
015dca95fc | ||
|
|
076c05cbd0 | ||
|
|
82c565f7dc | ||
|
|
44bae39538 | ||
|
|
1188ea9cd5 |
@@ -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",
|
||||
|
||||
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++;
|
||||
}
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
---
|
||||
name: twenty-lead-intro-call-summary
|
||||
description: Turn a sales/discovery-call transcript into a faithful, structured qualification brief for Twenty's partner/CRM pipeline. Use this whenever the user has a call recording or transcript — including a meetily recordings folder or a transcripts.json — and wants to summarize, recap, qualify, or extract a brief from a prospect/discovery call. Trigger even when they don't say "brief": phrases like "summarize this call", "what did we learn from the X call", "qualify this lead from the call", "turn this transcript into something I can hand a partner", or pointing at a transcript file all count. Produces a tight deal one-pager (company, needs, implementation complexity, does-it-need-a-partner, partner-facing brief) plus an appendix (product/GTM feedback, terminology, quotes). It is a faithful extraction, not a lossy summary.
|
||||
trigger: /twenty-lead-intro-call-summary
|
||||
---
|
||||
|
||||
# twenty-lead-intro-call-summary
|
||||
|
||||
Turn a discovery/sales-call transcript into a **qualification brief** that (a) loses no
|
||||
decision-relevant detail and (b) can be used to qualify the deal and hand it to an
|
||||
implementation partner. Designed for Twenty's partner/CRM pipeline, tuned for messy,
|
||||
label-less ASR transcripts (e.g. meetily exports).
|
||||
|
||||
The output is two parts: a tight **Part A deal one-pager** (everything the deal/partner
|
||||
work needs) and a **Part B appendix** (not deal-specific: product feedback, terminology,
|
||||
quotes). The structure is what prevents loss — a "summary that loses nothing" is a
|
||||
contradiction, so this is a *structured extraction* against a fixed schema where every
|
||||
dimension has a slot and gaps are marked rather than silently dropped.
|
||||
|
||||
## Step 1 — Get the transcript
|
||||
|
||||
The transcript is provided by the user — usually pasted text, or a path they point you at
|
||||
(a `.txt`/`.vtt`/`.srt`, or a meetily recording folder / `transcripts.json`). If they give a
|
||||
path, read it: a meetily `transcripts.json` is `{ "segments": [ { "text": ... } ] }` —
|
||||
concatenate the `text` values in order; for `.vtt`/`.srt`, drop the cue numbers and
|
||||
timecodes.
|
||||
|
||||
**If no transcript is provided, ask for it before doing anything else.** Don't fabricate or
|
||||
proceed without one.
|
||||
|
||||
For very long transcripts, read the whole thing before writing — coverage is the point.
|
||||
|
||||
## Step 2 — Produce the brief
|
||||
|
||||
Follow these rules and fill this exact schema. Why each rule matters is noted, because
|
||||
faithful extraction depends on judgment, not rote form-filling.
|
||||
|
||||
**Rules**
|
||||
- Extract ONLY what is in the transcript. Never invent or assume. If a field isn't covered,
|
||||
write "Not discussed." (Visible gaps beat confident fabrication — a missing field is a
|
||||
signal to ask on the next call.)
|
||||
- Separate stated FACTS from your INFERENCES; mark any inference "(inferred)". (Downstream
|
||||
matching trusts the facts; don't contaminate them with guesses.)
|
||||
- Preserve specifics verbatim: numbers, dates, names + roles, tool/CRM names, prices,
|
||||
budgets, exact requirements. Don't round or paraphrase numbers. (Specifics are where
|
||||
nuance and matching signal live; paraphrase kills them.)
|
||||
- Use the customer's own words for needs and objections; quote pivotal lines.
|
||||
- Don't smooth over contradictions or vagueness — note them. (A flagged contradiction is
|
||||
more useful than a falsely tidy summary.)
|
||||
- If the transcript has NO speaker labels, infer from context who is the vendor (Twenty)
|
||||
and who is the prospect, and attribute accordingly. Names get garbled by speech-to-text —
|
||||
flag any uncertain name with "(uncertain)" and never invent a name.
|
||||
- Keep PART A tight — it's the one-pager the deal/partner work runs on. Push everything not
|
||||
specific to this deal (product/website feedback, terminology, supporting quotes) to
|
||||
PART B. State each fact once; never repeat it across sections.
|
||||
|
||||
**Output (use these exact headers)**
|
||||
|
||||
```
|
||||
== PART A — DEAL ONE-PAGER ==
|
||||
|
||||
1. ONE-LINE SUMMARY
|
||||
2. COMPANY — name, what they do, size/employees, HQ + countries of operation, industry
|
||||
3. PEOPLE ON THE CALL — name, role/title, side (Twenty vs prospect); infer roles if
|
||||
unlabeled and flag uncertain names
|
||||
4. CURRENT SITUATION — what CRM/tools they use today; specific pains
|
||||
5. WHY THEY'RE INTERESTED IN TWENTY
|
||||
6. WHAT THEY WANT — bulleted needs/requirements, verbatim where possible
|
||||
7. IMPLEMENTATION COMPLEXITY (for partner matching)
|
||||
- Deployment: cloud / self-host / both / unclear (+ the evidence)
|
||||
- Data model: custom objects, multi-tenant, row-level security, migrations
|
||||
- Integrations / custom apps needed
|
||||
- Workflows / automation needs
|
||||
- Scale: number of seats/users
|
||||
- Region + language the partner would need to cover
|
||||
8. COMMERCIALS — budget or prices discussed, plan tier (Pro/Org/Enterprise), seat count,
|
||||
deal value, who pays
|
||||
9. TIMELINE & DECISION — key dates, decision-makers, urgency, decision process
|
||||
10. OBJECTIONS / RISKS / FEARS — including anything that could kill the deal
|
||||
11. ALTERNATIVES — competitors or other options they're weighing
|
||||
12. DOES THIS DEAL NEED A PARTNER? — yes / no / maybe + why; and if yes, what kind
|
||||
(scope, region, language, seniority/tier)
|
||||
13. NEXT STEPS / OPEN QUESTIONS / FOLLOW-UPS
|
||||
14. PARTNER-FACING BRIEF — a 2-4 sentence narrative a partner can skim to decide yes/no,
|
||||
drawn only from PART A
|
||||
|
||||
== PART B — APPENDIX (not deal-specific) ==
|
||||
|
||||
15. PRODUCT / WEBSITE / GTM FEEDBACK — any feedback on the product, pricing page, website
|
||||
wording, onboarding, or trial; capture even if off-topic for qualification
|
||||
16. TERMINOLOGY / DOMAIN-LANGUAGE NOTES — words used on the call that mean different things
|
||||
to each side or carry domain-specific meaning (e.g. "partner", "donor", jargon)
|
||||
17. KEY VERBATIM QUOTES — 3-8 direct quotes that capture intent, needs, or objections
|
||||
```
|
||||
|
||||
## Step 3 — Output and save
|
||||
|
||||
Print the brief in the conversation. Then offer to save it as Markdown — default to a
|
||||
sibling of the source (e.g. next to the recording) or, for Twenty work, the
|
||||
`partners-experience/research/` folder, named `YYYY-MM-DD-<company>-call-summary.md`. Don't
|
||||
write the file unless the user wants it saved.
|
||||
|
||||
## How the fields map to the Partner model (for matching)
|
||||
|
||||
Section 7 + 12 are the matching axes: `Deployment → deploymentExpertise`, the scope needs →
|
||||
`partnerScope`, `Region + language` → partner region/languages, scale → capacity, the
|
||||
"needs a partner?" tier → `partnerTier`. Keeping these explicit is what lets the brief drop
|
||||
straight into the opportunity→partner handover flow.
|
||||
|
||||
## Notes
|
||||
|
||||
- This is tuned for Twenty discovery calls, but the schema generalizes — swap the vendor
|
||||
framing in section 5/12 if reused elsewhere.
|
||||
- The worked reference example lives at
|
||||
`partners-experience/research/2026-05-21-tsf-call-summary-final.md` (and the prompt alone
|
||||
at `partners-experience/research/call-summary-prompt.md`).
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: twenty-partner-design-doc
|
||||
description: Use when turning a qualified Twenty lead — a call-summary brief plus any client braindump/docs — into an implementation design doc a partner can scope and quote from. Trigger when pointed at a lead folder (e.g. partners-experience/<LEAD>/) and asked to "draft a design doc", "translate this into Twenty terms", "scope this for a partner", or "prep the partner handoff" for a discovery-qualified prospect. Chains after twenty-lead-intro-call-summary.
|
||||
trigger: /twenty-partner-design-doc
|
||||
---
|
||||
|
||||
# twenty-partner-design-doc
|
||||
|
||||
Turn a qualified lead's materials into a **design doc**: a translation of the customer's needs into **Twenty terms** that an implementation **partner reads to scope and quote** the work.
|
||||
|
||||
**The doctrine — what to produce, the 12-section structure, the rules, the verification process, and the common mistakes — lives in `design-doc-doctrine.md` in this folder. Read it and follow it.** This file is only the Claude Code wrapper: how to gather inputs, which tools to use for each step, and where to save.
|
||||
|
||||
## Inputs
|
||||
|
||||
- A lead folder (e.g. `partners-experience/<LEAD>/`) containing a `twenty-lead-intro-call-summary` output plus any braindump / docs / notes.
|
||||
- If there is a raw transcript but no brief, run **twenty-lead-intro-call-summary** first — this skill chains after it.
|
||||
- Read everything. Convert `.docx` with `textutil -convert txt "<file>" -output /tmp/out.txt` (macOS) or an equivalent extractor.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Gather** — read all source materials in full. Coverage is the point.
|
||||
2. **Extract needs, grounded** — facts vs inferences (`(inf.)`); never invent. Per the doctrine.
|
||||
3. **Draft** the doc in the doctrine's 12-section structure, applying every rule in the doctrine.
|
||||
4. **Verify load-bearing claims live** — use **WebFetch** against the Twenty doc map in the doctrine's Verification section before asserting any capability. Build the §11 appendix as you go.
|
||||
5. **Reconcile discrepancies** — sources that disagree (call vs braindump; a name differing across/within sources) get flagged both ways, never silently resolved.
|
||||
6. **Resolve ❓ with the operator** — after a full v1 draft, use **AskUserQuestion** to ask the Twenty team member the unknowns a Twenty insider can answer; leave customer-facing unknowns as ❓. **If running autonomously** (no operator — a subagent/batch run), skip the questions and leave every unknown as ❓ in the body and §11.
|
||||
7. **Self-check, then save** — scan the output for: an em dash, a bare `~`, first-person voice outside customer quotes, local file paths, a header that isn't the four-field table, a point repeated across sections instead of a §N cross-reference, a leftover glossary / domain-language section, runtime/env-var mechanics that belong in the technical phase, and any capability claim stated as fact without a §11 source. Fix, then save to the lead folder as `YYYY-MM-DD-<lead>-design-doc.md`.
|
||||
|
||||
## Worked example
|
||||
|
||||
Reference: `partners-experience/TSF/2026-05-26-tsf-design-doc.md` shows the target **coverage, flag discipline, and §11 verification appendix**. It predates the current concision / table-header / no-glossary / no-em-dash rules, so where its formatting differs, **follow this doctrine over the example.**
|
||||
|
||||
## Notes
|
||||
|
||||
- Chain: **twenty-lead-intro-call-summary → twenty-partner-design-doc**; the output feeds the opportunity→partner handover (`designDocStatus` / `designDocUrl`).
|
||||
- **Phase 2:** `design-doc-doctrine.md` is written to be portable. A future `defineSkill` in this app (a sibling `*.skill.ts` in `src/skills/`) would import it as its `content`, driving an in-product agent — with a verify logic-function tool replacing the WebFetch step, and a Workflow Action triggering it when sales toggles `partnerEligible`. Keep doctrine changes in that file so both the Claude Code skill and the `defineSkill` stay in sync.
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
# Design-doc doctrine: translating a lead into Twenty terms
|
||||
|
||||
**Portable doctrine.** This file is tool-agnostic. It defines *what* a Twenty partner design doc is, its structure, the rules, and how to verify it. Two consumers use it: the Claude Code skill `twenty-partner-design-doc` (see `SKILL.md` in this folder), and, in a later phase, the `content` of a Twenty `defineSkill` driving an in-product agent. Keep it free of any one tool's mechanics (no file paths, no tool names).
|
||||
|
||||
## Purpose
|
||||
|
||||
Produce a **design doc** that translates a qualified lead's needs into **Twenty terms** (data model, views, automations, integrations, permissions, reporting, hosting) so an implementation **partner can scope and quote** the work.
|
||||
|
||||
**Core principle:** identify *all* the work with **no blindspots**, **ground every claim** in the source or in live Twenty docs, and **present options rather than prescribe**. The partner quotes off this doc, so a confident-but-wrong capability claim or a hidden requirement is the worst failure.
|
||||
|
||||
**Stay on the client's outcome.** Every line must change what the partner *builds* or what the client *receives*. The doc is a design, not a meeting record: a requirement's backstory (why the vendor or a third party does or doesn't satisfy it) is not a build input. Capture the **consequence**, cut the backstory.
|
||||
|
||||
The doc is **partner-facing** and may be forwarded verbatim. It is a *suggestion to make scoping easier*, not a spec that constrains how the partner builds.
|
||||
|
||||
## Output structure (fixed)
|
||||
|
||||
**Header: a compact table, not a stack of bold lines.** Four fields only:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Lead | name (one-line description of who they are) |
|
||||
| Date | YYYY-MM-DD |
|
||||
| Author | Twenty (partnerships) |
|
||||
| Status | one short line (e.g. "Draft for partner review") |
|
||||
|
||||
Keep the header to those four fields. The Status line stays short: a load-bearing caveat (e.g. "data model is an inference pending discovery") goes in the "What this is" callout, not in the header cell. Do not list source materials, internal timelines, or who-promised-what: not load-bearing, not customer-safe (the doc is forwarded verbatim).
|
||||
|
||||
After the table, one **"What this is"** callout framing the doc as a partner-scoping suggestion across the full surface, *not* an MVP.
|
||||
|
||||
Flag legend: 🟥 heavy / product-constrained · ❓ open question to resolve before quoting · `(inf.)` modelling inference.
|
||||
|
||||
1. **Context**: the 30-second read: who they are, what they want, deployment requirement, scale, language/region.
|
||||
2. **Data model in Twenty terms**: the core. Present objects as a **table, one row per object**: `Object | Std/Custom | Represents | Key fields (traceable, (inf.)-tagged) | Core relations`. Spell out SELECT option sets. **Model the relationships, not just the fields**: who introduced/sourced a record (e.g. an ambassador to Opportunity `sourcedBy` link), parent/child, ownership carry as much scoping signal as the attributes. State product constraints inline (no formula fields; custom objects auto-get attachments/notes/tasks/timeline). Where a customer term collides with a Twenty term (their "partner" = a donor), note the mapping inline at first use; do **not** add a glossary section for it.
|
||||
3. **Views & navigation**: pipelines/kanban, tables, per-record page layouts.
|
||||
4. **Automations**: only the automations the customer named; give Workflow-or-logic-function for each. Name the automation and its trigger; defer runtime and build mechanics (see *Scope altitude*).
|
||||
5. **Integrations** *(conditional: include only when the customer explicitly names an external system to connect)*: external systems to Twenty: direction, *indicative* mechanism (not prescriptive), data flow, risks/unknowns. **If no integration is named, omit this section entirely and renumber the rest**; never infer integrations as a default. If you spot a likely one, raise it as a single ❓ in §10, not a whole section.
|
||||
6. **Roles, permissions & RLS**: map named roles to Twenty's object / field / row-level model; answer "do we need RLS?" against verified, plan-gated capability.
|
||||
7. **Reporting & analytics**: map reporting asks to native Dashboards; flag gaps (ratios needing rollups; export/sharing limits), each with a solution.
|
||||
8. **Hosting & compliance**: cloud vs self-host, data-residency requirement (verify), GDPR. Flag contradictions.
|
||||
9. **Suggested phasing**: "(the partner's call, not Twenty's)" layers, labelled a suggestion.
|
||||
10. **Open questions / blindspot-killers**: the list a partner must resolve before pricing.
|
||||
11. **References & verification**: a table mapping each load-bearing claim to its source doc, plus an explicit list of what the docs could NOT confirm (the ❓s).
|
||||
|
||||
Scale each section to its content. **Coverage of surface area matters more than depth per item.**
|
||||
|
||||
## Rules (and why each matters)
|
||||
|
||||
- **Be concise: maximum signal per word.** Say a lot in few words. Cut throat-clearing, scene-setting, hedges, and feature-tour prose; prefer a table or a tight clause to a paragraph. Length is not coverage: a short doc that names every requirement beats a long one that pads each. A hesitant buyer reads a focused doc; a bloated one reads as cost.
|
||||
- **Never repeat yourself.** State each fact, constraint, or claim once, in its home section; elsewhere point to it by section number (§N) rather than restate. §10 (open questions) and §11 (references) are deliberate roll-ups: there, give the pointer and the decision the item gates, not a re-explanation of the body. Repetition is the main source of bloat, and two copies of a claim drift out of sync.
|
||||
- **Scope altitude: name the decision, defer the mechanics.** A design doc scopes the work; it is not the technical implementation spec. State the *decision* and its *cost or scope consequence*; leave the implementation nitty-gritty (specific env-var names, runtime internals, isolation models, SDK function signatures) to the later technical phase. Example: "production automations need a sandboxed/serverless logic-function backend, an infra cost that belongs in the platform workstream" carries the quote signal; the exact `LOGIC_FUNCTION_TYPE` / `LAMBDA` / region/role/key settings do not belong in a scoping doc. Deep mechanics inflate length and date fast without changing the quote.
|
||||
- **Ground everything; tag inferences `(inf.)`; never grow scope.** The partner quotes off this, so an invented field or requirement inflates the quote or sets a false expectation. If the *concept* is from the source but the *field name* is yours, that is an inference: tag it. Values lifted from the source (the customer's own category list becoming SELECT options) are *grounded*; only names/fields you coin are inferences.
|
||||
- **Record the design consequence, not the backstory.** State a requirement and the **client's path** that follows from it; do not litigate *why* it's true. When a requirement traces to vendor-internal or third-party detail (corporate structure, legal domicile, ownership, internal commercial arrangements, who-confirms-what-with-whom), keep only the consequence: it is a commercial matter, not a build input, however much airtime it got on the call. (E.g. *"client needs a European vendor"* becomes *"partner-hosted EU self-host is the path; managed cloud only under a European-contracting arrangement"*, **not** a write-up of the vendor's statutes / HQ / ownership / sign-off.) An open item the client is waiting on is recorded as the **decision it gates** (a ❓ in §10), not as a narrative of the vendor's situation.
|
||||
- **Database discipline: reuse and extend standard objects; add a custom object only at a genuine wall.** Company / Person / Opportunity plus the built-in Notes / Tasks / Timeline cover most CRM needs; every new object multiplies build and maintenance. A **human actor is a Person with a role flag before it is a new object**: create an object only when it needs its own pipeline or reporting. Name the wall when you add one.
|
||||
- **When the brief is thin, under-reach the inferred model; don't fill the gap.** A blurry situation (no discovery call, sparse notes) is a reason to model the *fewest, most certain* objects and leave the rest as ❓ open questions, not to compensate with an elaborate inferred domain. An over-detailed inference reads as scope and cost the customer never asked for and can scare a hesitant buyer off. Lead with standard objects plus the one or two custom objects the domain unmistakably needs; everything else is a question to confirm, not a row in the table. Say plainly, up front, that the model is a minimal starting sketch to validate.
|
||||
- **Present build approaches; don't prescribe.** An automation can be a no-code Workflow *or* a logic function in an app: say both. Prescribing one penalizes a partner who would do the other. The doc identifies the *need*, not the *build*.
|
||||
- **Every problem carries a path; never a dead-end flag.** If you flag a constraint (e.g. dashboards can't share externally), pair it with at least one solution (CSV export, a front-component, a public site on the API) or a question that resolves it. A flag with no path is useless to someone pricing the work.
|
||||
- **Flag what an approach can't satisfy.** The doc's value is surfacing walls and limits per requirement so the partner prices around them, not picking the one true solution.
|
||||
- **Partner-facing voice.** Say **"Twenty," never first person** ("we / our / ours"). **No local file paths** in the output: cite shareable `docs.twenty.com` URLs only. (Customer quotes that contain "we/our" are fine: they are quotes.)
|
||||
- **No characterisations or asides; only requirements and capabilities.** The doc is customer-forwardable, so keep out the source chat's off-hand remarks: characterisations of the buyer (budget, temperament, sophistication), named comparisons to competing vendors, and internal partnerships notes (deadlines, who promised what). If price-sensitivity or a competitor displacement genuinely shapes the build, state it neutrally as a requirement (e.g. cost is a selection criterion), never as a quote or judgement.
|
||||
- **Verify before asserting capability.** Any "Twenty can / can't / has / lacks X" that moves the quote must be verified live (see Verification). **Undocumented ≠ impossible.**
|
||||
|
||||
## Formatting
|
||||
|
||||
- One line per paragraph: **no mid-sentence hard wraps** (they render as broken lines).
|
||||
- **Never use em dashes (the long dash).** Restructure the sentence, or use a colon, comma, parentheses, or a period instead.
|
||||
- **Never a bare `~`** for "approximately": GitHub markdown pairs `~...~` into strikethrough. Write "around" / "about".
|
||||
- Mark unverified capability claims ❓, never as fact.
|
||||
|
||||
## Verification
|
||||
|
||||
Any statement of the form **"Twenty can / can't / has / lacks X"** that changes the partner's quote MUST be verified **live** before it is stated as fact. Model training is stale on a fast-moving product; the worst failure is a confident, authoritative-sounding claim that is wrong.
|
||||
|
||||
**Source hierarchy (what to trust, in order):**
|
||||
1. **Live docs** (`docs.twenty.com`): primary truth. For the highest-stakes claims, read the page's primary text rather than trust a summary.
|
||||
2. **Established Twenty SDK build patterns** (hands-on): build-level facts the customer docs omit (no formula fields; custom objects auto-get attachments/notes/tasks/timeline; two-file relations).
|
||||
3. **The Twenty operator** (a Twenty team member): best for "is it shipped / internal / undocumented."
|
||||
4. **Model training**: never the sole basis for a high-stakes claim.
|
||||
|
||||
**Right doc layer (the trap):** capabilities live in two layers, so check the right one.
|
||||
- **Product capabilities** (what the CRM does for the *customer*): the **user guide + pricing page**. Covers roles / row-level permissions, dashboards, plans, hosting, data residency.
|
||||
- **App capabilities** (what an *app* can define): the **developer/extend** docs. Covers field types, fields, logic functions, views, page layouts.
|
||||
Checking only the app layer is how "row-level not supported" (wrong) happens: row-level is a **product feature on the Organization plan**. The converse also holds: SDK build patterns *are* sufficient to assert **build-layer** facts even when the customer docs are silent (e.g. auto system relations), so do not demote a well-established build fact to ❓.
|
||||
|
||||
**Always verify (the load-bearing checklist), live, every run:**
|
||||
1. Field types & constraints (e.g. no formula/computed fields).
|
||||
2. Standard-object extension & relabeling (add fields ✓; relabel / edit built-in SELECT options?).
|
||||
3. Roles & permissions: object / field / row-level, and plan-gating (which tier).
|
||||
4. Dashboards & reporting: chart/widget types, beta status, export / external-sharing limits.
|
||||
5. Automation surfaces: Workflows vs logic functions, what each can do.
|
||||
6. Integration mechanisms: webhooks, HTTP triggers, scheduled functions, connections.
|
||||
7. Hosting & deployment: cloud plans & regions / **EU data residency**, self-host availability & requirements.
|
||||
Plus: any other capability claim the draft makes that carries a 🟥 or ❓ flag.
|
||||
|
||||
**Fallback chain:**
|
||||
- Docs confirm → state it as fact; record the source in §11.
|
||||
- Docs silent or ambiguous → ask the operator (if available).
|
||||
- Operator unavailable or unsure → render it as a ❓ open question. **Never assert.** When you fetched a page and it was simply *silent*, record that as "docs silent (URL)" rather than leaving the claim unsourced.
|
||||
|
||||
**§11 appendix format:** a `Claim (§) | Verified against` table, then an explicit "**Could not be confirmed in public docs (❓: check with Twenty directly):**" list. Unverified items stay ❓ in the body too, never silently promoted to fact. Cite the **human-readable (non-`.md`) URL** here: the `.md` twin is for *your* fetch, not for the partner (a `.md` link renders as raw markdown in a browser).
|
||||
|
||||
**Where to verify (Twenty doc map):** docs base = `https://docs.twenty.com/`. Fetch **`<path>.md`** for the clean markdown twin (the form to prefer). Paths:
|
||||
- Product / user-guide: `user-guide/dashboards/overview` · `user-guide/dashboards/capabilities/widgets` · `user-guide/permissions-access/how-tos/permissions-faq` · `user-guide/data-model/overview` · `user-guide/data-model/capabilities/fields` · `user-guide/data-migration/how-tos/export-your-data`
|
||||
- Developer / extend: `developers/extend/apps/data/objects` · `developers/extend/apps/data/extending-objects` · `developers/extend/apps/data/relations` · `developers/extend/apps/logic/logic-functions` · `developers/extend/apps/logic/connections` · `developers/extend/apps/layout/views` · `developers/extend/apps/layout/page-layouts` · `developers/extend/apps/config/roles`
|
||||
- Self-host: `developers/self-host/self-host`
|
||||
- Pricing & plans: `https://twenty.com/pricing` (**marketing page, no `.md`**; fetch as HTML).
|
||||
If a `.md` 404s, drop the suffix or re-derive from the docs index: the map can go stale.
|
||||
|
||||
## Common mistakes
|
||||
|
||||
| Mistake | Reality / fix |
|
||||
|---|---|
|
||||
| "Twenty isn't a BI tool" / "can't do row-level" | Stale training. Twenty has Dashboards; row-level is on the Organization plan. **Verify live.** |
|
||||
| Checked only the app-SDK doc for a product capability | Row-level lives in the product/pricing layer. **Verify the right layer.** |
|
||||
| Added fields not in the source | Scope growth, wrong quote. Ground every field; tag inferences `(inf.)`. |
|
||||
| Made a human actor (e.g. ambassador) its own object by default | A human is a Person + role flag first; an object only if it needs its own pipeline/reporting. |
|
||||
| Flagged an automation as "Workflow" | Prescribes the build, penalizes app-builders. Present both. |
|
||||
| Flagged a limit with no fix | Dead-end flag. Pair every problem with a path. |
|
||||
| Spelled out runtime/env-var mechanics (e.g. `LOGIC_FUNCTION_TYPE` / `LAMBDA`, region/role/key) | Wrong altitude. Name the decision + its cost; defer the mechanics to the technical phase. |
|
||||
| Same point restated across sections | State it once in its home section; cross-reference by §N. |
|
||||
| Padded prose / feature-tour narration | Maximum signal per word. State the content; cut the scene-setting. |
|
||||
| Added a domain-language map / glossary section | Removed. Note a genuine term collision inline in §2; no standalone glossary. |
|
||||
| First-person "not ours" in a partner doc | Say "Twenty." The doc is forwarded verbatim. |
|
||||
| Local file paths in the output | Mean nothing to a partner. Cite `docs.twenty.com` only. |
|
||||
| `§2` as prose; fields modelled but not relationships | Use a per-object field **table**; model the links, not just attributes. |
|
||||
| Hard-wrapped mid-sentence / used `~` / used an em dash | Broken lines / accidental strikethrough / banned dash. One line per paragraph; "around" not `~`; colon or comma, never an em dash. |
|
||||
| Wrote up the vendor's corporate status / legal domicile / ownership / sign-off | Backstory, not a build input. Record only the **consequence**: requirement → the client's path; gate the open item as a ❓ in §10. |
|
||||
| Filled a thin brief with an elaborate inferred model | Over-reach scares a hesitant buyer with unrequested scope. Model the few certain objects; flag the rest as ❓; say it's a minimal sketch. |
|
||||
| Added an Integrations section with connectors the customer never named | Integrations is conditional, not default. Omit it absent a named system; at most flag one ❓. |
|
||||
| Kept buyer characterisations / competitor asides / internal timelines | Not customer-safe. State needs neutrally; cut the rest. |
|
||||
| Listed source materials / who-promised-what in the header, or stacked it as bold lines | Header is a four-field table. Not customer-safe content goes nowhere. |
|
||||
+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,7 +42,7 @@
|
||||
"dependencies": {
|
||||
"@genql/cli": "^3.0.3",
|
||||
"@genql/runtime": "^2.10.0",
|
||||
"esbuild": "^0.27.3",
|
||||
"esbuild": "^0.28.0",
|
||||
"graphql": "^16.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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` 就是常见情况。
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "{agentLabel} Agentrol"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} van {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Magtiging het misluk. Probeer asseblief weer."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Otoriseer"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Gemagtigde URL na knipbord gekopieer"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Magtiging word verwerk..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Terug na {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Terug na inhoud"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Kan nie skandeer nie? Kopieer die"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Kan nie skandeer nie? Kopieer die"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Kanselleer"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Installeer"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "دور الوكيل {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} من {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "فشل التفويض. يرجى المحاولة مرة أخرى."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "اعتماد"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "تم نسخ رابط URL المرخص إلى الحافظة"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "جاري التفويض..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "العودة إلى {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "العودة إلى المحتوى"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "هل لا يمكنك المسح؟ انسخ الـ"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "هل لا يمكنك المسح؟ انسخ الـ"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "إلغاء"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "تثبيت"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "Rol d'agent {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} de {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "L'autorització ha fallat. Torna-ho a intentar."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Autoritzar"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "URL autoritzat copiat al porta-retalls"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Autoritzant..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Torna a {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Tornar al contingut"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "No pots escanejar? Copia la"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "No pots escanejar? Copia la"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel·la"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Instal·la"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "Role agenta {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} z {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Autorizace selhala. Zkuste to prosím znovu."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Autorizovat"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Autorizovaná URL zkopírována do schránky"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Probíhá autorizace..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Zpět na {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Zpět k obsahu"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Nemůžete skenovat? Zkopírujte"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Nemůžete skenovat? Zkopírujte"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Storno"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Nainstalovat"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "{agentLabel} Agentrolle"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} af {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Autorisering mislykkedes. Prøv igen."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Autoriser"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Autoriseret URL kopieret til udklipsholder"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Autoriserer..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Tilbage til {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Tilbage til indhold"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Kan ikke scanne? Kopier"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Kan ikke scanne? Kopier"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Annuller"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "{agentLabel}-Agentenrolle"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} von {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Autorisierung fehlgeschlagen. Bitte versuchen Sie es erneut."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Autorisieren"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Autorisierte URL in die Zwischenablage kopiert"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Wird autorisiert..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Zurück zu {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Zurück zum Inhalt"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Kann nicht gescannt werden? Kopieren Sie das"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Kann nicht gescannt werden? Kopieren Sie das"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Installieren"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "{agentLabel} Ρόλος Πράκτορα"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} του {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Η εξουσιοδότηση απέτυχε. Δοκιμάστε ξανά."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Εξουσιοδότηση"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Το εξουσιοδοτημένο URL αντιγράφηκε στο πρόχειρο"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Γίνεται εξουσιοδότηση..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Πίσω στο {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Επιστροφή στο περιεχόμενο"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Δεν μπορείτε να σαρώσετε; Αντιγράψτε το
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Δεν μπορείτε να σαρώσετε; Αντιγράψτε το
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Ακύρωση"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Εγκατάσταση"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -231,6 +231,11 @@ msgstr "{agentLabel} Agent Role"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} of {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr "{appDisplayName} would like to:"
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2549,7 +2554,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Authorization failed. Please try again."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Authorize"
|
||||
|
||||
@@ -2564,7 +2569,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Authorized URL copied to clipboard"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Authorizing..."
|
||||
|
||||
@@ -2648,6 +2653,11 @@ msgstr "Back to {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Back to content"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr "Back to settings"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3008,7 +3018,6 @@ msgstr "Can't scan? Copy the"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3020,6 +3029,7 @@ msgstr "Can't scan? Copy the"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel"
|
||||
@@ -8522,6 +8532,11 @@ msgstr "Insert a JSON input, then press \"Run Function\"."
|
||||
msgid "Install"
|
||||
msgstr "Install"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr "Install {appDisplayName} on your workspace"
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "Rol de agente de {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} de {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "La autorización falló. Por favor, inténtelo de nuevo."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Autorizar"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "URL autorizada copiada al portapapeles"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Autorizando..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Volver a {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Volver al contenido"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "¿No puedes escanear? Copia el"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "¿No puedes escanear? Copia el"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Instalar"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "Agenttirooli: {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} {fieldLabel} määrä"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Valtuutus epäonnistui. Yritä uudelleen."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Valtuuta"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Valtuutettu URL kopioitu leikepöydälle"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Valtuutetaan..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Takaisin kohteeseen {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Takaisin sisältöön"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Et voi skannata? Kopioi"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Et voi skannata? Kopioi"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Peruuta"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Asenna"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "Rôle d'agent {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} de {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Échec de l'autorisation. Veuillez réessayer."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Autoriser"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "URL autorisée copiée dans le presse-papiers"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Autorisation en cours..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Retour à {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Retour au contenu"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Impossible de scanner ? Copiez le"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Impossible de scanner ? Copiez le"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Installer"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -236,6 +236,11 @@ msgstr "תפקיד הסוכן {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} של {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "ההרשאה נכשלה. נא לנסות שוב."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "אשר"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "כתובת URL המורשית הועתקה ללוח הגזירים"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "מתבצעת הרשאה..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "חזרה אל {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "חזרה לתוכן"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "לא ניתן לסרוק? העתק את"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "לא ניתן לסרוק? העתק את"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "בטל"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "התקן"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "{agentLabel} ügynök szerepköre"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} a(z) {fieldLabel} alapján"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Az engedélyezés sikertelen. Kérjük, próbálja meg újra."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Engedélyezés"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Engedélyezett URL vágólapra másolva"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Engedélyezés..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Vissza ide: {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Vissza a tartalomhoz"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Nem tud beolvasni? Másolja a"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Nem tud beolvasni? Másolja a"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Mégse"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Telepítés"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "Ruolo dell'agente {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} di {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Autorizzazione non riuscita. Per favore riprova."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Autorizza"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "URL autorizzato copiato negli appunti"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Autorizzazione in corso..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Torna a {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Torna al contenuto"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Non puoi scansionare? Copia il"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Non puoi scansionare? Copia il"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Annulla"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Installa"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "{agentLabel} のエージェントの役割"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{fieldLabel}の{aggregateLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "認可に失敗しました。もう一度お試しください。"
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "認可する"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "認可されたURLがクリップボードにコピーされました"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "認可中..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "{linkText} に戻る"
|
||||
msgid "Back to content"
|
||||
msgstr "コンテンツに戻る"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "スキャンできませんか? コピーしてください"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "スキャンできませんか? コピーしてください"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "インストール"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "{agentLabel} 에이전트 역할"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{fieldLabel}의 {aggregateLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "승인에 실패했습니다. 다시 시도해 주세요."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "승인"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "승인된 URL이 클립보드에 복사되었습니다"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "승인 중..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "{linkText}(으)로 돌아가기"
|
||||
msgid "Back to content"
|
||||
msgstr "콘텐츠로 돌아가기"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "스캔이 불가능합니까?"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "스캔이 불가능합니까?"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "취소"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "설치"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "Agentrol {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} van {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Autorisatie mislukt. Probeer het opnieuw."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Autoriseren"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Geautoriseerde URL gekopieerd naar klembord"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Autoriseren..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Terug naar {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Terug naar inhoud"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Kan je niet scannen? Kopieer de"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Kan je niet scannen? Kopieer de"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Annuleren"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Installeren"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "{agentLabel} agentrolle"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} av {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Autorisering mislyktes. Vennligst prøv igjen."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Autoriser"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Autoriserte URL kopiert til utklippstavlen"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Autoriserer..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Tilbake til {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Tilbake til innhold"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Kan ikke skanne? Kopier "
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Kan ikke skanne? Kopier "
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Avbryt"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Installer"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "Rola agenta {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} z {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Autoryzacja nie powiodła się. Spróbuj ponownie."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Autoryzuj"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Autoryzowany URL skopiowany do schowka"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Autoryzowanie..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Powrót do {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Powrót do treści"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Nie można zeskanować? Skopiuj"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Nie można zeskanować? Skopiuj"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Anuluj"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Zainstaluj"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -231,6 +231,11 @@ msgstr ""
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2549,7 +2554,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr ""
|
||||
|
||||
@@ -2564,7 +2569,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr ""
|
||||
|
||||
@@ -2648,6 +2653,11 @@ msgstr ""
|
||||
msgid "Back to content"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3008,7 +3018,6 @@ msgstr ""
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3020,6 +3029,7 @@ msgstr ""
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
@@ -8499,6 +8509,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "Função do agente {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} de {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Falha na autorização. Por favor, tente novamente."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Autorizar"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "URL autorizado copiado para a área de transferência"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Autorizando..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Voltar para {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Voltar ao conteúdo"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Não consegue escanear? Copie o"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Não consegue escanear? Copie o"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Instalar"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "{agentLabel} Função do Agente"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} de {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "A autorização falhou. Tente novamente."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Autorizar"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "URL autorizada copiada para a área de transferência"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Autorizando..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Voltar a {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Voltar ao conteúdo"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Não consegue escanear? Copie o"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Não consegue escanear? Copie o"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Instalar"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "Rolul agentului {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} din {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Autorizarea a eșuat. Vă rugăm să încercați din nou."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Autorizează"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "URL-ul autorizat copiat în clipboard"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Se autorizează..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Înapoi la {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Înapoi la conținut"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Nu poți scana? Copiază"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Nu poți scana? Copiază"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Anulare"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Instalează"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
Binary file not shown.
@@ -236,6 +236,11 @@ msgstr "Улога агента {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} од {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Овлашћивање није успело. Покушајте поново."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Овласти"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Ауторизовани URL копиран у клипборд"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Овлашћивање..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Назад на {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Назад на садржај"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Не можете да скенирате? Копирајте"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Не можете да скенирате? Копирајте"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Откажи"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "{agentLabel} agentroll"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} för {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Auktoriseringen misslyckades. Försök igen."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Auktorisera"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Auktoriserad URL kopierad till urklipp"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Auktoriserar..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Tillbaka till {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Tillbaka till innehåll"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Kan inte skanna? Kopiera "
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Kan inte skanna? Kopiera "
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Avbryt"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Installera"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "{agentLabel} Ajan Rolü"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel}, {fieldLabel}'in"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Yetkilendirme başarısız oldu. Lütfen tekrar deneyin."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Yetkilendir"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Yetkilendirilmiş URL panoya kopyalandı"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Yetkilendiriliyor..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "{linkText}'e geri dön"
|
||||
msgid "Back to content"
|
||||
msgstr "İçeriğe geri dön"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Tarayamaz mısınız? Kopyalayın."
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Tarayamaz mısınız? Kopyalayın."
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "İptal"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Yükle"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "Роль агента {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Авторизація не вдалася. Будь ласка, спробуйте ще раз."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Авторизувати"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "Авторизований URL скопійовано в буфер обміну"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Авторизація..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Назад до {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Повернутися до вмісту"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Не можете сканувати? Скопіюйте"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Не можете сканувати? Скопіюйте"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Скасувати"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Встановити"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "Vai trò tác nhân {agentLabel}"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} của {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "Ủy quyền không thành công. Vui lòng thử lại."
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "Ủy quyền"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "URL được ủy quyền đã được sao chép vào bảng tạm"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "Đang ủy quyền..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "Quay lại {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Quay lại nội dung"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "Không thể quét? Sao chép"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "Không thể quét? Sao chép"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Hủy bỏ"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "Cài đặt"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "{agentLabel} 代理角色"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} 的 {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "授权失败。请重试。"
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "授权"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "授权 URL 已复制到剪贴板"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "正在授权..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "返回 {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "返回内容"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "无法扫描?复制"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "无法扫描?复制"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "安装"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -236,6 +236,11 @@ msgstr "{agentLabel} 代理角色"
|
||||
msgid "{aggregateLabel} of {fieldLabel}"
|
||||
msgstr "{aggregateLabel} 的 {fieldLabel}"
|
||||
|
||||
#. js-lingui-id: u1mVnV
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "{appDisplayName} would like to:"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: ZZZxmp
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
msgid "{appName} would like to:"
|
||||
@@ -2554,7 +2559,7 @@ msgid "Authorization failed. Please try again."
|
||||
msgstr "授權失敗。請再試一次。"
|
||||
|
||||
#. js-lingui-id: yIVrHZ
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorize"
|
||||
msgstr "授權"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgid "Authorized URL copied to clipboard"
|
||||
msgstr "授權 URL 已複製到剪貼板"
|
||||
|
||||
#. js-lingui-id: 8RmQki
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
msgid "Authorizing..."
|
||||
msgstr "正在授權..."
|
||||
|
||||
@@ -2653,6 +2658,11 @@ msgstr "返回 {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "返回內容"
|
||||
|
||||
#. js-lingui-id: 9aZHfH
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Back to settings"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3013,7 +3023,6 @@ msgstr "無法掃描? 複製"
|
||||
#. js-lingui-id: dEgA5A
|
||||
#: src/pages/settings/applications/components/SettingsApplicationConnectScopePickerModal.tsx
|
||||
#: src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
|
||||
#: src/pages/auth/Authorize.tsx
|
||||
#: src/modules/ui/layout/modal/components/ConfirmationModal.tsx
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
|
||||
@@ -3025,6 +3034,7 @@ msgstr "無法掃描? 複製"
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx
|
||||
#: src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter.tsx
|
||||
#: src/modules/applications/components/AuthorizeActionButtons.tsx
|
||||
#: src/modules/activities/emails/components/EmailComposer.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
@@ -8504,6 +8514,11 @@ msgstr ""
|
||||
msgid "Install"
|
||||
msgstr "安裝"
|
||||
|
||||
#. js-lingui-id: XOHdRf
|
||||
#: src/modules/marketplace/components/SettingsApplicationInstallPermissionValidationModal.tsx
|
||||
msgid "Install {appDisplayName} on your workspace"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: 4ZM8/a
|
||||
#: src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts
|
||||
msgid "Install and manage applications"
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext, useState } from 'react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Avatar, IconRefresh } from 'twenty-ui/display';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type AppConnectionHeaderProps = {
|
||||
appLogoUrl?: string | null;
|
||||
appName: string;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledAppLogoTile = styled.div`
|
||||
align-items: center;
|
||||
backdrop-filter: ${themeCssVariables.blur.strong};
|
||||
background: ${themeCssVariables.background.primary};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
box-shadow: ${themeCssVariables.boxShadow.strong};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: ${themeCssVariables.spacing[12]};
|
||||
justify-content: center;
|
||||
padding: ${themeCssVariables.spacing[1]};
|
||||
width: ${themeCssVariables.spacing[12]};
|
||||
`;
|
||||
|
||||
const StyledAppLogo = styled.img`
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
height: ${themeCssVariables.spacing[10]};
|
||||
object-fit: cover;
|
||||
width: ${themeCssVariables.spacing[10]};
|
||||
`;
|
||||
|
||||
const StyledLinkIconContainer = styled.div`
|
||||
align-items: center;
|
||||
background: ${themeCssVariables.background.primary};
|
||||
border-radius: ${themeCssVariables.border.radius.rounded};
|
||||
box-shadow: ${themeCssVariables.boxShadow.strong};
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: ${themeCssVariables.spacing[6]};
|
||||
justify-content: center;
|
||||
width: ${themeCssVariables.spacing[6]};
|
||||
`;
|
||||
|
||||
export const AppConnectionHeader = ({
|
||||
appLogoUrl,
|
||||
appName,
|
||||
}: AppConnectionHeaderProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const [hasAppLogoError, setHasAppLogoError] = useState(false);
|
||||
|
||||
const showAppLogoImage = isNonEmptyString(appLogoUrl) && !hasAppLogoError;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledAppLogoTile>
|
||||
<StyledAppLogo src={'/images/integrations/twenty-logo.svg'} alt="" />
|
||||
</StyledAppLogoTile>
|
||||
<StyledLinkIconContainer aria-hidden>
|
||||
<IconRefresh size={theme.icon.size.md} stroke={theme.icon.stroke.lg} />
|
||||
</StyledLinkIconContainer>
|
||||
<StyledAppLogoTile>
|
||||
{showAppLogoImage ? (
|
||||
<StyledAppLogo
|
||||
src={appLogoUrl}
|
||||
alt=""
|
||||
onError={() => setHasAppLogoError(true)}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
size="xl"
|
||||
placeholder={appName}
|
||||
placeholderColorSeed={appName}
|
||||
type="squared"
|
||||
/>
|
||||
)}
|
||||
</StyledAppLogoTile>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { MainButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type AuthorizeActionButtonsProps = {
|
||||
onAuthorize: () => void;
|
||||
onCancel: () => void;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
display: grid;
|
||||
gap: ${themeCssVariables.spacing[3]};
|
||||
grid-template-columns: repeat(
|
||||
2,
|
||||
minmax(${themeCssVariables.spacing[0]}, 1fr)
|
||||
);
|
||||
margin-top: ${themeCssVariables.spacing[8]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledAuthorizeButton = styled(MainButton)`
|
||||
box-shadow: none;
|
||||
`;
|
||||
|
||||
const StyledCancelButton = styled(MainButton)`
|
||||
box-shadow: none;
|
||||
`;
|
||||
|
||||
export const AuthorizeActionButtons = ({
|
||||
onAuthorize,
|
||||
onCancel,
|
||||
isLoading,
|
||||
}: AuthorizeActionButtonsProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<StyledButtonContainer>
|
||||
<StyledCancelButton
|
||||
title={t`Cancel`}
|
||||
variant="secondary"
|
||||
onClick={onCancel}
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<StyledAuthorizeButton
|
||||
title={isLoading ? t`Authorizing...` : t`Authorize`}
|
||||
onClick={onAuthorize}
|
||||
disabled={isLoading}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
);
|
||||
};
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import { AppConnectionHeader } from '@/applications/components/AppConnectionHeader';
|
||||
import { AuthorizeActionButtons } from '@/applications/components/AuthorizeActionButtons';
|
||||
import {
|
||||
buildPermissionSummaryFromRoleManifest,
|
||||
type PermissionSummaryItem,
|
||||
} from '@/marketplace/utils/buildPermissionSummaryFromRoleManifest';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMemo } from 'react';
|
||||
import { type RoleManifest } from 'twenty-shared/application';
|
||||
import { IconChevronLeft } from 'twenty-ui/display';
|
||||
import { LightButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { StyledAppModal } from '~/pages/settings/applications/components/SettingsAppModalLayout';
|
||||
|
||||
type SettingsApplicationInstallPermissionValidationModalProps = {
|
||||
modalInstanceId: string;
|
||||
appDisplayName: string;
|
||||
appLogoUrl?: string;
|
||||
defaultRole?: RoleManifest;
|
||||
onAuthorize: () => void;
|
||||
isInstalling?: boolean;
|
||||
};
|
||||
|
||||
const StyledFullscreenContainer = styled.div`
|
||||
align-items: center;
|
||||
background: ${themeCssVariables.background.secondary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledLightButton = styled(LightButton)`
|
||||
left: ${themeCssVariables.spacing[4]};
|
||||
position: absolute;
|
||||
top: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledAppConnectionHeaderContainer = styled.div`
|
||||
margin-bottom: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.lg};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
margin-bottom: ${themeCssVariables.spacing[6]};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StyledPermissionsCard = styled.div`
|
||||
background: ${themeCssVariables.background.primary};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
margin-bottom: ${themeCssVariables.spacing[6]};
|
||||
padding: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledPermissionsTitle = styled.div`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
margin-bottom: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
const StyledPermissionRow = styled.div`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding: ${themeCssVariables.spacing[2]} 0;
|
||||
`;
|
||||
|
||||
const StyledPermissionIcon = styled.div`
|
||||
color: ${themeCssVariables.color.blue9};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const SettingsApplicationInstallPermissionValidationModal = ({
|
||||
modalInstanceId,
|
||||
appDisplayName,
|
||||
appLogoUrl,
|
||||
defaultRole,
|
||||
onAuthorize,
|
||||
isInstalling,
|
||||
}: SettingsApplicationInstallPermissionValidationModalProps) => {
|
||||
const { closeModal } = useModal();
|
||||
|
||||
const permissionItems: PermissionSummaryItem[] = useMemo(() => {
|
||||
if (!defaultRole) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return buildPermissionSummaryFromRoleManifest(defaultRole);
|
||||
}, [defaultRole]);
|
||||
|
||||
const handleAuthorize = () => {
|
||||
closeModal(modalInstanceId);
|
||||
onAuthorize();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
closeModal(modalInstanceId);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledAppModal
|
||||
modalId={modalInstanceId}
|
||||
isClosable
|
||||
onClose={handleClose}
|
||||
size="fullscreen"
|
||||
padding="none"
|
||||
overlay="transparent"
|
||||
>
|
||||
<StyledFullscreenContainer>
|
||||
<StyledLightButton
|
||||
Icon={IconChevronLeft}
|
||||
title={t`Back to settings`}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
|
||||
<StyledContent>
|
||||
<StyledAppConnectionHeaderContainer>
|
||||
<AppConnectionHeader
|
||||
appLogoUrl={appLogoUrl}
|
||||
appName={appDisplayName}
|
||||
/>
|
||||
</StyledAppConnectionHeaderContainer>
|
||||
|
||||
<StyledTitle>
|
||||
{t`Install ${appDisplayName} on your workspace`}
|
||||
</StyledTitle>
|
||||
|
||||
{permissionItems.length > 0 && (
|
||||
<StyledPermissionsCard>
|
||||
<StyledPermissionsTitle>
|
||||
{t`${appDisplayName} would like to:`}
|
||||
</StyledPermissionsTitle>
|
||||
{permissionItems.map((item) => (
|
||||
<StyledPermissionRow key={item.label}>
|
||||
<StyledPermissionIcon>
|
||||
<item.Icon size={16} />
|
||||
</StyledPermissionIcon>
|
||||
{item.label}
|
||||
</StyledPermissionRow>
|
||||
))}
|
||||
</StyledPermissionsCard>
|
||||
)}
|
||||
|
||||
<AuthorizeActionButtons
|
||||
onCancel={handleClose}
|
||||
onAuthorize={handleAuthorize}
|
||||
isLoading={isInstalling}
|
||||
/>
|
||||
</StyledContent>
|
||||
</StyledFullscreenContainer>
|
||||
</StyledAppModal>
|
||||
);
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useInstallMarketplaceApp } from '@/marketplace/hooks/useInstallMarketplaceApp';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const INSTALL_PERMISSION_VALIDATION_MODAL_ID =
|
||||
'install-permission-validation-modal';
|
||||
|
||||
export const useInstallMarketplaceAppWithPermissionValidation = () => {
|
||||
const { openModal } = useModal();
|
||||
const { install, isInstalling } = useInstallMarketplaceApp();
|
||||
|
||||
const requestInstall = useCallback(() => {
|
||||
openModal(INSTALL_PERMISSION_VALIDATION_MODAL_ID);
|
||||
}, [openModal]);
|
||||
|
||||
return {
|
||||
requestInstall,
|
||||
install,
|
||||
isInstalling,
|
||||
modalInstanceId: INSTALL_PERMISSION_VALIDATION_MODAL_ID,
|
||||
};
|
||||
};
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
import { type RoleManifest } from 'twenty-shared/application';
|
||||
import { SystemPermissionFlag } from 'twenty-shared/constants';
|
||||
|
||||
import { buildPermissionSummaryFromRoleManifest } from '@/marketplace/utils/buildPermissionSummaryFromRoleManifest';
|
||||
|
||||
const baseRole: RoleManifest = {
|
||||
universalIdentifier: 'test-role',
|
||||
label: 'Test Role',
|
||||
};
|
||||
|
||||
describe('buildPermissionSummaryFromRoleManifest', () => {
|
||||
it('should return empty array when role has no permissions', () => {
|
||||
const result = buildPermissionSummaryFromRoleManifest(baseRole);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
describe('record permissions', () => {
|
||||
it('should return "Read records" for read-only', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Read records');
|
||||
});
|
||||
|
||||
it('should return "Write records" for write-only', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canUpdateAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Write records');
|
||||
});
|
||||
|
||||
it('should return "Read and write records" for read+write', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Read and write records');
|
||||
});
|
||||
|
||||
it('should return "Delete records" for soft-delete only', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Delete records');
|
||||
});
|
||||
|
||||
it('should return "Delete records" for destroy only', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canDestroyAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Delete records');
|
||||
});
|
||||
|
||||
it('should return "Read and delete records" for read+delete', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Read and delete records');
|
||||
});
|
||||
|
||||
it('should return "Read, write, and delete records" with Oxford comma for all three', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Read, write, and delete records');
|
||||
});
|
||||
|
||||
it('should treat destroy the same as soft-delete for label purposes', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result[0].label).toBe('Read, write, and delete records');
|
||||
});
|
||||
});
|
||||
|
||||
describe('object permissions fallback', () => {
|
||||
it('should show "Access specific object records" when objectPermissions exist but no global record flags', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
objectPermissions: [
|
||||
{
|
||||
universalIdentifier: 'perm-1',
|
||||
objectUniversalIdentifier: 'obj-1',
|
||||
canReadObjectRecords: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Access specific object records');
|
||||
});
|
||||
|
||||
it('should not show object permissions fallback when global record flags are set', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: true,
|
||||
objectPermissions: [
|
||||
{
|
||||
universalIdentifier: 'perm-1',
|
||||
objectUniversalIdentifier: 'obj-1',
|
||||
canReadObjectRecords: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Read records');
|
||||
});
|
||||
});
|
||||
|
||||
describe('permission flags', () => {
|
||||
it('should add DATA_MODEL flag as "Read and write data model configuration"', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.DATA_MODEL],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Read and write data model configuration');
|
||||
});
|
||||
|
||||
it('should add WORKFLOWS flag', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.WORKFLOWS],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Manage workflows');
|
||||
});
|
||||
|
||||
it('should add SECURITY flag', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.SECURITY],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Manage security settings');
|
||||
});
|
||||
|
||||
it('should add WORKSPACE_MEMBERS flag', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
permissionFlagUniversalIdentifiers: [
|
||||
SystemPermissionFlag.WORKSPACE_MEMBERS,
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Manage workspace members');
|
||||
});
|
||||
|
||||
it('should add BILLING flag', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.BILLING],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Manage billing');
|
||||
});
|
||||
|
||||
it('should add API_KEYS_AND_WEBHOOKS flag', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
permissionFlagUniversalIdentifiers: [
|
||||
SystemPermissionFlag.API_KEYS_AND_WEBHOOKS,
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Manage API keys and webhooks');
|
||||
});
|
||||
|
||||
it('should ignore unknown permission flags', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
permissionFlagUniversalIdentifiers: [
|
||||
'unknown-flag-00000000-0000-0000-0000-000000000000',
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('settings and tools', () => {
|
||||
it('should add "Update workspace settings" when canUpdateAllSettings is true', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canUpdateAllSettings: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Update workspace settings');
|
||||
});
|
||||
|
||||
it('should add "Access all tools" when canAccessAllTools is true', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canAccessAllTools: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Access all tools');
|
||||
});
|
||||
});
|
||||
|
||||
describe('combined permissions', () => {
|
||||
it('should combine all permission types in correct order', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canUpdateAllSettings: true,
|
||||
canAccessAllTools: true,
|
||||
permissionFlagUniversalIdentifiers: [
|
||||
SystemPermissionFlag.DATA_MODEL,
|
||||
SystemPermissionFlag.WORKFLOWS,
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result.map((item) => item.label)).toEqual([
|
||||
'Read and write records',
|
||||
'Read and write data model configuration',
|
||||
'Update workspace settings',
|
||||
'Access all tools',
|
||||
'Manage workflows',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle a role with only undefined/false permission values', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canAccessAllTools: false,
|
||||
objectPermissions: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import { type RoleManifest } from 'twenty-shared/application';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconCode,
|
||||
type IconComponent,
|
||||
IconCurrencyDollar,
|
||||
IconDatabase,
|
||||
IconHierarchy,
|
||||
IconKey,
|
||||
IconSettings,
|
||||
IconSettingsAutomation,
|
||||
IconTool,
|
||||
IconUsers,
|
||||
} from 'twenty-ui/display';
|
||||
import { SystemPermissionFlag } from 'twenty-shared/constants';
|
||||
|
||||
export type PermissionSummaryItem = {
|
||||
Icon: IconComponent;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const buildPermissionSummaryFromRoleManifest = (
|
||||
defaultRole: RoleManifest,
|
||||
): PermissionSummaryItem[] => {
|
||||
const items: PermissionSummaryItem[] = [];
|
||||
|
||||
const canRead = defaultRole.canReadAllObjectRecords ?? false;
|
||||
const canUpdate = defaultRole.canUpdateAllObjectRecords ?? false;
|
||||
const canSoftDelete = defaultRole.canSoftDeleteAllObjectRecords ?? false;
|
||||
const canDestroy = defaultRole.canDestroyAllObjectRecords ?? false;
|
||||
|
||||
if (canRead || canUpdate || canSoftDelete || canDestroy) {
|
||||
const capabilities: string[] = [];
|
||||
|
||||
if (canRead) {
|
||||
capabilities.push('read');
|
||||
}
|
||||
|
||||
if (canUpdate) {
|
||||
capabilities.push('write');
|
||||
}
|
||||
|
||||
if (canSoftDelete || canDestroy) {
|
||||
capabilities.push('delete');
|
||||
}
|
||||
|
||||
const label =
|
||||
capabilities.length <= 2
|
||||
? capabilities.join(' and ')
|
||||
: capabilities.slice(0, -1).join(', ') +
|
||||
', and ' +
|
||||
capabilities[capabilities.length - 1];
|
||||
|
||||
items.push({
|
||||
Icon: IconDatabase,
|
||||
label: label.charAt(0).toUpperCase() + label.slice(1) + ' records',
|
||||
});
|
||||
}
|
||||
|
||||
if ((defaultRole.objectPermissions ?? []).length > 0 && items.length === 0) {
|
||||
items.push({
|
||||
Icon: IconDatabase,
|
||||
label: 'Access specific object records',
|
||||
});
|
||||
}
|
||||
|
||||
const hasDataModelFlag = (
|
||||
defaultRole.permissionFlagUniversalIdentifiers ?? []
|
||||
).some((flag) => flag === SystemPermissionFlag.DATA_MODEL);
|
||||
|
||||
if (hasDataModelFlag) {
|
||||
items.push({
|
||||
Icon: IconHierarchy,
|
||||
label: 'Read and write data model configuration',
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultRole.canUpdateAllSettings) {
|
||||
items.push({
|
||||
Icon: IconSettings,
|
||||
label: 'Update workspace settings',
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultRole.canAccessAllTools) {
|
||||
items.push({
|
||||
Icon: IconTool,
|
||||
label: 'Access all tools',
|
||||
});
|
||||
}
|
||||
|
||||
const otherFlags = (
|
||||
defaultRole.permissionFlagUniversalIdentifiers ?? []
|
||||
).filter((flag) => flag !== SystemPermissionFlag.DATA_MODEL);
|
||||
|
||||
const flagLabels: Record<string, { label: string; Icon: IconComponent }> = {
|
||||
[SystemPermissionFlag.WORKFLOWS]: {
|
||||
label: 'Manage workflows',
|
||||
Icon: IconSettingsAutomation,
|
||||
},
|
||||
[SystemPermissionFlag.SECURITY]: {
|
||||
label: 'Manage security settings',
|
||||
Icon: IconKey,
|
||||
},
|
||||
[SystemPermissionFlag.WORKSPACE_MEMBERS]: {
|
||||
label: 'Manage workspace members',
|
||||
Icon: IconUsers,
|
||||
},
|
||||
[SystemPermissionFlag.BILLING]: {
|
||||
label: 'Manage billing',
|
||||
Icon: IconCurrencyDollar,
|
||||
},
|
||||
[SystemPermissionFlag.API_KEYS_AND_WEBHOOKS]: {
|
||||
label: 'Manage API keys and webhooks',
|
||||
Icon: IconCode,
|
||||
},
|
||||
};
|
||||
|
||||
for (const flag of otherFlags) {
|
||||
const config = flagLabels[flag];
|
||||
|
||||
if (isDefined(config)) {
|
||||
items.push(config);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
import { type FieldActorValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMemo } from 'react';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { AvatarOrIcon, Chip } from 'twenty-ui/components';
|
||||
import { AvatarOrIcon, Chip, ChipVariant } from 'twenty-ui/components';
|
||||
import {
|
||||
IconApi,
|
||||
IconCalendar,
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
IconSettingsAutomation,
|
||||
IconUpload,
|
||||
IconWebhook,
|
||||
type IconComponent,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
type ActorDisplayProps = Partial<FieldActorValue> & {
|
||||
@@ -43,6 +43,38 @@ const PROVIDERS_ICON_MAPPING = {
|
||||
},
|
||||
};
|
||||
|
||||
const getLeftIcon = ({
|
||||
source,
|
||||
context,
|
||||
}: Pick<ActorDisplayProps, 'source' | 'context'>):
|
||||
| IconComponent
|
||||
| undefined => {
|
||||
switch (source) {
|
||||
case 'API':
|
||||
return IconApi;
|
||||
case 'IMPORT':
|
||||
return IconUpload;
|
||||
case 'EMAIL':
|
||||
return PROVIDERS_ICON_MAPPING.EMAIL[context?.provider ?? 'default'];
|
||||
case 'CALENDAR':
|
||||
return (
|
||||
PROVIDERS_ICON_MAPPING.CALENDAR[
|
||||
context?.provider as keyof typeof PROVIDERS_ICON_MAPPING.CALENDAR
|
||||
] ?? PROVIDERS_ICON_MAPPING.CALENDAR.default
|
||||
);
|
||||
case 'SYSTEM':
|
||||
return IconRobot;
|
||||
case 'WORKFLOW':
|
||||
return IconSettingsAutomation;
|
||||
case 'WEBHOOK':
|
||||
return IconWebhook;
|
||||
case 'APPLICATION':
|
||||
return IconPlug;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const ActorDisplay = ({
|
||||
name,
|
||||
source,
|
||||
@@ -50,40 +82,14 @@ export const ActorDisplay = ({
|
||||
avatarUrl,
|
||||
context,
|
||||
}: ActorDisplayProps) => {
|
||||
const LeftIcon = useMemo(() => {
|
||||
switch (source) {
|
||||
case 'API':
|
||||
return IconApi;
|
||||
case 'IMPORT':
|
||||
return IconUpload;
|
||||
case 'EMAIL':
|
||||
return PROVIDERS_ICON_MAPPING.EMAIL[context?.provider ?? 'default'];
|
||||
case 'CALENDAR':
|
||||
return (
|
||||
PROVIDERS_ICON_MAPPING.CALENDAR[
|
||||
context?.provider as keyof typeof PROVIDERS_ICON_MAPPING.CALENDAR
|
||||
] ?? PROVIDERS_ICON_MAPPING.CALENDAR.default
|
||||
);
|
||||
case 'SYSTEM':
|
||||
return IconRobot;
|
||||
case 'WORKFLOW':
|
||||
return IconSettingsAutomation;
|
||||
case 'WEBHOOK':
|
||||
return IconWebhook;
|
||||
case 'APPLICATION':
|
||||
return IconPlug;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}, [source, context?.provider]);
|
||||
|
||||
const isIconInverted =
|
||||
source === 'API' || source === 'IMPORT' || source === 'SYSTEM';
|
||||
const LeftIcon = getLeftIcon({ source, context });
|
||||
|
||||
return (
|
||||
<Chip
|
||||
label={name ?? ''}
|
||||
clickable={false}
|
||||
emptyLabel={t`Untitled`}
|
||||
variant={ChipVariant.Transparent}
|
||||
leftComponent={
|
||||
<AvatarOrIcon
|
||||
placeholderColorSeed={workspaceMemberId ?? undefined}
|
||||
@@ -91,7 +97,6 @@ export const ActorDisplay = ({
|
||||
placeholder={name}
|
||||
Icon={LeftIcon}
|
||||
avatarUrl={avatarUrl ?? undefined}
|
||||
isIconInverted={isIconInverted}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user