Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27b0a9e0f3 | ||
|
|
c6d4162b73 | ||
|
|
93de331428 | ||
|
|
24055527c8 | ||
|
|
c58bf9cf06 | ||
|
|
07a4cf2d26 | ||
|
|
1cfdd2e8ed | ||
|
|
6f4f7a1198 | ||
|
|
0626f3e469 | ||
|
|
630f3a0fd7 | ||
|
|
b813d64324 | ||
|
|
c9ca4dfa14 | ||
|
|
2dfa742543 |
@@ -27,11 +27,8 @@ 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:command-no-deps ai:sync-models-dev
|
||||
run: npx nx run twenty-server:ts-node-no-deps-transpile-only -- ./scripts/ai-sync-models-dev.ts
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
@@ -61,3 +58,11 @@ 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
|
||||
|
||||
@@ -150,3 +150,11 @@ 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
|
||||
|
||||
@@ -138,3 +138,11 @@ 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
|
||||
|
||||
@@ -102,3 +102,11 @@ 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
|
||||
|
||||
@@ -30,6 +30,11 @@ 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
|
||||
@@ -43,6 +48,8 @@ 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,8 +6,14 @@ const query = `query FindOnePerson($objectRecordId: UUID!) {
|
||||
person(
|
||||
filter: {or: [{deletedAt: {is: NULL}}, {deletedAt: {is: NOT_NULL}}], id: {eq: $objectRecordId}}
|
||||
) {
|
||||
company {
|
||||
name
|
||||
previousCompanies {
|
||||
edges {
|
||||
node {
|
||||
company {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
emails {
|
||||
primaryEmail
|
||||
@@ -43,8 +49,8 @@ const query = `query FindOnePerson($objectRecordId: UUID!) {
|
||||
}`
|
||||
|
||||
test('Create and update record', async ({ page }) => {
|
||||
await page.getByRole('link', { name: 'People' }).click();
|
||||
await page.getByRole('button', { name: 'Create new record' }).click();
|
||||
await page.goto('/objects/people');
|
||||
await page.getByRole('button', { name: 'Create new Person' }).click();
|
||||
|
||||
// Generate a random email for testing
|
||||
const randomEmail = `testuser_${Math.random().toString(36).substring(2, 10)}@example.com`;
|
||||
@@ -107,29 +113,17 @@ test('Create and update record', async ({ page }) => {
|
||||
await options.getByText('Hybrid').first().click({force: true});
|
||||
recordFieldList.getByText('Work Preference').first().click({force: true});
|
||||
|
||||
// Fill company relation
|
||||
const companyRelationWidget = page.getByTestId(/dynamic-relation-widget-.+-Company/);
|
||||
await expect(companyRelationWidget).toBeVisible();
|
||||
// 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');
|
||||
|
||||
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;
|
||||
// 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];
|
||||
|
||||
// Check data was saved
|
||||
const { authToken } = await getAccessAuthToken(page);
|
||||
@@ -155,6 +149,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.company.name).toBe('VMware');
|
||||
expect(findOnePersonReponseBody.data.person.previousCompanies.edges[0].node.company.name).toBe('VMware');
|
||||
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"lingui:extract": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["^build"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "lingui extract --overwrite --clean"
|
||||
@@ -45,6 +46,7 @@
|
||||
},
|
||||
"lingui:compile": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["^build"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "lingui compile --typescript"
|
||||
|
||||
@@ -292,6 +292,7 @@
|
||||
},
|
||||
"lingui:extract": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["^build"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "lingui extract --overwrite --clean"
|
||||
@@ -299,6 +300,7 @@
|
||||
},
|
||||
"lingui:compile": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["^build"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "lingui compile --typescript"
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9479,6 +9479,15 @@ 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
|
||||
@@ -9505,15 +9514,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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,6 +9479,15 @@ 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
|
||||
@@ -9505,15 +9514,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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.
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9486,6 +9486,15 @@ 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
|
||||
@@ -9512,15 +9521,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
@@ -9484,6 +9484,15 @@ 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
|
||||
@@ -9510,15 +9519,6 @@ 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
|
||||
|
||||
+8
-8
@@ -15,19 +15,18 @@ 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 { 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 { 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 {
|
||||
type NavigationMenuItemClickParams,
|
||||
useNavigationMenuItemSectionItems,
|
||||
} from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
|
||||
import { WorkspaceSectionContainer } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
|
||||
import { preloadNavigationMenuItemDndKit } from '@/navigation-menu-item/display/dnd/preloadNavigationMenuItemDndKit';
|
||||
import { WorkspaceSectionContainer } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
@@ -35,6 +34,7 @@ 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 sidebar item`,
|
||||
pageTitle: t`New menu item`,
|
||||
pageIcon: IconColumnInsertRight,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ export const WorkspaceSectionAddMenuItemButton = () => {
|
||||
setSelectedNavigationMenuItemInEditMode(null);
|
||||
navigateSidePanel({
|
||||
page: SidePanelPages.NavigationMenuAddItem,
|
||||
pageTitle: t`New sidebar item`,
|
||||
pageTitle: t`New menu item`,
|
||||
pageIcon: IconColumnInsertRight,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ export const useOpenAddItemToFolderPage = () => {
|
||||
});
|
||||
navigateSidePanel({
|
||||
page: SidePanelPages.NavigationMenuAddItem,
|
||||
pageTitle: t`New sidebar item`,
|
||||
pageTitle: t`New menu item`,
|
||||
pageIcon: IconColumnInsertRight,
|
||||
resetNavigationStack,
|
||||
});
|
||||
|
||||
+7
-7
@@ -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 sidebar item`,
|
||||
pageTitle: t`New menu item`,
|
||||
pageIcon: IconColumnInsertRight,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ const StyledTr = styled.div<{
|
||||
div.table-cell,
|
||||
div.table-cell-0-0 {
|
||||
&:not(:first-of-type) {
|
||||
background-color: ${themeCssVariables.background.tertiary};
|
||||
background-color: ${themeCssVariables.accent.quaternary};
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-color: ${themeCssVariables.border.color.medium};
|
||||
}
|
||||
|
||||
+2
-3
@@ -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,8 +18,7 @@ export const assertFieldWidgetOrThrow: AssertFieldWidgetOrThrow = (
|
||||
);
|
||||
|
||||
if (
|
||||
!isNonEmptyString(widget.configuration.__typename) ||
|
||||
widget.configuration.__typename !== 'FieldConfiguration'
|
||||
widget.configuration.configurationType !== WidgetConfigurationType.FIELD
|
||||
) {
|
||||
throw new Error(
|
||||
`Expected FieldConfiguration but got ${widget.configuration.__typename}`,
|
||||
|
||||
@@ -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 { 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 { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
|
||||
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 sidebar item`,
|
||||
pageTitle: t`New menu item`,
|
||||
pageIcon: IconColumnInsertRight,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ 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';
|
||||
|
||||
@@ -22,6 +23,16 @@ 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`
|
||||
|
||||
@@ -279,6 +279,7 @@
|
||||
},
|
||||
"lingui:extract": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["^build"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "lingui extract --overwrite --clean"
|
||||
@@ -286,6 +287,7 @@
|
||||
},
|
||||
"lingui:compile": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["^build"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "lingui compile --typescript"
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
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);
|
||||
});
|
||||
@@ -1,360 +0,0 @@
|
||||
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,7 +1,6 @@
|
||||
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';
|
||||
@@ -71,7 +70,6 @@ import { AutomatedTriggerModule } from 'src/modules/workflow/workflow-trigger/au
|
||||
StaleRegistrationCleanupModule,
|
||||
],
|
||||
providers: [
|
||||
AiSyncModelsDevCommand,
|
||||
DataSeedWorkspaceCommand,
|
||||
ConfirmationQuestion,
|
||||
CronRegisterAllCommand,
|
||||
|
||||
+272
-23
@@ -1,25 +1,40 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { InjectDataSource, 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 { Repository } from 'typeorm';
|
||||
import { DataSource, type QueryRunner, Repository } from 'typeorm';
|
||||
import { v4 as uuidv4 } 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 { 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 command menu items for existing workspaces and enable IS_COMMAND_MENU_ITEM_ENABLED feature flag',
|
||||
'Backfill missing standard and workflow command menu items for existing workspaces and enable IS_COMMAND_MENU_ITEM_ENABLED feature flag',
|
||||
})
|
||||
export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@@ -27,10 +42,13 @@ 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);
|
||||
}
|
||||
@@ -42,7 +60,7 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of missing standard command menu items for workspace ${workspaceId}`,
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of command menu items for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const isFeatureFlagAlreadyEnabled =
|
||||
@@ -59,6 +77,52 @@ 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 },
|
||||
@@ -90,30 +154,21 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
),
|
||||
);
|
||||
|
||||
const numberOfCommandMenuItemsToCreate = commandMenuItemsToCreate.length;
|
||||
|
||||
if (numberOfCommandMenuItemsToCreate === 0) {
|
||||
if (commandMenuItemsToCreate.length === 0) {
|
||||
this.logger.log(
|
||||
`No missing standard command menu items for workspace ${workspaceId}, enabling feature flag only`,
|
||||
`No missing standard command menu items for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (!isDryRun) {
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED],
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${numberOfCommandMenuItemsToCreate} missing standard command menu item(s) for workspace ${workspaceId}`,
|
||||
`Found ${commandMenuItemsToCreate.length} missing standard command menu item(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would create ${numberOfCommandMenuItemsToCreate} command menu item(s) and enable IS_COMMAND_MENU_ITEM_ENABLED for workspace ${workspaceId}`,
|
||||
`[DRY RUN] Would create ${commandMenuItemsToCreate.length} standard command menu item(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
@@ -132,6 +187,7 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
queryRunner,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -145,13 +201,206 @@ 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) and enabled IS_COMMAND_MENU_ITEM_ENABLED for workspace ${workspaceId}`,
|
||||
`Successfully backfilled ${commandMenuItemsToCreate.length} standard command menu item(s) 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+352
-59
@@ -1,24 +1,84 @@
|
||||
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 { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||
import {
|
||||
GRID_POSITIONS,
|
||||
VERTICAL_LIST_LAYOUT_POSITIONS,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
|
||||
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 standard object relation fields in existing page layouts',
|
||||
'Backfill FIELD widgets for relation fields in existing page layouts',
|
||||
})
|
||||
export class BackfillFieldWidgetsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@@ -43,50 +103,254 @@ export class BackfillFieldWidgetsCommand extends ActiveOrSuspendedWorkspacesMigr
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of FIELD widgets for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
|
||||
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||
shouldIncludeRecordPageLayouts: true,
|
||||
now: new Date().toISOString(),
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
});
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
]);
|
||||
|
||||
const standardFieldWidgets = Object.values(
|
||||
standardAllFlatEntityMaps.flatPageLayoutWidgetMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(widget) =>
|
||||
widget.configuration?.configurationType ===
|
||||
WidgetConfigurationType.FIELD,
|
||||
);
|
||||
// Build a set of fieldMetadataIds that already have a FIELD widget
|
||||
const existingFieldWidgetFieldIds = new Set<string>();
|
||||
|
||||
if (standardFieldWidgets.length === 0) {
|
||||
this.logger.log(
|
||||
`No FIELD widgets found in standard configs for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const { flatPageLayoutWidgetMaps: existingWidgetMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatPageLayoutWidgetMaps',
|
||||
]);
|
||||
// Build object ID → objectMetadata map
|
||||
const objectById = new Map<string, FlatObjectMetadata>();
|
||||
|
||||
const widgetsToCreate = standardFieldWidgets.filter(
|
||||
(widget) =>
|
||||
!isDefined(
|
||||
existingWidgetMaps.byUniversalIdentifier[widget.universalIdentifier],
|
||||
),
|
||||
);
|
||||
for (const obj of Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (isDefined(obj)) {
|
||||
objectById.set(obj.id, obj);
|
||||
}
|
||||
}
|
||||
|
||||
if (widgetsToCreate.length === 0) {
|
||||
// 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) {
|
||||
this.logger.log(
|
||||
`All FIELD widgets already exist for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
@@ -95,44 +359,73 @@ export class BackfillFieldWidgetsCommand extends ActiveOrSuspendedWorkspacesMigr
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${widgetsToCreate.length} FIELD widget(s) to create for workspace ${workspaceId}`,
|
||||
`Found ${totalWidgets} FIELD widget(s) to create for workspace ${workspaceId} (${standardWidgetsToCreate.length} standard, ${customWidgetsToCreate.length} custom)`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would create ${widgetsToCreate.length} FIELD widget(s) for workspace ${workspaceId}`,
|
||||
`[DRY RUN] Would create ${totalWidgets} FIELD widget(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: widgetsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
if (standardWidgetsToCreate.length > 0) {
|
||||
const result =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: standardWidgetsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
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}`,
|
||||
);
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully created ${widgetsToCreate.length} FIELD widget(s) for workspace ${workspaceId}`,
|
||||
`Successfully created ${totalWidgets} FIELD widget(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -31,6 +31,7 @@ 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: [
|
||||
@@ -52,6 +53,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
ApplicationRegistrationModule,
|
||||
WorkspaceMigrationModule,
|
||||
FeatureFlagModule,
|
||||
WorkflowCommonModule,
|
||||
],
|
||||
providers: [
|
||||
IdentifyPermissionFlagMetadataCommand,
|
||||
|
||||
+2
@@ -178,6 +178,8 @@ 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,
|
||||
|
||||
+77
-18
@@ -32,6 +32,10 @@ const createMockStream = (): Readable => {
|
||||
return stream;
|
||||
};
|
||||
|
||||
const createMockResponse = () => ({
|
||||
setHeader: jest.fn(),
|
||||
});
|
||||
|
||||
describe('FileController', () => {
|
||||
let controller: FileController;
|
||||
let fileService: FileService;
|
||||
@@ -75,15 +79,16 @@ describe('FileController', () => {
|
||||
});
|
||||
|
||||
describe('getFileById', () => {
|
||||
it('should call fileService.getFileStreamById and pipe the result', async () => {
|
||||
it('should call fileService.getFileStreamById and pipe the result with headers', async () => {
|
||||
const mockStream = createMockStream();
|
||||
|
||||
jest
|
||||
.spyOn(fileService, 'getFileStreamById')
|
||||
.mockResolvedValue(mockStream);
|
||||
jest.spyOn(fileService, 'getFileStreamById').mockResolvedValue({
|
||||
stream: mockStream,
|
||||
mimeType: 'image/png',
|
||||
});
|
||||
|
||||
const mockRequest = { workspaceId: 'workspace-id' } as any;
|
||||
const mockResponse = {} as any;
|
||||
const mockResponse = createMockResponse() as any;
|
||||
|
||||
await controller.getFileById(
|
||||
mockResponse,
|
||||
@@ -97,9 +102,49 @@ 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')
|
||||
@@ -111,7 +156,7 @@ describe('FileController', () => {
|
||||
);
|
||||
|
||||
const mockRequest = { workspaceId: 'workspace-id' } as any;
|
||||
const mockResponse = {} as any;
|
||||
const mockResponse = createMockResponse() as any;
|
||||
|
||||
await expect(
|
||||
controller.getFileById(
|
||||
@@ -131,7 +176,7 @@ describe('FileController', () => {
|
||||
.mockRejectedValue(new Error('Storage unavailable'));
|
||||
|
||||
const mockRequest = { workspaceId: 'workspace-id' } as any;
|
||||
const mockResponse = {} as any;
|
||||
const mockResponse = createMockResponse() as any;
|
||||
|
||||
await expect(
|
||||
controller.getFileById(
|
||||
@@ -150,18 +195,19 @@ describe('FileController', () => {
|
||||
});
|
||||
|
||||
describe('getPublicAssets', () => {
|
||||
it('should call fileService.getFileStreamByPath and pipe the result', async () => {
|
||||
it('should call fileService.getFileStreamByPath and pipe with headers', async () => {
|
||||
const mockStream = createMockStream();
|
||||
|
||||
jest
|
||||
.spyOn(fileService, 'getFileStreamByPath')
|
||||
.mockResolvedValue(mockStream);
|
||||
jest.spyOn(fileService, 'getFileStreamByPath').mockResolvedValue({
|
||||
stream: mockStream,
|
||||
mimeType: 'image/png',
|
||||
});
|
||||
|
||||
const mockRequest = {
|
||||
params: { path: ['images', 'logo.png'] },
|
||||
} as any;
|
||||
|
||||
const mockResponse = {} as any;
|
||||
const mockResponse = createMockResponse() as any;
|
||||
|
||||
await controller.getPublicAssets(
|
||||
mockResponse,
|
||||
@@ -176,21 +222,34 @@ 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(mockStream);
|
||||
jest.spyOn(fileService, 'getFileStreamByPath').mockResolvedValue({
|
||||
stream: mockStream,
|
||||
mimeType: 'image/x-icon',
|
||||
});
|
||||
|
||||
const mockRequest = {
|
||||
params: { path: ['favicon.ico'] },
|
||||
} as any;
|
||||
|
||||
const mockResponse = {} as any;
|
||||
const mockResponse = createMockResponse() as any;
|
||||
|
||||
await controller.getPublicAssets(
|
||||
mockResponse,
|
||||
@@ -221,7 +280,7 @@ describe('FileController', () => {
|
||||
params: { path: ['missing-asset.png'] },
|
||||
} as any;
|
||||
|
||||
const mockResponse = {} as any;
|
||||
const mockResponse = createMockResponse() as any;
|
||||
|
||||
await expect(
|
||||
controller.getPublicAssets(
|
||||
@@ -244,7 +303,7 @@ describe('FileController', () => {
|
||||
params: { path: ['broken-asset.png'] },
|
||||
} as any;
|
||||
|
||||
const mockResponse = {} as any;
|
||||
const mockResponse = createMockResponse() as any;
|
||||
|
||||
await expect(
|
||||
controller.getPublicAssets(
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
SupportedFileFolder,
|
||||
} from 'src/engine/core-modules/file/guards/file-by-id.guard';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { setFileResponseHeaders } from 'src/engine/core-modules/file/utils/set-file-response-headers.utils';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
|
||||
|
||||
@@ -48,21 +49,23 @@ export class FileController {
|
||||
const filepath = join(...req.params.path);
|
||||
|
||||
try {
|
||||
const fileStream = await this.fileService.getFileStreamByPath({
|
||||
const { stream, mimeType } = await this.fileService.getFileStreamByPath({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
fileFolder: FileFolder.PublicAsset,
|
||||
filepath,
|
||||
});
|
||||
|
||||
fileStream.on('error', () => {
|
||||
setFileResponseHeaders(res, mimeType);
|
||||
|
||||
stream.on('error', () => {
|
||||
throw new FileException(
|
||||
'Error streaming file from storage',
|
||||
FileExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
});
|
||||
|
||||
fileStream.pipe(res);
|
||||
stream.pipe(res);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof FileStorageException &&
|
||||
@@ -93,20 +96,22 @@ export class FileController {
|
||||
const workspaceId = (req as any)?.workspaceId;
|
||||
|
||||
try {
|
||||
const fileStream = await this.fileService.getFileStreamById({
|
||||
const { stream, mimeType } = await this.fileService.getFileStreamById({
|
||||
fileId,
|
||||
workspaceId,
|
||||
fileFolder,
|
||||
});
|
||||
|
||||
fileStream.on('error', () => {
|
||||
setFileResponseHeaders(res, mimeType);
|
||||
|
||||
stream.on('error', () => {
|
||||
throw new FileException(
|
||||
'Error streaming file from storage',
|
||||
FileExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
});
|
||||
|
||||
fileStream.pipe(res);
|
||||
stream.pipe(res);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof FileStorageException &&
|
||||
|
||||
@@ -45,7 +45,15 @@ export class FileService {
|
||||
applicationId: string;
|
||||
filepath: string;
|
||||
fileFolder: FileFolder;
|
||||
}) {
|
||||
}): Promise<{ stream: Readable; mimeType: string }> {
|
||||
const file = await this.fileRepository.findOneOrFail({
|
||||
where: {
|
||||
path: `${fileFolder}/${filepath}`,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
},
|
||||
});
|
||||
|
||||
const application = await this.applicationRepository.findOneOrFail({
|
||||
where: {
|
||||
id: applicationId,
|
||||
@@ -53,12 +61,17 @@ export class FileService {
|
||||
},
|
||||
});
|
||||
|
||||
return this.fileStorageService.readFile({
|
||||
const stream = await this.fileStorageService.readFile({
|
||||
resourcePath: filepath,
|
||||
fileFolder,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
stream,
|
||||
mimeType: file.mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
async getFileStreamById({
|
||||
@@ -69,7 +82,7 @@ export class FileService {
|
||||
fileId: string;
|
||||
workspaceId: string;
|
||||
fileFolder: FileFolder;
|
||||
}): Promise<Readable> {
|
||||
}): Promise<{ stream: Readable; mimeType: string }> {
|
||||
const file = await this.fileRepository.findOneOrFail({
|
||||
where: {
|
||||
id: fileId,
|
||||
@@ -85,12 +98,17 @@ export class FileService {
|
||||
},
|
||||
});
|
||||
|
||||
return this.fileStorageService.readFile({
|
||||
const stream = await this.fileStorageService.readFile({
|
||||
resourcePath: removeFileFolderFromFileEntityPath(file.path),
|
||||
fileFolder,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
stream,
|
||||
mimeType: file.mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
async getFileContentById({
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { type Response } from 'express';
|
||||
|
||||
const INLINE_SAFE_MIME_TYPES = new Set([
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/avif',
|
||||
'image/bmp',
|
||||
'image/tiff',
|
||||
'application/pdf',
|
||||
'text/plain',
|
||||
'audio/mpeg',
|
||||
'audio/wav',
|
||||
'audio/ogg',
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
'image/x-icon',
|
||||
]);
|
||||
|
||||
export const setFileResponseHeaders = (res: Response, mimeType: string) => {
|
||||
const contentType = mimeType || 'application/octet-stream';
|
||||
const disposition = INLINE_SAFE_MIME_TYPES.has(contentType)
|
||||
? 'inline'
|
||||
: 'attachment';
|
||||
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('Content-Disposition', disposition);
|
||||
};
|
||||
+1
-1
@@ -191,7 +191,7 @@ export class EmailComposerService {
|
||||
const attachments: MessageAttachment[] = [];
|
||||
|
||||
for (const fileMetadata of files) {
|
||||
const stream = await this.fileService.getFileStreamById({
|
||||
const { stream } = await this.fileService.getFileStreamById({
|
||||
fileId: fileMetadata.id,
|
||||
workspaceId,
|
||||
fileFolder: FileFolder.Workflow,
|
||||
|
||||
@@ -1518,6 +1518,24 @@
|
||||
"maxOutputTokens": 8192,
|
||||
"supportsReasoning": true
|
||||
},
|
||||
{
|
||||
"name": "grok-4.20-0309-reasoning",
|
||||
"label": "Grok 4.20 (Reasoning)",
|
||||
"modelFamily": "grok",
|
||||
"inputCostPerMillionTokens": 2,
|
||||
"outputCostPerMillionTokens": 6,
|
||||
"cachedInputCostPerMillionTokens": 0.2,
|
||||
"longContextCost": {
|
||||
"inputCostPerMillionTokens": 4,
|
||||
"outputCostPerMillionTokens": 12,
|
||||
"thresholdTokens": 200000,
|
||||
"cachedInputCostPerMillionTokens": 0.4
|
||||
},
|
||||
"contextWindowTokens": 2000000,
|
||||
"maxOutputTokens": 30000,
|
||||
"modalities": ["image"],
|
||||
"supportsReasoning": true
|
||||
},
|
||||
{
|
||||
"name": "grok-2-latest",
|
||||
"label": "Grok 2 Latest",
|
||||
@@ -1550,6 +1568,23 @@
|
||||
"maxOutputTokens": 4096,
|
||||
"modalities": ["image"]
|
||||
},
|
||||
{
|
||||
"name": "grok-4.20-0309-non-reasoning",
|
||||
"label": "Grok 4.20 (Non-Reasoning)",
|
||||
"modelFamily": "grok",
|
||||
"inputCostPerMillionTokens": 2,
|
||||
"outputCostPerMillionTokens": 6,
|
||||
"cachedInputCostPerMillionTokens": 0.2,
|
||||
"longContextCost": {
|
||||
"inputCostPerMillionTokens": 4,
|
||||
"outputCostPerMillionTokens": 12,
|
||||
"thresholdTokens": 200000,
|
||||
"cachedInputCostPerMillionTokens": 0.4
|
||||
},
|
||||
"contextWindowTokens": 2000000,
|
||||
"maxOutputTokens": 30000,
|
||||
"modalities": ["image"]
|
||||
},
|
||||
{
|
||||
"name": "grok-3-fast",
|
||||
"label": "Grok 3 Fast",
|
||||
|
||||
+16
-7
@@ -23,6 +23,8 @@ import {
|
||||
} from 'src/engine/metadata-modules/message-channel/message-channel.exception';
|
||||
import { MessageChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor';
|
||||
import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.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 {
|
||||
MessageChannelPendingGroupEmailsAction,
|
||||
MessageChannelSyncStage,
|
||||
@@ -41,6 +43,7 @@ export class MessageChannelResolver {
|
||||
private readonly messageChannelMetadataService: MessageChannelMetadataService,
|
||||
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
|
||||
private readonly messagingProcessGroupEmailActionsService: MessagingProcessGroupEmailActionsService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
) {}
|
||||
|
||||
@Query(() => [MessageChannelDTO])
|
||||
@@ -116,13 +119,19 @@ export class MessageChannelResolver {
|
||||
isDefined(input.update.excludeGroupEmails) &&
|
||||
input.update.excludeGroupEmails !== messageChannel.excludeGroupEmails
|
||||
) {
|
||||
// Service expects WorkspaceEntity type but only reads .id
|
||||
await this.messagingProcessGroupEmailActionsService.markMessageChannelAsPendingGroupEmailsAction(
|
||||
messageChannel as unknown as MessageChannelWorkspaceEntity,
|
||||
workspace.id,
|
||||
input.update.excludeGroupEmails
|
||||
? MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION
|
||||
: MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT,
|
||||
const systemAuthContext = buildSystemAuthContext(workspace.id);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
await this.messagingProcessGroupEmailActionsService.markMessageChannelAsPendingGroupEmailsAction(
|
||||
messageChannel as unknown as MessageChannelWorkspaceEntity,
|
||||
workspace.id,
|
||||
input.update.excludeGroupEmails
|
||||
? MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION
|
||||
: MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT,
|
||||
);
|
||||
},
|
||||
systemAuthContext,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+14
-8
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard page layout metadata related entity ids 1`] = `
|
||||
{
|
||||
@@ -340,21 +340,24 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
|
||||
},
|
||||
},
|
||||
"workflowRunRecordPage": {
|
||||
"id": "00000000-0000-0000-0000-000000000093",
|
||||
"id": "00000000-0000-0000-0000-000000000094",
|
||||
"tabs": {
|
||||
"flow": {
|
||||
"id": "00000000-0000-0000-0000-000000000096",
|
||||
"id": "00000000-0000-0000-0000-000000000098",
|
||||
"widgets": {
|
||||
"workflowRun": {
|
||||
"id": "00000000-0000-0000-0000-000000000097",
|
||||
"id": "00000000-0000-0000-0000-000000000099",
|
||||
},
|
||||
},
|
||||
},
|
||||
"home": {
|
||||
"id": "00000000-0000-0000-0000-000000000094",
|
||||
"id": "00000000-0000-0000-0000-000000000095",
|
||||
"widgets": {
|
||||
"fields": {
|
||||
"id": "00000000-0000-0000-0000-000000000095",
|
||||
"id": "00000000-0000-0000-0000-000000000096",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000097",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -364,10 +367,10 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
|
||||
"id": "00000000-0000-0000-0000-000000000088",
|
||||
"tabs": {
|
||||
"flow": {
|
||||
"id": "00000000-0000-0000-0000-000000000091",
|
||||
"id": "00000000-0000-0000-0000-000000000092",
|
||||
"widgets": {
|
||||
"workflowVersion": {
|
||||
"id": "00000000-0000-0000-0000-000000000092",
|
||||
"id": "00000000-0000-0000-0000-000000000093",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -377,6 +380,9 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
|
||||
"fields": {
|
||||
"id": "00000000-0000-0000-0000-000000000090",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000091",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+12
@@ -1,8 +1,11 @@
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
|
||||
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 {
|
||||
GRID_POSITIONS,
|
||||
TAB_PROPS,
|
||||
VERTICAL_LIST_LAYOUT_POSITIONS,
|
||||
WIDGET_PROPS,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
|
||||
import {
|
||||
@@ -19,6 +22,15 @@ const WORKFLOW_RUN_PAGE_TABS = {
|
||||
universalIdentifier: '20202020-ac08-4008-8008-a0bcf10a8811',
|
||||
...WIDGET_PROPS.fields,
|
||||
},
|
||||
workflow: {
|
||||
universalIdentifier: '20202020-ac08-4008-8008-a0bcf10a8813',
|
||||
title: 'Workflow',
|
||||
type: WidgetType.FIELD,
|
||||
gridPosition: GRID_POSITIONS.FULL_WIDTH,
|
||||
position: VERTICAL_LIST_LAYOUT_POSITIONS.THIRD,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECTS.workflowRun.fields.workflow.universalIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
flow: {
|
||||
|
||||
+12
@@ -1,8 +1,11 @@
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
|
||||
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 {
|
||||
GRID_POSITIONS,
|
||||
TAB_PROPS,
|
||||
VERTICAL_LIST_LAYOUT_POSITIONS,
|
||||
WIDGET_PROPS,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
|
||||
import {
|
||||
@@ -19,6 +22,15 @@ const WORKFLOW_VERSION_PAGE_TABS = {
|
||||
universalIdentifier: '20202020-ac07-4007-8007-a0bcf10a7711',
|
||||
...WIDGET_PROPS.fields,
|
||||
},
|
||||
workflow: {
|
||||
universalIdentifier: '20202020-ac07-4007-8007-a0bcf10a7712',
|
||||
title: 'Workflow',
|
||||
type: WidgetType.FIELD,
|
||||
gridPosition: GRID_POSITIONS.FULL_WIDTH,
|
||||
position: VERTICAL_LIST_LAYOUT_POSITIONS.SECOND,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECTS.workflowVersion.fields.workflow.universalIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
flow: {
|
||||
|
||||
@@ -89,18 +89,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/groq@npm:^3.0.24":
|
||||
version: 3.0.24
|
||||
resolution: "@ai-sdk/groq@npm:3.0.24"
|
||||
dependencies:
|
||||
"@ai-sdk/provider": "npm:3.0.8"
|
||||
"@ai-sdk/provider-utils": "npm:4.0.15"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/10e2b017f52006fcbcd4c46b78a3f02e3b3df3f337982d1669785df1492ca9a8793276403c5bff2c59bfd11ff1f6c58bcc1d3c49738bffd7ba13f527e85137b6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/mistral@npm:^3.0.20":
|
||||
version: 3.0.25
|
||||
resolution: "@ai-sdk/mistral@npm:3.0.25"
|
||||
@@ -56663,7 +56651,6 @@ __metadata:
|
||||
"@ai-sdk/amazon-bedrock": "npm:^3.0.83"
|
||||
"@ai-sdk/anthropic": "npm:^3.0.46"
|
||||
"@ai-sdk/google": "npm:^3.0.30"
|
||||
"@ai-sdk/groq": "npm:^3.0.24"
|
||||
"@ai-sdk/mistral": "npm:^3.0.20"
|
||||
"@ai-sdk/openai": "npm:^3.0.30"
|
||||
"@ai-sdk/provider-utils": "npm:^4.0.15"
|
||||
|
||||
Reference in New Issue
Block a user