Compare commits

..
Author SHA1 Message Date
Félix Malfait 86b24382cc chore: remove attachment/note/task morph migration feature flags
- Drop IS_ATTACHMENT_MIGRATED, IS_NOTE_TARGET_MIGRATED, IS_TASK_TARGET_MIGRATED from FeatureFlagKey and defaults
- Frontend always uses morph relation paths for activities and attachments
- Custom object default relations: always morph for attachment, noteTarget, taskTarget (and timelineActivity)
- 1.17 upgrade commands skip when no legacy relation fields remain; no longer set flags
- Sync generated metadata GraphQL types (twenty-front, twenty-sdk)

Made-with: Cursor
2026-03-23 11:22:39 +01:00
117 changed files with 1159 additions and 2063 deletions
+4 -9
View File
@@ -27,8 +27,11 @@ jobs:
- name: Build dependencies
run: npx nx build twenty-shared
- name: Build twenty-server
run: npx nx build twenty-server
- name: Run catalog sync
run: npx nx run twenty-server:ts-node-no-deps-transpile-only -- ./scripts/ai-sync-models-dev.ts
run: npx nx run twenty-server:command-no-deps ai:sync-models-dev
- name: Check for changes
id: changes
@@ -58,11 +61,3 @@ jobs:
base: main
labels: ai, automated
delete-branch: true
- name: Trigger automerge
if: steps.changes.outputs.changed == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: automated-pr-ready
-8
View File
@@ -150,11 +150,3 @@ jobs:
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger i18n automerge
if: github.event_name != 'pull_request' && steps.check_changes.outputs.changes_detected == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
-8
View File
@@ -138,11 +138,3 @@ jobs:
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger i18n automerge
if: steps.compile_translations.outputs.changes_detected == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
-8
View File
@@ -102,11 +102,3 @@ jobs:
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger i18n automerge
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
-7
View File
@@ -30,11 +30,6 @@ COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
COPY ./packages/twenty-server /app/packages/twenty-server
RUN npx nx run twenty-server:lingui:extract && \
npx nx run twenty-server:lingui:compile && \
npx nx run twenty-emails:lingui:extract && \
npx nx run twenty-emails:lingui:compile
RUN npx nx run twenty-server:build
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-server
@@ -48,8 +43,6 @@ COPY ./packages/twenty-front /app/packages/twenty-front
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
RUN npx nx run twenty-front:lingui:extract && \
npx nx run twenty-front:lingui:compile
RUN npx nx build twenty-front
@@ -6,14 +6,8 @@ const query = `query FindOnePerson($objectRecordId: UUID!) {
person(
filter: {or: [{deletedAt: {is: NULL}}, {deletedAt: {is: NOT_NULL}}], id: {eq: $objectRecordId}}
) {
previousCompanies {
edges {
node {
company {
name
}
}
}
company {
name
}
emails {
primaryEmail
@@ -49,8 +43,8 @@ const query = `query FindOnePerson($objectRecordId: UUID!) {
}`
test('Create and update record', async ({ page }) => {
await page.goto('/objects/people');
await page.getByRole('button', { name: 'Create new Person' }).click();
await page.getByRole('link', { name: 'People' }).click();
await page.getByRole('button', { name: 'Create new record' }).click();
// Generate a random email for testing
const randomEmail = `testuser_${Math.random().toString(36).substring(2, 10)}@example.com`;
@@ -113,17 +107,29 @@ test('Create and update record', async ({ page }) => {
await options.getByText('Hybrid').first().click({force: true});
recordFieldList.getByText('Work Preference').first().click({force: true});
// Fill previous companies
await recordFieldList.getByText('Previous Companies').first().click({force: true});
await recordFieldList.getByText('Previous Companies').nth(1).click({force: true});
await page.getByPlaceholder('Search').fill('VMw');
await page.getByRole('listbox').first().getByText('VMware').click({force: true});
await page.keyboard.press('Escape');
// Fill company relation
const companyRelationWidget = page.getByTestId(/dynamic-relation-widget-.+-Company/);
await expect(companyRelationWidget).toBeVisible();
// Open full record page to get person ID
await page.getByRole('button', { name: /^Open/ }).click();
await page.waitForURL(/\/object\/person\//);
const newPersonId = page.url().match(/\/object\/person\/([a-f0-9-]+)/)?.[1];
await companyRelationWidget.hover();
await companyRelationWidget.locator('.tabler-icon-pencil').click();
await page.getByRole('textbox', { name: 'Search' }).fill('VMw');
await expect(page.getByRole('option', { name: 'VMware' })).toBeVisible();
const [updatePersonResponse] = await Promise.all([
page.waitForResponse(async (response) => {
if (!response.url().endsWith('/graphql')) {
return false;
}
const requestBody = response.request().postDataJSON();
return requestBody.operationName === 'UpdateOnePerson';
}),
await page.getByRole('option', { name: 'VMware' }).click({force: true})
]);
const body = await updatePersonResponse.json()
const newPersonId = body.data.updatePerson.id;
// Check data was saved
const { authToken } = await getAccessAuthToken(page);
@@ -149,6 +155,6 @@ test('Create and update record', async ({ page }) => {
expect(findOnePersonReponseBody.data.person.linkedinLink.primaryLinkUrl).toBe('linkedin.com/johndoe');
expect(findOnePersonReponseBody.data.person.phones.primaryPhoneNumber).toBe('611223344');
expect(findOnePersonReponseBody.data.person.workPreference).toEqual(['HYBRID']);
expect(findOnePersonReponseBody.data.person.previousCompanies.edges[0].node.company.name).toBe('VMware');
expect(findOnePersonReponseBody.data.person.company.name).toBe('VMware');
});
-2
View File
@@ -38,7 +38,6 @@
},
"lingui:extract": {
"executor": "nx:run-commands",
"dependsOn": ["^build"],
"options": {
"cwd": "{projectRoot}",
"command": "lingui extract --overwrite --clean"
@@ -46,7 +45,6 @@
},
"lingui:compile": {
"executor": "nx:run-commands",
"dependsOn": ["^build"],
"options": {
"cwd": "{projectRoot}",
"command": "lingui compile --typescript"
-2
View File
@@ -292,7 +292,6 @@
},
"lingui:extract": {
"executor": "nx:run-commands",
"dependsOn": ["^build"],
"options": {
"cwd": "{projectRoot}",
"command": "lingui extract --overwrite --clean"
@@ -300,7 +299,6 @@
},
"lingui:compile": {
"executor": "nx:run-commands",
"dependsOn": ["^build"],
"options": {
"cwd": "{projectRoot}",
"command": "lingui compile --typescript"
@@ -1700,7 +1700,6 @@ export type FeatureFlag = {
export enum FeatureFlagKey {
IS_AI_ENABLED = 'IS_AI_ENABLED',
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED',
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
IS_CONNECTED_ACCOUNT_MIGRATED = 'IS_CONNECTED_ACCOUNT_MIGRATED',
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
@@ -1713,13 +1712,11 @@ export enum FeatureFlagKey {
IS_MARKETPLACE_ENABLED = 'IS_MARKETPLACE_ENABLED',
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED = 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED',
IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED',
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED = 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED',
IS_RICH_TEXT_V1_MIGRATED = 'IS_RICH_TEXT_V1_MIGRATED',
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_USAGE_ANALYTICS_ENABLED = 'IS_USAGE_ANALYTICS_ENABLED'
}
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Nuwe sleutel"
msgid "New Key"
msgstr "Nuwe Sleutel"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Nuwe Wagwoord"
msgid "New record"
msgstr "Nuwe rekord"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "مفتاح جديد"
msgid "New Key"
msgstr "مفتاح جديد"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "كلمة سر جديدة"
msgid "New record"
msgstr "سجل جديد"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Nova clau"
msgid "New Key"
msgstr "Nova Clau"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Nova contrasenya"
msgid "New record"
msgstr "Nou registre"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Nový klíč"
msgid "New Key"
msgstr "Nový klíč"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Nové heslo"
msgid "New record"
msgstr "Nový záznam"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Ny nøgle"
msgid "New Key"
msgstr "Ny Nøgle"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Nyt Password"
msgid "New record"
msgstr "Ny post"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Neuer Schlüssel"
msgid "New Key"
msgstr "Neuer Schlüssel"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Neues Passwort"
msgid "New record"
msgstr "Neuer Datensatz"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Νέο κλειδί"
msgid "New Key"
msgstr "Νέο Κλειδί"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Νέος Κωδικός"
msgid "New record"
msgstr "Νέα εγγραφή"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9479,15 +9479,6 @@ msgstr "New key"
msgid "New Key"
msgstr "New Key"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr "New menu item"
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9514,6 +9505,15 @@ msgstr "New Password"
msgid "New record"
msgstr "New record"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr "New sidebar item"
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Nueva clave"
msgid "New Key"
msgstr "Nueva clave"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Nueva contraseña"
msgid "New record"
msgstr "Nuevo registro"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Uusi avain"
msgid "New Key"
msgstr "Uusi avain"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Uusi salasana"
msgid "New record"
msgstr "Uusi tietue"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Nouvelle clé"
msgid "New Key"
msgstr "Nouvelle clé"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Nouveau mot de passe"
msgid "New record"
msgstr "Nouvel enregistrement"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.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
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "מפתח חדש"
msgid "New Key"
msgstr "מפתח חדש"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "סיסמה חדשה"
msgid "New record"
msgstr "רשומה חדשה"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Új kulcs"
msgid "New Key"
msgstr "Új Kulcs"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Új Jelszó"
msgid "New record"
msgstr "Új rekord"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Nuova chiave"
msgid "New Key"
msgstr "Nuova chiave"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Nuova password"
msgid "New record"
msgstr "Nuovo record"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "新しいキー"
msgid "New Key"
msgstr "新しいキー"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "新しいパスワード"
msgid "New record"
msgstr "新しいレコード"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "새 키"
msgid "New Key"
msgstr "새 키"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "새 비밀번호"
msgid "New record"
msgstr "새 레코드"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Nieuwe sleutel"
msgid "New Key"
msgstr "Nieuwe Sleutel"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Nieuw wachtwoord"
msgid "New record"
msgstr "Nieuw record"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Ny nøkkel"
msgid "New Key"
msgstr "Ny Nøkkel"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Nytt Passord"
msgid "New record"
msgstr "Ny post"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Nowy klucz"
msgid "New Key"
msgstr "Nowy Klucz"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Nowe Hasło"
msgid "New record"
msgstr "Nowy rekord"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
@@ -9479,15 +9479,6 @@ msgstr ""
msgid "New Key"
msgstr ""
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9514,6 +9505,15 @@ msgstr ""
msgid "New record"
msgstr ""
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Nova chave"
msgid "New Key"
msgstr "Nova Chave"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Nova Senha"
msgid "New record"
msgstr "Novo registro"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Nova chave"
msgid "New Key"
msgstr "Nova Chave"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Nova Palavra-passe"
msgid "New record"
msgstr "Novo registo"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Cheie nouă"
msgid "New Key"
msgstr "Cheie nouă"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Parolă nouă"
msgid "New record"
msgstr "Înregistrare nouă"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
Binary file not shown.
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Нови кључ"
msgid "New Key"
msgstr "Нови кључ"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Нова лозинка"
msgid "New record"
msgstr "Нови запис"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9486,15 +9486,6 @@ msgstr "Ny nyckel"
msgid "New Key"
msgstr "Ny Nyckel"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9521,6 +9512,15 @@ msgstr "Nytt Lösenord"
msgid "New record"
msgstr "Ny post"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Yeni anahtar"
msgid "New Key"
msgstr "Yeni Anahtar"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Yeni Parola"
msgid "New record"
msgstr "Yeni kayıt"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Новий ключ"
msgid "New Key"
msgstr "Новий ключ"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Новий пароль"
msgid "New record"
msgstr "Новий запис"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "Khóa mới"
msgid "New Key"
msgstr "Khóa Mới"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "Mật khẩu Mới"
msgid "New record"
msgstr "Bản ghi mới"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "新密钥"
msgid "New Key"
msgstr "新密钥"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "新密码"
msgid "New record"
msgstr "新记录"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
+9 -9
View File
@@ -9484,15 +9484,6 @@ msgstr "新密鑰"
msgid "New Key"
msgstr "新密鑰"
#. js-lingui-id: K2wBR9
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New menu item"
msgstr ""
#. js-lingui-id: ROxeMs
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
#: src/pages/settings/admin-panel/SettingsAdminNewAiModel.tsx
@@ -9519,6 +9510,15 @@ msgstr "新密碼"
msgid "New record"
msgstr "新記錄"
#. js-lingui-id: x6Ckh1
#: src/modules/side-panel/hooks/useSidePanelMenu.ts
#: src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts
#: src/modules/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage.ts
#: src/modules/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton.tsx
#: src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
msgid "New sidebar item"
msgstr ""
#. js-lingui-id: gWnSyg
#: src/pages/settings/ai/SettingsSkillForm.tsx
#: src/pages/settings/ai/SettingsSkillForm.tsx
@@ -3,17 +3,11 @@ import { type ActivityTargetableObject } from '@/activities/types/ActivityTarget
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const useAttachments = (targetableObject: ActivityTargetableObject) => {
const isAttachmentMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
);
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
isMorphRelation: isAttachmentMigrated,
isMorphRelation: true,
});
const { records: attachments, loading } = useFindManyRecords<Attachment>({
@@ -4,12 +4,10 @@ import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivi
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useApolloClient, useMutation } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import {
FeatureFlagKey,
FieldMetadataType,
UploadFilesFieldFileDocument,
} from '~/generated-metadata/graphql';
@@ -19,10 +17,6 @@ export const useUploadAttachmentFile = () => {
const [uploadFilesFieldFile] = useMutation(UploadFilesFieldFileDocument, {
client: apolloClient,
});
const isAttachmentMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
);
const { objectMetadataItem: attachmentMetadata } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Attachment,
});
@@ -58,7 +52,7 @@ export const useUploadAttachmentFile = () => {
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
isMorphRelation: isAttachmentMigrated,
isMorphRelation: true,
});
const attachmentToCreate = {
@@ -12,9 +12,6 @@ import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMeta
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const useActivityTargetsForTargetableObjects = ({
objectNameSingular,
@@ -37,22 +34,12 @@ export const useActivityTargetsForTargetableObjects = ({
const objectMetadataItems = useAtomStateValue<EnrichedObjectMetadataItem[]>(
objectMetadataItemsSelector,
);
const isNoteTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
);
const isTaskTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
);
const isMorphRelation =
objectNameSingular === CoreObjectNameSingular.Task
? isTaskTargetMigrated
: isNoteTargetMigrated;
const activityTargetsFilter = getActivityTargetsFilter({
targetableObjects: targetableObjects,
activityObjectNameSingular: objectNameSingular,
objectMetadataItems,
isMorphRelation,
isMorphRelation: true,
});
const FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE =
@@ -13,9 +13,7 @@ import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hook
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { sortByAscString } from '~/utils/array/sortByAscString';
export const usePrepareFindManyActivitiesQuery = ({
@@ -35,12 +33,6 @@ export const usePrepareFindManyActivitiesQuery = ({
const cache = useApolloCoreClient().cache;
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const isNoteTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
);
const isTaskTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
);
const { upsertFindManyRecordsQueryInCache: upsertFindManyActivitiesInCache } =
useUpsertFindManyRecordsQueryInCache({
@@ -124,10 +116,7 @@ export const usePrepareFindManyActivitiesQuery = ({
findActivitiesOperationSignatureFactory({
objectNameSingular: activityObjectNameSingular,
objectMetadataItems,
isMorphRelation:
activityObjectNameSingular === CoreObjectNameSingular.Task
? isTaskTargetMigrated
: isNoteTargetMigrated,
isMorphRelation: true,
});
upsertFindManyActivitiesInCache({
@@ -11,12 +11,10 @@ import { searchRecordStoreFamilyState } from '@/object-record/record-picker/mult
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useCallback } from 'react';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
type UpdateActivityTargetFromCellProps = {
recordPickerInstanceId: string;
@@ -54,17 +52,6 @@ export const useUpdateActivityTargetFromCell = ({
? activityObjectNameSingular
: joinObjectNameSingular,
});
const isNoteTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
);
const isTaskTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
);
const isMorphRelation =
activityObjectNameSingular === CoreObjectNameSingular.Task
? isTaskTargetMigrated
: isNoteTargetMigrated;
const store = useStore();
const setRecordStore = useSetAtomFamilyState(
recordStoreFamilyState,
@@ -96,7 +83,7 @@ export const useUpdateActivityTargetFromCell = ({
activityObjectNameSingular,
targetObjectMetadataId: morphItem.objectMetadataId,
objectMetadataItems,
isMorphRelation,
isMorphRelation: true,
});
if (!isDefined(targetFieldName)) {
@@ -211,7 +198,6 @@ export const useUpdateActivityTargetFromCell = ({
activityObjectNameSingular,
createOneActivityTarget,
deleteOneActivityTarget,
isMorphRelation,
setRecordStore,
],
);
@@ -15,18 +15,19 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode';
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
import { preloadNavigationMenuItemDndKit } from '@/navigation-menu-item/display/dnd/preloadNavigationMenuItemDndKit';
import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
import {
type NavigationMenuItemClickParams,
useNavigationMenuItemSectionItems,
} from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
import { WorkspaceSectionContainer } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
import { preloadNavigationMenuItemDndKit } from '@/navigation-menu-item/display/dnd/preloadNavigationMenuItemDndKit';
import { WorkspaceSectionContainer } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
@@ -34,7 +35,6 @@ import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
const StyledRightIconsContainer = styled.div`
align-items: center;
@@ -166,7 +166,7 @@ export const WorkspaceSection = () => {
event?.stopPropagation();
navigateSidePanel({
page: SidePanelPages.NavigationMenuAddItem,
pageTitle: t`New menu item`,
pageTitle: t`New sidebar item`,
pageIcon: IconColumnInsertRight,
resetNavigationStack: true,
});
@@ -32,7 +32,7 @@ export const WorkspaceSectionAddMenuItemButton = () => {
setSelectedNavigationMenuItemInEditMode(null);
navigateSidePanel({
page: SidePanelPages.NavigationMenuAddItem,
pageTitle: t`New menu item`,
pageTitle: t`New sidebar item`,
pageIcon: IconColumnInsertRight,
resetNavigationStack: true,
});
@@ -34,7 +34,7 @@ export const useOpenAddItemToFolderPage = () => {
});
navigateSidePanel({
page: SidePanelPages.NavigationMenuAddItem,
pageTitle: t`New menu item`,
pageTitle: t`New sidebar item`,
pageIcon: IconColumnInsertRight,
resetNavigationStack,
});
@@ -2,15 +2,15 @@ import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconColumnInsertRight } from 'twenty-ui/display';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { type OrganizeActionsProps } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditOrganizeActions';
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/common/states/addMenuItemInsertionContextState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
import { type AddMenuItemInsertionContext } from '@/navigation-menu-item/common/types/AddMenuItemInsertionContext';
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
import { type OrganizeActionsProps } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditOrganizeActions';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { SidePanelPages } from 'twenty-shared/types';
@@ -119,7 +119,7 @@ export const useNavigationMenuItemEditOrganizeActions =
setAddMenuItemInsertionContext(context);
navigateSidePanel({
page: SidePanelPages.NavigationMenuAddItem,
pageTitle: t`New menu item`,
pageTitle: t`New sidebar item`,
pageIcon: IconColumnInsertRight,
resetNavigationStack: true,
});
@@ -23,7 +23,6 @@ import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePush
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { t } from '@lingui/core/macro';
import '@blocknote/core/fonts/inter.css';
import '@blocknote/mantine/style.css';
@@ -32,8 +31,6 @@ import '@blocknote/react/style.css';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import { useDebouncedCallback } from 'use-debounce';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
type RichTextFieldEditorProps = {
recordId: string;
objectNameSingular: string;
@@ -82,13 +79,9 @@ export const RichTextFieldEditor = ({
const focusId = `${recordId}-${fieldName}`;
const isAttachmentMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
);
const attachmentTargetFieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: objectNameSingular,
isMorphRelation: isAttachmentMigrated,
isMorphRelation: true,
});
const { records: attachments } = useFindManyRecords<Attachment>({
@@ -15,7 +15,7 @@ const StyledTr = styled.div<{
div.table-cell,
div.table-cell-0-0 {
&:not(:first-of-type) {
background-color: ${themeCssVariables.accent.quaternary};
background-color: ${themeCssVariables.background.tertiary};
border-bottom: 1px solid ${themeCssVariables.border.color.medium};
border-color: ${themeCssVariables.border.color.medium};
}
@@ -1,7 +1,7 @@
import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { isNonEmptyString } from '@sniptt/guards';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
type AssertFieldWidgetOrThrow = (
widget: PageLayoutWidget,
@@ -18,7 +18,8 @@ export const assertFieldWidgetOrThrow: AssertFieldWidgetOrThrow = (
);
if (
widget.configuration.configurationType !== WidgetConfigurationType.FIELD
!isNonEmptyString(widget.configuration.__typename) ||
widget.configuration.__typename !== 'FieldConfiguration'
) {
throw new Error(
`Expected FieldConfiguration but got ${widget.configuration.__typename}`,
@@ -11,11 +11,9 @@ import { StandaloneRichTextEditorContent } from '@/page-layout/widgets/standalon
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { styled } from '@linaria/react';
import { isDefined } from 'twenty-shared/utils';
import {
FeatureFlagKey,
PageLayoutType,
type StandaloneRichTextConfiguration,
} from '~/generated-metadata/graphql';
@@ -47,15 +45,12 @@ export const StandaloneRichTextWidget = ({
);
const { targetRecordIdentifier, layoutType } = useLayoutRenderingContext();
const isAttachmentMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
);
const isDashboard = layoutType === PageLayoutType.DASHBOARD;
const dashboardId = isDashboard ? targetRecordIdentifier?.id : undefined;
const attachmentTargetFieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: CoreObjectNameSingular.Dashboard,
isMorphRelation: isAttachmentMigrated,
isMorphRelation: true,
});
const configuration = widget.configuration as
@@ -1,11 +1,11 @@
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
import { emitSidePanelOpenEvent } from '@/ui/layout/side-panel/utils/emitSidePanelOpenEvent';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
@@ -60,7 +60,7 @@ export const useSidePanelMenu = () => {
} else if (isLayoutCustomizationModeEnabled) {
navigateSidePanel({
page: SidePanelPages.NavigationMenuAddItem,
pageTitle: t`New menu item`,
pageTitle: t`New sidebar item`,
pageIcon: IconColumnInsertRight,
resetNavigationStack: true,
});
@@ -12,7 +12,6 @@ import { styled } from '@linaria/react';
import { useCallback, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { TabButton } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { TabListDropdown } from './TabListDropdown';
import { TabListFromUrlOptionalEffect } from './TabListFromUrlOptionalEffect';
@@ -23,16 +22,6 @@ const StyledContainer = styled.div`
position: relative;
user-select: none;
width: 100%;
&::after {
background-color: ${themeCssVariables.border.color.light};
bottom: 0;
content: '';
height: 1px;
left: 0;
position: absolute;
right: 0;
}
`;
const StyledDropdownContainer = styled.div`
@@ -1544,9 +1544,6 @@ enum FeatureFlagKey {
IS_PUBLIC_DOMAIN_ENABLED
IS_EMAILING_DOMAIN_ENABLED
IS_DASHBOARD_V2_ENABLED
IS_ATTACHMENT_MIGRATED
IS_NOTE_TARGET_MIGRATED
IS_TASK_TARGET_MIGRATED
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED
IS_JUNCTION_RELATIONS_ENABLED
IS_COMMAND_MENU_ITEM_ENABLED
@@ -1256,7 +1256,7 @@ export interface FeatureFlag {
__typename: 'FeatureFlag'
}
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_APPLICATION_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_DASHBOARD_V2_ENABLED' | 'IS_ATTACHMENT_MIGRATED' | 'IS_NOTE_TARGET_MIGRATED' | 'IS_TASK_TARGET_MIGRATED' | 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED'
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_APPLICATION_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_DASHBOARD_V2_ENABLED' | 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED'
export interface SSOIdentityProvider {
id: Scalars['UUID']
@@ -9067,9 +9067,6 @@ export const enumFeatureFlagKey = {
IS_PUBLIC_DOMAIN_ENABLED: 'IS_PUBLIC_DOMAIN_ENABLED' as const,
IS_EMAILING_DOMAIN_ENABLED: 'IS_EMAILING_DOMAIN_ENABLED' as const,
IS_DASHBOARD_V2_ENABLED: 'IS_DASHBOARD_V2_ENABLED' as const,
IS_ATTACHMENT_MIGRATED: 'IS_ATTACHMENT_MIGRATED' as const,
IS_NOTE_TARGET_MIGRATED: 'IS_NOTE_TARGET_MIGRATED' as const,
IS_TASK_TARGET_MIGRATED: 'IS_TASK_TARGET_MIGRATED' as const,
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED: 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' as const,
IS_JUNCTION_RELATIONS_ENABLED: 'IS_JUNCTION_RELATIONS_ENABLED' as const,
IS_COMMAND_MENU_ITEM_ENABLED: 'IS_COMMAND_MENU_ITEM_ENABLED' as const,
-2
View File
@@ -279,7 +279,6 @@
},
"lingui:extract": {
"executor": "nx:run-commands",
"dependsOn": ["^build"],
"options": {
"cwd": "{projectRoot}",
"command": "lingui extract --overwrite --clean"
@@ -287,7 +286,6 @@
},
"lingui:compile": {
"executor": "nx:run-commands",
"dependsOn": ["^build"],
"options": {
"cwd": "{projectRoot}",
"command": "lingui compile --typescript"
@@ -1,343 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import {
type AiSdkPackage,
NATIVE_AI_SDK_PROVIDER_IDS,
} from 'twenty-shared/ai';
import { MODELS_DEV_API_URL } from 'src/engine/metadata-modules/ai/ai-models/constants/models-dev.const';
import { type ModelFamily } from 'src/engine/metadata-modules/ai/ai-models/types/model-family.enum';
import { type ModelsDevData } from 'src/engine/metadata-modules/ai/ai-models/types/models-dev-data.type';
import { inferModelFamily } from 'src/engine/metadata-modules/ai/ai-models/utils/infer-model-family.util';
const EXCLUDED_MODEL_PREFIXES = [
'text-embedding',
'embedding',
'dall-e',
'tts-',
'whisper',
'moderation',
'davinci',
'babbage',
'ada',
'curie',
'text-search',
'text-similarity',
'code-search',
'text-davinci',
'text-curie',
'text-babbage',
'text-ada',
'ft:',
'canary',
];
const EXCLUDED_MODEL_SUFFIXES = ['-audio-preview', '-realtime-preview'];
const LONG_CONTEXT_THRESHOLD_TOKENS = 200000;
const PROVIDER_LABELS: Record<string, string> = {
openai: 'OpenAI',
anthropic: 'Anthropic',
google: 'Google',
mistral: 'Mistral',
xai: 'xAI',
};
const API_KEY_TEMPLATES: Record<string, string> = {
openai: '{{OPENAI_API_KEY}}',
anthropic: '{{ANTHROPIC_API_KEY}}',
google: '{{GOOGLE_API_KEY}}',
mistral: '{{MISTRAL_API_KEY}}',
xai: '{{XAI_API_KEY}}',
};
type LongContextCostEntry = {
inputCostPerMillionTokens: number;
outputCostPerMillionTokens: number;
cachedInputCostPerMillionTokens?: number;
cacheCreationCostPerMillionTokens?: number;
thresholdTokens: number;
};
type GeneratedModel = {
name: string;
label: string;
description?: string;
modelFamily?: ModelFamily;
inputCostPerMillionTokens?: number;
outputCostPerMillionTokens?: number;
cachedInputCostPerMillionTokens?: number;
cacheCreationCostPerMillionTokens?: number;
longContextCost?: LongContextCostEntry;
contextWindowTokens?: number;
maxOutputTokens?: number;
modalities?: string[];
supportsReasoning?: boolean;
isDeprecated?: boolean;
};
type GeneratedProvider = {
npm: AiSdkPackage;
label: string;
apiKey: string;
models: GeneratedModel[];
};
const isLanguageModel = (modelId: string): boolean => {
const lowerId = modelId.toLowerCase();
for (const prefix of EXCLUDED_MODEL_PREFIXES) {
if (lowerId.startsWith(prefix)) return false;
}
for (const suffix of EXCLUDED_MODEL_SUFFIXES) {
if (lowerId.endsWith(suffix)) return false;
}
return true;
};
const meetsInclusionCriteria = (
modelData: Record<string, unknown>,
): boolean => {
if (modelData.status === 'beta') return false;
if (modelData.tool_call !== true) return false;
const cost = modelData.cost as
| { input?: number; output?: number }
| undefined;
if (cost?.input === undefined) return false;
const limit = modelData.limit as
| { context?: number; output?: number }
| undefined;
if (limit?.context === undefined) return false;
return true;
};
const extractCost = (
modelData: Record<string, unknown>,
model: GeneratedModel,
): void => {
const cost = modelData.cost as Record<string, unknown> | undefined;
if (!cost) return;
if (typeof cost.input === 'number') {
model.inputCostPerMillionTokens = cost.input;
}
if (typeof cost.output === 'number') {
model.outputCostPerMillionTokens = cost.output;
}
if (typeof cost.cache_read === 'number') {
model.cachedInputCostPerMillionTokens = cost.cache_read;
}
if (typeof cost.cache_write === 'number') {
model.cacheCreationCostPerMillionTokens = cost.cache_write;
}
const longCtx = cost.context_over_200k as
| Record<string, unknown>
| undefined;
if (longCtx && typeof longCtx.input === 'number') {
model.longContextCost = {
inputCostPerMillionTokens: longCtx.input as number,
outputCostPerMillionTokens: (longCtx.output as number) ?? 0,
thresholdTokens: LONG_CONTEXT_THRESHOLD_TOKENS,
};
if (typeof longCtx.cache_read === 'number') {
model.longContextCost.cachedInputCostPerMillionTokens =
longCtx.cache_read;
}
if (typeof longCtx.cache_write === 'number') {
model.longContextCost.cacheCreationCostPerMillionTokens =
longCtx.cache_write;
}
}
};
const extractLimits = (
modelData: Record<string, unknown>,
model: GeneratedModel,
): void => {
const limit = modelData.limit as Record<string, unknown> | undefined;
if (!limit) return;
if (typeof limit.context === 'number') {
model.contextWindowTokens = limit.context;
}
if (typeof limit.output === 'number') {
model.maxOutputTokens = limit.output;
}
};
const extractModalities = (
modelData: Record<string, unknown>,
model: GeneratedModel,
): void => {
const modalities = modelData.modalities as { input?: string[] } | undefined;
if (!modalities?.input) return;
const relevant = modalities.input.filter((modality) => modality !== 'text');
if (relevant.length > 0) {
model.modalities = relevant;
}
};
const buildModelsForProvider = (
providerName: string,
modelsDevModels: Record<string, { name: string } & Record<string, unknown>>,
): GeneratedModel[] => {
const qualifying: GeneratedModel[] = [];
for (const [modelId, modelData] of Object.entries(modelsDevModels)) {
if (!isLanguageModel(modelId)) continue;
if (!meetsInclusionCriteria(modelData)) continue;
const family = inferModelFamily(providerName, modelId);
const model: GeneratedModel = {
name: modelId,
label: modelData.name ?? modelId,
modelFamily: family,
};
extractCost(modelData, model);
extractLimits(modelData, model);
extractModalities(modelData, model);
if (modelData.reasoning === true) {
model.supportsReasoning = true;
}
if (modelData.status === 'deprecated') {
model.isDeprecated = true;
}
qualifying.push(model);
}
return qualifying;
};
const generateCatalog = (
data: ModelsDevData,
): Record<string, GeneratedProvider> => {
const result: Record<string, GeneratedProvider> = {};
for (const providerName of NATIVE_AI_SDK_PROVIDER_IDS) {
const providerData = data[providerName];
if (!providerData) {
// oxlint-disable-next-line no-console
console.warn(`Provider "${providerName}" not found in models.dev`);
continue;
}
const models = buildModelsForProvider(providerName, providerData.models);
if (models.length === 0) {
// oxlint-disable-next-line no-console
console.warn(`No qualifying models for "${providerName}", skipping`);
continue;
}
result[providerName] = {
npm: `@ai-sdk/${providerName}`,
label: PROVIDER_LABELS[providerName] ?? providerName,
apiKey: API_KEY_TEMPLATES[providerName] ?? '',
models,
};
}
return result;
};
const printSummary = (catalog: Record<string, GeneratedProvider>): void => {
// oxlint-disable-next-line no-console
console.log('=== Generation Summary ===');
let totalModels = 0;
let deprecatedCount = 0;
for (const [providerName, provider] of Object.entries(catalog)) {
const deprecatedModelCount = provider.models.filter(
(model) => model.isDeprecated,
).length;
const active = provider.models.length - deprecatedModelCount;
// oxlint-disable-next-line no-console
console.log(
` ${providerName}: ${provider.models.length} models (${active} active, ${deprecatedModelCount} deprecated)`,
);
totalModels += provider.models.length;
deprecatedCount += deprecatedModelCount;
}
// oxlint-disable-next-line no-console
console.log(
`Total: ${totalModels} models (${totalModels - deprecatedCount} active, ${deprecatedCount} deprecated)`,
);
};
const main = async (): Promise<void> => {
const dryRun = process.argv.includes('--dry-run');
// oxlint-disable-next-line no-console
console.log('Fetching models.dev API...');
const response = await fetch(MODELS_DEV_API_URL);
if (!response.ok) {
throw new Error(
`Failed to fetch: ${response.status} ${response.statusText}`,
);
}
const data: ModelsDevData = await response.json();
// oxlint-disable-next-line no-console
console.log(`Fetched ${Object.keys(data).length} providers from models.dev`);
const generated = generateCatalog(data);
const json = JSON.stringify(generated, null, 2) + '\n';
printSummary(generated);
if (dryRun) {
// oxlint-disable-next-line no-console
console.log('[DRY RUN] Would write ai-providers.json');
return;
}
const outputPath = path.resolve(
__dirname,
'..',
'src',
'engine',
'metadata-modules',
'ai',
'ai-models',
'ai-providers.json',
);
fs.writeFileSync(outputPath, json, 'utf-8');
// oxlint-disable-next-line no-console
console.log(`Wrote ${outputPath}`);
};
main().catch((error) => {
// oxlint-disable-next-line no-console
console.error('AI catalog sync failed:', error);
process.exit(1);
});
@@ -0,0 +1,360 @@
import * as fs from 'fs';
import * as path from 'path';
import { Logger } from '@nestjs/common';
import { Command, CommandRunner, Option } from 'nest-commander';
import {
type AiSdkPackage,
NATIVE_AI_SDK_PROVIDER_IDS,
} from 'twenty-shared/ai';
import { MODELS_DEV_API_URL } from 'src/engine/metadata-modules/ai/ai-models/constants/models-dev.const';
import { type ModelFamily } from 'src/engine/metadata-modules/ai/ai-models/types/model-family.enum';
import { type ModelsDevData } from 'src/engine/metadata-modules/ai/ai-models/types/models-dev-data.type';
import { inferModelFamily } from 'src/engine/metadata-modules/ai/ai-models/utils/infer-model-family.util';
const EXCLUDED_MODEL_PREFIXES = [
'text-embedding',
'embedding',
'dall-e',
'tts-',
'whisper',
'moderation',
'davinci',
'babbage',
'ada',
'curie',
'text-search',
'text-similarity',
'code-search',
'text-davinci',
'text-curie',
'text-babbage',
'text-ada',
'ft:',
'canary',
];
const EXCLUDED_MODEL_SUFFIXES = ['-audio-preview', '-realtime-preview'];
const LONG_CONTEXT_THRESHOLD_TOKENS = 200000;
type ProviderLabels = Record<string, string>;
const PROVIDER_LABELS: ProviderLabels = {
openai: 'OpenAI',
anthropic: 'Anthropic',
google: 'Google',
mistral: 'Mistral',
xai: 'xAI',
};
const API_KEY_TEMPLATES: Record<string, string> = {
openai: '{{OPENAI_API_KEY}}',
anthropic: '{{ANTHROPIC_API_KEY}}',
google: '{{GOOGLE_API_KEY}}',
mistral: '{{MISTRAL_API_KEY}}',
xai: '{{XAI_API_KEY}}',
};
type LongContextCostEntry = {
inputCostPerMillionTokens: number;
outputCostPerMillionTokens: number;
cachedInputCostPerMillionTokens?: number;
cacheCreationCostPerMillionTokens?: number;
thresholdTokens: number;
};
type GeneratedModel = {
name: string;
label: string;
description?: string;
modelFamily?: ModelFamily;
inputCostPerMillionTokens?: number;
outputCostPerMillionTokens?: number;
cachedInputCostPerMillionTokens?: number;
cacheCreationCostPerMillionTokens?: number;
longContextCost?: LongContextCostEntry;
contextWindowTokens?: number;
maxOutputTokens?: number;
modalities?: string[];
supportsReasoning?: boolean;
isDeprecated?: boolean;
};
type GeneratedProvider = {
npm: AiSdkPackage;
label: string;
apiKey: string;
models: GeneratedModel[];
};
type CommandOptions = { dryRun?: boolean };
@Command({
name: 'ai:sync-models-dev',
description:
'Generate ai-providers.json from models.dev API data with objective inclusion/deprecation criteria',
})
export class AiSyncModelsDevCommand extends CommandRunner {
private readonly logger = new Logger(AiSyncModelsDevCommand.name);
@Option({
flags: '-d, --dry-run',
description: 'Print what would change without writing ai-providers.json',
required: false,
})
parseDryRun(): boolean {
return true;
}
async run(_args: string[], options?: CommandOptions): Promise<void> {
const dryRun = options?.dryRun ?? false;
this.logger.log('Fetching models.dev API...');
const response = await fetch(MODELS_DEV_API_URL);
if (!response.ok) {
this.logger.error(
`Failed to fetch: ${response.status} ${response.statusText}`,
);
return;
}
const data: ModelsDevData = await response.json();
this.logger.log(
`Fetched ${Object.keys(data).length} providers from models.dev`,
);
const generated = this.generateCatalog(data);
const json = JSON.stringify(generated, null, 2) + '\n';
this.printSummary(generated);
if (dryRun) {
this.logger.log('[DRY RUN] Would write ai-providers.json');
return;
}
const outputPath = path.resolve(
process.cwd(),
'src',
'engine',
'metadata-modules',
'ai',
'ai-models',
'ai-providers.json',
);
fs.writeFileSync(outputPath, json, 'utf-8');
this.logger.log(`Wrote ${outputPath}`);
}
private generateCatalog(
data: ModelsDevData,
): Record<string, GeneratedProvider> {
const result: Record<string, GeneratedProvider> = {};
for (const providerName of NATIVE_AI_SDK_PROVIDER_IDS) {
const providerData = data[providerName];
if (!providerData) {
this.logger.warn(`Provider "${providerName}" not found in models.dev`);
continue;
}
const models = this.buildModelsForProvider(
providerName,
providerData.models,
);
if (models.length === 0) {
this.logger.warn(
`No qualifying models for "${providerName}", skipping`,
);
continue;
}
result[providerName] = {
npm: `@ai-sdk/${providerName}`,
label: PROVIDER_LABELS[providerName] ?? providerName,
apiKey: API_KEY_TEMPLATES[providerName] ?? '',
models,
};
}
return result;
}
private buildModelsForProvider(
providerName: string,
modelsDevModels: Record<string, { name: string } & Record<string, unknown>>,
): GeneratedModel[] {
const qualifying: GeneratedModel[] = [];
for (const [modelId, modelData] of Object.entries(modelsDevModels)) {
if (!this.isLanguageModel(modelId)) continue;
if (!this.meetsInclusionCriteria(modelData)) continue;
const family = inferModelFamily(providerName, modelId);
const model: GeneratedModel = {
name: modelId,
label: modelData.name ?? modelId,
modelFamily: family,
};
this.extractCost(modelData, model);
this.extractLimits(modelData, model);
this.extractModalities(modelData, model);
if (modelData.reasoning === true) {
model.supportsReasoning = true;
}
if (modelData.status === 'deprecated') {
model.isDeprecated = true;
}
qualifying.push(model);
}
return qualifying;
}
private extractCost(
modelData: Record<string, unknown>,
model: GeneratedModel,
): void {
const cost = modelData.cost as Record<string, unknown> | undefined;
if (!cost) return;
if (typeof cost.input === 'number') {
model.inputCostPerMillionTokens = cost.input;
}
if (typeof cost.output === 'number') {
model.outputCostPerMillionTokens = cost.output;
}
if (typeof cost.cache_read === 'number') {
model.cachedInputCostPerMillionTokens = cost.cache_read;
}
if (typeof cost.cache_write === 'number') {
model.cacheCreationCostPerMillionTokens = cost.cache_write;
}
const longCtx = cost.context_over_200k as
| Record<string, unknown>
| undefined;
if (longCtx && typeof longCtx.input === 'number') {
model.longContextCost = {
inputCostPerMillionTokens: longCtx.input as number,
outputCostPerMillionTokens: (longCtx.output as number) ?? 0,
thresholdTokens: LONG_CONTEXT_THRESHOLD_TOKENS,
};
if (typeof longCtx.cache_read === 'number') {
model.longContextCost.cachedInputCostPerMillionTokens =
longCtx.cache_read;
}
if (typeof longCtx.cache_write === 'number') {
model.longContextCost.cacheCreationCostPerMillionTokens =
longCtx.cache_write;
}
}
}
private extractLimits(
modelData: Record<string, unknown>,
model: GeneratedModel,
): void {
const limit = modelData.limit as Record<string, unknown> | undefined;
if (!limit) return;
if (typeof limit.context === 'number') {
model.contextWindowTokens = limit.context;
}
if (typeof limit.output === 'number') {
model.maxOutputTokens = limit.output;
}
}
private extractModalities(
modelData: Record<string, unknown>,
model: GeneratedModel,
): void {
const modalities = modelData.modalities as { input?: string[] } | undefined;
if (!modalities?.input) return;
const relevant = modalities.input.filter((modality) => modality !== 'text');
if (relevant.length > 0) {
model.modalities = relevant;
}
}
private isLanguageModel(modelId: string): boolean {
const lowerId = modelId.toLowerCase();
for (const prefix of EXCLUDED_MODEL_PREFIXES) {
if (lowerId.startsWith(prefix)) return false;
}
for (const suffix of EXCLUDED_MODEL_SUFFIXES) {
if (lowerId.endsWith(suffix)) return false;
}
return true;
}
private meetsInclusionCriteria(modelData: Record<string, unknown>): boolean {
if (modelData.status === 'beta') return false;
if (modelData.tool_call !== true) return false;
const cost = modelData.cost as
| { input?: number; output?: number }
| undefined;
if (cost?.input === undefined) return false;
const limit = modelData.limit as
| { context?: number; output?: number }
| undefined;
if (limit?.context === undefined) return false;
return true;
}
private printSummary(catalog: Record<string, GeneratedProvider>): void {
this.logger.log('=== Generation Summary ===');
let totalModels = 0;
let deprecatedCount = 0;
for (const [providerName, provider] of Object.entries(catalog)) {
const deprecatedModelCount = provider.models.filter(
(model) => model.isDeprecated,
).length;
const active = provider.models.length - deprecatedModelCount;
this.logger.log(
` ${providerName}: ${provider.models.length} models (${active} active, ${deprecatedModelCount} deprecated)`,
);
totalModels += provider.models.length;
deprecatedCount += deprecatedModelCount;
}
this.logger.log(
`Total: ${totalModels} models (${totalModels - deprecatedCount} active, ${deprecatedCount} deprecated)`,
);
}
}
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AiSyncModelsDevCommand } from 'src/database/commands/ai-sync-models-dev.command';
import { CronRegisterAllCommand } from 'src/database/commands/cron-register-all.command';
import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
import { ListOrphanedWorkspaceEntitiesCommand } from 'src/database/commands/list-and-delete-orphaned-workspace-entities.command';
@@ -70,6 +71,7 @@ import { AutomatedTriggerModule } from 'src/modules/workflow/workflow-trigger/au
StaleRegistrationCleanupModule,
],
providers: [
AiSyncModelsDevCommand,
DataSeedWorkspaceCommand,
ConfirmationQuestion,
CronRegisterAllCommand,
@@ -3,7 +3,6 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import {
FeatureFlagKey,
FieldMetadataType,
type FieldMetadataSettings,
} from 'twenty-shared/types';
@@ -14,7 +13,6 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
@@ -39,13 +37,12 @@ type RelationFieldMetadataSettings =
@Command({
name: 'upgrade:1-17:migrate-attachment-to-morph-relations',
description:
'Migrate attachment relations to morph relation fields and set feature flag',
'Migrate attachment relations to morph relation fields (idempotent)',
})
export class MigrateAttachmentToMorphRelationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly featureFlagService: FeatureFlagService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
@@ -61,19 +58,8 @@ export class MigrateAttachmentToMorphRelationsCommand extends ActiveOrSuspendedW
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
workspaceId,
);
this.logger.log(`Migrating attachments for workspace ${workspaceId}`);
if (isMigrated) {
this.logger.log(`Attachment migration already completed. Skipping...`);
return;
}
if (options.dryRun) {
this.logger.log(
`Would have migrated attachments for workspace ${workspaceId}. Skipping...`,
@@ -82,101 +68,107 @@ export class MigrateAttachmentToMorphRelationsCommand extends ActiveOrSuspendedW
return;
}
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'attachment';
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatApplicationMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatApplicationMaps',
]);
const attachmentTargetFieldUniversalIdentifiers = new Set<string>([
STANDARD_OBJECTS.attachment.fields.targetTask.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetNote.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetOpportunity.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetDashboard.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetWorkflow.universalIdentifier,
]);
const attachmentObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECTS.attachment.universalIdentifier,
});
if (!attachmentObjectMetadata) {
this.logger.error(
`🟥 Attachment object metadata not found for workspace ${workspaceId}`,
);
return;
}
const attachmentFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
attachmentObjectMetadata,
flatFieldMetadataMaps,
);
const attachmentTargetRelationFields = attachmentFieldMetadatas
.filter(isMorphOrRelationFlatFieldMetadata)
.filter(
(field): field is FlatFieldMetadata<FieldMetadataType.RELATION> =>
field.type === FieldMetadataType.RELATION,
)
.filter((field) => {
const isStandardAppField = this.isTwentyStandardApplicationField({
field,
flatApplicationMaps,
workspaceId,
});
const isStandardTarget =
isStandardAppField &&
attachmentTargetFieldUniversalIdentifiers.has(
field.universalIdentifier,
);
const targetObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: field.relationTargetObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
const isCustomTarget =
!isStandardAppField && targetObjectMetadata?.isCustom === true;
return isStandardTarget || isCustomTarget;
});
const fieldMigrations = attachmentTargetRelationFields.map((field) => {
const newFieldName = `target${capitalize(field.name)}`;
const relationSettings: RelationFieldMetadataSettings = field.settings;
const oldJoinColumnName =
relationSettings?.joinColumnName ??
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: newFieldName,
});
return {
field,
newFieldName,
oldJoinColumnName,
newJoinColumnName,
};
});
if (fieldMigrations.length === 0) {
this.logger.log(
`Attachment morph migration already applied or nothing to migrate. Skipping workspace ${workspaceId}...`,
);
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'attachment';
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatApplicationMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatApplicationMaps',
]);
const attachmentTargetFieldUniversalIdentifiers = new Set<string>([
STANDARD_OBJECTS.attachment.fields.targetTask.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetNote.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetOpportunity
.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetDashboard.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetWorkflow.universalIdentifier,
]);
const attachmentObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECTS.attachment.universalIdentifier,
});
if (!attachmentObjectMetadata) {
this.logger.error(
`🟥 Attachment object metadata not found for workspace ${workspaceId}`,
);
return;
}
const attachmentFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
attachmentObjectMetadata,
flatFieldMetadataMaps,
);
const attachmentTargetRelationFields = attachmentFieldMetadatas
.filter(isMorphOrRelationFlatFieldMetadata)
.filter(
(field): field is FlatFieldMetadata<FieldMetadataType.RELATION> =>
field.type === FieldMetadataType.RELATION,
)
.filter((field) => {
const isStandardAppField = this.isTwentyStandardApplicationField({
field,
flatApplicationMaps,
workspaceId,
});
const isStandardTarget =
isStandardAppField &&
attachmentTargetFieldUniversalIdentifiers.has(
field.universalIdentifier,
);
const targetObjectMetadata =
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: field.relationTargetObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
const isCustomTarget =
!isStandardAppField && targetObjectMetadata?.isCustom === true;
return isStandardTarget || isCustomTarget;
});
const fieldMigrations = attachmentTargetRelationFields.map((field) => {
const newFieldName = `target${capitalize(field.name)}`;
const relationSettings: RelationFieldMetadataSettings = field.settings;
const oldJoinColumnName =
relationSettings?.joinColumnName ??
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: newFieldName,
});
return {
field,
newFieldName,
oldJoinColumnName,
newJoinColumnName,
};
});
for (const { oldJoinColumnName, newJoinColumnName } of fieldMigrations) {
if (oldJoinColumnName === newJoinColumnName) {
this.logger.log(
@@ -253,11 +245,6 @@ export class MigrateAttachmentToMorphRelationsCommand extends ActiveOrSuspendedW
await queryRunner.commitTransaction();
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_ATTACHMENT_MIGRATED],
workspaceId,
);
const relatedMetadataNames =
getMetadataRelatedMetadataNames('fieldMetadata');
const relatedCacheKeysToInvalidate: WorkspaceCacheKeyName[] =
@@ -265,7 +252,6 @@ export class MigrateAttachmentToMorphRelationsCommand extends ActiveOrSuspendedW
const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [
'flatFieldMetadataMaps',
...relatedCacheKeysToInvalidate,
'featureFlagsMap',
];
this.logger.log(
@@ -282,10 +268,6 @@ export class MigrateAttachmentToMorphRelationsCommand extends ActiveOrSuspendedW
);
this.logger.log(`Cache flushed`);
this.logger.log(
`Set IS_ATTACHMENT_MIGRATED feature flag for workspace ${workspaceId}`,
);
this.logger.log(`Flush cache for workspace ${workspaceId}`);
await this.workspaceCacheStorageService.flush(workspaceId);
} catch (error) {
@@ -3,7 +3,6 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import {
FeatureFlagKey,
FieldMetadataType,
type FieldMetadataSettings,
} from 'twenty-shared/types';
@@ -14,7 +13,6 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
@@ -38,13 +36,12 @@ type RelationFieldMetadataSettings =
@Command({
name: 'upgrade:1-17:migrate-note-target-to-morph-relations',
description:
'Migrate noteTarget relations to morph relation fields and set feature flag',
'Migrate noteTarget relations to morph relation fields (idempotent)',
})
export class MigrateNoteTargetToMorphRelationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly featureFlagService: FeatureFlagService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
@@ -60,19 +57,8 @@ export class MigrateNoteTargetToMorphRelationsCommand extends ActiveOrSuspendedW
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
workspaceId,
);
this.logger.log(`Migrating noteTarget for workspace ${workspaceId}`);
if (isMigrated) {
this.logger.log(`NoteTarget migration already completed. Skipping...`);
return;
}
if (options.dryRun) {
this.logger.log(
`Would have migrated noteTarget for workspace ${workspaceId}. Skipping...`,
@@ -81,95 +67,102 @@ export class MigrateNoteTargetToMorphRelationsCommand extends ActiveOrSuspendedW
return;
}
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'noteTarget';
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatApplicationMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatApplicationMaps',
]);
const noteTargetFieldUniversalIdentifiers = new Set<string>([
STANDARD_OBJECTS.noteTarget.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.noteTarget.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.noteTarget.fields.targetOpportunity.universalIdentifier,
]);
const noteTargetObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECTS.noteTarget.universalIdentifier,
});
if (!noteTargetObjectMetadata) {
this.logger.error(
`🟥 ${tableName} object metadata not found for workspace ${workspaceId}`,
);
return;
}
const noteTargetFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
noteTargetObjectMetadata,
flatFieldMetadataMaps,
);
const noteTargetRelationFields = noteTargetFieldMetadatas
.filter(isMorphOrRelationFlatFieldMetadata)
.filter((field) => field.type === FieldMetadataType.RELATION)
.filter((field) => {
const isStandardAppField = this.isTwentyStandardApplicationField({
field,
flatApplicationMaps,
workspaceId,
});
const isStandardTarget =
isStandardAppField &&
noteTargetFieldUniversalIdentifiers.has(field.universalIdentifier);
const targetObjectMetadata = field.relationTargetObjectMetadataId
? findFlatEntityByIdInFlatEntityMaps({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: field.relationTargetObjectMetadataId,
})
: undefined;
const isCustomTarget =
!isStandardAppField && targetObjectMetadata?.isCustom === true;
return isStandardTarget || isCustomTarget;
});
const fieldMigrations = noteTargetRelationFields.map((field) => {
const newFieldName = `target${capitalize(field.name)}`;
const newFieldLabel = 'Target';
const relationSettings: RelationFieldMetadataSettings = field.settings;
const oldJoinColumnName =
relationSettings?.joinColumnName ??
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: newFieldName,
});
return {
field,
newFieldName,
newFieldLabel,
oldJoinColumnName,
newJoinColumnName,
};
});
if (fieldMigrations.length === 0) {
this.logger.log(
`NoteTarget morph migration already applied or nothing to migrate. Skipping workspace ${workspaceId}...`,
);
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'noteTarget';
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatApplicationMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatApplicationMaps',
]);
const noteTargetFieldUniversalIdentifiers = new Set<string>([
STANDARD_OBJECTS.noteTarget.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.noteTarget.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.noteTarget.fields.targetOpportunity
.universalIdentifier,
]);
const noteTargetObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECTS.noteTarget.universalIdentifier,
});
if (!noteTargetObjectMetadata) {
this.logger.error(
`🟥 ${tableName} object metadata not found for workspace ${workspaceId}`,
);
return;
}
const noteTargetFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
noteTargetObjectMetadata,
flatFieldMetadataMaps,
);
const noteTargetRelationFields = noteTargetFieldMetadatas
.filter(isMorphOrRelationFlatFieldMetadata)
.filter((field) => field.type === FieldMetadataType.RELATION)
.filter((field) => {
const isStandardAppField = this.isTwentyStandardApplicationField({
field,
flatApplicationMaps,
workspaceId,
});
const isStandardTarget =
isStandardAppField &&
noteTargetFieldUniversalIdentifiers.has(field.universalIdentifier);
const targetObjectMetadata = field.relationTargetObjectMetadataId
? findFlatEntityByIdInFlatEntityMaps({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: field.relationTargetObjectMetadataId,
})
: undefined;
const isCustomTarget =
!isStandardAppField && targetObjectMetadata?.isCustom === true;
return isStandardTarget || isCustomTarget;
});
const fieldMigrations = noteTargetRelationFields.map((field) => {
const newFieldName = `target${capitalize(field.name)}`;
const newFieldLabel = 'Target';
const relationSettings: RelationFieldMetadataSettings = field.settings;
const oldJoinColumnName =
relationSettings?.joinColumnName ??
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: newFieldName,
});
return {
field,
newFieldName,
newFieldLabel,
oldJoinColumnName,
newJoinColumnName,
};
});
// Rename columns
for (const { oldJoinColumnName, newJoinColumnName } of fieldMigrations) {
if (oldJoinColumnName === newJoinColumnName) {
@@ -250,11 +243,6 @@ export class MigrateNoteTargetToMorphRelationsCommand extends ActiveOrSuspendedW
await queryRunner.commitTransaction();
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_NOTE_TARGET_MIGRATED],
workspaceId,
);
const relatedMetadataNames =
getMetadataRelatedMetadataNames('fieldMetadata');
const relatedCacheKeysToInvalidate: WorkspaceCacheKeyName[] =
@@ -262,7 +250,6 @@ export class MigrateNoteTargetToMorphRelationsCommand extends ActiveOrSuspendedW
const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [
'flatFieldMetadataMaps',
...relatedCacheKeysToInvalidate,
'featureFlagsMap',
];
this.logger.log(
@@ -279,10 +266,6 @@ export class MigrateNoteTargetToMorphRelationsCommand extends ActiveOrSuspendedW
);
this.logger.log(`Cache flushed`);
this.logger.log(
`Set IS_NOTE_TARGET_MIGRATED feature flag for workspace ${workspaceId}`,
);
this.logger.log(`Flush cache for workspace ${workspaceId}`);
await this.workspaceCacheStorageService.flush(workspaceId);
} catch (error) {
@@ -3,7 +3,6 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import {
FeatureFlagKey,
FieldMetadataType,
type FieldMetadataSettings,
} from 'twenty-shared/types';
@@ -14,7 +13,6 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
@@ -38,13 +36,12 @@ type RelationFieldMetadataSettings =
@Command({
name: 'upgrade:1-17:migrate-task-target-to-morph-relations',
description:
'Migrate taskTarget relations to morph relation fields and set feature flag',
'Migrate taskTarget relations to morph relation fields (idempotent)',
})
export class MigrateTaskTargetToMorphRelationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly featureFlagService: FeatureFlagService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
@@ -60,19 +57,8 @@ export class MigrateTaskTargetToMorphRelationsCommand extends ActiveOrSuspendedW
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
workspaceId,
);
this.logger.log(`Migrating taskTarget for workspace ${workspaceId}`);
if (isMigrated) {
this.logger.log(`TaskTarget migration already completed. Skipping...`);
return;
}
if (options.dryRun) {
this.logger.log(
`Would have migrated taskTarget for workspace ${workspaceId}. Skipping...`,
@@ -81,95 +67,102 @@ export class MigrateTaskTargetToMorphRelationsCommand extends ActiveOrSuspendedW
return;
}
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'taskTarget';
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatApplicationMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatApplicationMaps',
]);
const taskTargetFieldUniversalIdentifiers = new Set<string>([
STANDARD_OBJECTS.taskTarget.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.taskTarget.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.taskTarget.fields.targetOpportunity.universalIdentifier,
]);
const taskTargetObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECTS.taskTarget.universalIdentifier,
});
if (!taskTargetObjectMetadata) {
this.logger.error(
`🟥 ${tableName} object metadata not found for workspace ${workspaceId}`,
);
return;
}
const taskTargetFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
taskTargetObjectMetadata,
flatFieldMetadataMaps,
);
const taskTargetRelationFields = taskTargetFieldMetadatas
.filter(isMorphOrRelationFlatFieldMetadata)
.filter((field) => field.type === FieldMetadataType.RELATION)
.filter((field) => {
const isStandardAppField = this.isTwentyStandardApplicationField({
field,
flatApplicationMaps,
workspaceId,
});
const isStandardTarget =
isStandardAppField &&
taskTargetFieldUniversalIdentifiers.has(field.universalIdentifier);
const targetObjectMetadata = field.relationTargetObjectMetadataId
? findFlatEntityByIdInFlatEntityMaps({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: field.relationTargetObjectMetadataId,
})
: undefined;
const isCustomTarget =
!isStandardAppField && targetObjectMetadata?.isCustom === true;
return isStandardTarget || isCustomTarget;
});
const fieldMigrations = taskTargetRelationFields.map((field) => {
const newFieldName = `target${capitalize(field.name)}`;
const newFieldLabel = 'Target';
const relationSettings: RelationFieldMetadataSettings = field.settings;
const oldJoinColumnName =
relationSettings?.joinColumnName ??
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: newFieldName,
});
return {
field,
newFieldName,
newFieldLabel,
oldJoinColumnName,
newJoinColumnName,
};
});
if (fieldMigrations.length === 0) {
this.logger.log(
`TaskTarget morph migration already applied or nothing to migrate. Skipping workspace ${workspaceId}...`,
);
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'taskTarget';
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatApplicationMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatApplicationMaps',
]);
const taskTargetFieldUniversalIdentifiers = new Set<string>([
STANDARD_OBJECTS.taskTarget.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.taskTarget.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.taskTarget.fields.targetOpportunity
.universalIdentifier,
]);
const taskTargetObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECTS.taskTarget.universalIdentifier,
});
if (!taskTargetObjectMetadata) {
this.logger.error(
`🟥 ${tableName} object metadata not found for workspace ${workspaceId}`,
);
return;
}
const taskTargetFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
taskTargetObjectMetadata,
flatFieldMetadataMaps,
);
const taskTargetRelationFields = taskTargetFieldMetadatas
.filter(isMorphOrRelationFlatFieldMetadata)
.filter((field) => field.type === FieldMetadataType.RELATION)
.filter((field) => {
const isStandardAppField = this.isTwentyStandardApplicationField({
field,
flatApplicationMaps,
workspaceId,
});
const isStandardTarget =
isStandardAppField &&
taskTargetFieldUniversalIdentifiers.has(field.universalIdentifier);
const targetObjectMetadata = field.relationTargetObjectMetadataId
? findFlatEntityByIdInFlatEntityMaps({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: field.relationTargetObjectMetadataId,
})
: undefined;
const isCustomTarget =
!isStandardAppField && targetObjectMetadata?.isCustom === true;
return isStandardTarget || isCustomTarget;
});
const fieldMigrations = taskTargetRelationFields.map((field) => {
const newFieldName = `target${capitalize(field.name)}`;
const newFieldLabel = 'Target';
const relationSettings: RelationFieldMetadataSettings = field.settings;
const oldJoinColumnName =
relationSettings?.joinColumnName ??
computeMorphOrRelationFieldJoinColumnName({ name: field.name });
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: newFieldName,
});
return {
field,
newFieldName,
newFieldLabel,
oldJoinColumnName,
newJoinColumnName,
};
});
// Rename columns
for (const { oldJoinColumnName, newJoinColumnName } of fieldMigrations) {
if (oldJoinColumnName === newJoinColumnName) {
@@ -250,11 +243,6 @@ export class MigrateTaskTargetToMorphRelationsCommand extends ActiveOrSuspendedW
await queryRunner.commitTransaction();
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_TASK_TARGET_MIGRATED],
workspaceId,
);
const relatedMetadataNames =
getMetadataRelatedMetadataNames('fieldMetadata');
const relatedCacheKeysToInvalidate: WorkspaceCacheKeyName[] =
@@ -262,7 +250,6 @@ export class MigrateTaskTargetToMorphRelationsCommand extends ActiveOrSuspendedW
const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [
'flatFieldMetadataMaps',
...relatedCacheKeysToInvalidate,
'featureFlagsMap',
];
this.logger.log(
@@ -279,10 +266,6 @@ export class MigrateTaskTargetToMorphRelationsCommand extends ActiveOrSuspendedW
);
this.logger.log(`Cache flushed`);
this.logger.log(
`Set IS_TASK_TARGET_MIGRATED feature flag for workspace ${workspaceId}`,
);
this.logger.log(`Flush cache for workspace ${workspaceId}`);
await this.workspaceCacheStorageService.flush(workspaceId);
} catch (error) {
@@ -1,40 +1,25 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import { Command } from 'nest-commander';
import { FeatureFlagKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { DataSource, type QueryRunner, Repository } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import { Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import {
WorkflowVersionStatus,
type WorkflowVersionWorkspaceEntity,
} from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { type WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import {
type WorkflowManualTrigger,
WorkflowTriggerType,
} from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
@Command({
name: 'upgrade:1-20:backfill-command-menu-items',
description:
'Backfill missing standard and workflow command menu items for existing workspaces and enable IS_COMMAND_MENU_ITEM_ENABLED feature flag',
'Backfill missing standard command menu items for existing workspaces and enable IS_COMMAND_MENU_ITEM_ENABLED feature flag',
})
export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@@ -42,13 +27,10 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly applicationService: ApplicationService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
@@ -60,7 +42,7 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
const isDryRun = options.dryRun ?? false;
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of command menu items for workspace ${workspaceId}`,
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of missing standard command menu items for workspace ${workspaceId}`,
);
const isFeatureFlagAlreadyEnabled =
@@ -77,52 +59,6 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await this.backfillStandardCommandMenuItems(
workspaceId,
isDryRun,
queryRunner,
);
await this.backfillWorkflowCommandMenuItems(
workspaceId,
isDryRun,
queryRunner,
);
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Rolling back backfill of command menu items for workspace ${workspaceId}: ${error.message}`,
);
throw error;
} finally {
await queryRunner.release();
}
if (!isDryRun) {
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED],
workspaceId,
);
}
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Completed backfill of command menu items for workspace ${workspaceId}`,
);
}
private async backfillStandardCommandMenuItems(
workspaceId: string,
isDryRun: boolean,
queryRunner: QueryRunner,
): Promise<void> {
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
@@ -154,21 +90,30 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
),
);
if (commandMenuItemsToCreate.length === 0) {
const numberOfCommandMenuItemsToCreate = commandMenuItemsToCreate.length;
if (numberOfCommandMenuItemsToCreate === 0) {
this.logger.log(
`No missing standard command menu items for workspace ${workspaceId}`,
`No missing standard command menu items for workspace ${workspaceId}, enabling feature flag only`,
);
if (!isDryRun) {
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED],
workspaceId,
);
}
return;
}
this.logger.log(
`Found ${commandMenuItemsToCreate.length} missing standard command menu item(s) for workspace ${workspaceId}`,
`Found ${numberOfCommandMenuItemsToCreate} missing standard command menu item(s) for workspace ${workspaceId}`,
);
if (isDryRun) {
this.logger.log(
`[DRY RUN] Would create ${commandMenuItemsToCreate.length} standard command menu item(s) for workspace ${workspaceId}`,
`[DRY RUN] Would create ${numberOfCommandMenuItemsToCreate} command menu item(s) and enable IS_COMMAND_MENU_ITEM_ENABLED for workspace ${workspaceId}`,
);
return;
@@ -187,7 +132,6 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
queryRunner,
},
);
@@ -201,206 +145,13 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
);
}
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED],
workspaceId,
);
this.logger.log(
`Successfully backfilled ${commandMenuItemsToCreate.length} standard command menu item(s) for workspace ${workspaceId}`,
`Successfully backfilled ${commandMenuItemsToCreate.length} standard command menu item(s) and enabled IS_COMMAND_MENU_ITEM_ENABLED for workspace ${workspaceId}`,
);
}
private async backfillWorkflowCommandMenuItems(
workspaceId: string,
isDryRun: boolean,
queryRunner: QueryRunner,
): Promise<void> {
const authContext = buildSystemAuthContext(workspaceId);
await this.twentyORMGlobalManager.executeInWorkspaceContext(async () => {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const activeWorkflowVersions = await workflowVersionRepository.find({
where: { status: WorkflowVersionStatus.ACTIVE },
});
const manualTriggerVersions = activeWorkflowVersions.filter(
(version) =>
isDefined(version.trigger) &&
version.trigger.type === WorkflowTriggerType.MANUAL,
);
if (manualTriggerVersions.length === 0) {
this.logger.log(
`No active workflow versions with manual triggers for workspace ${workspaceId}`,
);
return;
}
const { flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatCommandMenuItemMaps',
]);
const existingWorkflowVersionIds = new Set(
Object.values(existingFlatCommandMenuItemMaps.byUniversalIdentifier)
.filter(isDefined)
.map((item) => item.workflowVersionId)
.filter(isDefined),
);
const workflowRepository =
await this.twentyORMGlobalManager.getRepository<WorkflowWorkspaceEntity>(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const flatCommandMenuItemsToCreate: FlatCommandMenuItem[] = [];
for (const workflowVersion of manualTriggerVersions) {
if (existingWorkflowVersionIds.has(workflowVersion.id)) {
continue;
}
const workflow = await workflowRepository.findOne({
where: { id: workflowVersion.workflowId },
});
const label =
workflow && isNonEmptyString(workflow.name)
? workflow.name
: 'Manual Trigger';
const trigger = workflowVersion.trigger as WorkflowManualTrigger;
const { availabilityType, availabilityObjectMetadataId } =
await this.resolveManualTriggerAvailability(trigger, workspaceId);
const id = uuidv4();
const now = new Date().toISOString();
flatCommandMenuItemsToCreate.push({
id,
universalIdentifier: id,
workflowVersionId: workflowVersion.id,
frontComponentId: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: null,
label,
shortLabel: label,
icon: trigger.settings.icon ?? null,
isPinned: trigger.settings.isPinned ?? false,
position: 0,
hotKeys: null,
availabilityType,
availabilityObjectMetadataId: availabilityObjectMetadataId ?? null,
availabilityObjectMetadataUniversalIdentifier: null,
conditionalAvailabilityExpression: null,
workspaceId,
applicationId: workspaceCustomFlatApplication.id,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
createdAt: now,
updatedAt: now,
});
}
if (flatCommandMenuItemsToCreate.length === 0) {
this.logger.log(
`No missing workflow command menu items for workspace ${workspaceId}`,
);
return;
}
this.logger.log(
`Found ${flatCommandMenuItemsToCreate.length} missing workflow command menu item(s) for workspace ${workspaceId}`,
);
if (isDryRun) {
this.logger.log(
`[DRY RUN] Would create ${flatCommandMenuItemsToCreate.length} workflow command menu item(s) for workspace ${workspaceId}`,
);
return;
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
flatEntityToCreate: flatCommandMenuItemsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
queryRunner,
},
);
if (validateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to backfill workflow command menu items:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to backfill workflow command menu items for workspace ${workspaceId}`,
);
}
this.logger.log(
`Successfully backfilled ${flatCommandMenuItemsToCreate.length} workflow command menu item(s) for workspace ${workspaceId}`,
);
}, authContext);
}
private async resolveManualTriggerAvailability(
trigger: WorkflowManualTrigger,
workspaceId: string,
): Promise<{
availabilityType: CommandMenuItemAvailabilityType;
availabilityObjectMetadataId: string | undefined;
}> {
const availability = trigger.settings.availability;
if (!isDefined(availability) || availability.type === 'GLOBAL') {
return {
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
availabilityObjectMetadataId: undefined,
};
}
const { objectIdByNameSingular } =
await this.workflowCommonWorkspaceService.getFlatEntityMaps(workspaceId);
const objectId = objectIdByNameSingular[availability.objectNameSingular];
if (!isDefined(objectId)) {
this.logger.warn(
`Object metadata not found for "${availability.objectNameSingular}" in workspace ${workspaceId}, falling back to GLOBAL`,
);
return {
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
availabilityObjectMetadataId: undefined,
};
}
return {
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
availabilityObjectMetadataId: objectId,
};
}
}
@@ -1,84 +1,24 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import {
CoreObjectNameSingular,
FieldMetadataType,
PageLayoutTabLayoutMode,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { FieldDisplayMode } from 'src/engine/metadata-modules/page-layout-widget/enums/field-display-mode.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import {
GRID_POSITIONS,
VERTICAL_LIST_LAYOUT_POSITIONS,
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
const HOME_TAB_POSITION = 10;
const isActivityTargetField = (
fieldName: string,
objectNameSingular: string,
): boolean =>
(objectNameSingular === CoreObjectNameSingular.Note &&
fieldName === 'noteTargets') ||
(objectNameSingular === CoreObjectNameSingular.Task &&
fieldName === 'taskTargets');
const isJunctionRelationField = (field: FlatFieldMetadata): boolean => {
const settings = field.settings as
| { junctionTargetFieldId?: string }
| undefined;
return (
isDefined(settings?.junctionTargetFieldId) &&
typeof settings?.junctionTargetFieldId === 'string' &&
settings.junctionTargetFieldId.length > 0
);
};
const isRelationTargetAvailable = (
targetObject: FlatObjectMetadata | undefined,
): boolean => {
if (!isDefined(targetObject)) {
return false;
}
if (targetObject.isRemote) {
return false;
}
if (
targetObject.isSystem &&
targetObject.nameSingular !== CoreObjectNameSingular.WorkspaceMember
) {
return false;
}
return true;
};
@Command({
name: 'upgrade:1-20:backfill-field-widgets',
description:
'Backfill FIELD widgets for relation fields in existing page layouts',
'Backfill FIELD widgets for standard object relation fields in existing page layouts',
})
export class BackfillFieldWidgetsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@@ -103,254 +43,50 @@ export class BackfillFieldWidgetsCommand extends ActiveOrSuspendedWorkspacesMigr
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of FIELD widgets for workspace ${workspaceId}`,
);
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatPageLayoutMaps,
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatPageLayoutMaps',
'flatPageLayoutTabMaps',
'flatPageLayoutWidgetMaps',
]);
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
computeTwentyStandardApplicationAllFlatEntityMaps({
shouldIncludeRecordPageLayouts: true,
now: new Date().toISOString(),
workspaceId,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
});
// Build a set of fieldMetadataIds that already have a FIELD widget
const existingFieldWidgetFieldIds = new Set<string>();
const standardFieldWidgets = Object.values(
standardAllFlatEntityMaps.flatPageLayoutWidgetMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(widget) =>
widget.configuration?.configurationType ===
WidgetConfigurationType.FIELD,
);
for (const widget of Object.values(
flatPageLayoutWidgetMaps.byUniversalIdentifier,
)) {
if (
isDefined(widget) &&
widget.configuration?.configurationType ===
WidgetConfigurationType.FIELD &&
isDefined(widget.configuration.fieldMetadataId)
) {
existingFieldWidgetFieldIds.add(widget.configuration.fieldMetadataId);
}
if (standardFieldWidgets.length === 0) {
this.logger.log(
`No FIELD widgets found in standard configs for workspace ${workspaceId}`,
);
return;
}
// Build object ID → objectMetadata map
const objectById = new Map<string, FlatObjectMetadata>();
const { flatPageLayoutWidgetMaps: existingWidgetMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatPageLayoutWidgetMaps',
]);
for (const obj of Object.values(
flatObjectMetadataMaps.byUniversalIdentifier,
)) {
if (isDefined(obj)) {
objectById.set(obj.id, obj);
}
}
const widgetsToCreate = standardFieldWidgets.filter(
(widget) =>
!isDefined(
existingWidgetMaps.byUniversalIdentifier[widget.universalIdentifier],
),
);
// Build object ID → RECORD_PAGE layout map
const recordPageLayoutByObjectId = new Map<
string,
{ id: string; universalIdentifier: string }
>();
for (const layout of Object.values(
flatPageLayoutMaps.byUniversalIdentifier,
)) {
if (
isDefined(layout) &&
layout.type === PageLayoutType.RECORD_PAGE &&
isDefined(layout.objectMetadataId)
) {
recordPageLayoutByObjectId.set(layout.objectMetadataId, {
id: layout.id,
universalIdentifier: layout.universalIdentifier,
});
}
}
// Build pageLayoutId → home tab map
const homeTabByPageLayoutId = new Map<
string,
{
id: string;
universalIdentifier: string;
widgetCount: number;
}
>();
for (const tab of Object.values(
flatPageLayoutTabMaps.byUniversalIdentifier,
)) {
if (
isDefined(tab) &&
tab.position === HOME_TAB_POSITION &&
tab.layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST
) {
homeTabByPageLayoutId.set(tab.pageLayoutId, {
id: tab.id,
universalIdentifier: tab.universalIdentifier,
widgetCount: tab.widgetIds?.length ?? 0,
});
}
}
// Group fields by objectMetadataId
const fieldsByObjectId = new Map<string, FlatFieldMetadata[]>();
// Map morphId → all field IDs sharing that morphId (for dedup)
const fieldIdsByMorphId = new Map<string, string[]>();
for (const field of Object.values(
flatFieldMetadataMaps.byUniversalIdentifier,
)) {
if (!isDefined(field) || !isDefined(field.objectMetadataId)) {
continue;
}
const list = fieldsByObjectId.get(field.objectMetadataId) ?? [];
list.push(field);
fieldsByObjectId.set(field.objectMetadataId, list);
if (
field.type === FieldMetadataType.MORPH_RELATION &&
isDefined(field.morphId)
) {
const morphFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
morphFieldIds.push(field.id);
fieldIdsByMorphId.set(field.morphId, morphFieldIds);
}
}
const now = new Date().toISOString();
const standardWidgetsToCreate: FlatPageLayoutWidget[] = [];
const customWidgetsToCreate: FlatPageLayoutWidget[] = [];
const processedMorphIds = new Set<string>();
for (const object of objectById.values()) {
const pageLayout = recordPageLayoutByObjectId.get(object.id);
if (!isDefined(pageLayout)) {
continue;
}
const homeTab = homeTabByPageLayoutId.get(pageLayout.id);
if (!isDefined(homeTab)) {
continue;
}
const flatApplication: FlatApplication = object.isCustom
? workspaceCustomFlatApplication
: twentyStandardFlatApplication;
const targetList: FlatPageLayoutWidget[] = object.isCustom
? customWidgetsToCreate
: standardWidgetsToCreate;
const fields = fieldsByObjectId.get(object.id) ?? [];
let nextWidgetIndex = homeTab.widgetCount;
for (const field of fields) {
if (
field.type !== FieldMetadataType.RELATION &&
field.type !== FieldMetadataType.MORPH_RELATION
) {
continue;
}
if (isActivityTargetField(field.name, object.nameSingular)) {
continue;
}
if (isJunctionRelationField(field)) {
continue;
}
if (field.type === FieldMetadataType.RELATION) {
const targetObject = isDefined(field.relationTargetObjectMetadataId)
? objectById.get(field.relationTargetObjectMetadataId)
: undefined;
if (!isRelationTargetAvailable(targetObject)) {
continue;
}
}
// For morph relations, skip if any sibling field (same morphId)
// already has a widget or was already processed in this run
if (
field.type === FieldMetadataType.MORPH_RELATION &&
isDefined(field.morphId)
) {
if (processedMorphIds.has(field.morphId)) {
continue;
}
const siblingFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
const alreadyHasWidget = siblingFieldIds.some((id) =>
existingFieldWidgetFieldIds.has(id),
);
if (alreadyHasWidget) {
continue;
}
processedMorphIds.add(field.morphId);
}
if (existingFieldWidgetFieldIds.has(field.id)) {
continue;
}
const widget: FlatPageLayoutWidget = {
id: v4(),
universalIdentifier: v4(),
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
workspaceId,
pageLayoutTabId: homeTab.id,
pageLayoutTabUniversalIdentifier: homeTab.universalIdentifier,
title: field.label,
type: WidgetType.FIELD,
gridPosition: { ...GRID_POSITIONS.FULL_WIDTH },
position: {
...VERTICAL_LIST_LAYOUT_POSITIONS.SECOND,
index: nextWidgetIndex,
},
configuration: {
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: field.id,
fieldDisplayMode: FieldDisplayMode.CARD,
},
universalConfiguration: {
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: field.universalIdentifier,
fieldDisplayMode: FieldDisplayMode.CARD,
},
objectMetadataId: object.id,
objectMetadataUniversalIdentifier: object.universalIdentifier,
createdAt: now,
updatedAt: now,
deletedAt: null,
conditionalDisplay: null,
overrides: null,
};
targetList.push(widget);
existingFieldWidgetFieldIds.add(field.id);
nextWidgetIndex++;
}
}
const totalWidgets =
standardWidgetsToCreate.length + customWidgetsToCreate.length;
if (totalWidgets === 0) {
if (widgetsToCreate.length === 0) {
this.logger.log(
`All FIELD widgets already exist for workspace ${workspaceId}, skipping`,
);
@@ -359,73 +95,44 @@ export class BackfillFieldWidgetsCommand extends ActiveOrSuspendedWorkspacesMigr
}
this.logger.log(
`Found ${totalWidgets} FIELD widget(s) to create for workspace ${workspaceId} (${standardWidgetsToCreate.length} standard, ${customWidgetsToCreate.length} custom)`,
`Found ${widgetsToCreate.length} FIELD widget(s) to create for workspace ${workspaceId}`,
);
if (isDryRun) {
this.logger.log(
`[DRY RUN] Would create ${totalWidgets} FIELD widget(s) for workspace ${workspaceId}`,
`[DRY RUN] Would create ${widgetsToCreate.length} FIELD widget(s) for workspace ${workspaceId}`,
);
return;
}
if (standardWidgetsToCreate.length > 0) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: standardWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: widgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to create standard FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to create standard FIELD widgets for workspace ${workspaceId}`,
);
}
}
if (customWidgetsToCreate.length > 0) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: customWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to create custom FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to create custom FIELD widgets for workspace ${workspaceId}`,
);
}
if (validateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to create FIELD widgets:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to create FIELD widgets for workspace ${workspaceId}`,
);
}
this.logger.log(
`Successfully created ${totalWidgets} FIELD widget(s) for workspace ${workspaceId}`,
`Successfully created ${widgetsToCreate.length} FIELD widget(s) for workspace ${workspaceId}`,
);
}
}
@@ -31,7 +31,6 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
@Module({
imports: [
@@ -53,7 +52,6 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
ApplicationRegistrationModule,
WorkspaceMigrationModule,
FeatureFlagModule,
WorkflowCommonModule,
],
providers: [
IdentifyPermissionFlagMetadataCommand,
@@ -178,8 +178,6 @@ export class ApplicationInstallService {
);
}
// TODO: mimeType should be defined, default to application/octet-stream, which won't be displayed
// inline by the browser (forced download) due to Content-Disposition security headers.
await this.fileStorageService.writeFile({
sourceFile: content,
mimeType: undefined,
@@ -32,10 +32,6 @@ const createMockStream = (): Readable => {
return stream;
};
const createMockResponse = () => ({
setHeader: jest.fn(),
});
describe('FileController', () => {
let controller: FileController;
let fileService: FileService;
@@ -79,16 +75,15 @@ describe('FileController', () => {
});
describe('getFileById', () => {
it('should call fileService.getFileStreamById and pipe the result with headers', async () => {
it('should call fileService.getFileStreamById and pipe the result', async () => {
const mockStream = createMockStream();
jest.spyOn(fileService, 'getFileStreamById').mockResolvedValue({
stream: mockStream,
mimeType: 'image/png',
});
jest
.spyOn(fileService, 'getFileStreamById')
.mockResolvedValue(mockStream);
const mockRequest = { workspaceId: 'workspace-id' } as any;
const mockResponse = createMockResponse() as any;
const mockResponse = {} as any;
await controller.getFileById(
mockResponse,
@@ -102,49 +97,9 @@ describe('FileController', () => {
workspaceId: 'workspace-id',
fileFolder: FileFolder.CorePicture,
});
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'Content-Type',
'image/png',
);
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'X-Content-Type-Options',
'nosniff',
);
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'Content-Disposition',
'inline',
);
expect(mockStream.pipe).toHaveBeenCalledWith(mockResponse);
});
it('should force attachment disposition for non-safe MIME types', async () => {
const mockStream = createMockStream();
jest.spyOn(fileService, 'getFileStreamById').mockResolvedValue({
stream: mockStream,
mimeType: 'text/html',
});
const mockRequest = { workspaceId: 'workspace-id' } as any;
const mockResponse = createMockResponse() as any;
await controller.getFileById(
mockResponse,
mockRequest,
FileFolder.Workflow,
'file-123',
);
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'Content-Type',
'text/html',
);
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'Content-Disposition',
'attachment',
);
});
it('should throw FileException with FILE_NOT_FOUND when file is not found', async () => {
jest
.spyOn(fileService, 'getFileStreamById')
@@ -156,7 +111,7 @@ describe('FileController', () => {
);
const mockRequest = { workspaceId: 'workspace-id' } as any;
const mockResponse = createMockResponse() as any;
const mockResponse = {} as any;
await expect(
controller.getFileById(
@@ -176,7 +131,7 @@ describe('FileController', () => {
.mockRejectedValue(new Error('Storage unavailable'));
const mockRequest = { workspaceId: 'workspace-id' } as any;
const mockResponse = createMockResponse() as any;
const mockResponse = {} as any;
await expect(
controller.getFileById(
@@ -195,19 +150,18 @@ describe('FileController', () => {
});
describe('getPublicAssets', () => {
it('should call fileService.getFileStreamByPath and pipe with headers', async () => {
it('should call fileService.getFileStreamByPath and pipe the result', async () => {
const mockStream = createMockStream();
jest.spyOn(fileService, 'getFileStreamByPath').mockResolvedValue({
stream: mockStream,
mimeType: 'image/png',
});
jest
.spyOn(fileService, 'getFileStreamByPath')
.mockResolvedValue(mockStream);
const mockRequest = {
params: { path: ['images', 'logo.png'] },
} as any;
const mockResponse = createMockResponse() as any;
const mockResponse = {} as any;
await controller.getPublicAssets(
mockResponse,
@@ -222,34 +176,21 @@ describe('FileController', () => {
fileFolder: FileFolder.PublicAsset,
filepath: 'images/logo.png',
});
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'Content-Type',
'image/png',
);
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'X-Content-Type-Options',
'nosniff',
);
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'Content-Disposition',
'inline',
);
expect(mockStream.pipe).toHaveBeenCalledWith(mockResponse);
});
it('should handle single-segment path', async () => {
const mockStream = createMockStream();
jest.spyOn(fileService, 'getFileStreamByPath').mockResolvedValue({
stream: mockStream,
mimeType: 'image/x-icon',
});
jest
.spyOn(fileService, 'getFileStreamByPath')
.mockResolvedValue(mockStream);
const mockRequest = {
params: { path: ['favicon.ico'] },
} as any;
const mockResponse = createMockResponse() as any;
const mockResponse = {} as any;
await controller.getPublicAssets(
mockResponse,
@@ -280,7 +221,7 @@ describe('FileController', () => {
params: { path: ['missing-asset.png'] },
} as any;
const mockResponse = createMockResponse() as any;
const mockResponse = {} as any;
await expect(
controller.getPublicAssets(
@@ -303,7 +244,7 @@ describe('FileController', () => {
params: { path: ['broken-asset.png'] },
} as any;
const mockResponse = createMockResponse() as any;
const mockResponse = {} as any;
await expect(
controller.getPublicAssets(

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