Compare commits

..
Author SHA1 Message Date
Sonarly Claude Code 197f8f48d5 fix: enable scrolling for long rich text content in side panel
https://sonarly.com/issue/35135?type=bug

Users cannot scroll through long RICH_TEXT field content when viewing it in the side panel, making portions of the text inaccessible.

Fix: ## Summary

Added a two-container flex layout to `SidePanelEditRichTextPage` to enable vertical scrolling for long rich text content. The fix follows the exact CSS pattern used in `SidePanelComposeEmailPage` and other side panel pages.

## Root cause

The original `StyledContainer` had no height constraint, so it grew to fit all content. This prevented the parent `StyledSidePanelContent`'s `overflow-y: auto` from ever triggering because there was no overflow — the container just kept expanding.

## The fix

Split the single container into two styled components:

1. **`StyledContainer`** — outer wrapper with `height: 100%`, `display: flex`, `flex-direction: column`
   - Takes the full height from its parent (`StyledSidePanelContent` which has `flex: 1`)
   - Establishes the flex container for the scrollable content

2. **`StyledContent`** — inner scrollable area with `flex: 1`, `overflow-y: auto`
   - Fills available vertical space via `flex: 1`
   - Enables vertical scrolling when the BlockNote editor content exceeds the constrained height
   - Preserves the original padding and margin values for consistent spacing

This is the standard pattern for scrollable flex containers used throughout the side panel (see `SidePanelComposeEmailPage.tsx` lines 20-31 for identical structure).

## Why this works

CSS `overflow-y: auto` only triggers when:
1. The container has a constrained height (✓ via flex: 1 on StyledContent inside height: 100% parent)
2. The content exceeds that height (✓ BlockNote editor with long text grows beyond constrained height)

The original code failed condition (1) because StyledContainer had no height constraint and just grew infinitely.

## Testing

No automated tests exist for this component. The fix should be manually tested by:
1. Opening a rich text field in the side panel (Notes, Tasks, or custom rich text fields)
2. Adding content that exceeds the viewport height (e.g., 20+ paragraphs)
3. Verifying vertical scrollbar appears and content is accessible via scroll
2026-05-06 01:09:54 +00:00
212 changed files with 652 additions and 12826 deletions
+1 -2
View File
@@ -34,8 +34,7 @@
"@types/qs": "6.9.16",
"@wyw-in-js/transform@npm:0.6.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
"@opentelemetry/api": "1.9.1",
"chokidar": "^3.6.0"
"@opentelemetry/api": "1.9.1"
},
"version": "0.2.1",
"nx": {},
@@ -19,8 +19,8 @@
"test:watch": "vitest"
},
"dependencies": {
"twenty-client-sdk": "2.2.0",
"twenty-sdk": "2.2.0"
"twenty-client-sdk": "0.9.0",
"twenty-sdk": "0.9.0"
},
"devDependencies": {
"@types/node": "^24.7.2",
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId } from 'twenty-sdk/front-component';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { isDefined } from 'twenty-shared/utils';
@@ -72,7 +72,8 @@ const CardDisplay = ({
};
const PostCardPreview = () => {
const recordId = useRecordId();
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
const [postCard, setPostCard] = useState<PostCardRecord | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -1,11 +1,6 @@
import { useEffect } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
unmountFrontComponent,
updateProgress,
useRecordId,
} from 'twenty-sdk/front-component';
import { useSelectedRecordIds, updateProgress, enqueueSnackbar, unmountFrontComponent } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
@@ -15,7 +10,8 @@ const SYSTEM_PROMPT =
'text, nothing else — no greeting label, no sign-off label, just the message.';
const GeneratePostCardEffect = () => {
const recordId = useRecordId();
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
useEffect(() => {
if (recordId === null) {
@@ -1,42 +1,50 @@
import { useEffect } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
unmountFrontComponent,
updateProgress,
useRecordId,
} from 'twenty-sdk/front-component';
import { useSelectedRecordIds, updateProgress, enqueueSnackbar, unmountFrontComponent } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
const SendPostCardsEffect = () => {
const recordId = useRecordId();
const selectedRecordIds = useSelectedRecordIds();
useEffect(() => {
const send = async () => {
try {
if (selectedRecordIds.length === 0) {
await enqueueSnackbar({
message: 'No postcards selected',
variant: 'error',
});
await unmountFrontComponent();
return;
}
await updateProgress(0.1);
const client = new CoreApiClient();
await updateProgress(0.3);
if (recordId) {
for (let i = 0; i < selectedRecordIds.length; i++) {
await client.mutation({
updatePostCard: {
__args: {
id: recordId,
id: selectedRecordIds[i],
data: { status: 'SENT' },
},
id: true,
},
});
await enqueueSnackbar({
message: `Postcard sent`,
variant: 'success',
});
await updateProgress(0.3 + (0.7 * (i + 1)) / selectedRecordIds.length);
}
const count = selectedRecordIds.length;
await enqueueSnackbar({
message: `${count} postcard${count > 1 ? 's' : ''} sent`,
variant: 'success',
});
await unmountFrontComponent();
} catch (error) {
const message =
@@ -48,7 +56,7 @@ const SendPostCardsEffect = () => {
};
send();
}, [recordId]);
}, [selectedRecordIds]);
return null;
};
@@ -3317,8 +3317,8 @@ __metadata:
oxlint: "npm:^0.16.0"
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
twenty-client-sdk: "npm:2.2.0"
twenty-sdk: "npm:2.2.0"
twenty-client-sdk: "npm:0.9.0"
twenty-sdk: "npm:0.9.0"
typescript: "npm:^5.9.3"
vite-tsconfig-paths: "npm:^4.2.1"
vitest: "npm:^3.1.1"
@@ -4059,21 +4059,21 @@ __metadata:
languageName: node
linkType: hard
"twenty-client-sdk@npm:2.2.0":
version: 2.2.0
resolution: "twenty-client-sdk@npm:2.2.0"
"twenty-client-sdk@npm:0.9.0":
version: 0.9.0
resolution: "twenty-client-sdk@npm:0.9.0"
dependencies:
"@genql/cli": "npm:^3.0.3"
"@genql/runtime": "npm:^2.10.0"
esbuild: "npm:^0.25.0"
graphql: "npm:^16.8.1"
checksum: 10c0/90122593efa53440ae386960211a2c274b13a410942bf6f9bc35cf952ae55fe83280e29074677fd284fa3c79069fc578980db06a92a143c08e043f865dbbbd3c
checksum: 10c0/4b42a6622a9852fc3eca50c1131b116c5602af006ee5f1d3b4f1e97721bbc8ef5c2f3b60d9dee3ca9ca8c3c7ad4b292a7b55007b779b4c042997dbc29940ba54
languageName: node
linkType: hard
"twenty-sdk@npm:2.2.0":
version: 2.2.0
resolution: "twenty-sdk@npm:2.2.0"
"twenty-sdk@npm:0.9.0":
version: 0.9.0
resolution: "twenty-sdk@npm:0.9.0"
dependencies:
"@genql/cli": "npm:^3.0.3"
"@genql/runtime": "npm:^2.10.0"
@@ -4093,7 +4093,7 @@ __metadata:
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
tinyglobby: "npm:^0.2.15"
twenty-client-sdk: "npm:2.2.0"
twenty-client-sdk: "npm:0.9.0"
typescript: "npm:^5.9.2"
uuid: "npm:^13.0.0"
vite: "npm:^7.0.0"
@@ -4101,7 +4101,7 @@ __metadata:
zod: "npm:^4.1.11"
bin:
twenty: dist/cli.cjs
checksum: 10c0/8978b4b0aa5ea282c8f76799347d9d1812901ec609bc2bd9270261a6bc5589ad361f6102a06900a4511a29a8378cd3811c2c0bad9f01cb8adadabca6d96dfd0b
checksum: 10c0/27f93e5edac3265f819abacc853598435718020258a95d55518562e086e42daabae784964b42005d8e4ff30d1d6f13a12a38c656e18c60bad98da3f579a8e1c3
languageName: node
linkType: hard
@@ -2872,7 +2872,6 @@ enum AllMetadataName {
fieldPermission
frontComponent
webhook
applicationVariable
connectionProvider
}
@@ -2495,7 +2495,7 @@ export interface CollectionHash {
__typename: 'CollectionHash'
}
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'applicationVariable' | 'connectionProvider'
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'connectionProvider'
export interface MinimalObjectMetadata {
id: Scalars['UUID']
@@ -8854,7 +8854,6 @@ export const enumAllMetadataName = {
fieldPermission: 'fieldPermission' as const,
frontComponent: 'frontComponent' as const,
webhook: 'webhook' as const,
applicationVariable: 'applicationVariable' as const,
connectionProvider: 'connectionProvider' as const
}
@@ -185,7 +185,6 @@ export type AiSystemPromptSection = {
export enum AllMetadataName {
agent = 'agent',
applicationVariable = 'applicationVariable',
commandMenuItem = 'commandMenuItem',
connectionProvider = 'connectionProvider',
fieldMetadata = 'fieldMetadata',
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Toepassing suksesvol opgegradeer."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "تمت ترقية التطبيق بنجاح."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Aplicació actualitzada correctament."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Aplikace byla úspěšně aktualizována."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Applikationen blev opgraderet med succes."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr "Anwendung erfolgreich deinstalliert."
msgid "Application upgraded successfully."
msgstr "Anwendung erfolgreich aktualisiert."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Η εφαρμογή αναβαθμίστηκε με επιτυχία."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
-5
View File
@@ -2162,11 +2162,6 @@ msgstr "Application successfully uninstalled."
msgid "Application upgraded successfully."
msgstr "Application upgraded successfully."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr "application variable"
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Aplicación actualizada con éxito."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Sovellus päivitetty onnistuneesti."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Application mise à niveau avec succès."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
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
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "היישום שודרג בהצלחה."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Az alkalmazás sikeresen frissítve."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Applicazione aggiornata con successo."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "アプリケーションのアップグレードに成功しました。"
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "응용 프로그램이 성공적으로 업그레이드되었습니다."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Applicatie succesvol geüpgraded."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Applikasjonen ble oppgradert vellykket."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Aplikacja została pomyślnie zaktualizowana."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2162,11 +2162,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr ""
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr "Aplicação desinstalada com sucesso."
msgid "Application upgraded successfully."
msgstr "Aplicação atualizada com sucesso."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Aplicação atualizada com sucesso."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Aplicația a fost actualizată cu succes."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
Binary file not shown.
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Апликација је успешно надограђена."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Applikationen uppgraderades framgångsrikt."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Uygulama başarıyla yükseltildi."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Застосунок успішно оновлено."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Đã nâng cấp ứng dụng thành công."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "应用程序升级成功。"
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,11 +2167,6 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "應用程式升級成功。"
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -48,7 +48,6 @@ export const useMetadataErrorHandler = () => {
navigationMenuItem: t`navigation menu item`,
webhook: t`webhook`,
viewSort: t`view sort`,
applicationVariable: t`application variable`,
connectionProvider: t`connection provider`,
} as const satisfies Record<AllMetadataName, string>;
@@ -22,8 +22,16 @@ const RichTextFieldEditor = lazy(() =>
);
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`;
const StyledContent = styled.div`
box-sizing: border-box;
flex: 1;
margin: ${themeCssVariables.spacing[4]} -8px;
overflow-y: auto;
padding-inline: 44px 0px;
width: 100%;
`;
@@ -56,20 +64,22 @@ export const SidePanelEditRichTextPage = () => {
return (
<StyledContainer>
<Suspense fallback={<LoadingSkeleton />}>
{isActivityObject(objectNameSingular) ? (
<ActivityRichTextEditor
activityId={recordId}
activityObjectNameSingular={objectNameSingular}
/>
) : (
<RichTextFieldEditor
recordId={recordId}
objectNameSingular={objectNameSingular}
fieldName={fieldName}
/>
)}
</Suspense>
<StyledContent>
<Suspense fallback={<LoadingSkeleton />}>
{isActivityObject(objectNameSingular) ? (
<ActivityRichTextEditor
activityId={recordId}
activityObjectNameSingular={objectNameSingular}
/>
) : (
<RichTextFieldEditor
recordId={recordId}
objectNameSingular={objectNameSingular}
fieldName={fieldName}
/>
)}
</Suspense>
</StyledContent>
</StyledContainer>
);
};
-11
View File
@@ -76,14 +76,3 @@ export type FunctionExecutionResult = {
stackTrace: string;
};
};
export type ChokidarFsEvent =
| 'add'
| 'addDir'
| 'change'
| 'unlink'
| 'unlinkDir'
| 'ready'
| 'raw'
| 'error'
| 'all';
@@ -1,7 +1,7 @@
import path, { relative } from 'path';
import chokidar, { type FSWatcher } from 'chokidar';
import { type EventName } from 'chokidar/handler.js';
import { ASSETS_DIR } from 'twenty-shared/application';
import { type ChokidarFsEvent } from '@/cli/types';
export type ManifestWatcherOptions = {
appPath: string;
@@ -13,10 +13,7 @@ const IGNORED_DIRECTORY_NAMES = new Set(['node_modules', 'dist', '.twenty']);
export class ManifestWatcher {
private appPath: string;
private handleChangeDetected: (
filePath: string,
event: ChokidarFsEvent,
) => void;
private handleChangeDetected: (filePath: string, event: EventName) => void;
private verbose: boolean;
private watcher: FSWatcher | null = null;
@@ -10,7 +10,7 @@ import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifes
import { ManifestWatcher } from '@/cli/utilities/build/manifest/manifest-watcher';
import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
import type { Location } from 'esbuild';
import { type ChokidarFsEvent } from '@/cli/types';
import { type EventName } from 'chokidar/handler.js';
import { ASSETS_DIR } from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
@@ -102,10 +102,7 @@ export class StartWatchersOrchestratorStep {
]);
}
private handleChangeDetected(
sourcePath: string,
event: ChokidarFsEvent,
): void {
private handleChangeDetected(sourcePath: string, event: EventName): void {
this.state.addEvent({
message: `Change detected: ${sourcePath}`,
status: 'info',
@@ -1,33 +0,0 @@
import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.3.0', 1777966965587)
export class TransformApplicationVariableToSyncableEntityFastInstanceCommand
implements FastInstanceCommand
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'DROP INDEX "core"."IDX_78ae6cfe5f49a76c4bf842ad58"',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" DROP CONSTRAINT "IDX_APPLICATION_VARIABLE_KEY_APPLICATION_ID_UNIQUE"',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ADD "universalIdentifier" uuid',
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" DROP COLUMN "universalIdentifier"',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ADD CONSTRAINT "IDX_APPLICATION_VARIABLE_KEY_APPLICATION_ID_UNIQUE" UNIQUE ("key", "applicationId")',
);
await queryRunner.query(
'CREATE INDEX "IDX_78ae6cfe5f49a76c4bf842ad58" ON "core"."applicationVariable" ("workspaceId") ',
);
}
}
@@ -1,54 +0,0 @@
import { DataSource, QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
@RegisteredInstanceCommand('2.3.0', 1777966965588, { type: 'slow' })
export class BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand
implements SlowInstanceCommand
{
async runDataMigration(dataSource: DataSource): Promise<void> {
await dataSource.query(
'DELETE "core"."applicationVariable" WHERE "applicationId" IS NULL',
);
await dataSource.query(
'UPDATE "core"."applicationVariable" SET "universalIdentifier" = gen_random_uuid() WHERE "universalIdentifier" IS NULL',
);
}
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" DROP CONSTRAINT "FK_51adb49e7f8df35dd23e01c4830"',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ALTER COLUMN "applicationId" SET NOT NULL',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ADD CONSTRAINT "FK_51adb49e7f8df35dd23e01c4830" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ALTER COLUMN "universalIdentifier" SET NOT NULL',
);
await queryRunner.query(
'CREATE UNIQUE INDEX "IDX_44ecebdf70cbed17f89527b36b" ON "core"."applicationVariable" ("workspaceId", "universalIdentifier") ',
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'DROP INDEX "core"."IDX_44ecebdf70cbed17f89527b36b"',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ALTER COLUMN "universalIdentifier" DROP NOT NULL',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" DROP CONSTRAINT "FK_51adb49e7f8df35dd23e01c4830"',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ALTER COLUMN "applicationId" DROP NOT NULL',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ADD CONSTRAINT "FK_51adb49e7f8df35dd23e01c4830" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION',
);
}
}
@@ -26,8 +26,6 @@ import { AddToolAndWorkflowActionTriggerSettingsFastInstanceCommand } from 'src/
import { MigrateToolTriggerSettingsSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1797000002000-migrate-tool-trigger-settings';
import { ConnectionProviderSyncableEntityFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777896012579-connection-provider-syncable-entity';
import { RemoveUserDefaultAvatarUrlFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777915958318-remove-user-default-avatar-url';
import { TransformApplicationVariableToSyncableEntityFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777966965587-transform-application-variable-to-syncable-entity';
import { BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1777966965588-backfill-application-variable-universal-identifier';
export const INSTANCE_COMMANDS = [
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
@@ -56,6 +54,4 @@ export const INSTANCE_COMMANDS = [
MigrateToolTriggerSettingsSlowInstanceCommand,
ConnectionProviderSyncableEntityFastInstanceCommand,
RemoveUserDefaultAvatarUrlFastInstanceCommand,
TransformApplicationVariableToSyncableEntityFastInstanceCommand,
BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand,
];
@@ -1,6 +1,7 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined, isPlainObject } from 'twenty-shared/utils';
import { type CompositeFieldGroupByDefinition } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/types/composite-field-group-by-definition.type';
import { isGroupByDateFieldDefinition } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/is-group-by-date-field-definition.util';
import { isRelationNestedFieldSupportedInGroupBy } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/is-relation-nested-field-supported-in-group-by.util';
import { validateSingleKeyForGroupByOrThrow } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/validate-single-key-for-group-by-or-throw.util';
@@ -11,6 +11,10 @@ import { CreateApiKeyInput } from 'src/engine/core-modules/api-key/dtos/create-a
import { GetApiKeyInput } from 'src/engine/core-modules/api-key/dtos/get-api-key.input';
import { RevokeApiKeyInput } from 'src/engine/core-modules/api-key/dtos/revoke-api-key.input';
import { UpdateApiKeyInput } from 'src/engine/core-modules/api-key/dtos/update-api-key.input';
import {
ApiKeyException,
ApiKeyExceptionCode,
} from 'src/engine/core-modules/api-key/exceptions/api-key.exception';
import { apiKeyGraphqlApiExceptionHandler } from 'src/engine/core-modules/api-key/utils/api-key-graphql-api-exception-handler.util';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
@@ -16,6 +16,7 @@ import { ApplicationService } from 'src/engine/core-modules/application/applicat
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/build-from-to-all-universal-flat-entity-maps.util';
import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/get-application-sub-all-flat-entity-maps.util';
import { ApplicationVariableEntityService } from 'src/engine/core-modules/application/application-variable/application-variable.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
@@ -31,6 +32,7 @@ export class ApplicationSyncService {
constructor(
private readonly applicationService: ApplicationService,
private readonly applicationVariableService: ApplicationVariableEntityService,
private readonly applicationManifestMigrationService: ApplicationManifestMigrationService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceCacheService: WorkspaceCacheService,
@@ -143,6 +145,14 @@ export class ApplicationSyncService {
);
}
await this.applicationVariableService.upsertManyApplicationVariableEntities(
{
applicationVariables: manifest.application.applicationVariables,
applicationId: application.id,
workspaceId,
},
);
const resolvedRegistrationId =
applicationRegistrationId ?? application.applicationRegistrationId;
@@ -1,31 +0,0 @@
import { type UniversalFlatApplicationVariable } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-application-variable.type';
export const fromApplicationVariableManifestToUniversalFlatApplicationVariable =
({
key,
universalIdentifier,
description,
value,
isSecret,
applicationUniversalIdentifier,
now,
}: {
key: string;
universalIdentifier: string;
description?: string;
value?: string;
isSecret?: boolean;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatApplicationVariable => {
return {
universalIdentifier,
applicationUniversalIdentifier,
key,
value: isSecret ? '' : (value ?? ''), // We protect secret variable by not syncing its value at all
description: description ?? '',
isSecret: isSecret ?? false,
createdAt: now,
updatedAt: now,
};
};
@@ -4,7 +4,6 @@ import { isDefined } from 'twenty-shared/utils';
import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util';
import { fromApplicationVariableManifestToUniversalFlatApplicationVariable } from 'src/engine/core-modules/application/application-manifest/converters/from-application-variable-manifest-to-universal-flat-application-variable.util';
import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util';
import { fromConnectionProviderManifestToUniversalFlatConnectionProvider } from 'src/engine/core-modules/application/application-manifest/converters/from-connection-provider-manifest-to-universal-flat-connection-provider.util';
import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-field-manifest-to-universal-flat-field-metadata.util';
@@ -432,28 +431,6 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
}
}
for (const [key, applicationVariableManifest] of Object.entries(
manifest.application.applicationVariables ?? {},
)) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromApplicationVariableManifestToUniversalFlatApplicationVariable({
key,
universalIdentifier: applicationVariableManifest.universalIdentifier,
value:
'value' in applicationVariableManifest
? applicationVariableManifest.value
: undefined,
description: applicationVariableManifest.description,
isSecret: applicationVariableManifest.isSecret,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatApplicationVariableMaps,
});
}
for (const commandMenuItemManifest of manifest.commandMenuItems ?? []) {
if (!isDefined(commandMenuItemManifest.frontComponentUniversalIdentifier)) {
throw new Error(
@@ -32,6 +32,7 @@ import { RotateClientSecretDTO } from 'src/engine/core-modules/application/appli
import { TransferApplicationRegistrationOwnershipInput } from 'src/engine/core-modules/application/application-registration/dtos/transfer-application-registration-ownership.input';
import { UpdateApplicationRegistrationInput } from 'src/engine/core-modules/application/application-registration/dtos/update-application-registration.input';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
@@ -159,6 +159,220 @@ describe('ApplicationVariableEntityService', () => {
});
});
describe('upsertManyApplicationVariableEntities', () => {
it('should encrypt secret values when creating new variables', async () => {
repository.find.mockResolvedValue([]);
repository.save.mockResolvedValue({} as any);
repository.delete.mockResolvedValue({ affected: 0 } as any);
await service.upsertManyApplicationVariableEntities({
applicationVariables: {
SECRET_KEY: {
universalIdentifier: 'secret-key-123',
value: 'my-secret',
description: 'A secret key',
isSecret: true,
},
},
applicationId: mockApplicationId,
workspaceId: mockWorkspaceId,
});
expect(secretEncryptionService.encrypt).toHaveBeenCalledWith('my-secret');
expect(repository.save).toHaveBeenCalledWith([
{
key: 'SECRET_KEY',
value: 'encrypted_my-secret',
description: 'A secret key',
isSecret: true,
applicationId: mockApplicationId,
workspaceId: mockWorkspaceId,
},
]);
});
it('should not encrypt non-secret values when creating new variables', async () => {
repository.find.mockResolvedValue([]);
repository.save.mockResolvedValue({} as any);
repository.delete.mockResolvedValue({ affected: 0 } as any);
await service.upsertManyApplicationVariableEntities({
applicationVariables: {
PUBLIC_URL: {
universalIdentifier: 'public-url-123',
value: 'https://example.com',
description: 'Public URL',
isSecret: false,
},
},
applicationId: mockApplicationId,
workspaceId: mockWorkspaceId,
});
expect(secretEncryptionService.encrypt).not.toHaveBeenCalled();
expect(repository.save).toHaveBeenCalledWith([
{
key: 'PUBLIC_URL',
value: 'https://example.com',
description: 'Public URL',
isSecret: false,
applicationId: mockApplicationId,
workspaceId: mockWorkspaceId,
},
]);
});
it('should handle undefined isSecret as false', async () => {
repository.find.mockResolvedValue([]);
repository.save.mockResolvedValue({} as any);
repository.delete.mockResolvedValue({ affected: 0 } as any);
await service.upsertManyApplicationVariableEntities({
applicationVariables: {
SOME_VAR: {
universalIdentifier: 'some-var-123',
value: 'some-value',
description: 'Some variable',
},
},
applicationId: mockApplicationId,
workspaceId: mockWorkspaceId,
});
expect(secretEncryptionService.encrypt).not.toHaveBeenCalled();
expect(repository.save).toHaveBeenCalledWith([
expect.objectContaining({
isSecret: false,
}),
]);
});
it('should update existing variables without changing values', async () => {
const existingVariable = {
id: '1',
key: 'EXISTING_VAR',
value: 'existing-encrypted-value',
isSecret: true,
applicationId: mockApplicationId,
} as ApplicationVariableEntity;
repository.find.mockResolvedValue([existingVariable]);
repository.save.mockResolvedValue({} as any);
repository.delete.mockResolvedValue({ affected: 0 } as any);
await service.upsertManyApplicationVariableEntities({
applicationVariables: {
EXISTING_VAR: {
universalIdentifier: 'existing-var-123',
value: 'new-value',
description: 'Updated description',
isSecret: true,
},
},
applicationId: mockApplicationId,
workspaceId: mockWorkspaceId,
});
expect(repository.save).toHaveBeenCalledWith([
{
id: '1',
description: 'Updated description',
isSecret: true,
},
]);
expect(repository.update).not.toHaveBeenCalled();
});
it('should update existing value when shouldUpdateValue is true', async () => {
const existingVariable = {
id: '1',
key: 'EXISTING_VAR',
value: 'existing-encrypted-value',
isSecret: true,
applicationId: mockApplicationId,
} as ApplicationVariableEntity;
repository.find.mockResolvedValue([existingVariable]);
repository.save.mockResolvedValue({} as any);
repository.delete.mockResolvedValue({ affected: 0 } as any);
await service.upsertManyApplicationVariableEntities({
applicationVariables: {
EXISTING_VAR: {
universalIdentifier: 'existing-var-123',
value: 'new-value',
description: 'Updated description',
isSecret: true,
},
},
applicationId: mockApplicationId,
workspaceId: mockWorkspaceId,
shouldUpdateValue: true,
});
expect(repository.save).toHaveBeenCalledWith([
{
id: '1',
description: 'Updated description',
value: 'encrypted_new-value',
isSecret: true,
},
]);
});
it('should update existing value if isSecret changes', async () => {
const existingVariable = {
id: '1',
key: 'EXISTING_VAR',
value: 'existing-encrypted-value',
isSecret: true,
applicationId: mockApplicationId,
} as ApplicationVariableEntity;
repository.find.mockResolvedValue([existingVariable]);
repository.save.mockResolvedValue({} as any);
repository.delete.mockResolvedValue({ affected: 0 } as any);
await service.upsertManyApplicationVariableEntities({
applicationVariables: {
EXISTING_VAR: {
universalIdentifier: 'existing-var-123',
value: 'new-value',
description: 'Updated description',
isSecret: false,
},
},
applicationId: mockApplicationId,
workspaceId: mockWorkspaceId,
});
expect(repository.save).toHaveBeenCalledWith([
{
id: '1',
description: 'Updated description',
value: 'new-value',
isSecret: false,
},
]);
expect(repository.update).not.toHaveBeenCalled();
});
it('should handle undefined applicationVariables', async () => {
await service.upsertManyApplicationVariableEntities({
applicationVariables: undefined,
applicationId: mockApplicationId,
workspaceId: mockWorkspaceId,
});
expect(repository.find).not.toHaveBeenCalled();
expect(repository.save).not.toHaveBeenCalled();
expect(repository.update).not.toHaveBeenCalled();
expect(
workspaceCacheService.invalidateAndRecompute,
).not.toHaveBeenCalled();
});
});
describe('getDisplayValue', () => {
it('should return plain value for non-secret variables', () => {
const variable = {
@@ -6,10 +6,7 @@ import {
ApplicationVariableEntityException,
ApplicationVariableEntityExceptionCode,
} from 'src/engine/core-modules/application/application-variable/application-variable.exception';
import {
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@Catch(ApplicationVariableEntityException)
export class ApplicationVariableEntityExceptionFilter
@@ -19,8 +16,6 @@ export class ApplicationVariableEntityExceptionFilter
switch (exception.code) {
case ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND:
throw new NotFoundError(exception);
case ApplicationVariableEntityExceptionCode.INVALID_APPLICATION_VARIABLE_INPUT:
throw new UserInputError(exception);
default:
assertUnreachable(exception.code);
}
@@ -5,23 +5,41 @@ import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import type { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { EntityRelation } from 'src/engine/workspace-manager/workspace-migration/types/entity-relation.interface';
@Entity({
name: 'applicationVariable',
schema: 'core',
})
@ObjectType('ApplicationVariable')
export class ApplicationVariableEntity extends SyncableEntity {
@Unique('IDX_APPLICATION_VARIABLE_KEY_APPLICATION_ID_UNIQUE', [
'key',
'applicationId',
])
export class ApplicationVariableEntity {
@IDField(() => UUIDScalarType)
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
@Index()
workspaceId: string;
@ManyToOne('WorkspaceEntity', { onDelete: 'CASCADE' })
@JoinColumn({ name: 'workspaceId' })
workspace: EntityRelation<WorkspaceEntity>;
@Column({ nullable: false, type: 'text' })
key: string;
@@ -34,6 +52,20 @@ export class ApplicationVariableEntity extends SyncableEntity {
@Column({ nullable: false, type: 'boolean', default: false })
isSecret: boolean;
@Column({ nullable: true, type: 'uuid' })
applicationId?: string;
@ManyToOne(
() => ApplicationEntity,
(application) => application.applicationVariables,
{
onDelete: 'CASCADE',
nullable: true,
},
)
@JoinColumn({ name: 'applicationId' })
application: EntityRelation<ApplicationEntity> | null;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -6,7 +6,6 @@ import { CustomException } from 'src/utils/custom-exception';
export enum ApplicationVariableEntityExceptionCode {
APPLICATION_VARIABLE_NOT_FOUND = 'APPLICATION_VARIABLE_NOT_FOUND',
INVALID_APPLICATION_VARIABLE_INPUT = 'INVALID_APPLICATION_VARIABLE_INPUT',
}
const getApplicationVariableEntityExceptionUserFriendlyMessage = (
@@ -15,8 +14,6 @@ const getApplicationVariableEntityExceptionUserFriendlyMessage = (
switch (code) {
case ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND:
return msg`Application variable not found.`;
case ApplicationVariableEntityExceptionCode.INVALID_APPLICATION_VARIABLE_INPUT:
return msg`Invalid application variable input.`;
default:
assertUnreachable(code);
}
@@ -3,24 +3,21 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity';
import { ApplicationVariableEntityResolver } from 'src/engine/core-modules/application/application-variable/application-variable.resolver';
import { ApplicationVariableEntityService } from 'src/engine/core-modules/application/application-variable/application-variable.service';
import { WorkspaceApplicationVariableMapCacheService } from 'src/engine/core-modules/application/application-variable/workspace-application-variable-map-cache.service';
import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module';
import { FlatApplicationVariableModule } from 'src/engine/metadata-modules/flat-application-variable/flat-application-variable.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@Module({
imports: [
NestjsQueryTypeOrmModule.forFeature([ApplicationVariableEntity]),
TypeOrmModule.forFeature([ApplicationVariableEntity, ApplicationEntity]),
TypeOrmModule.forFeature([ApplicationVariableEntity]),
PermissionsModule,
WorkspaceCacheModule,
SecretEncryptionModule,
FlatApplicationVariableModule,
],
providers: [
ApplicationVariableEntityService,
@@ -1,8 +1,9 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ApplicationVariables } from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { In, Not, Repository } from 'typeorm';
import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity';
import {
@@ -22,6 +23,14 @@ export class ApplicationVariableEntityService {
private readonly secretEncryptionService: SecretEncryptionService,
) {}
private encryptSecretValue(value: string, isSecret: boolean): string {
if (!isSecret) {
return value;
}
return this.secretEncryptionService.encrypt(value);
}
getDisplayValue(applicationVariable: ApplicationVariableEntity): string {
if (!applicationVariable.isSecret) {
return applicationVariable.value;
@@ -33,6 +42,48 @@ export class ApplicationVariableEntityService {
});
}
// Decrypted plaintext value. Server-side only — never expose via GraphQL.
// Used by trusted server flows that need the raw secret (e.g. exchanging an
// OAuth client secret with a third-party provider).
getRawValue(applicationVariable: ApplicationVariableEntity): string {
if (!applicationVariable.isSecret) {
return applicationVariable.value;
}
return this.secretEncryptionService.decrypt(applicationVariable.value);
}
async findOneByKey({
applicationId,
key,
}: {
applicationId: string;
key: string;
}): Promise<ApplicationVariableEntity | null> {
return this.applicationVariableRepository.findOne({
where: { applicationId, key },
});
}
async getRawValueByKeyOrThrow({
applicationId,
key,
}: {
applicationId: string;
key: string;
}): Promise<string> {
const variable = await this.findOneByKey({ applicationId, key });
if (!isDefined(variable)) {
throw new ApplicationVariableEntityException(
`Application variable "${key}" not found for application ${applicationId}`,
ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND,
);
}
return this.getRawValue(variable);
}
async update({
key,
plainTextValue,
@@ -54,9 +105,10 @@ export class ApplicationVariableEntityService {
);
}
const encryptedValue = existingVariable.isSecret
? this.secretEncryptionService.encrypt(plainTextValue)
: plainTextValue;
const encryptedValue = this.encryptSecretValue(
plainTextValue,
existingVariable.isSecret,
);
await this.applicationVariableRepository.update(
{ key, applicationId },
@@ -69,4 +121,79 @@ export class ApplicationVariableEntityService {
'applicationVariableMaps',
]);
}
async upsertManyApplicationVariableEntities({
applicationVariables,
applicationId,
workspaceId,
shouldUpdateValue = false,
}: {
applicationVariables?: ApplicationVariables;
applicationId: string;
workspaceId: string;
shouldUpdateValue?: boolean;
}) {
if (!isDefined(applicationVariables)) {
return;
}
const keys = Object.keys(applicationVariables);
const existingVariables = await this.applicationVariableRepository.find({
where: {
applicationId,
key: In(keys),
},
});
const existingVariablesByKey = new Map(
existingVariables.map((variable) => [variable.key, variable]),
);
const entitiesToSave: Partial<ApplicationVariableEntity>[] = [];
for (const [key, { value, description, isSecret }] of Object.entries(
applicationVariables,
)) {
const existingVariable = existingVariablesByKey.get(key);
const isSecretValue = isSecret ?? false;
const encryptedValue = this.encryptSecretValue(
value ?? '',
isSecretValue,
);
if (isDefined(existingVariable)) {
entitiesToSave.push({
id: existingVariable.id,
description: description ?? '',
isSecret: isSecretValue,
...(shouldUpdateValue || existingVariable.isSecret !== isSecretValue
? { value: encryptedValue }
: {}),
});
} else {
entitiesToSave.push({
key,
value: encryptedValue,
description: description ?? '',
isSecret: isSecretValue,
applicationId,
workspaceId,
});
}
}
if (entitiesToSave.length > 0) {
await this.applicationVariableRepository.save(entitiesToSave);
}
await this.applicationVariableRepository.delete({
applicationId,
key: Not(In(keys)),
});
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'applicationVariableMaps',
]);
}
}
@@ -1,5 +1,6 @@
import { type FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type';
import type { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatApplicationVariable } from 'src/engine/core-modules/application/application-variable/types/flat-application-variable.type';
export type ApplicationVariableCacheMaps =
FlatEntityMaps<FlatApplicationVariable>;
export type ApplicationVariableCacheMaps = {
byId: Partial<Record<string, FlatApplicationVariable>>;
byApplicationId: Partial<Record<string, FlatApplicationVariable[]>>;
};
@@ -1,4 +1,4 @@
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity';
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
export type FlatApplicationVariable = FlatEntityFrom<ApplicationVariableEntity>;
@@ -0,0 +1,16 @@
import { type ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity';
import { type FlatApplicationVariable } from 'src/engine/core-modules/application/application-variable/types/flat-application-variable.type';
export const fromApplicationVariableEntityToFlatApplicationVariable = (
entity: ApplicationVariableEntity,
): FlatApplicationVariable => ({
id: entity.id,
key: entity.key,
value: entity.value,
description: entity.description,
isSecret: entity.isSecret,
applicationId: entity.applicationId,
workspaceId: entity.workspaceId,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),
});
@@ -2,17 +2,14 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity';
import { type ApplicationVariableCacheMaps } from 'src/engine/core-modules/application/application-variable/types/application-variable-cache-maps.type';
import { fromApplicationVariableEntityToFlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/utils/from-application-variable-entity-to-flat-application-variable.util';
import { fromApplicationVariableEntityToFlatApplicationVariable } from 'src/engine/core-modules/application/application-variable/utils/from-application-variable-entity-to-flat-application-variable.util';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@WorkspaceCache('applicationVariableMaps')
@@ -20,8 +17,6 @@ export class WorkspaceApplicationVariableMapCacheService extends WorkspaceCacheP
constructor(
@InjectRepository(ApplicationVariableEntity)
private readonly applicationVariableRepository: Repository<ApplicationVariableEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
) {
super();
}
@@ -29,32 +24,43 @@ export class WorkspaceApplicationVariableMapCacheService extends WorkspaceCacheP
async computeForCache(
workspaceId: string,
): Promise<ApplicationVariableCacheMaps> {
const [applicationVariableEntities, applications] = await Promise.all([
this.applicationVariableRepository.find({
where: { workspaceId },
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
}),
]);
const applicationVariableEntities = await this.applicationVariableRepository
.createQueryBuilder('applicationVariable')
.innerJoin('applicationVariable.application', 'application')
.where('application.workspaceId = :workspaceId', { workspaceId })
.getMany();
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const applicationVariableMaps = createEmptyFlatEntityMaps();
const applicationVariableMaps: ApplicationVariableCacheMaps = {
byId: {},
byApplicationId: {},
};
for (const entity of applicationVariableEntities) {
const flatApplicationVariable =
fromApplicationVariableEntityToFlatApplicationVariable({
entity,
applicationIdToUniversalIdentifierMap,
});
fromApplicationVariableEntityToFlatApplicationVariable(entity);
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatApplicationVariable,
flatEntityMapsToMutate: applicationVariableMaps,
});
applicationVariableMaps.byId[flatApplicationVariable.id] =
flatApplicationVariable;
if (!isDefined(flatApplicationVariable.applicationId)) {
continue;
}
if (
!isDefined(
applicationVariableMaps.byApplicationId[
flatApplicationVariable.applicationId
],
)
) {
applicationVariableMaps.byApplicationId[
flatApplicationVariable.applicationId
] = [flatApplicationVariable];
continue;
}
applicationVariableMaps.byApplicationId[
flatApplicationVariable.applicationId
]?.push(flatApplicationVariable);
}
return applicationVariableMaps;
@@ -588,26 +588,6 @@ msgstr "Toepassingsregistrasieveranderlike nie gevind nie."
msgid "Application upgrade failed."
msgstr "Toepassingsopgradering het misluk."
#. js-lingui-id: 5MJQ4s
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable key is required"
msgstr ""
#. js-lingui-id: K+YEBR
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable key must be unique"
msgstr ""
#. js-lingui-id: e6tjp1
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable not found"
msgstr ""
#. js-lingui-id: 9qtW5o
#: src/engine/core-modules/application/application-variable/application-variable.exception.ts
msgid "Application variable not found."
@@ -3207,11 +3187,6 @@ msgstr "Ongeldige agent-invoer."
msgid "Invalid aggregate fields parameter."
msgstr "Ongeldige aggregaatvelde-parameter."
#. js-lingui-id: 56bSD0
#: src/engine/core-modules/application/application-variable/application-variable.exception.ts
msgid "Invalid application variable input."
msgstr ""
#. js-lingui-id: KNrRpL
#: src/engine/metadata-modules/permissions/permissions.exception.ts
msgid "Invalid argument provided."
@@ -588,26 +588,6 @@ msgstr "متغير تسجيل التطبيق غير موجود."
msgid "Application upgrade failed."
msgstr "فشلت ترقية التطبيق."
#. js-lingui-id: 5MJQ4s
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable key is required"
msgstr ""
#. js-lingui-id: K+YEBR
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable key must be unique"
msgstr ""
#. js-lingui-id: e6tjp1
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable not found"
msgstr ""
#. js-lingui-id: 9qtW5o
#: src/engine/core-modules/application/application-variable/application-variable.exception.ts
msgid "Application variable not found."
@@ -3207,11 +3187,6 @@ msgstr "إدخال الوكيل غير صالح."
msgid "Invalid aggregate fields parameter."
msgstr "معامل حقول التجميع غير صالح."
#. js-lingui-id: 56bSD0
#: src/engine/core-modules/application/application-variable/application-variable.exception.ts
msgid "Invalid application variable input."
msgstr ""
#. js-lingui-id: KNrRpL
#: src/engine/metadata-modules/permissions/permissions.exception.ts
msgid "Invalid argument provided."
@@ -588,26 +588,6 @@ msgstr "Variable de registre d'aplicació no trobada."
msgid "Application upgrade failed."
msgstr "L'actualització de l'aplicació ha fallat."
#. js-lingui-id: 5MJQ4s
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable key is required"
msgstr ""
#. js-lingui-id: K+YEBR
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable key must be unique"
msgstr ""
#. js-lingui-id: e6tjp1
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable not found"
msgstr ""
#. js-lingui-id: 9qtW5o
#: src/engine/core-modules/application/application-variable/application-variable.exception.ts
msgid "Application variable not found."
@@ -3207,11 +3187,6 @@ msgstr "Entrada d'agent no vàlida."
msgid "Invalid aggregate fields parameter."
msgstr "Paràmetre de camps agregats no vàlid."
#. js-lingui-id: 56bSD0
#: src/engine/core-modules/application/application-variable/application-variable.exception.ts
msgid "Invalid application variable input."
msgstr ""
#. js-lingui-id: KNrRpL
#: src/engine/metadata-modules/permissions/permissions.exception.ts
msgid "Invalid argument provided."
@@ -588,26 +588,6 @@ msgstr "Proměnná registrace aplikace nenalezena."
msgid "Application upgrade failed."
msgstr "Aktualizace aplikace se nezdařila."
#. js-lingui-id: 5MJQ4s
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable key is required"
msgstr ""
#. js-lingui-id: K+YEBR
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable key must be unique"
msgstr ""
#. js-lingui-id: e6tjp1
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable not found"
msgstr ""
#. js-lingui-id: 9qtW5o
#: src/engine/core-modules/application/application-variable/application-variable.exception.ts
msgid "Application variable not found."
@@ -3207,11 +3187,6 @@ msgstr "Neplatný vstup agenta."
msgid "Invalid aggregate fields parameter."
msgstr "Neplatný parametr agregačních polí."
#. js-lingui-id: 56bSD0
#: src/engine/core-modules/application/application-variable/application-variable.exception.ts
msgid "Invalid application variable input."
msgstr ""
#. js-lingui-id: KNrRpL
#: src/engine/metadata-modules/permissions/permissions.exception.ts
msgid "Invalid argument provided."
@@ -588,26 +588,6 @@ msgstr "Applikationsregistreringsvariabel ikke fundet."
msgid "Application upgrade failed."
msgstr "Applikationsopgradering mislykkedes."
#. js-lingui-id: 5MJQ4s
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable key is required"
msgstr ""
#. js-lingui-id: K+YEBR
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable key must be unique"
msgstr ""
#. js-lingui-id: e6tjp1
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts
msgid "Application variable not found"
msgstr ""
#. js-lingui-id: 9qtW5o
#: src/engine/core-modules/application/application-variable/application-variable.exception.ts
msgid "Application variable not found."
@@ -3207,11 +3187,6 @@ msgstr "Ugyldigt agentinput."
msgid "Invalid aggregate fields parameter."
msgstr "Ugyldig parameter for aggregerede felter."
#. js-lingui-id: 56bSD0
#: src/engine/core-modules/application/application-variable/application-variable.exception.ts
msgid "Invalid application variable input."
msgstr ""
#. js-lingui-id: KNrRpL
#: src/engine/metadata-modules/permissions/permissions.exception.ts
msgid "Invalid argument provided."

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