Compare commits

..
Author SHA1 Message Date
Charles Bochet 48f8bb6b6a fix: prettier formatting on normalizeUrlOrigin 2026-03-20 16:29:12 +01:00
Charles Bochet 8c5f256f49 fix: simplify normalizeUrlOrigin to use standard URL API and fix lint
Use URL() properties directly instead of manual string parsing.
Remove real customer name from test data. Fix duplicate imports and
prettier formatting.
2026-03-20 16:21:11 +01:00
Charles Bochet 53fb2bef12 Fix 2026-03-20 15:46:31 +01:00
DAL\Administrator 9bdde8b793 fix(links): preserve percent-encoded URLs during normalization 2026-03-20 15:44:00 +08:00
3cbd9f2b5d i18n - translations (#18786)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-20 00:39:40 +01:00
Charles BochetandGitHub cee4cf6452 feat: migrate ConnectedAccount infrastructure entities to metadata schema (#18784)
## Summary

- Migrates 4 entities (`connectedAccount`, `messageChannel`,
`calendarChannel`, `messageFolder`) from per-workspace schemas to the
shared `core` metadata schema
- Introduces a `IS_CONNECTED_ACCOUNT_MIGRATED` feature flag to control
the migration: when enabled, reads come from core metadata and all
writes are dual-written to both workspace and core
- Extracts 12 enums from workspace entity files to `twenty-shared` for
reuse across frontend and backend
- Creates new TypeORM entities, metadata services, GraphQL
resolvers/DTOs, and exception interceptors per entity
- Each entity owns its own data access module
(`ConnectedAccountDataAccessModule`, `MessageChannelDataAccessModule`,
`CalendarChannelDataAccessModule`, `MessageFolderDataAccessModule`) — no
umbrella infrastructure module
- Adds a 1.20 upgrade command that backfills data from workspace schemas
to core (preserving UUIDs) and enables the feature flag
- Replaces direct repository access with data access service calls
across ~50 files in messaging, calendar, and connected-account modules
- Adds `lastSignedInAt` and `oidcTokenClaims` fields to the new
`ConnectedAccountEntity`
- Drops unused `lastSyncHistoryId` field from the migrated connected
account entity

## Test plan

- [x] Lint passes (`npx nx lint:diff-with-main twenty-server`)
- [x] Typecheck passes (`npx nx typecheck twenty-server`)
- [x] All unit tests pass (477 suites, 4267 tests, 0 failures)
- [ ] Manual test: verify messaging sync works with feature flag
disabled (existing behavior)
- [ ] Manual test: run upgrade command on a workspace, verify data
backfilled to core tables
- [ ] Manual test: verify messaging/calendar sync works with feature
flag enabled (dual-write path)
- [ ] Manual test: verify GraphQL metadata resolvers return correct data
when flag enabled
2026-03-20 00:34:58 +01:00
Abdul RahmanandGitHub cd594ce8bd Fix: Hide object from Opened section when it already has a workspace nav item (#18766)
Previously, the Opened section was always hidden for all workflow
objects. We now base the “in nav” check on the full set of object
metadata IDs that have a workspace nav item, instead of only active
non-system objects. So: if an object has a nav item (e.g. workflow
runs), it no longer appears under Opened; if it doesn’t, it can appear
there. The hook was simplified to only expose
`objectMetadataIdsInWorkspaceNav`, and the unused return value was
removed.
2026-03-19 21:57:11 +00:00
26139ee463 fix: stop event propagation when removing file in AI chat preview (#18779)
## Summary

When clicking the ✕ button on an uploaded file in the AI chatbot context
preview, the click event bubbled up to the `StyledClickableContainer`
parent, which triggered `handleClick` (opening the file preview modal)
instead of — or in addition to — calling `onRemove`.

**Root cause:** `StyledClickableContainer` has `onClick={handleClick}`
at the div level. The `AvatarOrIcon` (X button) `onClick={onRemove}`
callback didn't stop propagation, so the event continued to bubble and
opened the preview.

**Fix:** Wrap `onRemove` in a `handleRemove` callback that calls
`e.stopPropagation()` before invoking the original handler.

Fixes #18298

---------

Co-authored-by: victorjzq <zhiqiangjia@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-19 19:07:19 +00:00
8005b35b56 Fix relation connect where failing on mixed-case email and URL (#18605)
Related to issue #17711 
Follow-up to PR #17774  which fixed the frontend link normalization only
## Summary
- Normalize `primaryEmail` (lowercase) and `primaryLinkUrl` in relation
`connect.where` composite values
- Applied in both frontend spreadsheet import and backend
`DataArgProcessorService` to cover all entry points (UI import, GraphQL
API)

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-03-19 17:56:26 +00:00
Abdul RahmanandGitHub 02bfebccc2 Fix: cascade delete favorite folder children on backend (#18765) 2026-03-19 17:55:28 +00:00
Thomas TrompetteandGitHub b86e6189c0 Remove postition from Timeline Activities + fix workflow title placeholder (#18777)
- Position were not properly displayed because we never implemented a
display for this
- Untitled placeholder was not displayed anymore
<img width="359" height="117" alt="Capture d’écran 2026-03-19 à 17 11
25"
src="https://github.com/user-attachments/assets/64c90d81-8262-4176-ae25-804748e36b1e"
/>
2026-03-19 17:25:35 +00:00
a9f8a7e1fa Fix: prevent record navigation when clicking Remove from favorite in nav sidebar (#18760)
https://github.com/user-attachments/assets/96abf04d-726a-4225-846e-e5d701a583a2

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-19 14:29:12 +00:00
5526d2e5d0 Separate metadata and object record publisher (#18740)
- Separate both publishers
- Renaming
- Removal of the old SSE endpoint

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-19 13:24:38 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Charles Bochet
0d27a255a9 chore(deps): bump nodemailer from 7.0.11 to 7.0.13 (#18754)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 7.0.11
to 7.0.13.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/nodemailer/nodemailer/releases">nodemailer's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.13</h2>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.12...v7.0.13">7.0.13</a>
(2026-01-27)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>downgrade transient connection error logs to warn level (<a
href="https://github.com/nodemailer/nodemailer/commit/4c041db85d560e98bc5e1fd5d5a191835c5b7d2f">4c041db</a>)</li>
</ul>
<h2>v7.0.12</h2>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.12">7.0.12</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>added support for REQUIRETLS (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1793">#1793</a>)
(<a
href="https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84">053ce6a</a>)</li>
<li>use 8bit encoding for message/rfc822 attachments (<a
href="https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b">adf8611</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md">nodemailer's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.12...v7.0.13">7.0.13</a>
(2026-01-27)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>downgrade transient connection error logs to warn level (<a
href="https://github.com/nodemailer/nodemailer/commit/4c041db85d560e98bc5e1fd5d5a191835c5b7d2f">4c041db</a>)</li>
</ul>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.12">7.0.12</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>added support for REQUIRETLS (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1793">#1793</a>)
(<a
href="https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84">053ce6a</a>)</li>
<li>use 8bit encoding for message/rfc822 attachments (<a
href="https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b">adf8611</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/893119505aa25723dd9a7d90c8dfd223d28a0cc4"><code>8931195</code></a>
chore(master): release 7.0.13 (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1798">#1798</a>)</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/9398d633d2c1bf5bf08a0e258ff17d5d7a45f0e6"><code>9398d63</code></a>
Bumped deps</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/4c041db85d560e98bc5e1fd5d5a191835c5b7d2f"><code>4c041db</code></a>
fix: downgrade transient connection error logs to warn level</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/a208a0bc86a315d037d5be8849fab5862c488baa"><code>a208a0b</code></a>
chore(master): release 7.0.12 (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1785">#1785</a>)</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84"><code>053ce6a</code></a>
fix: added support for REQUIRETLS (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1793">#1793</a>)</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b"><code>adf8611</code></a>
fix: use 8bit encoding for message/rfc822 attachments</li>
<li>See full diff in <a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.13">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nodemailer&package-manager=npm_and_yarn&previous-version=7.0.11&new-version=7.0.13)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-19 09:22:17 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ef2a113a16 chore(deps): bump @dagrejs/dagre from 1.1.3 to 1.1.8 (#18753)
Bumps [@dagrejs/dagre](https://github.com/dagrejs/dagre) from 1.1.3 to
1.1.8.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/dagrejs/dagre/commit/7e4d15f191678f7f05f3c86d9071a193230e7e00"><code>7e4d15f</code></a>
Building for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/d3908e2c13148c9143db585accc10ae0b6634657"><code>d3908e2</code></a>
Bumping version</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/ce295f8e073c4fe96c9e36ecf08ae2940e5e6a10"><code>ce295f8</code></a>
Build for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/b64b9057726eee17f24f73579ba0668527276448"><code>b64b905</code></a>
Bumping the version</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/de169d24c13d06c1e9c560f4f4f8f98650109b94"><code>de169d2</code></a>
Merge pull request <a
href="https://redirect.github.com/dagrejs/dagre/issues/481">#481</a>
from Nathan-Fenner/nf/improve-network-simplex-perform...</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/065e0d8374f4c1c35a7cb4b84df37aaa31598d86"><code>065e0d8</code></a>
improve performance of graph node ranking</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/00d3178d671e49de9c032e3abd281dc9f2739e73"><code>00d3178</code></a>
Typo</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/3982a69d2b323b06aa969a4ec09829d37fe6e7bd"><code>3982a69</code></a>
Bump version and set as pre-release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/1339f5516508dba0cbcc4ef1c0587e7384bec23d"><code>1339f55</code></a>
Building for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/9459f01bc815f16b87db727821d8401acbad2cd3"><code>9459f01</code></a>
Bumping the version</li>
<li>Additional commits viewable in <a
href="https://github.com/dagrejs/dagre/compare/v1.1.3...v1.1.8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@dagrejs/dagre&package-manager=npm_and_yarn&previous-version=1.1.3&new-version=1.1.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 09:11:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e0e25eac2b chore(deps): bump @ai-sdk/mistral from 3.0.20 to 3.0.25 (#18755)
Bumps [@ai-sdk/mistral](https://github.com/vercel/ai) from 3.0.20 to
3.0.25.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/ai/commit/4c1613ac8ff9f638da3a25fa6a1aa40362eb0e7c"><code>4c1613a</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13131">#13131</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/64ac0fdd80da66d1e920a8a6309ebe05b9d686ad"><code>64ac0fd</code></a>
Backport: fix(security): validate redirect targets in download functions
to p...</li>
<li><a
href="https://github.com/vercel/ai/commit/f622bf85c114f810b00dd2825b56e044bce578a7"><code>f622bf8</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13126">#13126</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/e2a59ef9273ef2ad97c8aec5bf5c0a7b9e8c436c"><code>e2a59ef</code></a>
Backport: fix(provider/google): preserve groundingMetadata when streamed
befo...</li>
<li><a
href="https://github.com/vercel/ai/commit/ebf43a6765e6fa337e8d5b6cf065625a1794c21c"><code>ebf43a6</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13120">#13120</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/258900473214256af40b035b66f15362cbc53287"><code>2589004</code></a>
Backport: feat(openai): add GPT-5.4 model support (<a
href="https://redirect.github.com/vercel/ai/issues/13117">#13117</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/d23121fd71036611e0142a9233de4d1a1f5d54c0"><code>d23121f</code></a>
Backport: chore(ai): add optional ChatRequestOptions to
`addToolApprovalRespo...</li>
<li><a
href="https://github.com/vercel/ai/commit/55a2acf625c78d5c68ac2abc02bc26c3cd0ffa6e"><code>55a2acf</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13101">#13101</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/45d71c381b756e03310f81bdd322aa7460cb5a15"><code>45d71c3</code></a>
Backport: fix(google): use VALIDATED function calling mode when any tool
has ...</li>
<li><a
href="https://github.com/vercel/ai/commit/ee92dc766422fffde3bae06437510296c4e860a8"><code>ee92dc7</code></a>
ci(release): add <code>--tag ai-v6</code> to <code>ci:release</code>
script (<a
href="https://redirect.github.com/vercel/ai/issues/13096">#13096</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vercel/ai/compare/@ai-sdk/mistral@3.0.20...@ai-sdk/mistral@3.0.25">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@ai-sdk/mistral&package-manager=npm_and_yarn&previous-version=3.0.20&new-version=3.0.25)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 09:10:58 +00:00
Charles BochetandGitHub 8ab8f80687 fix: use unique concurrency group per merge queue entry (#18756)
## Summary

- Fixes merge queue PRs blocking each other by changing the concurrency
group in `ci-merge-queue.yaml`
- The old concurrency group used `merge_group.base_ref` which resolves
to `refs/heads/main` for every PR, causing all merge queue entries to
serialize behind a single concurrency slot
- Now uses `github.ref` (unique per entry:
`refs/heads/gh-readonly-queue/main/pr-NUMBER-SHA`), matching what all
other CI workflows already do

## Recommended ruleset changes (in GitHub Settings > Rules > Rulesets >
"CI Status Checks")

- **Grouping strategy**: Switch `ALLGREEN` to `NONE` -- each PR is still
tested against the correct base (including all PRs ahead of it in the
queue), but failures only affect the failing PR instead of ejecting the
entire group. `max_entries_to_build: 5` still allows parallel
speculative testing.
- **`min_entries_to_merge_wait_minutes`**: Reduce from 5 to 1 -- the
5-minute wait adds unnecessary latency to every merge.

## Test plan

- [ ] Enqueue 2+ PRs in the merge queue and verify both trigger e2e
tests in parallel instead of one blocking the other
2026-03-19 10:10:47 +01:00
a370a26b79 i18n - docs translations (#18749)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-19 09:36:37 +01:00
292 changed files with 10629 additions and 4200 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'merge_group' && github.event.merge_group.base_ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'merge_group' }}
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
e2e-test:
@@ -21,19 +21,51 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
## المتطلبات الأساسية
* Node.js 24+ وYarn 4
* مساحة عمل Twenty ومفتاح واجهة برمجة التطبيقات (أنشئ واحدًا على https://app.twenty.com/settings/api-webhooks)
* Docker (لخادم تطوير Twenty المحلي)
## البدء
أنشئ تطبيقًا جديدًا باستخدام المُهيئ الرسمي، ثم قم بالمصادقة وابدأ التطوير:
أنشئ تطبيقًا جديدًا باستخدام المولّد الرسمي. يمكنه بدء مثيل محلي من Twenty تلقائيًا لك:
```bash filename="Terminal"
# إنشاء تطبيق جديد (يتضمن جميع الأمثلة افتراضيًا)
# إنشاء تطبيق جديد — ستعرض واجهة سطر الأوامر خيار بدء خادم Twenty محلي
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# ابدأ وضع التطوير: يُزامن التغييرات المحلية تلقائيًا مع مساحة العمل الخاصة بك
yarn twenty app:dev
yarn twenty dev
```
### إدارة الخادم المحلي
يتضمن SDK أوامر لإدارة خادم تطوير Twenty محلي (صورة Docker متكاملة تتضمن PostgreSQL وRedis والخادم والعامل):
```bash filename="Terminal"
# ابدأ الخادم المحلي (يسحب الصورة إذا لزم الأمر)
yarn twenty server start
# تحقّق من حالة الخادم
yarn twenty server status
# بثّ سجلات الخادم
yarn twenty server logs
# أوقف الخادم
yarn twenty server stop
# أعد ضبط جميع البيانات وابدأ من جديد
yarn twenty server reset
```
يأتي الخادم المحلي مهيأً مسبقًا بمساحة عمل ومستخدم (`tim@apple.dev` / `tim@apple.dev`)، بحيث يمكنك البدء في التطوير فورًا دون أي إعداد يدوي.
### المصادقة
وصّل تطبيقك بالخادم المحلي باستخدام OAuth:
```bash filename="Terminal"
# المصادقة عبر OAuth (يفتح المتصفح)
yarn twenty remote add --local
```
يدعم المُنشئ وضعين للتحكم في ملفات الأمثلة التي سيتم تضمينها:
@@ -21,19 +21,51 @@ Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. An
## Voraussetzungen
* Node.js 24+ und Yarn 4
* Ein Twenty-Workspace und ein API-Schlüssel (unter https://app.twenty.com/settings/api-webhooks erstellen)
* Docker (für den lokalen Twenty-Dev-Server)
## Erste Schritte
Erstellen Sie mit dem offiziellen Scaffolder eine neue App, authentifizieren Sie sich und beginnen Sie mit der Entwicklung:
Erstelle eine neue App mit dem offiziellen Scaffolder. Der Scaffolder kann für dich automatisch eine lokale Twenty-Instanz starten:
```bash filename="Terminal"
# Eine neue App erstellen (enthält standardmäßig alle Beispiele)
# Eine neue App erstellen — die CLI bietet an, einen lokalen Twenty-Server zu starten
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Dev-Modus starten: synchronisiert lokale Änderungen automatisch mit deinem Arbeitsbereich
yarn twenty app:dev
yarn twenty dev
```
### Lokale Serververwaltung
Das SDK enthält Befehle zur Verwaltung eines lokalen Twenty-Dev-Servers (All-in-One-Docker-Image mit PostgreSQL, Redis, Server und Worker):
```bash filename="Terminal"
# Den lokalen Server starten (lädt das Image bei Bedarf herunter)
yarn twenty server start
# Serverstatus prüfen
yarn twenty server status
# Serverprotokolle streamen
yarn twenty server logs
# Server stoppen
yarn twenty server stop
# Alle Daten zurücksetzen und neu starten
yarn twenty server reset
```
Der lokale Server ist bereits mit einem Arbeitsbereich und einem Benutzer (`tim@apple.dev` / `tim@apple.dev`) vorbefüllt, sodass Sie ohne manuelle Einrichtung sofort mit der Entwicklung beginnen können.
### Authentifizierung
Verbinden Sie Ihre App mithilfe von OAuth mit dem lokalen Server:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
Das Scaffolding-Tool unterstützt zwei Modi, um zu steuern, welche Beispieldateien enthalten sind:
@@ -21,19 +21,51 @@ Le app ti consentono di creare e gestire le personalizzazioni di Twenty **come c
## Prerequisiti
* Node.js 24+ e Yarn 4
* Uno spazio di lavoro Twenty e una chiave API (creane una su https://app.twenty.com/settings/api-webhooks)
* Docker (per il server di sviluppo locale di Twenty)
## Per iniziare
Crea una nuova app utilizzando lo scaffolder ufficiale, quindi autenticati e inizia a sviluppare:
Crea una nuova app utilizzando lo scaffolder ufficiale. Può avviare automaticamente un'istanza locale di Twenty per te:
```bash filename="Terminal"
# Crea lo scaffold di una nuova app (include tutti gli esempi per impostazione predefinita)
# Crea lo scaffold di una nuova app — la CLI offrirà di avviare un server locale di Twenty
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Avvia la modalità di sviluppo: sincronizza automaticamente le modifiche locali con il tuo workspace
yarn twenty app:dev
yarn twenty dev
```
### Gestione del server locale
L'SDK include comandi per gestire un server di sviluppo locale di Twenty (immagine Docker all-in-one con PostgreSQL, Redis, server e worker):
```bash filename="Terminal"
# Avvia il server locale (scarica l'immagine se necessario)
yarn twenty server start
# Verifica lo stato del server
yarn twenty server status
# Segui i log del server
yarn twenty server logs
# Arresta il server
yarn twenty server stop
# Reimposta tutti i dati e riparti da zero
yarn twenty server reset
```
Il server locale è preconfigurato con uno spazio di lavoro e un utente (`tim@apple.dev` / `tim@apple.dev`), così puoi iniziare a sviluppare immediatamente senza alcuna configurazione manuale.
### Autenticazione
Collega la tua app al server locale tramite OAuth:
```bash filename="Terminal"
# Autenticati tramite OAuth (apre il browser)
yarn twenty remote add --local
```
Lo strumento di scaffolding supporta due modalità per controllare quali file di esempio vengono inclusi:
@@ -21,19 +21,51 @@ Os aplicativos permitem criar e gerenciar personalizações do Twenty **como có
## Pré-requisitos
* Node.js 24+ e Yarn 4
* Um espaço de trabalho do Twenty e uma chave de API (crie uma em https://app.twenty.com/settings/api-webhooks)
* Docker (para o servidor de desenvolvimento local do Twenty)
## Primeiros passos
Crie um novo aplicativo usando o gerador oficial, depois autentique-se e comece a desenvolver:
Crie um novo app usando o gerador oficial de estrutura. Ele pode iniciar automaticamente uma instância local do Twenty para você:
```bash filename="Terminal"
# Criar a estrutura de um novo app (inclui todos os exemplos por padrão)
# Criar a estrutura de um novo app — a CLI oferecerá iniciar um servidor local do Twenty
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Iniciar modo de desenvolvimento: sincroniza automaticamente as alterações locais com seu workspace
yarn twenty app:dev
yarn twenty dev
```
### Gerenciamento do Servidor Local
O SDK inclui comandos para gerenciar um servidor de desenvolvimento local do Twenty (imagem Docker all-in-one com PostgreSQL, Redis, servidor e worker):
```bash filename="Terminal"
# Iniciar o servidor local (faz pull da imagem se necessário)
yarn twenty server start
# Verificar o status do servidor
yarn twenty server status
# Transmitir os logs do servidor
yarn twenty server logs
# Parar o servidor
yarn twenty server stop
# Redefinir todos os dados e começar do zero
yarn twenty server reset
```
O servidor local já vem pré-configurado com um espaço de trabalho e um usuário (`tim@apple.dev` / `tim@apple.dev`), para que você possa começar a desenvolver imediatamente, sem qualquer configuração manual.
### Autenticação
Conecte seu aplicativo ao servidor local usando OAuth:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
O gerador de estrutura oferece suporte a dois modos para controlar quais arquivos de exemplo são incluídos:
@@ -21,19 +21,51 @@ Aplicațiile vă permit să construiți și să gestionați personalizările Twe
## Cerințe
* Node.js 24+ și Yarn 4
* Un spațiu de lucru Twenty și o cheie API (creați una la https://app.twenty.com/settings/api-webhooks)
* Docker (pentru serverul local de dezvoltare Twenty)
## Începeți
Creați o aplicație nouă folosind generatorul oficial, apoi autentificați-vă și începeți să dezvoltați:
Creează o aplicație nouă folosind generatorul oficial. Poate porni automat o instanță Twenty locală pentru tine:
```bash filename="Terminal"
# Creează scheletul unei aplicații noi (include toate exemplele în mod implicit)
# Creează scheletul unei aplicații noi — CLI-ul îți va oferi opțiunea de a porni un server Twenty local
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Pornește modul de dezvoltare: sincronizează automat modificările locale cu spațiul tău de lucru
yarn twenty app:dev
yarn twenty dev
```
### Gestionarea serverului local
SDK-ul include comenzi pentru a gestiona un server local de dezvoltare Twenty (imagine Docker all-in-one cu PostgreSQL, Redis, server și worker):
```bash filename="Terminal"
# Pornește serverul local (descarcă imaginea dacă este necesar)
yarn twenty server start
# Verifică starea serverului
yarn twenty server status
# Afișează în timp real jurnalele serverului
yarn twenty server logs
# Oprește serverul
yarn twenty server stop
# Resetează toate datele și pornește de la zero
yarn twenty server reset
```
Serverul local vine preconfigurat cu un spațiu de lucru și un utilizator (`tim@apple.dev` / `tim@apple.dev`), astfel încât să poți începe să dezvolți imediat, fără nicio configurare manuală.
### Autentificare
Conectează-ți aplicația la serverul local folosind OAuth:
```bash filename="Terminal"
# Autentifică-te prin OAuth (se deschide browserul)
yarn twenty remote add --local
```
Generatorul de schelet acceptă două moduri pentru a controla ce fișiere de exemplu sunt incluse:
@@ -21,19 +21,51 @@ description: Создавайте и управляйте настройками
## Требования
* Node.js 24+ и Yarn 4
* Рабочее пространство Twenty и ключ API (создайте его на https://app.twenty.com/settings/api-webhooks)
* Docker (для локального сервера разработки Twenty)
## Начало работы
Создайте новое приложение с помощью официального генератора, затем выполните аутентификацию и начните разработку:
Создайте новое приложение с помощью официального генератора каркаса. Он может автоматически запустить локальный экземпляр Twenty:
```bash filename="Terminal"
# Создать каркас нового приложения (по умолчанию включает все примеры)
# Создать каркас нового приложения — CLI предложит запустить локальный сервер Twenty
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Запустить режим разработки: автоматически синхронизирует локальные изменения с вашим рабочим пространством
yarn twenty app:dev
yarn twenty dev
```
### Управление локальным сервером
SDK включает команды для управления локальным сервером разработки Twenty (универсальный образ Docker с PostgreSQL, Redis, сервером и воркером):
```bash filename="Terminal"
# Запустить локальный сервер (при необходимости будет загружен образ)
yarn twenty server start
# Проверить статус сервера
yarn twenty server status
# Просмотр логов сервера в реальном времени
yarn twenty server logs
# Остановить сервер
yarn twenty server stop
# Сбросить все данные и начать с нуля
yarn twenty server reset
```
Локальный сервер уже содержит рабочее пространство и пользователя (`tim@apple.dev` / `tim@apple.dev`), так что вы можете сразу начать разработку без какой-либо ручной настройки.
### Аутентификация
Подключите своё приложение к локальному серверу с помощью OAuth:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
Генератор каркаса поддерживает два режима для управления тем, какие файлы-примеры включаются:
@@ -21,19 +21,51 @@ Uygulamalar, Twenty özelleştirmelerini **kod olarak** oluşturup yönetmenizi
## Ön Gereksinimler
* Node.js 24+ ve Yarn 4
* Bir Twenty çalışma alanı ve bir API anahtarı (https://app.twenty.com/settings/api-webhooks adresinde oluşturun)
* Docker (yerel Twenty geliştirme sunucusu için)
## Başlarken
Resmi scaffolder aracını kullanarak yeni bir uygulama oluşturun, ardından kimlik doğrulaması yapıp geliştirmeye başlayın:
Resmi iskelet oluşturucusunu kullanarak yeni bir uygulama oluşturun. Sizin için otomatik olarak yerel bir Twenty örneğini başlatabilir:
```bash filename="Terminal"
# Yeni bir uygulamanın iskeletini oluşturun (varsayılan olarak tüm örnekleri içerir)
# Yeni bir uygulamanın iskeletini oluşturun — CLI yerel bir Twenty sunucusunu başlatmayı önerecektir
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Geliştirme modunu başlatın: yerel değişiklikleri otomatik olarak çalışma alanınızla senkronize eder
yarn twenty app:dev
# Geliştirme modunu başlatın: yerel değişiklikleri çalışma alanınızla otomatik olarak senkronize eder
yarn twenty dev
```
### Yerel Sunucu Yönetimi
SDK, yerel bir Twenty geliştirme sunucusunu yönetmek için komutlar içerir (PostgreSQL, Redis, sunucu ve worker içeren hepsi bir arada Docker imajı):
```bash filename="Terminal"
# Yerel sunucuyu başlatın (gerekirse imajı indirir)
yarn twenty server start
# Sunucu durumunu kontrol edin
yarn twenty server status
# Sunucu günlüklerini akış olarak görüntüleyin
yarn twenty server logs
# Sunucuyu durdurun
yarn twenty server stop
# Tüm verileri sıfırlayın ve temiz bir başlangıç yapın
yarn twenty server reset
```
Yerel sunucu, bir çalışma alanı ve kullanıcıyla (`tim@apple.dev` / `tim@apple.dev`) önceden yapılandırılmış olarak gelir; böylece herhangi bir manuel kurulum gerektirmeden hemen geliştirmeye başlayabilirsiniz.
### Kimlik Doğrulama
Uygulamanızı OAuth kullanarak yerel sunucuya bağlayın:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
İskelet oluşturucu, hangi örnek dosyaların dahil edileceğini kontrol etmek için iki modu destekler:
@@ -21,19 +21,51 @@ description: 以代码的形式构建并管理 Twenty 自定义项。
## 先决条件
* Node.js 24+ 和 Yarn 4
* 一个 Twenty 工作空间和一个 API 密钥(在 https://app.twenty.com/settings/api-webhooks 创建)
* Docker (用于本地 Twenty 开发服务器)
## 开始使用
使用官方脚手架创建一个新应用,然后进行身份验证并开始开发
使用官方脚手架创建一个新应用。 它可以为你自动启动一个本地 Twenty 实例
```bash filename="Terminal"
# 创建一个新应用脚手架(默认包含所有示例)
# Scaffold a new app — the CLI will offer to start a local Twenty server
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# 启动开发模式:会将本地更改自动同步到你的工作区
yarn twenty app:dev
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
### 本地服务器管理
该 SDK 包含用于管理本地 Twenty 开发服务器的命令(该服务器是一体化 Docker 镜像,内含 PostgreSQL、Redis、服务器和工作进程):
```bash filename="Terminal"
# Start the local server (pulls the image if needed)
yarn twenty server start
# Check server status
yarn twenty server status
# Stream server logs
yarn twenty server logs
# Stop the server
yarn twenty server stop
# Reset all data and start fresh
yarn twenty server reset
```
本地服务器预置了一个工作区和用户 (`tim@apple.dev` / `tim@apple.dev`),因此你可以无需任何手动设置即可立即开始开发。
### 身份验证
使用 OAuth 将你的应用连接到本地服务器:
```bash filename="Terminal"
# 通过 OAuth 进行身份验证 (将打开浏览器)
yarn twenty remote add --local
```
脚手架工具支持两种模式,用于控制包含哪些示例文件:
@@ -761,6 +761,56 @@ export type BooleanFieldComparison = {
isNot?: InputMaybe<Scalars['Boolean']>;
};
export type CalendarChannel = {
__typename?: 'CalendarChannel';
connectedAccountId: Scalars['UUID'];
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy;
createdAt: Scalars['DateTime'];
handle: Scalars['String'];
id: Scalars['UUID'];
isContactAutoCreationEnabled: Scalars['Boolean'];
isSyncEnabled: Scalars['Boolean'];
syncCursor?: Maybe<Scalars['String']>;
syncStage: CalendarChannelSyncStage;
syncStageStartedAt?: Maybe<Scalars['DateTime']>;
syncStatus: CalendarChannelSyncStatus;
syncedAt?: Maybe<Scalars['DateTime']>;
throttleFailureCount: Scalars['Float'];
updatedAt: Scalars['DateTime'];
visibility: CalendarChannelVisibility;
};
export enum CalendarChannelContactAutoCreationPolicy {
AS_ORGANIZER = 'AS_ORGANIZER',
AS_PARTICIPANT = 'AS_PARTICIPANT',
AS_PARTICIPANT_AND_ORGANIZER = 'AS_PARTICIPANT_AND_ORGANIZER',
NONE = 'NONE'
}
export enum CalendarChannelSyncStage {
CALENDAR_EVENTS_IMPORT_ONGOING = 'CALENDAR_EVENTS_IMPORT_ONGOING',
CALENDAR_EVENTS_IMPORT_PENDING = 'CALENDAR_EVENTS_IMPORT_PENDING',
CALENDAR_EVENTS_IMPORT_SCHEDULED = 'CALENDAR_EVENTS_IMPORT_SCHEDULED',
CALENDAR_EVENT_LIST_FETCH_ONGOING = 'CALENDAR_EVENT_LIST_FETCH_ONGOING',
CALENDAR_EVENT_LIST_FETCH_PENDING = 'CALENDAR_EVENT_LIST_FETCH_PENDING',
CALENDAR_EVENT_LIST_FETCH_SCHEDULED = 'CALENDAR_EVENT_LIST_FETCH_SCHEDULED',
FAILED = 'FAILED',
PENDING_CONFIGURATION = 'PENDING_CONFIGURATION'
}
export enum CalendarChannelSyncStatus {
ACTIVE = 'ACTIVE',
FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS',
FAILED_UNKNOWN = 'FAILED_UNKNOWN',
NOT_SYNCED = 'NOT_SYNCED',
ONGOING = 'ONGOING'
}
export enum CalendarChannelVisibility {
METADATA = 'METADATA',
SHARE_EVERYTHING = 'SHARE_EVERYTHING'
}
export type CalendarConfiguration = {
__typename?: 'CalendarConfiguration';
configurationType: WidgetConfigurationType;
@@ -927,6 +977,25 @@ export type ConfigVariablesGroupData = {
variables: Array<ConfigVariable>;
};
export type ConnectedAccountDto = {
__typename?: 'ConnectedAccountDTO';
accessToken?: Maybe<Scalars['String']>;
authFailedAt?: Maybe<Scalars['DateTime']>;
connectionParameters?: Maybe<Scalars['JSON']>;
createdAt: Scalars['DateTime'];
handle: Scalars['String'];
handleAliases?: Maybe<Array<Scalars['String']>>;
id: Scalars['UUID'];
lastCredentialsRefreshedAt?: Maybe<Scalars['DateTime']>;
lastSignedInAt?: Maybe<Scalars['DateTime']>;
oidcTokenClaims?: Maybe<Scalars['JSON']>;
provider: Scalars['String'];
refreshToken?: Maybe<Scalars['String']>;
scopes?: Maybe<Array<Scalars['String']>>;
updatedAt: Scalars['DateTime'];
userWorkspaceId: Scalars['UUID'];
};
export type ConnectedImapSmtpCaldavAccount = {
__typename?: 'ConnectedImapSmtpCaldavAccount';
accountOwnerId: Scalars['UUID'];
@@ -1008,6 +1077,17 @@ export type CreateApprovedAccessDomainInput = {
email: Scalars['String'];
};
export type CreateCalendarChannelInput = {
connectedAccountId: Scalars['UUID'];
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy;
handle: Scalars['String'];
id?: InputMaybe<Scalars['UUID']>;
isContactAutoCreationEnabled: Scalars['Boolean'];
isSyncEnabled: Scalars['Boolean'];
syncStage: CalendarChannelSyncStage;
visibility: CalendarChannelVisibility;
};
export type CreateCommandMenuItemInput = {
availabilityObjectMetadataId?: InputMaybe<Scalars['UUID']>;
availabilityType?: InputMaybe<CommandMenuItemAvailabilityType>;
@@ -1023,6 +1103,16 @@ export type CreateCommandMenuItemInput = {
workflowVersionId?: InputMaybe<Scalars['UUID']>;
};
export type CreateConnectedAccountInput = {
accessToken?: InputMaybe<Scalars['String']>;
handle: Scalars['String'];
id?: InputMaybe<Scalars['UUID']>;
provider: Scalars['String'];
refreshToken?: InputMaybe<Scalars['String']>;
scopes?: InputMaybe<Array<Scalars['String']>>;
userWorkspaceId: Scalars['UUID'];
};
export type CreateFieldInput = {
defaultValue?: InputMaybe<Scalars['JSON']>;
description?: InputMaybe<Scalars['String']>;
@@ -1069,6 +1159,33 @@ export type CreateLogicFunctionFromSourceInput = {
universalIdentifier?: InputMaybe<Scalars['UUID']>;
};
export type CreateMessageChannelInput = {
connectedAccountId: Scalars['UUID'];
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
excludeGroupEmails: Scalars['Boolean'];
excludeNonProfessionalEmails: Scalars['Boolean'];
handle: Scalars['String'];
id?: InputMaybe<Scalars['UUID']>;
isContactAutoCreationEnabled: Scalars['Boolean'];
isSyncEnabled: Scalars['Boolean'];
messageFolderImportPolicy: MessageFolderImportPolicy;
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
syncStage: MessageChannelSyncStage;
type: MessageChannelType;
visibility: MessageChannelVisibility;
};
export type CreateMessageFolderInput = {
externalId?: InputMaybe<Scalars['String']>;
id?: InputMaybe<Scalars['UUID']>;
isSentFolder: Scalars['Boolean'];
isSynced: Scalars['Boolean'];
messageChannelId: Scalars['UUID'];
name?: InputMaybe<Scalars['String']>;
parentFolderId?: InputMaybe<Scalars['UUID']>;
pendingSyncAction: MessageFolderPendingSyncAction;
};
export type CreateNavigationMenuItemInput = {
color?: InputMaybe<Scalars['String']>;
folderId?: InputMaybe<Scalars['UUID']>;
@@ -1618,6 +1735,7 @@ export enum FeatureFlagKey {
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED',
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
IS_CONNECTED_ACCOUNT_MIGRATED = 'IS_CONNECTED_ACCOUNT_MIGRATED',
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED = 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED',
IS_DRAFT_EMAIL_ENABLED = 'IS_DRAFT_EMAIL_ENABLED',
@@ -2289,6 +2407,98 @@ export type MarketplaceAppRoleObjectPermission = {
objectUniversalIdentifier: Scalars['String'];
};
export type MessageChannel = {
__typename?: 'MessageChannel';
connectedAccountId: Scalars['UUID'];
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
createdAt: Scalars['DateTime'];
excludeGroupEmails: Scalars['Boolean'];
excludeNonProfessionalEmails: Scalars['Boolean'];
handle: Scalars['String'];
id: Scalars['UUID'];
isContactAutoCreationEnabled: Scalars['Boolean'];
isSyncEnabled: Scalars['Boolean'];
messageFolderImportPolicy: MessageFolderImportPolicy;
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
syncCursor?: Maybe<Scalars['String']>;
syncStage: MessageChannelSyncStage;
syncStageStartedAt?: Maybe<Scalars['DateTime']>;
syncStatus: MessageChannelSyncStatus;
syncedAt?: Maybe<Scalars['DateTime']>;
throttleFailureCount: Scalars['Float'];
throttleRetryAfter?: Maybe<Scalars['DateTime']>;
type: MessageChannelType;
updatedAt: Scalars['DateTime'];
visibility: MessageChannelVisibility;
};
export enum MessageChannelContactAutoCreationPolicy {
NONE = 'NONE',
SENT = 'SENT',
SENT_AND_RECEIVED = 'SENT_AND_RECEIVED'
}
export enum MessageChannelPendingGroupEmailsAction {
GROUP_EMAILS_DELETION = 'GROUP_EMAILS_DELETION',
GROUP_EMAILS_IMPORT = 'GROUP_EMAILS_IMPORT',
NONE = 'NONE'
}
export enum MessageChannelSyncStage {
FAILED = 'FAILED',
MESSAGES_IMPORT_ONGOING = 'MESSAGES_IMPORT_ONGOING',
MESSAGES_IMPORT_PENDING = 'MESSAGES_IMPORT_PENDING',
MESSAGES_IMPORT_SCHEDULED = 'MESSAGES_IMPORT_SCHEDULED',
MESSAGE_LIST_FETCH_ONGOING = 'MESSAGE_LIST_FETCH_ONGOING',
MESSAGE_LIST_FETCH_PENDING = 'MESSAGE_LIST_FETCH_PENDING',
MESSAGE_LIST_FETCH_SCHEDULED = 'MESSAGE_LIST_FETCH_SCHEDULED',
PENDING_CONFIGURATION = 'PENDING_CONFIGURATION'
}
export enum MessageChannelSyncStatus {
ACTIVE = 'ACTIVE',
FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS',
FAILED_UNKNOWN = 'FAILED_UNKNOWN',
NOT_SYNCED = 'NOT_SYNCED',
ONGOING = 'ONGOING'
}
export enum MessageChannelType {
EMAIL = 'EMAIL',
SMS = 'SMS'
}
export enum MessageChannelVisibility {
METADATA = 'METADATA',
SHARE_EVERYTHING = 'SHARE_EVERYTHING',
SUBJECT = 'SUBJECT'
}
export type MessageFolder = {
__typename?: 'MessageFolder';
createdAt: Scalars['DateTime'];
externalId?: Maybe<Scalars['String']>;
id: Scalars['UUID'];
isSentFolder: Scalars['Boolean'];
isSynced: Scalars['Boolean'];
messageChannelId: Scalars['UUID'];
name?: Maybe<Scalars['String']>;
parentFolderId?: Maybe<Scalars['UUID']>;
pendingSyncAction: MessageFolderPendingSyncAction;
syncCursor?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
};
export enum MessageFolderImportPolicy {
ALL_FOLDERS = 'ALL_FOLDERS',
SELECTED_FOLDERS = 'SELECTED_FOLDERS'
}
export enum MessageFolderPendingSyncAction {
FOLDER_DELETION = 'FOLDER_DELETION',
NONE = 'NONE'
}
export type MetadataEvent = {
__typename?: 'MetadataEvent';
metadataName: Scalars['String'];
@@ -2361,8 +2571,10 @@ export type Mutation = {
createApplicationRegistration: CreateApplicationRegistration;
createApplicationRegistrationVariable: ApplicationRegistrationVariable;
createApprovedAccessDomain: ApprovedAccessDomain;
createCalendarChannel: CalendarChannel;
createChatThread: AgentChatThread;
createCommandMenuItem: CommandMenuItem;
createConnectedAccount: ConnectedAccountDto;
createDatabaseConfigVariable: Scalars['Boolean'];
createDevelopmentApplication: DevelopmentApplication;
createEmailingDomain: EmailingDomain;
@@ -2370,6 +2582,8 @@ export type Mutation = {
createManyViewFieldGroups: Array<ViewFieldGroup>;
createManyViewFields: Array<ViewField>;
createManyViewGroups: Array<ViewGroup>;
createMessageChannel: MessageChannel;
createMessageFolder: MessageFolder;
createNavigationMenuItem: NavigationMenuItem;
createOIDCIdentityProvider: SetupSso;
createObjectEvent: Analytics;
@@ -2397,12 +2611,16 @@ export type Mutation = {
deleteApplicationRegistration: Scalars['Boolean'];
deleteApplicationRegistrationVariable: Scalars['Boolean'];
deleteApprovedAccessDomain: Scalars['Boolean'];
deleteCalendarChannel: CalendarChannel;
deleteCommandMenuItem: CommandMenuItem;
deleteConnectedAccount: ConnectedAccountDto;
deleteCurrentWorkspace: Workspace;
deleteDatabaseConfigVariable: Scalars['Boolean'];
deleteEmailingDomain: Scalars['Boolean'];
deleteFrontComponent: FrontComponent;
deleteJobs: DeleteJobsResponse;
deleteMessageChannel: MessageChannel;
deleteMessageFolder: MessageFolder;
deleteNavigationMenuItem: NavigationMenuItem;
deleteOneAgent: Agent;
deleteOneField: Field;
@@ -2487,10 +2705,14 @@ export type Mutation = {
updateApiKey?: Maybe<ApiKey>;
updateApplicationRegistration: ApplicationRegistration;
updateApplicationRegistrationVariable: ApplicationRegistrationVariable;
updateCalendarChannel: CalendarChannel;
updateCommandMenuItem: CommandMenuItem;
updateConnectedAccount: ConnectedAccountDto;
updateDatabaseConfigVariable: Scalars['Boolean'];
updateFrontComponent: FrontComponent;
updateLabPublicFeatureFlag: FeatureFlag;
updateMessageChannel: MessageChannel;
updateMessageFolder: MessageFolder;
updateNavigationMenuItem: NavigationMenuItem;
updateOneAgent: Agent;
updateOneApplicationVariable: Scalars['Boolean'];
@@ -2608,11 +2830,21 @@ export type MutationCreateApprovedAccessDomainArgs = {
};
export type MutationCreateCalendarChannelArgs = {
input: CreateCalendarChannelInput;
};
export type MutationCreateCommandMenuItemArgs = {
input: CreateCommandMenuItemInput;
};
export type MutationCreateConnectedAccountArgs = {
input: CreateConnectedAccountInput;
};
export type MutationCreateDatabaseConfigVariableArgs = {
key: Scalars['String'];
value: Scalars['JSON'];
@@ -2651,6 +2883,16 @@ export type MutationCreateManyViewGroupsArgs = {
};
export type MutationCreateMessageChannelArgs = {
input: CreateMessageChannelInput;
};
export type MutationCreateMessageFolderArgs = {
input: CreateMessageFolderInput;
};
export type MutationCreateNavigationMenuItemArgs = {
input: CreateNavigationMenuItemInput;
};
@@ -2789,11 +3031,21 @@ export type MutationDeleteApprovedAccessDomainArgs = {
};
export type MutationDeleteCalendarChannelArgs = {
id: Scalars['UUID'];
};
export type MutationDeleteCommandMenuItemArgs = {
id: Scalars['UUID'];
};
export type MutationDeleteConnectedAccountArgs = {
id: Scalars['UUID'];
};
export type MutationDeleteDatabaseConfigVariableArgs = {
key: Scalars['String'];
};
@@ -2815,6 +3067,16 @@ export type MutationDeleteJobsArgs = {
};
export type MutationDeleteMessageChannelArgs = {
id: Scalars['UUID'];
};
export type MutationDeleteMessageFolderArgs = {
id: Scalars['UUID'];
};
export type MutationDeleteNavigationMenuItemArgs = {
id: Scalars['UUID'];
};
@@ -3217,11 +3479,21 @@ export type MutationUpdateApplicationRegistrationVariableArgs = {
};
export type MutationUpdateCalendarChannelArgs = {
input: UpdateCalendarChannelInput;
};
export type MutationUpdateCommandMenuItemArgs = {
input: UpdateCommandMenuItemInput;
};
export type MutationUpdateConnectedAccountArgs = {
input: UpdateConnectedAccountInput;
};
export type MutationUpdateDatabaseConfigVariableArgs = {
key: Scalars['String'];
value: Scalars['JSON'];
@@ -3238,6 +3510,16 @@ export type MutationUpdateLabPublicFeatureFlagArgs = {
};
export type MutationUpdateMessageChannelArgs = {
input: UpdateMessageChannelInput;
};
export type MutationUpdateMessageFolderArgs = {
input: UpdateMessageFolderInput;
};
export type MutationUpdateNavigationMenuItemArgs = {
input: UpdateOneNavigationMenuItemInput;
};
@@ -3687,21 +3969,6 @@ export type ObjectStandardOverrides = {
translations?: Maybe<Scalars['JSON']>;
};
export type OnDbEvent = {
__typename?: 'OnDbEvent';
action: DatabaseEventAction;
eventDate: Scalars['DateTime'];
objectNameSingular: Scalars['String'];
record: Scalars['JSON'];
updatedFields?: Maybe<Array<Scalars['String']>>;
};
export type OnDbEventInput = {
action?: InputMaybe<DatabaseEventAction>;
objectNameSingular?: InputMaybe<Scalars['String']>;
recordId?: InputMaybe<Scalars['UUID']>;
};
/** Onboarding status */
export enum OnboardingStatus {
BOOK_ONBOARDING = 'BOOK_ONBOARDING',
@@ -3954,6 +4221,8 @@ export type Query = {
applicationRegistrationTarballUrl?: Maybe<Scalars['String']>;
barChartData: BarChartData;
billingPortalSession: BillingSession;
calendarChannel?: Maybe<CalendarChannel>;
calendarChannels: Array<CalendarChannel>;
chatMessages: Array<AgentMessage>;
chatThread: AgentChatThread;
chatThreads: AgentChatThreadConnection;
@@ -3961,6 +4230,8 @@ export type Query = {
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
commandMenuItem?: Maybe<CommandMenuItem>;
commandMenuItems: Array<CommandMenuItem>;
connectedAccount?: Maybe<ConnectedAccountDto>;
connectedAccounts: Array<ConnectedAccountDto>;
currentUser: User;
currentWorkspace: Workspace;
enterpriseCheckoutSession?: Maybe<Scalars['String']>;
@@ -4035,6 +4306,10 @@ export type Query = {
indexMetadatas: IndexConnection;
lineChartData: LineChartData;
listPlans: Array<BillingPlan>;
messageChannel?: Maybe<MessageChannel>;
messageChannels: Array<MessageChannel>;
messageFolder?: Maybe<MessageFolder>;
messageFolders: Array<MessageFolder>;
minimalMetadata: MinimalMetadata;
navigationMenuItem?: Maybe<NavigationMenuItem>;
navigationMenuItems: Array<NavigationMenuItem>;
@@ -4076,6 +4351,16 @@ export type QueryBillingPortalSessionArgs = {
};
export type QueryCalendarChannelArgs = {
id: Scalars['UUID'];
};
export type QueryCalendarChannelsArgs = {
connectedAccountId?: InputMaybe<Scalars['UUID']>;
};
export type QueryChatMessagesArgs = {
threadId: Scalars['UUID'];
};
@@ -4109,6 +4394,11 @@ export type QueryCommandMenuItemArgs = {
};
export type QueryConnectedAccountArgs = {
id: Scalars['UUID'];
};
export type QueryEnterpriseCheckoutSessionArgs = {
billingInterval?: InputMaybe<Scalars['String']>;
};
@@ -4372,6 +4662,26 @@ export type QueryLineChartDataArgs = {
};
export type QueryMessageChannelArgs = {
id: Scalars['UUID'];
};
export type QueryMessageChannelsArgs = {
connectedAccountId?: InputMaybe<Scalars['UUID']>;
};
export type QueryMessageFolderArgs = {
id: Scalars['UUID'];
};
export type QueryMessageFoldersArgs = {
messageChannelId?: InputMaybe<Scalars['UUID']>;
};
export type QueryNavigationMenuItemArgs = {
id: Scalars['UUID'];
};
@@ -4738,7 +5048,6 @@ export type StandardOverrides = {
export type Subscription = {
__typename?: 'Subscription';
logicFunctionLogs: LogicFunctionLogs;
onDbEvent: OnDbEvent;
onEventSubscription?: Maybe<EventSubscription>;
};
@@ -4748,11 +5057,6 @@ export type SubscriptionLogicFunctionLogsArgs = {
};
export type SubscriptionOnDbEventArgs = {
input: OnDbEventInput;
};
export type SubscriptionOnEventSubscriptionArgs = {
eventStreamId: Scalars['String'];
};
@@ -4892,6 +5196,18 @@ export type UpdateApplicationRegistrationVariablePayload = {
value?: InputMaybe<Scalars['String']>;
};
export type UpdateCalendarChannelInput = {
id: Scalars['UUID'];
update: UpdateCalendarChannelInputUpdates;
};
export type UpdateCalendarChannelInputUpdates = {
contactAutoCreationPolicy?: InputMaybe<CalendarChannelContactAutoCreationPolicy>;
isContactAutoCreationEnabled?: InputMaybe<Scalars['Boolean']>;
isSyncEnabled?: InputMaybe<Scalars['Boolean']>;
visibility?: InputMaybe<CalendarChannelVisibility>;
};
export type UpdateCommandMenuItemInput = {
availabilityObjectMetadataId?: InputMaybe<Scalars['UUID']>;
availabilityType?: InputMaybe<CommandMenuItemAvailabilityType>;
@@ -4905,6 +5221,18 @@ export type UpdateCommandMenuItemInput = {
shortLabel?: InputMaybe<Scalars['String']>;
};
export type UpdateConnectedAccountInput = {
id: Scalars['UUID'];
update: UpdateConnectedAccountInputUpdates;
};
export type UpdateConnectedAccountInputUpdates = {
accessToken?: InputMaybe<Scalars['String']>;
handleAliases?: InputMaybe<Array<Scalars['String']>>;
refreshToken?: InputMaybe<Scalars['String']>;
scopes?: InputMaybe<Array<Scalars['String']>>;
};
export type UpdateFieldInput = {
defaultValue?: InputMaybe<Scalars['JSON']>;
description?: InputMaybe<Scalars['String']>;
@@ -4961,6 +5289,33 @@ export type UpdateLogicFunctionFromSourceInputUpdates = {
toolInputSchema?: InputMaybe<Scalars['JSON']>;
};
export type UpdateMessageChannelInput = {
id: Scalars['UUID'];
update: UpdateMessageChannelInputUpdates;
};
export type UpdateMessageChannelInputUpdates = {
contactAutoCreationPolicy?: InputMaybe<MessageChannelContactAutoCreationPolicy>;
excludeGroupEmails?: InputMaybe<Scalars['Boolean']>;
excludeNonProfessionalEmails?: InputMaybe<Scalars['Boolean']>;
isContactAutoCreationEnabled?: InputMaybe<Scalars['Boolean']>;
isSyncEnabled?: InputMaybe<Scalars['Boolean']>;
messageFolderImportPolicy?: InputMaybe<MessageFolderImportPolicy>;
visibility?: InputMaybe<MessageChannelVisibility>;
};
export type UpdateMessageFolderInput = {
id: Scalars['UUID'];
update: UpdateMessageFolderInputUpdates;
};
export type UpdateMessageFolderInputUpdates = {
isSynced?: InputMaybe<Scalars['Boolean']>;
name?: InputMaybe<Scalars['String']>;
pendingSyncAction?: InputMaybe<MessageFolderPendingSyncAction>;
syncCursor?: InputMaybe<Scalars['String']>;
};
export type UpdateNavigationMenuItemInput = {
color?: InputMaybe<Scalars['String']>;
folderId?: InputMaybe<Scalars['UUID']>;
@@ -72,11 +72,13 @@ export const AgentChatFilePreview = ({
);
const rightComponent = onRemove ? (
<AvatarOrIcon
Icon={IconX}
IconColor={theme.font.color.secondary}
onClick={onRemove}
/>
<div onClick={(e) => e.stopPropagation()}>
<AvatarOrIcon
Icon={IconX}
IconColor={theme.font.color.secondary}
onClick={onRemove}
/>
</div>
) : undefined;
const hasRightDivider = isDefined(onRemove);
@@ -1,8 +1,3 @@
import { useMemo } from 'react';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
@@ -10,68 +5,28 @@ import { isDefined } from 'twenty-shared/utils';
import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
export const useWorkspaceNavigationMenuItems = (): {
workspaceNavigationMenuItemsObjectMetadataItems: ObjectMetadataItem[];
objectMetadataIdsInWorkspaceNav: Set<string>;
} => {
const { workspaceNavigationMenuItems: rawWorkspaceNavigationMenuItems } =
useNavigationMenuItemsData();
const views = useAtomStateValue(viewsSelector);
const workspaceFolderIds = useMemo(
() =>
new Set(
rawWorkspaceNavigationMenuItems
.filter(isNavigationMenuItemFolder)
.map((item) => item.id),
),
[rawWorkspaceNavigationMenuItems],
);
const workspaceNavigationMenuItemsIncludingFolderItems = useMemo(
() =>
rawWorkspaceNavigationMenuItems.filter(
(item) =>
!isDefined(item.folderId) ||
(isDefined(item.folderId) && workspaceFolderIds.has(item.folderId)),
),
[rawWorkspaceNavigationMenuItems, workspaceFolderIds],
);
const workspaceNavigationMenuItemViewIds = new Set(
workspaceNavigationMenuItemsIncludingFolderItems
const workspaceNavViewIds = new Set(
rawWorkspaceNavigationMenuItems
.map((item) => item.viewId)
.filter((viewId) => isDefined(viewId)),
);
const navigationMenuItemViewObjectMetadataIds = new Set(
views.reduce<string[]>((acc, view) => {
if (workspaceNavigationMenuItemViewIds.has(view.id)) {
acc.push(view.objectMetadataId);
}
return acc;
}, []),
);
const navigationMenuItemRecordObjectMetadataIds = new Set(
workspaceNavigationMenuItemsIncludingFolderItems
const objectMetadataIdsInWorkspaceNav = new Set([
...views
.filter((view) => workspaceNavViewIds.has(view.id))
.map((view) => view.objectMetadataId),
...rawWorkspaceNavigationMenuItems
.map((item) => item.targetObjectMetadataId)
.filter((objectMetadataId) => isDefined(objectMetadataId)),
);
const allNavigationMenuItemObjectMetadataIds = new Set([
...navigationMenuItemViewObjectMetadataIds,
...navigationMenuItemRecordObjectMetadataIds,
]);
const { activeNonSystemObjectMetadataItems } =
useFilteredObjectMetadataItems();
const activeNonSystemObjectMetadataItemsInWorkspaceNavigationMenuItems: ObjectMetadataItem[] =
activeNonSystemObjectMetadataItems.filter((item: ObjectMetadataItem) =>
allNavigationMenuItemObjectMetadataIds.has(item.id),
);
return {
workspaceNavigationMenuItemsObjectMetadataItems:
activeNonSystemObjectMetadataItemsInWorkspaceNavigationMenuItems,
objectMetadataIdsInWorkspaceNav,
};
};
@@ -1,14 +0,0 @@
import { normalizeUrl } from '@/navigation-menu-item/display/link/utils/normalizeUrl';
describe('normalizeUrl', () => {
it('should leave url unchanged when it has protocol, otherwise prepend https', () => {
expect(normalizeUrl('https://example.com')).toBe('https://example.com');
expect(normalizeUrl('example.com')).toBe('https://example.com');
expect(normalizeUrl(' example.com ')).toBe('https://example.com');
});
it('should return empty string for empty or whitespace input', () => {
expect(normalizeUrl('')).toBe('');
expect(normalizeUrl(' ')).toBe('');
});
});
@@ -1,11 +0,0 @@
export const normalizeUrl = (url: string) => {
const trimmedUrl = url.trim();
if (trimmedUrl === '') {
return trimmedUrl;
}
return trimmedUrl.startsWith('http://') || trimmedUrl.startsWith('https://')
? trimmedUrl
: `https://${trimmedUrl}`;
};
@@ -3,15 +3,8 @@ import { useParams } from 'react-router-dom';
import { useWorkspaceNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useWorkspaceNavigationMenuItems';
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useLingui } from '@lingui/react/macro';
const WORKFLOW_OBJECTS_IN_SIDEBAR = [
CoreObjectNameSingular.Workflow,
CoreObjectNameSingular.WorkflowRun,
CoreObjectNameSingular.WorkflowVersion,
];
export const NavigationDrawerOpenedSection = () => {
const { t } = useLingui();
@@ -19,8 +12,7 @@ export const NavigationDrawerOpenedSection = () => {
const filteredActiveNonSystemObjectMetadataItems =
activeObjectMetadataItems.filter((item) => !item.isRemote);
const { workspaceNavigationMenuItemsObjectMetadataItems } =
useWorkspaceNavigationMenuItems();
const { objectMetadataIdsInWorkspaceNav } = useWorkspaceNavigationMenuItems();
const {
objectNamePlural: currentObjectNamePlural,
@@ -41,18 +33,12 @@ export const NavigationDrawerOpenedSection = () => {
return;
}
const isWorkflowObjectInSidebar = WORKFLOW_OBJECTS_IN_SIDEBAR.includes(
objectMetadataItem.nameSingular as CoreObjectNameSingular,
const isObjectAlreadyInNavbar = objectMetadataIdsInWorkspaceNav.has(
objectMetadataItem.id,
);
const shouldDisplayObjectInOpenedSection =
!isWorkflowObjectInSidebar &&
!workspaceNavigationMenuItemsObjectMetadataItems
.map((item) => item.id)
.includes(objectMetadataItem.id);
return (
shouldDisplayObjectInOpenedSection && (
!isObjectAlreadyInNavbar && (
<NavigationDrawerSectionForObjectMetadataItems
sectionTitle={t`Opened`}
objectMetadataItems={[objectMetadataItem]}
@@ -1,12 +1,11 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, normalizeUrl } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorLink';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition';
import { normalizeUrl } from '@/navigation-menu-item/display/link/utils/normalizeUrl';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
export const useAddLinkToNavigationMenuDraft = () => {
@@ -1,7 +1,7 @@
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { getAbsoluteUrl } from 'twenty-shared/utils';
import { ensureAbsoluteUrl } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { extractDomainFromUrl } from '@/navigation-menu-item/display/link/utils/extractDomainFromUrl';
@@ -45,7 +45,7 @@ export const SidePanelEditLinkItemView = ({
const currentName = selectedItem.name ?? defaultLabel;
const currentDomain = selectedItem.link
? extractDomainFromUrl(getAbsoluteUrl(selectedItem.link))
? extractDomainFromUrl(ensureAbsoluteUrl(selectedItem.link))
: undefined;
const canAutoUpdateName =
currentName === defaultLabel ||
@@ -57,7 +57,7 @@ export const SidePanelEditLinkItemView = ({
if (!canAutoUpdateName) return;
const trimmed = value.trim();
if (!isNonEmptyString(trimmed)) return;
const domain = extractDomainFromUrl(getAbsoluteUrl(trimmed));
const domain = extractDomainFromUrl(ensureAbsoluteUrl(trimmed));
if (domain !== undefined) {
setLastAutoSetName(domain);
onUpdateLink(selectedItem.id, { name: domain });
@@ -67,7 +67,7 @@ export const SidePanelEditLinkItemView = ({
const handleUrlBlur = (event: React.FocusEvent<HTMLInputElement>) => {
const value = event.target.value.trim();
if (isNonEmptyString(value)) {
onUpdateLink(selectedItem.id, { link: getAbsoluteUrl(value) });
onUpdateLink(selectedItem.id, { link: ensureAbsoluteUrl(value) });
setUrlEditInput('');
}
};
@@ -11,7 +11,7 @@ import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecor
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { FieldMetadataSettingsOnClickAction } from 'twenty-shared/types';
import { getAbsoluteUrl, isDefined } from 'twenty-shared/utils';
import { ensureAbsoluteUrl, isDefined } from 'twenty-shared/utils';
import { IconArrowUpRight, IconCopy } from 'twenty-ui/display';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
@@ -69,7 +69,7 @@ export const useGetSecondaryRecordTableCellButton = () => {
if (isFieldLinks(fieldDefinition)) {
const url = (fieldValue as FieldLinksValue).primaryLinkUrl ?? '';
openLinkOnClick = () => {
window.open(getAbsoluteUrl(url), '_blank');
window.open(ensureAbsoluteUrl(url), '_blank');
};
copyOnClick = () => {
copyToClipboard(url, t`Link copied to clipboard`);
@@ -7,6 +7,7 @@ import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFi
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -41,8 +42,8 @@ export const RecordTitleCellSingleTextDisplayMode = ({
const recordStore = useAtomFamilyStateValue(recordStoreFamilyState, recordId);
const isEmpty =
recordStore?.[fieldDefinition.metadata.fieldName]?.trim() === '';
const fieldValue = recordStore?.[fieldDefinition.metadata.fieldName];
const isEmpty = !isDefined(fieldValue) || fieldValue.trim() === '';
const { openRecordTitleCell } = useRecordTitleCell();
@@ -519,6 +519,94 @@ describe('buildRecordFromImportedStructuredRow', () => {
});
});
it('should lowercase relation email composite subfield', () => {
const importedStructuredRow: ImportedStructuredRow = {
'emailField (relationField)': 'John.Doe@Example.COM',
};
const spreadsheetImportFields = [
{
fieldMetadataItemId: '6',
isNestedField: false,
isRelationConnectField: true,
label: 'Relation Field / Email Field',
key: 'emailField (relationField)',
fieldMetadataType: FieldMetadataType.RELATION,
uniqueFieldMetadataItem: {
name: 'emailField',
type: FieldMetadataType.EMAILS,
},
compositeSubFieldKey: 'primaryEmail',
},
] as SpreadsheetImportField[];
const result = buildRecordFromImportedStructuredRow({
importedStructuredRow,
fieldMetadataItems: fields,
spreadsheetImportFields,
});
expect(result).toEqual({
relationField: {
connect: {
where: {
emailField: {
primaryEmail: 'john.doe@example.com',
},
},
},
},
createdBy: {
source: 'IMPORT',
context: {},
},
});
});
it('should normalize relation links composite subfield', () => {
const importedStructuredRow: ImportedStructuredRow = {
'domainNameField (relationField)': 'HTTPS://Example.COM/path/',
};
const spreadsheetImportFields = [
{
fieldMetadataItemId: '6',
isNestedField: false,
isRelationConnectField: true,
label: 'Relation Field / Domain Name Field',
key: 'domainNameField (relationField)',
fieldMetadataType: FieldMetadataType.RELATION,
uniqueFieldMetadataItem: {
name: 'linksField',
type: FieldMetadataType.LINKS,
},
compositeSubFieldKey: 'primaryLinkUrl',
},
] as SpreadsheetImportField[];
const result = buildRecordFromImportedStructuredRow({
importedStructuredRow,
fieldMetadataItems: fields,
spreadsheetImportFields,
});
expect(result).toEqual({
relationField: {
connect: {
where: {
linksField: {
primaryLinkUrl: 'https://example.com/path',
},
},
},
},
createdBy: {
source: 'IMPORT',
context: {},
},
});
});
it('should return empty record for empty imported row', () => {
const importedStructuredRow: ImportedStructuredRow = {};
@@ -12,7 +12,7 @@ import {
assertUnreachable,
isDefined,
isEmptyObject,
lowercaseUrlOriginAndRemoveTrailingSlash,
normalizeUrlOrigin,
} from 'twenty-shared/utils';
import { z } from 'zod';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
@@ -185,7 +185,7 @@ export const buildRecordFromImportedStructuredRow = ({
},
[FieldMetadataType.LINKS]: {
primaryLinkLabel: castToString,
primaryLinkUrl: lowercaseUrlOriginAndRemoveTrailingSlash,
primaryLinkUrl: normalizeUrlOrigin,
secondaryLinks: linkArrayJSONSchema.parse,
},
@@ -202,7 +202,7 @@ export const buildRecordFromImportedStructuredRow = ({
},
[FieldMetadataType.EMAILS]: {
primaryEmail: castToString,
primaryEmail: (value: unknown) => castToString(value).toLowerCase(),
additionalEmails: stringArrayJSONSchema.parse,
},
[FieldMetadataType.FULL_NAME]: {
@@ -13,7 +13,7 @@ import { FieldMetadataType } from 'twenty-shared/types';
import {
getUniqueConstraintsFields,
isDefined,
lowercaseUrlOriginAndRemoveTrailingSlash,
normalizeUrlOrigin,
} from 'twenty-shared/utils';
type Column = {
@@ -104,9 +104,7 @@ const getUniqueValues = (
.primaryLinkUrl,
)
) {
return lowercaseUrlOriginAndRemoveTrailingSlash(
row?.[columnName]?.toString().trim() || '',
);
return normalizeUrlOrigin(row?.[columnName]?.toString().trim() || '');
}
return row?.[columnName]?.toString().trim().toLowerCase();
@@ -40,8 +40,7 @@ export const SettingsNavigationDrawerItem = ({
to={href}
Icon={item.Icon}
active={isActive}
soon={item.soon}
isNew={item.isNew}
modifier={item.modifier}
onClick={item.onClick}
/>
</AdvancedSettingsWrapper>
@@ -56,8 +55,7 @@ export const SettingsNavigationDrawerItem = ({
to={href || undefined}
Icon={item.Icon}
active={isActive}
soon={item.soon}
isNew={item.isNew}
modifier={item.modifier}
onClick={item.onClick}
/>
);
@@ -7,7 +7,10 @@ import { billingState } from '@/client-config/states/billingState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl';
import { type NavigationDrawerItemIndentationLevel } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import {
type NavigationDrawerItemIndentationLevel,
type NavigationDrawerItemModifier,
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { t } from '@lingui/core/macro';
@@ -57,8 +60,7 @@ export type SettingsNavigationItem = {
isHidden?: boolean;
subItems?: SettingsNavigationItem[];
isAdvanced?: boolean;
soon?: boolean;
isNew?: boolean;
modifier?: NavigationDrawerItemModifier;
};
const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
@@ -179,7 +181,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
isHidden:
!isApplicationEnabled ||
!permissionMap[PermissionFlagType.WORKSPACE],
isNew: true,
modifier: 'new',
},
{
label: t`AI`,
@@ -187,7 +189,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
Icon: IconSparkles,
isHidden:
!isAIEnabled || !permissionMap[PermissionFlagType.WORKSPACE],
isNew: true,
modifier: 'new',
},
{
label: t`Security`,
@@ -45,7 +45,7 @@ export const SignInAppNavigationDrawerMock = ({
label={t`Search`}
Icon={IconSearch}
onClick={() => {}}
keyboard={[getOsControlSymbol(), 'K']}
modifier={{ keyboard: [getOsControlSymbol(), 'K'] }}
/>
<NavigationDrawerItem
label={t`Settings`}
@@ -37,6 +37,11 @@ const DEFAULT_INDENTATION_LEVEL = 1;
export type NavigationDrawerItemIndentationLevel = 1 | 2;
export type NavigationDrawerItemModifier =
| 'soon'
| 'new'
| { keyboard: string[] };
export type NavigationDrawerItemProps = {
className?: string;
label: string;
@@ -48,17 +53,12 @@ export type NavigationDrawerItemProps = {
Icon?: IconComponent | ((props: TablerIconsProps) => JSX.Element);
iconColor?: string | null;
active?: boolean;
danger?: boolean;
soon?: boolean;
isNew?: boolean;
count?: number;
keyboard?: string[];
modifier?: NavigationDrawerItemModifier;
rightOptions?: ReactNode;
alwaysShowRightOptions?: boolean;
isDragging?: boolean;
isRightOptionsDropdownOpen?: boolean;
triggerEvent?: TriggerEventType;
mouseUpNavigation?: boolean;
preventCollapseOnMobile?: boolean;
isSelectedInEditMode?: boolean;
variant?: 'default' | 'tertiary';
@@ -67,14 +67,13 @@ export type NavigationDrawerItemProps = {
type StyledItemProps = Pick<
NavigationDrawerItemProps,
| 'active'
| 'danger'
| 'indentationLevel'
| 'soon'
| 'to'
| 'isDragging'
| 'isSelectedInEditMode'
| 'variant'
> & {
isSoon: boolean;
isNavigationDrawerExpanded: boolean;
hasRightOptions: boolean;
href?: string;
@@ -92,14 +91,11 @@ const StyledItem = styled.button<StyledItemProps>`
: '1px solid transparent'};
border-radius: ${themeCssVariables.border.radius.sm};
box-sizing: border-box;
color: ${({ active, danger, soon, variant }) => {
color: ${({ active, isSoon, variant }) => {
if (active === true) {
return themeCssVariables.font.color.primary;
}
if (danger === true) {
return themeCssVariables.color.red;
}
if (soon === true) {
if (isSoon) {
return themeCssVariables.font.color.light;
}
if (variant === 'tertiary') {
@@ -107,8 +103,8 @@ const StyledItem = styled.button<StyledItemProps>`
}
return themeCssVariables.font.color.secondary;
}};
cursor: ${({ soon, isDragging }) =>
isDragging ? 'grabbing' : soon ? 'default' : 'pointer'};
cursor: ${({ isSoon, isDragging }) =>
isDragging ? 'grabbing' : isSoon ? 'default' : 'pointer'};
display: flex;
font-family: ${themeCssVariables.font.family};
font-size: ${themeCssVariables.font.size.md};
@@ -122,7 +118,7 @@ const StyledItem = styled.button<StyledItemProps>`
? themeCssVariables.spacing['0.5']
: themeCssVariables.spacing[1]};
padding-top: ${themeCssVariables.spacing[1]};
pointer-events: ${({ soon }) => (soon ? 'none' : 'auto')};
pointer-events: ${({ isSoon }) => (isSoon ? 'none' : 'auto')};
text-decoration: none;
user-select: none;
width: ${({ isNavigationDrawerExpanded, hasRightOptions }) =>
@@ -132,10 +128,7 @@ const StyledItem = styled.button<StyledItemProps>`
&:hover {
background: ${themeCssVariables.background.transparent.light};
color: ${({ danger }) =>
danger
? themeCssVariables.color.red
: themeCssVariables.font.color.primary};
color: ${themeCssVariables.font.color.primary};
}
&:hover .keyboard-shortcuts {
@@ -172,20 +165,6 @@ const StyledItemSecondaryLabel = styled.span`
font-weight: ${themeCssVariables.font.weight.regular};
`;
const StyledItemCount = styled.span`
align-items: center;
background-color: ${themeCssVariables.color.blue};
border-radius: ${themeCssVariables.border.radius.rounded};
color: ${themeCssVariables.grayScale.gray1};
display: flex;
font-size: ${themeCssVariables.font.size.xs};
font-weight: ${themeCssVariables.font.weight.semiBold};
height: 16px;
justify-content: center;
margin-left: auto;
width: 16px;
`;
const StyledKeyBoardShortcut = styled.span`
align-items: center;
background: ${themeCssVariables.background.transparent.lighter};
@@ -277,18 +256,13 @@ export const NavigationDrawerItem = ({
to,
onClick,
active,
danger,
soon,
isNew,
count,
keyboard,
modifier,
subItemState,
rightOptions,
alwaysShowRightOptions = false,
isDragging,
isRightOptionsDropdownOpen,
triggerEvent,
mouseUpNavigation = false,
preventCollapseOnMobile = false,
isSelectedInEditMode = false,
variant = 'default',
@@ -301,10 +275,15 @@ export const NavigationDrawerItem = ({
const { navigationItemId } = useNavigationDrawerTooltip(label, to);
const isSoon = modifier === 'soon';
const isNew = modifier === 'new';
const keyboardKeys =
isDefined(modifier) && typeof modifier === 'object'
? modifier.keyboard
: undefined;
const showBreadcrumb = indentationLevel === 2;
const showStyledSpacer = Boolean(
soon || isNew || count || keyboard || rightOptions,
);
const showStyledSpacer = isDefined(modifier) || isDefined(rightOptions);
const handleMobileNavigation = () => {
if (isMobile && !preventCollapseOnMobile) {
@@ -314,6 +293,7 @@ export const NavigationDrawerItem = ({
const isExternalLink =
isDefined(to) && (to.startsWith('http://') || to.startsWith('https://'));
const isInternalLink = isDefined(to) && !isExternalLink;
const handleExternalLinkClick = () => {
handleMobileNavigation();
@@ -332,40 +312,37 @@ export const NavigationDrawerItem = ({
triggerEvent,
});
const elementType = isExternalLink
? 'a'
: isInternalLink
? Link
: isDefined(rightOptions)
? 'div'
: undefined;
return (
<StyledNavigationDrawerItemContainer>
<StyledItem
id={navigationItemId}
className={`navigation-drawer-item ${className || ''}`}
onClick={
mouseUpNavigation ? onClick : handleMouseDownNavigationClickClick
}
onMouseDown={mouseUpNavigation ? undefined : handleMouseDown}
onClick={handleMouseDownNavigationClickClick}
onMouseDown={handleMouseDown}
active={active}
aria-selected={active}
danger={danger}
soon={soon}
isSoon={isSoon}
variant={variant}
as={
to
? isExternalLink
? 'a'
: Link
: isDefined(rightOptions)
? 'div'
: undefined
}
role={to ? undefined : isDefined(rightOptions) ? 'button' : undefined}
to={isExternalLink ? undefined : to}
href={isExternalLink ? to : undefined}
target={isExternalLink ? '_blank' : undefined}
rel={isExternalLink ? 'noopener noreferrer' : undefined}
draggable={to && !isExternalLink ? false : undefined}
indentationLevel={indentationLevel}
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
isDragging={isDragging}
hasRightOptions={isDefined(rightOptions)}
isSelectedInEditMode={isSelectedInEditMode}
as={elementType}
role={!to && isDefined(rightOptions) ? 'button' : undefined}
to={isInternalLink ? to : undefined}
href={isExternalLink ? to : undefined}
target={isExternalLink ? '_blank' : undefined}
rel={isExternalLink ? 'noopener noreferrer' : undefined}
draggable={isInternalLink ? false : undefined}
>
<StyledItemElementsContainer>
{showBreadcrumb && (
@@ -419,7 +396,7 @@ export const NavigationDrawerItem = ({
{showStyledSpacer && <StyledSpacer />}
{soon && (
{isSoon && (
<NavigationDrawerAnimatedCollapseWrapper>
<Pill label={t`Soon`} />
</NavigationDrawerAnimatedCollapseWrapper>
@@ -431,23 +408,25 @@ export const NavigationDrawerItem = ({
</NavigationDrawerAnimatedCollapseWrapper>
)}
{!!count && (
<NavigationDrawerAnimatedCollapseWrapper>
<StyledItemCount>{count}</StyledItemCount>
</NavigationDrawerAnimatedCollapseWrapper>
)}
{keyboard && (
{isDefined(keyboardKeys) && (
<NavigationDrawerAnimatedCollapseWrapper>
<StyledKeyBoardShortcut className="keyboard-shortcuts">
<Label>{keyboard}</Label>
<Label>{keyboardKeys}</Label>
</StyledKeyBoardShortcut>
</NavigationDrawerAnimatedCollapseWrapper>
)}
{isDefined(rightOptions) && (
<NavigationDrawerAnimatedCollapseWrapper>
<StyledRightOptionsContainer>
{/* When StyledItem renders as a Link, we need both handlers to
prevent navigation when interacting with rightOptions:
- onMouseDown: stops useMouseDownNavigation from calling navigate()
- onClickCapture: prevents the native <a> follow since the child's
stopPropagation blocks Link's own preventDefault */}
<StyledRightOptionsContainer
onMouseDown={(e) => e.stopPropagation()}
onClickCapture={(e) => e.preventDefault()}
>
<StyledRightOptionsVisbility
data-visible={
isMobile ||
@@ -14,10 +14,7 @@ export const NavigationDrawerSubItem = ({
to,
onClick,
active,
danger,
soon,
count,
keyboard,
modifier,
subItemState,
rightOptions,
isDragging,
@@ -37,10 +34,7 @@ export const NavigationDrawerSubItem = ({
to={to}
onClick={onClick}
active={active}
danger={danger}
soon={soon}
count={count}
keyboard={keyboard}
modifier={modifier}
rightOptions={rightOptions}
isDragging={isDragging}
isSelectedInEditMode={isSelectedInEditMode}
@@ -86,24 +86,19 @@ export const Default: Story = {
label="Notifications"
to="/inbox"
Icon={IconBell}
soon={true}
modifier="soon"
/>
<NavigationDrawerItem
label="Search"
Icon={IconSearch}
keyboard={[`${getOsControlSymbol()}`, 'K']}
modifier={{ keyboard: [`${getOsControlSymbol()}`, 'K'] }}
/>
<NavigationDrawerItem
label="Settings"
to="/settings/profile"
Icon={IconSettings}
/>
<NavigationDrawerItem
label="Tasks"
to="/tasks"
Icon={IconCheckbox}
count={2}
/>
<NavigationDrawerItem label="Tasks" to="/tasks" Icon={IconCheckbox} />
</NavigationDrawerSection>
<NavigationDrawerSection>
@@ -105,23 +105,14 @@ export const NewPill: Story = {
args={{
label: 'New Feature',
Icon: IconSearch,
isNew: true,
modifier: 'new',
}}
/>
<Story
args={{
label: 'New Feature with Count',
label: 'Feature with Keyboard Shortcut',
Icon: IconSearch,
isNew: true,
count: 5,
}}
/>
<Story
args={{
label: 'New Feature with Keyboard Shortcut',
Icon: IconSearch,
isNew: true,
keyboard: [getOsControlSymbol(), 'N'],
modifier: { keyboard: [getOsControlSymbol(), 'N'] },
}}
/>
</StyledContainer>
@@ -195,12 +186,6 @@ export const Catalog: CatalogStory<Story, typeof NavigationDrawerItem> = {
pseudo: { hover: ['.hover'] },
catalog: {
dimensions: [
{
name: 'danger',
values: [true, false],
props: (danger: boolean) => ({ danger }),
labels: (danger: boolean) => (danger ? 'Danger' : 'No Danger'),
},
{
name: 'active',
values: [true, false],
@@ -216,23 +201,19 @@ export const Catalog: CatalogStory<Story, typeof NavigationDrawerItem> = {
},
{
name: 'adornments',
values: [
'Without Adornments',
'Soon Pill',
'New Pill',
'Count',
'Keyboard Keys',
],
values: ['Without Modifier', 'Soon', 'New', 'Keyboard Keys'],
props: (adornmentName: string) =>
adornmentName === 'Soon Pill'
? { soon: true }
: adornmentName === 'New Pill'
? { isNew: true }
: adornmentName === 'Count'
? { count: 3 }
: adornmentName === 'Keyboard Keys'
? { keyboard: [getOsControlSymbol(), 'K'] }
: {},
adornmentName === 'Soon'
? { modifier: 'soon' }
: adornmentName === 'New'
? { modifier: 'new' }
: adornmentName === 'Keyboard Keys'
? {
modifier: {
keyboard: [getOsControlSymbol(), 'K'],
},
}
: {},
},
],
},
@@ -1236,107 +1236,6 @@ enum PageLayoutType {
DASHBOARD
}
type FileWithSignedUrl {
id: UUID!
path: String!
size: Float!
createdAt: DateTime!
url: String!
}
type RecordIdentifier {
id: UUID!
labelIdentifier: String!
imageIdentifier: String
}
type NavigationMenuItem {
id: UUID!
userWorkspaceId: UUID
targetRecordId: UUID
targetObjectMetadataId: UUID
viewId: UUID
type: NavigationMenuItemType!
name: String
link: String
icon: String
color: String
folderId: UUID
position: Float!
applicationId: UUID
createdAt: DateTime!
updatedAt: DateTime!
targetRecordIdentifier: RecordIdentifier
}
enum NavigationMenuItemType {
VIEW
FOLDER
LINK
OBJECT
RECORD
}
type ObjectRecordEventProperties {
updatedFields: [String!]
before: JSON
after: JSON
diff: JSON
}
type MetadataEvent {
type: MetadataEventAction!
metadataName: String!
recordId: String!
properties: ObjectRecordEventProperties!
updatedCollectionHash: String
}
"""Metadata Event Action"""
enum MetadataEventAction {
CREATED
UPDATED
DELETED
}
type ObjectRecordEvent {
action: DatabaseEventAction!
objectNameSingular: String!
recordId: String!
userId: String
workspaceMemberId: String
properties: ObjectRecordEventProperties!
}
"""Database Event Action"""
enum DatabaseEventAction {
CREATED
UPDATED
DELETED
DESTROYED
RESTORED
UPSERTED
}
type ObjectRecordEventWithQueryIds {
queryIds: [String!]!
objectRecordEvent: ObjectRecordEvent!
}
type EventSubscription {
eventStreamId: String!
objectRecordEventsWithQueryIds: [ObjectRecordEventWithQueryIds!]!
metadataEvents: [MetadataEvent!]!
}
type OnDbEvent {
action: DatabaseEventAction!
objectNameSingular: String!
eventDate: DateTime!
record: JSON!
updatedFields: [String!]
}
type Analytics {
"""Boolean that confirms query was dispatched"""
success: Boolean!
@@ -1523,6 +1422,14 @@ type ApprovedAccessDomain {
createdAt: DateTime!
}
type FileWithSignedUrl {
id: UUID!
path: String!
size: Float!
createdAt: DateTime!
url: String!
}
type WorkspaceInvitation {
id: UUID!
email: String!
@@ -1642,6 +1549,7 @@ enum FeatureFlagKey {
IS_DRAFT_EMAIL_ENABLED
IS_RICH_TEXT_V1_MIGRATED
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED
IS_CONNECTED_ACCOUNT_MIGRATED
}
type SSOIdentityProvider {
@@ -1958,6 +1866,91 @@ type WorkspaceInviteHashValid {
isValid: Boolean!
}
type RecordIdentifier {
id: UUID!
labelIdentifier: String!
imageIdentifier: String
}
type NavigationMenuItem {
id: UUID!
userWorkspaceId: UUID
targetRecordId: UUID
targetObjectMetadataId: UUID
viewId: UUID
type: NavigationMenuItemType!
name: String
link: String
icon: String
color: String
folderId: UUID
position: Float!
applicationId: UUID
createdAt: DateTime!
updatedAt: DateTime!
targetRecordIdentifier: RecordIdentifier
}
enum NavigationMenuItemType {
VIEW
FOLDER
LINK
OBJECT
RECORD
}
type ObjectRecordEventProperties {
updatedFields: [String!]
before: JSON
after: JSON
diff: JSON
}
type MetadataEvent {
type: MetadataEventAction!
metadataName: String!
recordId: String!
properties: ObjectRecordEventProperties!
updatedCollectionHash: String
}
"""Metadata Event Action"""
enum MetadataEventAction {
CREATED
UPDATED
DELETED
}
type ObjectRecordEvent {
action: DatabaseEventAction!
objectNameSingular: String!
recordId: String!
userId: String
workspaceMemberId: String
properties: ObjectRecordEventProperties!
}
"""Database Event Action"""
enum DatabaseEventAction {
CREATED
UPDATED
DELETED
DESTROYED
RESTORED
UPSERTED
}
type ObjectRecordEventWithQueryIds {
queryIds: [String!]!
objectRecordEvent: ObjectRecordEvent!
}
type EventSubscription {
eventStreamId: String!
objectRecordEventsWithQueryIds: [ObjectRecordEventWithQueryIds!]!
metadataEvents: [MetadataEvent!]!
}
type LogicFunctionExecutionResult {
"""Execution result in JSON format"""
data: JSON
@@ -2212,6 +2205,163 @@ type AgentTurn {
createdAt: DateTime!
}
type CalendarChannel {
id: UUID!
handle: String!
syncStatus: CalendarChannelSyncStatus!
syncStage: CalendarChannelSyncStage!
visibility: CalendarChannelVisibility!
isContactAutoCreationEnabled: Boolean!
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy!
isSyncEnabled: Boolean!
syncCursor: String
syncedAt: DateTime
syncStageStartedAt: DateTime
throttleFailureCount: Float!
connectedAccountId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
}
enum CalendarChannelSyncStatus {
NOT_SYNCED
ONGOING
ACTIVE
FAILED_INSUFFICIENT_PERMISSIONS
FAILED_UNKNOWN
}
enum CalendarChannelSyncStage {
PENDING_CONFIGURATION
CALENDAR_EVENT_LIST_FETCH_PENDING
CALENDAR_EVENT_LIST_FETCH_SCHEDULED
CALENDAR_EVENT_LIST_FETCH_ONGOING
CALENDAR_EVENTS_IMPORT_PENDING
CALENDAR_EVENTS_IMPORT_SCHEDULED
CALENDAR_EVENTS_IMPORT_ONGOING
FAILED
}
enum CalendarChannelVisibility {
METADATA
SHARE_EVERYTHING
}
enum CalendarChannelContactAutoCreationPolicy {
AS_PARTICIPANT_AND_ORGANIZER
AS_PARTICIPANT
AS_ORGANIZER
NONE
}
type ConnectedAccountDTO {
id: UUID!
handle: String!
provider: String!
accessToken: String
refreshToken: String
lastCredentialsRefreshedAt: DateTime
authFailedAt: DateTime
handleAliases: [String!]
scopes: [String!]
connectionParameters: JSON
lastSignedInAt: DateTime
oidcTokenClaims: JSON
userWorkspaceId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
}
type MessageChannel {
id: UUID!
visibility: MessageChannelVisibility!
handle: String!
type: MessageChannelType!
isContactAutoCreationEnabled: Boolean!
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy!
messageFolderImportPolicy: MessageFolderImportPolicy!
excludeNonProfessionalEmails: Boolean!
excludeGroupEmails: Boolean!
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction!
isSyncEnabled: Boolean!
syncCursor: String
syncedAt: DateTime
syncStatus: MessageChannelSyncStatus!
syncStage: MessageChannelSyncStage!
syncStageStartedAt: DateTime
throttleFailureCount: Float!
throttleRetryAfter: DateTime
connectedAccountId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
}
enum MessageChannelVisibility {
METADATA
SUBJECT
SHARE_EVERYTHING
}
enum MessageChannelType {
EMAIL
SMS
}
enum MessageChannelContactAutoCreationPolicy {
SENT_AND_RECEIVED
SENT
NONE
}
enum MessageFolderImportPolicy {
ALL_FOLDERS
SELECTED_FOLDERS
}
enum MessageChannelPendingGroupEmailsAction {
GROUP_EMAILS_DELETION
GROUP_EMAILS_IMPORT
NONE
}
enum MessageChannelSyncStatus {
NOT_SYNCED
ONGOING
ACTIVE
FAILED_INSUFFICIENT_PERMISSIONS
FAILED_UNKNOWN
}
enum MessageChannelSyncStage {
PENDING_CONFIGURATION
MESSAGE_LIST_FETCH_PENDING
MESSAGE_LIST_FETCH_SCHEDULED
MESSAGE_LIST_FETCH_ONGOING
MESSAGES_IMPORT_PENDING
MESSAGES_IMPORT_SCHEDULED
MESSAGES_IMPORT_ONGOING
FAILED
}
type MessageFolder {
id: UUID!
name: String
syncCursor: String
isSentFolder: Boolean!
isSynced: Boolean!
parentFolderId: UUID
externalId: String
pendingSyncAction: MessageFolderPendingSyncAction!
messageChannelId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
}
enum MessageFolderPendingSyncAction {
FOLDER_DELETION
NONE
}
type CollectionHash {
collectionName: AllMetadataName!
hash: String!
@@ -3018,6 +3168,14 @@ type Query {
findWorkspaceFromInviteHash(inviteHash: String!): Workspace!
validatePasswordResetToken(passwordResetToken: String!): ValidatePasswordResetToken!
getSSOIdentityProviders: [FindAvailableSSOIDP!]!
messageFolders(messageChannelId: UUID): [MessageFolder!]!
messageFolder(id: UUID!): MessageFolder
calendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
calendarChannel(id: UUID!): CalendarChannel
messageChannels(connectedAccountId: UUID): [MessageChannel!]!
messageChannel(id: UUID!): MessageChannel
connectedAccounts: [ConnectedAccountDTO!]!
connectedAccount(id: UUID!): ConnectedAccountDTO
webhooks: [Webhook!]!
webhook(id: UUID!): Webhook
minimalMetadata: MinimalMetadata!
@@ -3315,6 +3473,18 @@ type Mutation {
createSAMLIdentityProvider(input: SetupSAMLSsoInput!): SetupSso!
deleteSSOIdentityProvider(input: DeleteSsoInput!): DeleteSso!
editSSOIdentityProvider(input: EditSsoInput!): EditSso!
createMessageFolder(input: CreateMessageFolderInput!): MessageFolder!
updateMessageFolder(input: UpdateMessageFolderInput!): MessageFolder!
deleteMessageFolder(id: UUID!): MessageFolder!
createCalendarChannel(input: CreateCalendarChannelInput!): CalendarChannel!
updateCalendarChannel(input: UpdateCalendarChannelInput!): CalendarChannel!
deleteCalendarChannel(id: UUID!): CalendarChannel!
createMessageChannel(input: CreateMessageChannelInput!): MessageChannel!
updateMessageChannel(input: UpdateMessageChannelInput!): MessageChannel!
deleteMessageChannel(id: UUID!): MessageChannel!
createConnectedAccount(input: CreateConnectedAccountInput!): ConnectedAccountDTO!
updateConnectedAccount(input: UpdateConnectedAccountInput!): ConnectedAccountDTO!
deleteConnectedAccount(id: UUID!): ConnectedAccountDTO!
createWebhook(input: CreateWebhookInput!): Webhook!
updateWebhook(input: UpdateWebhookInput!): Webhook!
deleteWebhook(id: UUID!): Webhook!
@@ -4211,6 +4381,105 @@ input EditSsoInput {
status: SSOIdentityProviderStatus!
}
input CreateMessageFolderInput {
id: UUID
name: String
isSentFolder: Boolean!
isSynced: Boolean!
externalId: String
pendingSyncAction: MessageFolderPendingSyncAction!
messageChannelId: UUID!
parentFolderId: UUID
}
input UpdateMessageFolderInput {
id: UUID!
update: UpdateMessageFolderInputUpdates!
}
input UpdateMessageFolderInputUpdates {
name: String
syncCursor: String
isSynced: Boolean
pendingSyncAction: MessageFolderPendingSyncAction
}
input CreateCalendarChannelInput {
id: UUID
handle: String!
visibility: CalendarChannelVisibility!
syncStage: CalendarChannelSyncStage!
connectedAccountId: UUID!
isContactAutoCreationEnabled: Boolean!
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy!
isSyncEnabled: Boolean!
}
input UpdateCalendarChannelInput {
id: UUID!
update: UpdateCalendarChannelInputUpdates!
}
input UpdateCalendarChannelInputUpdates {
visibility: CalendarChannelVisibility
isContactAutoCreationEnabled: Boolean
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy
isSyncEnabled: Boolean
}
input CreateMessageChannelInput {
id: UUID
handle: String!
visibility: MessageChannelVisibility!
type: MessageChannelType!
syncStage: MessageChannelSyncStage!
connectedAccountId: UUID!
isContactAutoCreationEnabled: Boolean!
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy!
messageFolderImportPolicy: MessageFolderImportPolicy!
excludeNonProfessionalEmails: Boolean!
excludeGroupEmails: Boolean!
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction!
isSyncEnabled: Boolean!
}
input UpdateMessageChannelInput {
id: UUID!
update: UpdateMessageChannelInputUpdates!
}
input UpdateMessageChannelInputUpdates {
visibility: MessageChannelVisibility
isContactAutoCreationEnabled: Boolean
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy
messageFolderImportPolicy: MessageFolderImportPolicy
isSyncEnabled: Boolean
excludeNonProfessionalEmails: Boolean
excludeGroupEmails: Boolean
}
input CreateConnectedAccountInput {
id: UUID
handle: String!
provider: String!
accessToken: String
refreshToken: String
scopes: [String!]
userWorkspaceId: UUID!
}
input UpdateConnectedAccountInput {
id: UUID!
update: UpdateConnectedAccountInputUpdates!
}
input UpdateConnectedAccountInputUpdates {
accessToken: String
refreshToken: String
handleAliases: [String!]
scopes: [String!]
}
input CreateWebhookInput {
id: UUID
targetUrl: String!
@@ -4316,17 +4585,10 @@ enum FileFolder {
}
type Subscription {
onDbEvent(input: OnDbEventInput!): OnDbEvent!
onEventSubscription(eventStreamId: String!): EventSubscription
logicFunctionLogs(input: LogicFunctionLogsInput!): LogicFunctionLogs!
}
input OnDbEventInput {
action: DatabaseEventAction
objectNameSingular: String
recordId: UUID
}
input LogicFunctionLogsInput {
applicationId: UUID
applicationUniversalIdentifier: UUID
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,382 @@
import { InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import { Command } from 'nest-commander';
import { FeatureFlagKey } from 'twenty-shared/types';
import { Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Command({
name: 'upgrade:1-20:migrate-messaging-infrastructure-to-metadata',
description:
'Backfill connectedAccount, messageChannel, calendarChannel, and messageFolder to core metadata schema',
})
export class MigrateMessagingInfrastructureToMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
@InjectRepository(MessageFolderEntity)
private readonly messageFolderRepository: Repository<MessageFolderEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly featureFlagService: FeatureFlagService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
const isAlreadyMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
workspaceId,
);
if (isAlreadyMigrated) {
this.logger.log(
`IS_CONNECTED_ACCOUNT_MIGRATED already enabled for workspace ${workspaceId}, skipping`,
);
return;
}
const connectedAccountWorkspaceRepository =
await this.twentyORMGlobalManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const messageChannelWorkspaceRepository =
await this.twentyORMGlobalManager.getRepository<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const calendarChannelWorkspaceRepository =
await this.twentyORMGlobalManager.getRepository<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const messageFolderWorkspaceRepository =
await this.twentyORMGlobalManager.getRepository<MessageFolderWorkspaceEntity>(
workspaceId,
'messageFolder',
);
const connectedAccounts = await connectedAccountWorkspaceRepository.find();
const messageChannels = await messageChannelWorkspaceRepository.find();
const calendarChannels = await calendarChannelWorkspaceRepository.find();
const messageFolders = await messageFolderWorkspaceRepository.find();
const workspaceMemberIdToUserWorkspaceIdMap =
await this.buildWorkspaceMemberIdToUserWorkspaceIdMap(workspaceId);
const connectedAccountsWithMissingHandle = connectedAccounts.filter(
(account) => !account.handle,
);
const connectedAccountsWithUnresolvedOwner = connectedAccounts.filter(
(account) =>
!workspaceMemberIdToUserWorkspaceIdMap.has(account.accountOwnerId),
);
const messageChannelsWithMissingHandle = messageChannels.filter(
(channel) => !channel.handle,
);
const calendarChannelsWithMissingHandle = calendarChannels.filter(
(channel) => !channel.handle,
);
if (isDryRun) {
this.logger.log(
`[DRY RUN] Workspace ${workspaceId}: ` +
`${connectedAccounts.length} connected accounts, ` +
`${messageChannels.length} message channels, ` +
`${calendarChannels.length} calendar channels, ` +
`${messageFolders.length} message folders`,
);
if (connectedAccountsWithMissingHandle.length > 0) {
this.logger.warn(
`[DRY RUN] ${connectedAccountsWithMissingHandle.length} connected accounts have empty handle`,
);
}
if (connectedAccountsWithUnresolvedOwner.length > 0) {
this.logger.warn(
`[DRY RUN] ${connectedAccountsWithUnresolvedOwner.length} connected accounts have unresolvable accountOwnerId (no matching userWorkspace)`,
);
}
if (messageChannelsWithMissingHandle.length > 0) {
this.logger.warn(
`[DRY RUN] ${messageChannelsWithMissingHandle.length} message channels have empty handle`,
);
}
if (calendarChannelsWithMissingHandle.length > 0) {
this.logger.warn(
`[DRY RUN] ${calendarChannelsWithMissingHandle.length} calendar channels have empty handle`,
);
}
return;
}
let migratedConnectedAccountIds = new Set(
connectedAccounts.map((account) => account.id),
);
let migratedMessageChannelIds = new Set(
messageChannels.map((channel) => channel.id),
);
if (connectedAccounts.length > 0) {
const coreConnectedAccounts = connectedAccounts
.filter((workspaceEntity) => {
const userWorkspaceId = workspaceMemberIdToUserWorkspaceIdMap.get(
workspaceEntity.accountOwnerId,
);
if (!userWorkspaceId) {
this.logger.warn(
`Skipping connected account ${workspaceEntity.id}: no userWorkspace found for workspaceMember ${workspaceEntity.accountOwnerId}`,
);
return false;
}
return true;
})
.map((workspaceEntity) => {
const handleAliases = isNonEmptyString(workspaceEntity.handleAliases)
? workspaceEntity.handleAliases
.split(',')
.map((alias) => alias.trim())
: null;
return {
id: workspaceEntity.id,
handle: workspaceEntity.handle ?? '',
provider: workspaceEntity.provider,
accessToken: workspaceEntity.accessToken,
refreshToken: workspaceEntity.refreshToken,
lastCredentialsRefreshedAt:
workspaceEntity.lastCredentialsRefreshedAt,
authFailedAt: workspaceEntity.authFailedAt,
handleAliases,
scopes: workspaceEntity.scopes,
connectionParameters:
workspaceEntity.connectionParameters as Record<
string,
unknown
> | null,
userWorkspaceId: workspaceMemberIdToUserWorkspaceIdMap.get(
workspaceEntity.accountOwnerId,
)!,
workspaceId,
createdAt: workspaceEntity.createdAt,
updatedAt: workspaceEntity.updatedAt,
};
});
if (coreConnectedAccounts.length > 0) {
await this.connectedAccountRepository.save(
coreConnectedAccounts as unknown as ConnectedAccountEntity[],
);
this.logger.log(
`Migrated ${coreConnectedAccounts.length} connected accounts for workspace ${workspaceId}`,
);
}
migratedConnectedAccountIds = new Set(
coreConnectedAccounts.map((account) => account.id),
);
}
if (messageChannels.length > 0) {
const coreMessageChannels = messageChannels
.filter((workspaceEntity) =>
migratedConnectedAccountIds.has(workspaceEntity.connectedAccountId),
)
.map((workspaceEntity) => ({
id: workspaceEntity.id,
visibility: workspaceEntity.visibility,
handle: workspaceEntity.handle ?? '',
type: workspaceEntity.type,
isContactAutoCreationEnabled:
workspaceEntity.isContactAutoCreationEnabled,
contactAutoCreationPolicy: workspaceEntity.contactAutoCreationPolicy,
messageFolderImportPolicy: workspaceEntity.messageFolderImportPolicy,
excludeNonProfessionalEmails:
workspaceEntity.excludeNonProfessionalEmails,
excludeGroupEmails: workspaceEntity.excludeGroupEmails,
pendingGroupEmailsAction: workspaceEntity.pendingGroupEmailsAction,
isSyncEnabled: workspaceEntity.isSyncEnabled,
syncCursor: workspaceEntity.syncCursor,
syncedAt: workspaceEntity.syncedAt
? new Date(workspaceEntity.syncedAt)
: null,
syncStatus: workspaceEntity.syncStatus ?? 'NOT_SYNCED',
syncStage: workspaceEntity.syncStage,
syncStageStartedAt: workspaceEntity.syncStageStartedAt
? new Date(workspaceEntity.syncStageStartedAt)
: null,
throttleFailureCount: workspaceEntity.throttleFailureCount,
throttleRetryAfter: workspaceEntity.throttleRetryAfter
? new Date(workspaceEntity.throttleRetryAfter)
: null,
connectedAccountId: workspaceEntity.connectedAccountId,
workspaceId,
createdAt: workspaceEntity.createdAt,
updatedAt: workspaceEntity.updatedAt,
}));
if (coreMessageChannels.length > 0) {
await this.messageChannelRepository.save(
coreMessageChannels as unknown as MessageChannelEntity[],
);
this.logger.log(
`Migrated ${coreMessageChannels.length} message channels for workspace ${workspaceId}`,
);
}
migratedMessageChannelIds = new Set(
coreMessageChannels.map((channel) => channel.id),
);
}
if (calendarChannels.length > 0) {
const coreCalendarChannels = calendarChannels
.filter((workspaceEntity) =>
migratedConnectedAccountIds.has(workspaceEntity.connectedAccountId),
)
.map((workspaceEntity) => ({
id: workspaceEntity.id,
handle: workspaceEntity.handle ?? '',
syncStatus: workspaceEntity.syncStatus ?? 'NOT_SYNCED',
syncStage: workspaceEntity.syncStage,
visibility: workspaceEntity.visibility,
isContactAutoCreationEnabled:
workspaceEntity.isContactAutoCreationEnabled,
contactAutoCreationPolicy: workspaceEntity.contactAutoCreationPolicy,
isSyncEnabled: workspaceEntity.isSyncEnabled,
syncCursor: workspaceEntity.syncCursor,
syncedAt: workspaceEntity.syncedAt
? new Date(workspaceEntity.syncedAt)
: null,
syncStageStartedAt: workspaceEntity.syncStageStartedAt
? new Date(workspaceEntity.syncStageStartedAt)
: null,
throttleFailureCount: workspaceEntity.throttleFailureCount,
connectedAccountId: workspaceEntity.connectedAccountId,
workspaceId,
createdAt: workspaceEntity.createdAt,
updatedAt: workspaceEntity.updatedAt,
}));
await this.calendarChannelRepository.save(
coreCalendarChannels as unknown as CalendarChannelEntity[],
);
this.logger.log(
`Migrated ${coreCalendarChannels.length} calendar channels for workspace ${workspaceId}`,
);
}
if (messageFolders.length > 0) {
const coreMessageFolders = messageFolders
.filter((workspaceEntity) =>
migratedMessageChannelIds.has(workspaceEntity.messageChannelId),
)
.map((workspaceEntity) => ({
id: workspaceEntity.id,
name: workspaceEntity.name,
syncCursor: workspaceEntity.syncCursor,
isSentFolder: workspaceEntity.isSentFolder,
isSynced: workspaceEntity.isSynced,
parentFolderId: isNonEmptyString(workspaceEntity.parentFolderId)
? workspaceEntity.parentFolderId
: null,
externalId: workspaceEntity.externalId,
pendingSyncAction: workspaceEntity.pendingSyncAction,
messageChannelId: workspaceEntity.messageChannelId,
workspaceId,
createdAt: workspaceEntity.createdAt,
updatedAt: workspaceEntity.updatedAt,
}));
await this.messageFolderRepository.save(
coreMessageFolders as unknown as MessageFolderEntity[],
);
this.logger.log(
`Migrated ${coreMessageFolders.length} message folders for workspace ${workspaceId}`,
);
}
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED],
workspaceId,
);
this.logger.log(
`Enabled IS_CONNECTED_ACCOUNT_MIGRATED for workspace ${workspaceId}`,
);
}
private async buildWorkspaceMemberIdToUserWorkspaceIdMap(
workspaceId: string,
): Promise<Map<string, string>> {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
const workspaceMembers = await workspaceMemberRepository.find();
const userWorkspaces = await this.userWorkspaceRepository.find({
where: { workspaceId },
select: ['id', 'userId'],
});
const userWorkspaceIdByUserId = new Map(
userWorkspaces.map((userWorkspace) => [
userWorkspace.userId,
userWorkspace.id,
]),
);
return new Map(
workspaceMembers
.filter((member) => userWorkspaceIdByUserId.has(member.userId))
.map((member) => [
member.id,
userWorkspaceIdByUserId.get(member.userId)!,
]),
);
}
}
@@ -8,13 +8,19 @@ import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/u
import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command';
import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command';
import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command';
import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command';
import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command';
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@@ -23,7 +29,14 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
@Module({
imports: [
TypeOrmModule.forFeature([WorkspaceEntity]),
TypeOrmModule.forFeature([
WorkspaceEntity,
ConnectedAccountEntity,
MessageChannelEntity,
CalendarChannelEntity,
MessageFolderEntity,
UserWorkspaceEntity,
]),
DataSourceModule,
WorkspaceCacheModule,
WorkspaceCacheStorageModule,
@@ -44,6 +57,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
BackfillPageLayoutsCommand,
SeedCliApplicationRegistrationCommand,
MigrateRichTextToTextCommand,
MigrateMessagingInfrastructureToMetadataCommand,
],
exports: [
IdentifyPermissionFlagMetadataCommand,
@@ -55,6 +69,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
BackfillPageLayoutsCommand,
SeedCliApplicationRegistrationCommand,
MigrateRichTextToTextCommand,
MigrateMessagingInfrastructureToMetadataCommand,
],
})
export class V1_20_UpgradeVersionCommandModule {}
@@ -40,6 +40,7 @@ import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/u
import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command';
import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command';
import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command';
import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command';
import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command';
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -101,6 +102,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly backfillPageLayoutsCommand: BackfillPageLayoutsCommand,
protected readonly seedCliApplicationRegistrationCommand: SeedCliApplicationRegistrationCommand,
protected readonly migrateRichTextToTextCommand: MigrateRichTextToTextCommand,
protected readonly migrateMessagingInfrastructureToMetadataCommand: MigrateMessagingInfrastructureToMetadataCommand,
) {
super(
workspaceRepository,
@@ -159,6 +161,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.backfillCommandMenuItemsCommand,
this.backfillPageLayoutsCommand,
this.seedCliApplicationRegistrationCommand,
this.migrateMessagingInfrastructureToMetadataCommand,
];
this.allCommands = {
@@ -0,0 +1,139 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddMessagingInfrastructureMetadataEntities1773945207801
implements MigrationInterface
{
name = 'AddMessagingInfrastructureMetadataEntities1773945207801';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "core"."connectedAccount" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "handle" character varying NOT NULL, "provider" character varying NOT NULL, "accessToken" character varying, "refreshToken" character varying, "lastCredentialsRefreshedAt" TIMESTAMP WITH TIME ZONE, "authFailedAt" TIMESTAMP WITH TIME ZONE, "handleAliases" character varying array, "scopes" character varying array, "connectionParameters" jsonb, "lastSignedInAt" TIMESTAMP WITH TIME ZONE, "oidcTokenClaims" jsonb, "userWorkspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_8e7a0a0bbc2e06ac2acf89b7f3a" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TYPE "core"."messageChannel_visibility_enum" AS ENUM('METADATA', 'SUBJECT', 'SHARE_EVERYTHING')`,
);
await queryRunner.query(
`CREATE TYPE "core"."messageChannel_type_enum" AS ENUM('EMAIL', 'SMS')`,
);
await queryRunner.query(
`CREATE TYPE "core"."messageChannel_contactautocreationpolicy_enum" AS ENUM('SENT_AND_RECEIVED', 'SENT', 'NONE')`,
);
await queryRunner.query(
`CREATE TYPE "core"."messageChannel_messagefolderimportpolicy_enum" AS ENUM('ALL_FOLDERS', 'SELECTED_FOLDERS')`,
);
await queryRunner.query(
`CREATE TYPE "core"."messageChannel_pendinggroupemailsaction_enum" AS ENUM('GROUP_EMAILS_DELETION', 'GROUP_EMAILS_IMPORT', 'NONE')`,
);
await queryRunner.query(
`CREATE TYPE "core"."messageChannel_syncstatus_enum" AS ENUM('NOT_SYNCED', 'ONGOING', 'ACTIVE', 'FAILED_INSUFFICIENT_PERMISSIONS', 'FAILED_UNKNOWN')`,
);
await queryRunner.query(
`CREATE TYPE "core"."messageChannel_syncstage_enum" AS ENUM('PENDING_CONFIGURATION', 'MESSAGE_LIST_FETCH_PENDING', 'MESSAGE_LIST_FETCH_SCHEDULED', 'MESSAGE_LIST_FETCH_ONGOING', 'MESSAGES_IMPORT_PENDING', 'MESSAGES_IMPORT_SCHEDULED', 'MESSAGES_IMPORT_ONGOING', 'FAILED')`,
);
await queryRunner.query(
`CREATE TABLE "core"."messageChannel" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "visibility" "core"."messageChannel_visibility_enum" NOT NULL, "handle" character varying NOT NULL, "type" "core"."messageChannel_type_enum" NOT NULL, "isContactAutoCreationEnabled" boolean NOT NULL, "contactAutoCreationPolicy" "core"."messageChannel_contactautocreationpolicy_enum" NOT NULL, "messageFolderImportPolicy" "core"."messageChannel_messagefolderimportpolicy_enum" NOT NULL, "excludeNonProfessionalEmails" boolean NOT NULL, "excludeGroupEmails" boolean NOT NULL, "pendingGroupEmailsAction" "core"."messageChannel_pendinggroupemailsaction_enum" NOT NULL, "isSyncEnabled" boolean NOT NULL, "syncCursor" character varying, "syncedAt" TIMESTAMP WITH TIME ZONE, "syncStatus" "core"."messageChannel_syncstatus_enum" NOT NULL DEFAULT 'NOT_SYNCED', "syncStage" "core"."messageChannel_syncstage_enum" NOT NULL, "syncStageStartedAt" TIMESTAMP WITH TIME ZONE, "throttleFailureCount" integer NOT NULL DEFAULT '0', "throttleRetryAfter" TIMESTAMP WITH TIME ZONE, "connectedAccountId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_438b9412475f39712ed065f77af" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TYPE "core"."messageFolder_pendingsyncaction_enum" AS ENUM('FOLDER_DELETION', 'NONE')`,
);
await queryRunner.query(
`CREATE TABLE "core"."messageFolder" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying, "syncCursor" character varying, "isSentFolder" boolean NOT NULL, "isSynced" boolean NOT NULL, "parentFolderId" uuid, "externalId" character varying, "pendingSyncAction" "core"."messageFolder_pendingsyncaction_enum" NOT NULL, "messageChannelId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_85cb5a339d9f7f1106dde9e4db8" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TYPE "core"."calendarChannel_syncstatus_enum" AS ENUM('NOT_SYNCED', 'ONGOING', 'ACTIVE', 'FAILED_INSUFFICIENT_PERMISSIONS', 'FAILED_UNKNOWN')`,
);
await queryRunner.query(
`CREATE TYPE "core"."calendarChannel_syncstage_enum" AS ENUM('PENDING_CONFIGURATION', 'CALENDAR_EVENT_LIST_FETCH_PENDING', 'CALENDAR_EVENT_LIST_FETCH_SCHEDULED', 'CALENDAR_EVENT_LIST_FETCH_ONGOING', 'CALENDAR_EVENTS_IMPORT_PENDING', 'CALENDAR_EVENTS_IMPORT_SCHEDULED', 'CALENDAR_EVENTS_IMPORT_ONGOING', 'FAILED')`,
);
await queryRunner.query(
`CREATE TYPE "core"."calendarChannel_visibility_enum" AS ENUM('METADATA', 'SHARE_EVERYTHING')`,
);
await queryRunner.query(
`CREATE TYPE "core"."calendarChannel_contactautocreationpolicy_enum" AS ENUM('AS_PARTICIPANT_AND_ORGANIZER', 'AS_PARTICIPANT', 'AS_ORGANIZER', 'NONE')`,
);
await queryRunner.query(
`CREATE TABLE "core"."calendarChannel" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "handle" character varying NOT NULL, "syncStatus" "core"."calendarChannel_syncstatus_enum" NOT NULL DEFAULT 'NOT_SYNCED', "syncStage" "core"."calendarChannel_syncstage_enum" NOT NULL, "visibility" "core"."calendarChannel_visibility_enum" NOT NULL, "isContactAutoCreationEnabled" boolean NOT NULL, "contactAutoCreationPolicy" "core"."calendarChannel_contactautocreationpolicy_enum" NOT NULL, "isSyncEnabled" boolean NOT NULL, "syncCursor" character varying, "syncedAt" TIMESTAMP WITH TIME ZONE, "syncStageStartedAt" TIMESTAMP WITH TIME ZONE, "throttleFailureCount" integer NOT NULL DEFAULT '0', "connectedAccountId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_796d701c0c35518517d0f3e0e0b" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "core"."connectedAccount" ADD CONSTRAINT "FK_1c7af038a011e99c27044793c6a" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."messageChannel" ADD CONSTRAINT "FK_22d9a21a23fdd99295dc0efc177" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."messageChannel" ADD CONSTRAINT "FK_2e966cbb240771c67630d52895c" FOREIGN KEY ("connectedAccountId") REFERENCES "core"."connectedAccount"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."messageFolder" ADD CONSTRAINT "FK_e7fb85af997d06d8f7cc7512801" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."messageFolder" ADD CONSTRAINT "FK_4237a2fe8a6583354f807c2f8fe" FOREIGN KEY ("messageChannelId") REFERENCES "core"."messageChannel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."calendarChannel" ADD CONSTRAINT "FK_bb5ebadf91b73c8050fb0a092fa" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."calendarChannel" ADD CONSTRAINT "FK_c7bc368c97a18a072413d67cf45" FOREIGN KEY ("connectedAccountId") REFERENCES "core"."connectedAccount"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."calendarChannel" DROP CONSTRAINT "FK_c7bc368c97a18a072413d67cf45"`,
);
await queryRunner.query(
`ALTER TABLE "core"."calendarChannel" DROP CONSTRAINT "FK_bb5ebadf91b73c8050fb0a092fa"`,
);
await queryRunner.query(
`ALTER TABLE "core"."messageFolder" DROP CONSTRAINT "FK_4237a2fe8a6583354f807c2f8fe"`,
);
await queryRunner.query(
`ALTER TABLE "core"."messageFolder" DROP CONSTRAINT "FK_e7fb85af997d06d8f7cc7512801"`,
);
await queryRunner.query(
`ALTER TABLE "core"."messageChannel" DROP CONSTRAINT "FK_2e966cbb240771c67630d52895c"`,
);
await queryRunner.query(
`ALTER TABLE "core"."messageChannel" DROP CONSTRAINT "FK_22d9a21a23fdd99295dc0efc177"`,
);
await queryRunner.query(
`ALTER TABLE "core"."connectedAccount" DROP CONSTRAINT "FK_1c7af038a011e99c27044793c6a"`,
);
await queryRunner.query(`DROP TABLE "core"."calendarChannel"`);
await queryRunner.query(
`DROP TYPE "core"."calendarChannel_contactautocreationpolicy_enum"`,
);
await queryRunner.query(
`DROP TYPE "core"."calendarChannel_visibility_enum"`,
);
await queryRunner.query(
`DROP TYPE "core"."calendarChannel_syncstage_enum"`,
);
await queryRunner.query(
`DROP TYPE "core"."calendarChannel_syncstatus_enum"`,
);
await queryRunner.query(`DROP TABLE "core"."messageFolder"`);
await queryRunner.query(
`DROP TYPE "core"."messageFolder_pendingsyncaction_enum"`,
);
await queryRunner.query(`DROP TABLE "core"."messageChannel"`);
await queryRunner.query(`DROP TYPE "core"."messageChannel_syncstage_enum"`);
await queryRunner.query(
`DROP TYPE "core"."messageChannel_syncstatus_enum"`,
);
await queryRunner.query(
`DROP TYPE "core"."messageChannel_pendinggroupemailsaction_enum"`,
);
await queryRunner.query(
`DROP TYPE "core"."messageChannel_messagefolderimportpolicy_enum"`,
);
await queryRunner.query(
`DROP TYPE "core"."messageChannel_contactautocreationpolicy_enum"`,
);
await queryRunner.query(`DROP TYPE "core"."messageChannel_type_enum"`);
await queryRunner.query(
`DROP TYPE "core"."messageChannel_visibility_enum"`,
);
await queryRunner.query(`DROP TABLE "core"."connectedAccount"`);
}
}
@@ -1,6 +1,6 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
import { DataArgProcessorService } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service';
import { type SystemWorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
@@ -116,6 +116,118 @@ describe('DataArgProcessorService', () => {
expect(dataArgProcessorService).toBeDefined();
});
it('should normalize relation connect where composite values', async () => {
const flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata> = {
byUniversalIdentifier: {
'company-universal-id': {
id: 'company-id',
name: 'company',
type: FieldMetadataType.RELATION,
isNullable: true,
objectMetadataId: 'object-id',
universalIdentifier: 'company-universal-id',
relationTargetObjectMetadataId: 'target-company-object-id',
settings: {
relationType: RelationType.MANY_TO_ONE,
joinColumnName: 'companyId',
},
} as FlatFieldMetadata,
'emails-universal-id': {
id: 'emails-id',
name: 'emails',
type: FieldMetadataType.EMAILS,
isNullable: true,
objectMetadataId: 'target-company-object-id',
universalIdentifier: 'emails-universal-id',
} as FlatFieldMetadata,
'domainName-universal-id': {
id: 'domainName-id',
name: 'domainName',
type: FieldMetadataType.LINKS,
isNullable: true,
objectMetadataId: 'target-company-object-id',
universalIdentifier: 'domainName-universal-id',
} as FlatFieldMetadata,
},
universalIdentifierById: {
'company-id': 'company-universal-id',
'emails-id': 'emails-universal-id',
'domainName-id': 'domainName-universal-id',
},
universalIdentifiersByApplicationId: {},
};
const flatObjectMetadata = {
id: 'object-id',
nameSingular: 'testObject',
namePlural: 'testObjects',
isCustom: false,
fieldIds: ['company-id'],
universalIdentifier: 'test-object-universal-id',
labelIdentifierFieldMetadataUniversalIdentifier: null,
imageIdentifierFieldMetadataUniversalIdentifier: null,
} as FlatObjectMetadata;
const flatObjectMetadataMaps = {
byUniversalIdentifier: {
'target-company-universal-id': {
id: 'target-company-object-id',
nameSingular: 'company',
namePlural: 'companies',
isCustom: false,
fieldIds: ['emails-id', 'domainName-id'],
universalIdentifier: 'target-company-universal-id',
labelIdentifierFieldMetadataUniversalIdentifier: null,
imageIdentifierFieldMetadataUniversalIdentifier: null,
} as FlatObjectMetadata,
},
universalIdentifierById: {
'target-company-object-id': 'target-company-universal-id',
},
universalIdentifiersByApplicationId: {},
};
const result = await dataArgProcessorService.process({
partialRecordInputs: [
{
company: {
connect: {
where: {
emails: {
primaryEmail: 'User@Example.COM',
},
domainName: {
primaryLinkUrl: 'HTTPS://Example.COM/path/',
},
},
},
},
},
],
authContext: createMockAuthContext(),
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
});
expect(result).toEqual([
{
company: {
connect: {
where: {
emails: {
primaryEmail: 'user@example.com',
},
domainName: {
primaryLinkUrl: 'https://example.com/path',
},
},
},
},
},
]);
});
describe('failing inputs validation', () => {
const fieldMetadataTypesToTest = Object.keys(
failingInputsByFieldMetadataType,
@@ -146,6 +258,11 @@ describe('DataArgProcessorService', () => {
authContext: createMockAuthContext(),
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps: {
byUniversalIdentifier: {},
universalIdentifierById: {},
universalIdentifiersByApplicationId: {},
},
}),
).rejects.toThrowErrorMatchingSnapshot();
});
@@ -183,6 +300,11 @@ describe('DataArgProcessorService', () => {
authContext: createMockAuthContext(),
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps: {
byUniversalIdentifier: {},
universalIdentifierById: {},
universalIdentifiersByApplicationId: {},
},
});
expect(result).toBeDefined();
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { isNull, isUndefined } from '@sniptt/guards';
import { isNull, isObject, isUndefined } from '@sniptt/guards';
import {
FieldMetadataSettingsMapping,
FieldMetadataType,
@@ -15,7 +15,6 @@ import {
} from 'twenty-shared/utils';
import { transformActorField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-actor-field.util';
import { isRelationNestedOperation } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-relation-nested-operation.util';
import { transformAddressField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-address-field.util';
import { transformArrayField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-array-field.util';
import { transformCurrencyField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-currency-field.util';
@@ -23,6 +22,7 @@ import { transformFullNameField } from 'src/engine/api/common/common-args-proces
import { transformNumericField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-numeric-field.util';
import { transformRawJsonField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-raw-json-field.util';
import { transformTextField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util';
import { isRelationNestedOperation } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-relation-nested-operation.util';
import { validateActorFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-actor-field-or-throw.util';
import { validateAddressFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-address-field-or-throw.util';
import { validateArrayFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util';
@@ -56,12 +56,12 @@ import { transformLinksValue } from 'src/engine/core-modules/record-transformer/
import { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util';
import { transformRichTextValue } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text.util';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@Injectable()
@@ -73,12 +73,14 @@ export class DataArgProcessorService {
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
shouldBackfillPositionIfUndefined = true,
}: {
partialRecordInputs: Partial<ObjectRecord>[] | undefined;
authContext: WorkspaceAuthContext;
flatObjectMetadata: FlatObjectMetadata;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
shouldBackfillPositionIfUndefined?: boolean;
}): Promise<Partial<ObjectRecord>[]> {
if (!isDefined(partialRecordInputs)) {
@@ -158,6 +160,8 @@ export class DataArgProcessorService {
fieldMetadata,
key,
value,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
);
}
processedRecords.push(processedRecord);
@@ -170,6 +174,8 @@ export class DataArgProcessorService {
fieldMetadata: FlatFieldMetadata,
key: string,
value: unknown,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
): Promise<unknown> {
switch (fieldMetadata.type) {
case FieldMetadataType.POSITION:
@@ -260,6 +266,29 @@ export class DataArgProcessorService {
);
}
const connectOperation = value as Record<
string,
Record<string, unknown>
>;
const connectWhere = connectOperation.connect?.where;
if (isObject(connectWhere)) {
const processedWhere = await this.processConnectWhere(
connectWhere as Record<string, unknown>,
fieldMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
);
return {
...connectOperation,
connect: {
...connectOperation.connect,
where: processedWhere,
},
};
}
return value;
}
case FieldMetadataType.PHONES: {
@@ -325,4 +354,85 @@ export class DataArgProcessorService {
);
}
}
private async processConnectWhere(
connectWhere: Record<string, unknown>,
relationFieldMetadata: FlatFieldMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
): Promise<Record<string, unknown>> {
if (!isDefined(relationFieldMetadata.relationTargetObjectMetadataId)) {
throw new CommonQueryRunnerException(
`Relation target object metadata id not found for field ${relationFieldMetadata.name}`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
const targetObjectMetadata =
findFlatEntityByIdInFlatEntityMaps<FlatObjectMetadata>({
flatEntityId: relationFieldMetadata.relationTargetObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(targetObjectMetadata)) {
throw new CommonQueryRunnerException(
`Relation target object metadata not found for field ${relationFieldMetadata.name}`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
targetObjectMetadata,
);
const processedWhere: Record<string, unknown> = {};
for (const [whereKey, whereValue] of Object.entries(connectWhere)) {
const fieldId = fieldIdByName[whereKey];
if (!isDefined(fieldId)) {
processedWhere[whereKey] = whereValue;
continue;
}
const whereFieldMetadata =
findFlatEntityByIdInFlatEntityMaps<FlatFieldMetadata>({
flatEntityId: fieldId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!isDefined(whereFieldMetadata)) {
processedWhere[whereKey] = whereValue;
continue;
}
try {
const processedValue = await this.processField(
whereFieldMetadata,
whereKey,
whereValue,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
);
// Only keep original keys — processField may add null subfields that alter WHERE semantics
if (isObject(whereValue) && isObject(processedValue)) {
const originalKeys = new Set(Object.keys(whereValue));
processedWhere[whereKey] = Object.fromEntries(
Object.entries(processedValue).filter(([k]) => originalKeys.has(k)),
);
} else {
processedWhere[whereKey] = processedValue;
}
} catch {
processedWhere[whereKey] = whereValue;
}
}
return processedWhere;
}
}
@@ -145,8 +145,12 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
args: CommonInput<CreateManyQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<CreateManyQueryArgs>> {
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
queryRunnerContext;
const {
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = queryRunnerContext;
return {
...args,
@@ -155,6 +159,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
}),
};
}
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import { type ObjectRecord } from 'twenty-shared/types';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service';
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
@@ -15,6 +14,7 @@ import {
CreateOneQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@@ -52,14 +52,19 @@ export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerServ
args: CommonInput<CreateOneQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<CreateOneQueryArgs>> {
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
queryRunnerContext;
const {
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = queryRunnerContext;
const coercedData = await this.dataArgProcessor.process({
partialRecordInputs: [args.data],
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
});
return {
@@ -176,8 +176,12 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne
args: CommonInput<FindDuplicatesQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<FindDuplicatesQueryArgs>> {
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
queryRunnerContext;
const {
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = queryRunnerContext;
const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
@@ -202,6 +206,7 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
shouldBackfillPositionIfUndefined: false,
}),
};
@@ -100,8 +100,12 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer
args: CommonInput<UpdateManyQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<UpdateManyQueryArgs>> {
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
queryRunnerContext;
const {
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = queryRunnerContext;
return {
...args,
@@ -116,6 +120,7 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
shouldBackfillPositionIfUndefined: false,
})
)[0],
@@ -65,8 +65,12 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ
args: CommonInput<UpdateOneQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<UpdateOneQueryArgs>> {
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
queryRunnerContext;
const {
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = queryRunnerContext;
return {
...args,
@@ -76,6 +80,7 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
shouldBackfillPositionIfUndefined: false,
})
)[0],
@@ -1,3 +1,5 @@
import { registerEnumType } from '@nestjs/graphql';
export enum DatabaseEventAction {
CREATED = 'created',
UPDATED = 'updated',
@@ -6,3 +8,8 @@ export enum DatabaseEventAction {
RESTORED = 'restored',
UPSERTED = 'upserted',
}
registerEnumType(DatabaseEventAction, {
name: 'DatabaseEventAction',
description: 'Database Event Action',
});
@@ -20,7 +20,7 @@ import { CallWebhookJobsJob } from 'src/engine/metadata-modules/webhook/jobs/cal
import { WorkspaceEventBatchForWebhook } from 'src/engine/metadata-modules/webhook/types/workspace-event-batch-for-webhook.type';
import { CallDatabaseEventTriggerJobsJob } from 'src/engine/core-modules/logic-function/logic-function-trigger/triggers/database-event/call-database-event-trigger-jobs.job';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { WorkspaceEventEmitterService } from 'src/engine/workspace-event-emitter/workspace-event-emitter.service';
import { ObjectRecordEventPublisher } from 'src/engine/subscriptions/object-record-event/object-record-event-publisher';
import { UpsertTimelineActivityFromInternalEvent } from 'src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job';
@Injectable()
@@ -32,7 +32,7 @@ export class EntityEventsToDbListener {
private readonly webhookQueueService: MessageQueueService,
@InjectMessageQueue(MessageQueue.triggerQueue)
private readonly triggerQueueService: MessageQueueService,
private readonly workspaceEventEmitterService: WorkspaceEventEmitterService,
private readonly objectRecordEventPublisher: ObjectRecordEventPublisher,
) {}
@OnDatabaseBatchEvent('*', DatabaseEventAction.CREATED)
@@ -79,7 +79,7 @@ export class EntityEventsToDbListener {
};
const promises = [
this.workspaceEventEmitterService.publish(batchEvent),
this.objectRecordEventPublisher.publish(batchEvent),
this.webhookQueueService.add<WorkspaceEventBatchForWebhook<T>>(
CallWebhookJobsJob.name,
batchEventForWebhook,
@@ -59,6 +59,9 @@ import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-inv
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module';
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
@@ -104,6 +107,9 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
ConnectedAccountModule,
MessagingCommonModule,
MessagingFolderSyncManagerModule,
CalendarChannelDataAccessModule,
ConnectedAccountDataAccessModule,
MessageChannelDataAccessModule,
WorkspaceSSOModule,
FeatureFlagModule,
WorkspaceInvitationModule,
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { v4 } from 'uuid';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
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';
@@ -9,7 +10,6 @@ import {
CalendarChannelSyncStage,
CalendarChannelSyncStatus,
CalendarChannelVisibility,
type CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
export type CreateCalendarChannelInput = {
@@ -25,6 +25,7 @@ export type CreateCalendarChannelInput = {
export class CreateCalendarChannelService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
) {}
async createCalendarChannel(
@@ -43,15 +44,12 @@ export class CreateCalendarChannelService {
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const calendarChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const newCalendarChannelId = v4();
const newCalendarChannel = await calendarChannelRepository.save(
await this.calendarChannelDataAccessService.save(
workspaceId,
{
id: v4(),
id: newCalendarChannelId,
connectedAccountId,
handle,
visibility:
@@ -63,11 +61,10 @@ export class CreateCalendarChannelService {
? CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING
: CalendarChannelSyncStage.PENDING_CONFIGURATION,
},
{},
manager,
);
return newCalendarChannel.id;
return newCalendarChannelId;
},
authContext,
);
@@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common';
import { type ConnectedAccountProvider } from 'twenty-shared/types';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
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 { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
export type CreateConnectedAccountInput = {
workspaceId: string;
@@ -23,6 +23,7 @@ export type CreateConnectedAccountInput = {
export class CreateConnectedAccountService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
) {}
async createConnectedAccount(
@@ -43,13 +44,8 @@ export class CreateConnectedAccountService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
await connectedAccountRepository.save(
await this.connectedAccountDataAccessService.save(
workspaceId,
{
id: connectedAccountId,
handle,
@@ -59,7 +55,6 @@ export class CreateConnectedAccountService {
accountOwnerId,
scopes,
},
{},
manager,
);
}, authContext);
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
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';
@@ -29,6 +30,7 @@ export type CreateMessageChannelInput = {
export class CreateMessageChannelService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
private readonly syncMessageFoldersService: SyncMessageFoldersService,
) {}
@@ -48,15 +50,10 @@ export class CreateMessageChannelService {
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const messageChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const newMessageChannelId = v4();
await messageChannelRepository.insert(
await this.messageChannelDataAccessService.save(
workspaceId,
{
id: newMessageChannelId,
connectedAccountId,
@@ -76,17 +73,19 @@ export class CreateMessageChannelService {
manager,
);
const createdMessageChannel = await messageChannelRepository.findOne({
where: { id: newMessageChannelId },
relations: ['connectedAccount', 'messageFolders'],
});
const createdMessageChannel =
await this.messageChannelDataAccessService.findOne(workspaceId, {
where: { id: newMessageChannelId },
relations: ['connectedAccount', 'messageFolders'],
});
if (!isDefined(createdMessageChannel)) {
throw new Error('Message channel not found');
}
await this.syncMessageFoldersService.syncMessageFolders({
messageChannel: createdMessageChannel,
messageChannel:
createdMessageChannel as unknown as MessageChannelWorkspaceEntity,
workspaceId,
});
@@ -11,6 +11,9 @@ import { GoogleApisServiceAvailabilityService } from 'src/engine/core-modules/au
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -36,22 +39,16 @@ describe('GoogleAPIsService', () => {
let messagingChannelSyncStatusService: MessageChannelSyncStatusService;
let createMessageChannelService: CreateMessageChannelService;
const mockConnectedAccountRepository = {
const mockConnectedAccountDataAccessService = {
findOne: jest.fn(),
find: jest.fn(),
update: jest.fn(),
};
const mockCalendarChannelRepository = {
findOne: jest.fn(),
const mockMessageChannelDataAccessService = {
find: jest.fn(),
update: jest.fn(),
};
const mockMessageChannelRepository = {
findOne: jest.fn(),
const mockCalendarChannelDataAccessService = {
find: jest.fn(),
update: jest.fn(),
};
const mockWorkspaceMemberRepository = {
@@ -84,12 +81,6 @@ describe('GoogleAPIsService', () => {
getRepository: jest
.fn()
.mockImplementation((_workspaceId, entity) => {
if (entity === 'connectedAccount')
return mockConnectedAccountRepository;
if (entity === 'calendarChannel')
return mockCalendarChannelRepository;
if (entity === 'messageChannel')
return mockMessageChannelRepository;
if (entity === 'workspaceMember')
return mockWorkspaceMemberRepository;
@@ -188,6 +179,18 @@ describe('GoogleAPIsService', () => {
isFeatureEnabled: jest.fn().mockResolvedValue(false),
},
},
{
provide: ConnectedAccountDataAccessService,
useValue: mockConnectedAccountDataAccessService,
},
{
provide: MessageChannelDataAccessService,
useValue: mockMessageChannelDataAccessService,
},
{
provide: CalendarChannelDataAccessService,
useValue: mockCalendarChannelDataAccessService,
},
],
}).compile();
@@ -221,7 +224,7 @@ describe('GoogleAPIsService', () => {
provider: ConnectedAccountProvider.GOOGLE,
} as ConnectedAccountWorkspaceEntity;
mockConnectedAccountRepository.findOne.mockResolvedValue(
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(
existingConnectedAccount,
);
@@ -237,11 +240,11 @@ describe('GoogleAPIsService', () => {
syncStage: CalendarChannelSyncStage.FAILED,
};
mockCalendarChannelRepository.find.mockResolvedValue([
mockCalendarChannelDataAccessService.find.mockResolvedValue([
failedCalendarChannel,
]);
mockMessageChannelRepository.find.mockResolvedValue([]);
mockMessageChannelDataAccessService.find.mockResolvedValue([]);
await service.refreshGoogleRefreshToken({
handle: 'test@example.com',
@@ -18,6 +18,9 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
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';
@@ -29,16 +32,14 @@ import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/se
import {
CalendarChannelSyncStage,
type CalendarChannelVisibility,
type CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import {
MessageChannelSyncStage,
MessageChannelSyncStatus,
type MessageChannelVisibility,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessagingMessageListFetchJob,
@@ -65,6 +66,9 @@ export class GoogleAPIsService {
private readonly googleAPIScopesService: GoogleAPIScopesService,
private readonly googleApisServiceAvailabilityService: GoogleApisServiceAvailabilityService,
private readonly featureFlagService: FeatureFlagService,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
) {}
async refreshGoogleRefreshToken(input: {
@@ -128,31 +132,14 @@ export class GoogleAPIsService {
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { handle, accountOwnerId: workspaceMemberId },
});
const connectedAccount =
await this.connectedAccountDataAccessService.findOne(workspaceId, {
where: { handle, accountOwnerId: workspaceMemberId },
});
const existingAccountId = connectedAccount?.id;
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
const calendarChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const messageChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const workspaceDataSource =
await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
@@ -200,7 +187,6 @@ export class GoogleAPIsService {
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
connectedAccount,
manager,
},
);
@@ -229,9 +215,10 @@ export class GoogleAPIsService {
);
if (isMessagingEnabled) {
const messageChannels = await messageChannelRepository.find({
where: { connectedAccountId: newOrExistingConnectedAccountId },
});
const messageChannels =
await this.messageChannelDataAccessService.find(workspaceId, {
connectedAccountId: newOrExistingConnectedAccountId,
});
if (!isMessagingAvailable && messageChannels.length > 0) {
await this.messagingChannelSyncStatusService.markAsFailed(
@@ -261,9 +248,12 @@ export class GoogleAPIsService {
}
if (isCalendarEnabled) {
const calendarChannels = await calendarChannelRepository.find({
where: { connectedAccountId: newOrExistingConnectedAccountId },
});
const calendarChannels =
await this.calendarChannelDataAccessService.find(workspaceId, {
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
if (!isCalendarAvailable && calendarChannels.length > 0) {
await this.calendarChannelSyncStatusService.markAsFailedInsufficientPermissionsAndFlushCalendarEventsToImport(
@@ -8,6 +8,9 @@ import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/serv
import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service';
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -33,22 +36,16 @@ describe('MicrosoftAPIsService', () => {
let calendarChannelSyncStatusService: CalendarChannelSyncStatusService;
let createMessageChannelService: CreateMessageChannelService;
const mockConnectedAccountRepository = {
const mockConnectedAccountDataAccessService = {
findOne: jest.fn(),
find: jest.fn(),
update: jest.fn(),
};
const mockCalendarChannelRepository = {
findOne: jest.fn(),
const mockMessageChannelDataAccessService = {
find: jest.fn(),
update: jest.fn(),
};
const mockMessageChannelRepository = {
findOne: jest.fn(),
const mockCalendarChannelDataAccessService = {
find: jest.fn(),
update: jest.fn(),
};
const mockWorkspaceMemberRepository = {
@@ -81,12 +78,6 @@ describe('MicrosoftAPIsService', () => {
getRepository: jest
.fn()
.mockImplementation((_workspaceId, entity) => {
if (entity === 'connectedAccount')
return mockConnectedAccountRepository;
if (entity === 'calendarChannel')
return mockCalendarChannelRepository;
if (entity === 'messageChannel')
return mockMessageChannelRepository;
if (entity === 'workspaceMember')
return mockWorkspaceMemberRepository;
@@ -162,6 +153,18 @@ describe('MicrosoftAPIsService', () => {
provide: getQueueToken(MessageQueue.calendarQueue),
useValue: mockCalendarQueueService,
},
{
provide: ConnectedAccountDataAccessService,
useValue: mockConnectedAccountDataAccessService,
},
{
provide: MessageChannelDataAccessService,
useValue: mockMessageChannelDataAccessService,
},
{
provide: CalendarChannelDataAccessService,
useValue: mockCalendarChannelDataAccessService,
},
],
}).compile();
@@ -195,7 +198,7 @@ describe('MicrosoftAPIsService', () => {
provider: ConnectedAccountProvider.MICROSOFT,
} as ConnectedAccountWorkspaceEntity;
mockConnectedAccountRepository.findOne.mockResolvedValue(
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(
existingConnectedAccount,
);
@@ -211,11 +214,11 @@ describe('MicrosoftAPIsService', () => {
syncStage: CalendarChannelSyncStage.FAILED,
};
mockCalendarChannelRepository.find.mockResolvedValue([
mockCalendarChannelDataAccessService.find.mockResolvedValue([
failedCalendarChannel,
]);
mockMessageChannelRepository.find.mockResolvedValue([
mockMessageChannelDataAccessService.find.mockResolvedValue([
{
id: 'message-channel-id',
connectedAccountId: 'existing-account-id',
@@ -12,6 +12,9 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
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';
@@ -23,15 +26,13 @@ import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/se
import {
CalendarChannelSyncStage,
type CalendarChannelVisibility,
type CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import {
MessageChannelSyncStage,
type MessageChannelVisibility,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessagingMessageListFetchJob,
@@ -55,6 +56,9 @@ export class MicrosoftAPIsService {
private readonly createConnectedAccountService: CreateConnectedAccountService,
private readonly updateConnectedAccountOnReconnectService: UpdateConnectedAccountOnReconnectService,
private readonly twentyConfigService: TwentyConfigService,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
) {}
async refreshMicrosoftRefreshToken(input: {
@@ -82,31 +86,14 @@ export class MicrosoftAPIsService {
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { handle, accountOwnerId: workspaceMemberId },
});
const connectedAccount =
await this.connectedAccountDataAccessService.findOne(workspaceId, {
where: { handle, accountOwnerId: workspaceMemberId },
});
const existingAccountId = connectedAccount?.id;
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
const calendarChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const messageChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const workspaceDataSource =
await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
@@ -156,7 +143,6 @@ export class MicrosoftAPIsService {
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
connectedAccount,
manager,
},
);
@@ -197,11 +183,10 @@ export class MicrosoftAPIsService {
if (
this.twentyConfigService.get('MESSAGING_PROVIDER_MICROSOFT_ENABLED')
) {
const messageChannels = await messageChannelRepository.find({
where: {
const messageChannels =
await this.messageChannelDataAccessService.find(workspaceId, {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
});
for (const messageChannel of messageChannels) {
if (
@@ -222,11 +207,12 @@ export class MicrosoftAPIsService {
if (
this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED')
) {
const calendarChannels = await calendarChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
const calendarChannels =
await this.calendarChannelDataAccessService.find(workspaceId, {
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const calendarChannel of calendarChannels) {
if (
@@ -1,9 +1,9 @@
import { Injectable } from '@nestjs/common';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
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 { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
export type UpdateConnectedAccountOnReconnectInput = {
workspaceId: string;
@@ -11,7 +11,6 @@ export type UpdateConnectedAccountOnReconnectInput = {
accessToken: string;
refreshToken: string;
scopes: string[];
connectedAccount: ConnectedAccountWorkspaceEntity;
manager: WorkspaceEntityManager;
};
@@ -19,6 +18,7 @@ export type UpdateConnectedAccountOnReconnectInput = {
export class UpdateConnectedAccountOnReconnectService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
) {}
async updateConnectedAccountOnReconnect(
@@ -30,19 +30,13 @@ export class UpdateConnectedAccountOnReconnectService {
accessToken,
refreshToken,
scopes,
manager,
} = input;
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
await connectedAccountRepository.update(
await this.connectedAccountDataAccessService.update(
workspaceId,
{
id: connectedAccountId,
},
@@ -52,7 +46,6 @@ export class UpdateConnectedAccountOnReconnectService {
scopes,
authFailedAt: null,
},
manager,
);
}, authContext);
}
@@ -65,7 +65,6 @@ import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { RowLevelPermissionModule } from 'src/engine/metadata-modules/row-level-permission-predicate/row-level-permission.module';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
import { TrashCleanupModule } from 'src/engine/trash-cleanup/trash-cleanup.module';
import { MetadataEventEmitterModule } from 'src/engine/metadata-event-emitter/metadata-event-emitter.module';
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
import { ChannelSyncModule } from 'src/modules/connected-account/channel-sync/channel-sync.module';
import { DashboardModule } from 'src/modules/dashboard/dashboard.module';
@@ -110,7 +109,6 @@ import { FileModule } from './file/file.module';
PostgresCredentialsModule,
WorkflowApiModule,
WorkspaceEventEmitterModule,
MetadataEventEmitterModule,
ActorModule,
TelemetryModule,
AdminPanelModule,
@@ -1,3 +1,5 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
@@ -146,4 +148,53 @@ describe('objectRecordChangedValues', () => {
expect(result).toEqual(expectedChanges);
});
it('ignores changes to POSITION fields', () => {
const positionFieldId = 'position-field-id';
const positionUniversalId = 'position-universal-id';
const objectMetadataWithPosition: FlatObjectMetadata = {
...mockObjectMetadata,
fieldIds: [positionFieldId],
};
const flatFieldMetadataMapsWithPosition: FlatEntityMaps<FlatFieldMetadata> =
{
byUniversalIdentifier: {
[positionUniversalId]: {
id: positionFieldId,
name: 'position',
type: FieldMetadataType.POSITION,
universalIdentifier: positionUniversalId,
} as FlatFieldMetadata,
},
universalIdentifierById: {
[positionFieldId]: positionUniversalId,
},
universalIdentifiersByApplicationId: {},
};
const oldRecord = {
id: '74316f58-29b0-4a6a-b8fa-d2b506d5516n',
position: 1,
name: 'Original',
};
const newRecord = {
id: '74316f58-29b0-4a6a-b8fa-d2b506d5516n',
position: 5,
name: 'Updated',
};
const result = objectRecordChangedValues(
oldRecord,
newRecord,
objectMetadataWithPosition,
flatFieldMetadataMapsWithPosition,
);
expect(result).toEqual({
name: { before: 'Original', after: 'Updated' },
});
expect(result).not.toHaveProperty('position');
});
});
@@ -56,7 +56,8 @@ export const objectRecordChangedValues = (
if (
key === 'updatedAt' ||
key === 'searchVector' ||
field?.type === FieldMetadataType.RELATION
field?.type === FieldMetadataType.RELATION ||
field?.type === FieldMetadataType.POSITION
) {
return acc;
}
@@ -554,6 +554,11 @@ msgstr ""
msgid "Calendar channel is misconfigured."
msgstr "Kalenderkanaal is verkeerd gekonfigureer."
#. js-lingui-id: 3J2x1Q
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Calendar channel not found."
msgstr ""
#. js-lingui-id: 1t3DGy
#: src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts
msgid "Calendar event not found."
@@ -920,6 +925,7 @@ msgid "Connect is not allowed for {connectFieldName} on {objectMetadataNameSingu
msgstr "Konnekteer is nie toegelaat vir {connectFieldName} op {objectMetadataNameSingular} nie."
#. js-lingui-id: /OJTzk
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
#: src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception.ts
msgid "Connected account not found."
msgstr "Gekoppelde rekening nie gevind nie."
@@ -1774,6 +1780,11 @@ msgstr "Ongeldige oudit-tipe."
msgid "Invalid CALDAV credentials. Please check your username and password."
msgstr "Ongeldige CALDAV-geloofsbriewe. Maak seker jou gebruikersnaam en wagwoord is korrek."
#. js-lingui-id: cizVEW
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Invalid calendar channel input."
msgstr ""
#. js-lingui-id: X7+bC6
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid calling code {callingCode}"
@@ -1866,6 +1877,11 @@ msgstr ""
msgid "Invalid connected account ID."
msgstr "Ongeldige gekoppelde rekening-ID."
#. js-lingui-id: DH8Ofc
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
msgid "Invalid connected account input."
msgstr ""
#. js-lingui-id: 1PtrY0
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid country code {countryCode}"
@@ -2018,6 +2034,16 @@ msgstr "Ongeldige etiketformaat."
msgid "Invalid limit parameter."
msgstr "Ongeldige limiet-parameter."
#. js-lingui-id: fNaXqL
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Invalid message channel input."
msgstr ""
#. js-lingui-id: zUyPBT
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Invalid message folder input."
msgstr ""
#. js-lingui-id: iBBCnf
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
@@ -2679,6 +2705,16 @@ msgstr "Mei"
msgid "Message channel not found"
msgstr "Boodskapkanaal nie gevind nie"
#. js-lingui-id: vjIbbe
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Message channel not found."
msgstr ""
#. js-lingui-id: BZbxut
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Message folder not found."
msgstr ""
#. js-lingui-id: PslTPV
#: src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter.ts
msgid "Metadata validation failed"
@@ -554,6 +554,11 @@ msgstr ""
msgid "Calendar channel is misconfigured."
msgstr "تم تكوين قناة التقويم بشكل غير صحيح."
#. js-lingui-id: 3J2x1Q
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Calendar channel not found."
msgstr ""
#. js-lingui-id: 1t3DGy
#: src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts
msgid "Calendar event not found."
@@ -920,6 +925,7 @@ msgid "Connect is not allowed for {connectFieldName} on {objectMetadataNameSingu
msgstr "الاتصال غير مسموح لـ{connectFieldName} في {objectMetadataNameSingular}."
#. js-lingui-id: /OJTzk
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
#: src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception.ts
msgid "Connected account not found."
msgstr "الحساب المتصل غير موجود."
@@ -1774,6 +1780,11 @@ msgstr "نوع التدقيق غير صالح."
msgid "Invalid CALDAV credentials. Please check your username and password."
msgstr "بيانات الاعتماد الخاصة بـ CALDAV غير صحيحة. يُرجى التحقق من اسم المستخدم وكلمة المرور."
#. js-lingui-id: cizVEW
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Invalid calendar channel input."
msgstr ""
#. js-lingui-id: X7+bC6
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid calling code {callingCode}"
@@ -1866,6 +1877,11 @@ msgstr ""
msgid "Invalid connected account ID."
msgstr "معرّف الحساب المتصل غير صالح."
#. js-lingui-id: DH8Ofc
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
msgid "Invalid connected account input."
msgstr ""
#. js-lingui-id: 1PtrY0
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid country code {countryCode}"
@@ -2018,6 +2034,16 @@ msgstr "تنسيق التسمية غير صالح."
msgid "Invalid limit parameter."
msgstr "معامل الحد غير صالح."
#. js-lingui-id: fNaXqL
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Invalid message channel input."
msgstr ""
#. js-lingui-id: zUyPBT
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Invalid message folder input."
msgstr ""
#. js-lingui-id: iBBCnf
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
@@ -2679,6 +2705,16 @@ msgstr "مايو"
msgid "Message channel not found"
msgstr "قناة الرسائل غير موجودة"
#. js-lingui-id: vjIbbe
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Message channel not found."
msgstr ""
#. js-lingui-id: BZbxut
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Message folder not found."
msgstr ""
#. js-lingui-id: PslTPV
#: src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter.ts
msgid "Metadata validation failed"
@@ -554,6 +554,11 @@ msgstr ""
msgid "Calendar channel is misconfigured."
msgstr "El canal del calendari està configurat incorrectament."
#. js-lingui-id: 3J2x1Q
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Calendar channel not found."
msgstr ""
#. js-lingui-id: 1t3DGy
#: src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts
msgid "Calendar event not found."
@@ -920,6 +925,7 @@ msgid "Connect is not allowed for {connectFieldName} on {objectMetadataNameSingu
msgstr "No està permès connectar per a {connectFieldName} a {objectMetadataNameSingular}"
#. js-lingui-id: /OJTzk
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
#: src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception.ts
msgid "Connected account not found."
msgstr "Compte connectat no trobat."
@@ -1774,6 +1780,11 @@ msgstr "Tipus d'auditoria no vàlid."
msgid "Invalid CALDAV credentials. Please check your username and password."
msgstr "Credencials CALDAV invàlides. Comprova el teu nom d'usuari i contrasenya."
#. js-lingui-id: cizVEW
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Invalid calendar channel input."
msgstr ""
#. js-lingui-id: X7+bC6
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid calling code {callingCode}"
@@ -1866,6 +1877,11 @@ msgstr ""
msgid "Invalid connected account ID."
msgstr "ID de compte connectat no vàlid."
#. js-lingui-id: DH8Ofc
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
msgid "Invalid connected account input."
msgstr ""
#. js-lingui-id: 1PtrY0
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid country code {countryCode}"
@@ -2018,6 +2034,16 @@ msgstr "Format d'etiqueta no vàlid."
msgid "Invalid limit parameter."
msgstr "Paràmetre de límit no vàlid."
#. js-lingui-id: fNaXqL
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Invalid message channel input."
msgstr ""
#. js-lingui-id: zUyPBT
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Invalid message folder input."
msgstr ""
#. js-lingui-id: iBBCnf
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
@@ -2679,6 +2705,16 @@ msgstr "Maig"
msgid "Message channel not found"
msgstr "Canal de missatges no trobat"
#. js-lingui-id: vjIbbe
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Message channel not found."
msgstr ""
#. js-lingui-id: BZbxut
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Message folder not found."
msgstr ""
#. js-lingui-id: PslTPV
#: src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter.ts
msgid "Metadata validation failed"
@@ -554,6 +554,11 @@ msgstr ""
msgid "Calendar channel is misconfigured."
msgstr "Kanál kalendáře je chybně nakonfigurován."
#. js-lingui-id: 3J2x1Q
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Calendar channel not found."
msgstr ""
#. js-lingui-id: 1t3DGy
#: src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts
msgid "Calendar event not found."
@@ -920,6 +925,7 @@ msgid "Connect is not allowed for {connectFieldName} on {objectMetadataNameSingu
msgstr "Připojení není povoleno pro {connectFieldName} na {objectMetadataNameSingular}."
#. js-lingui-id: /OJTzk
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
#: src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception.ts
msgid "Connected account not found."
msgstr "Propojený účet nenalezen."
@@ -1774,6 +1780,11 @@ msgstr "Neplatný typ auditu."
msgid "Invalid CALDAV credentials. Please check your username and password."
msgstr "Neplatné přihlašovací údaje pro CALDAV. Zkontrolujte své uživatelské jméno a heslo."
#. js-lingui-id: cizVEW
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Invalid calendar channel input."
msgstr ""
#. js-lingui-id: X7+bC6
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid calling code {callingCode}"
@@ -1866,6 +1877,11 @@ msgstr ""
msgid "Invalid connected account ID."
msgstr "Neplatné ID propojeného účtu."
#. js-lingui-id: DH8Ofc
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
msgid "Invalid connected account input."
msgstr ""
#. js-lingui-id: 1PtrY0
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid country code {countryCode}"
@@ -2018,6 +2034,16 @@ msgstr "Neplatný formát štítku."
msgid "Invalid limit parameter."
msgstr "Neplatný parametr limitu."
#. js-lingui-id: fNaXqL
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Invalid message channel input."
msgstr ""
#. js-lingui-id: zUyPBT
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Invalid message folder input."
msgstr ""
#. js-lingui-id: iBBCnf
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
@@ -2679,6 +2705,16 @@ msgstr "Květen"
msgid "Message channel not found"
msgstr "Kanál zpráv nenalezen"
#. js-lingui-id: vjIbbe
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Message channel not found."
msgstr ""
#. js-lingui-id: BZbxut
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Message folder not found."
msgstr ""
#. js-lingui-id: PslTPV
#: src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter.ts
msgid "Metadata validation failed"
@@ -554,6 +554,11 @@ msgstr ""
msgid "Calendar channel is misconfigured."
msgstr "Kalenderkanal er fejlkonfigureret."
#. js-lingui-id: 3J2x1Q
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Calendar channel not found."
msgstr ""
#. js-lingui-id: 1t3DGy
#: src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts
msgid "Calendar event not found."
@@ -920,6 +925,7 @@ msgid "Connect is not allowed for {connectFieldName} on {objectMetadataNameSingu
msgstr "Connect er ikke tilladt for {connectFieldName} på {objectMetadataNameSingular}"
#. js-lingui-id: /OJTzk
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
#: src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception.ts
msgid "Connected account not found."
msgstr "Tilknyttet konto ikke fundet."
@@ -1774,6 +1780,11 @@ msgstr "Ugyldig revisionstype."
msgid "Invalid CALDAV credentials. Please check your username and password."
msgstr "Ugyldige CALDAV-legitimationsoplysninger. Tjek venligst dit brugernavn og din adgangskode."
#. js-lingui-id: cizVEW
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Invalid calendar channel input."
msgstr ""
#. js-lingui-id: X7+bC6
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid calling code {callingCode}"
@@ -1866,6 +1877,11 @@ msgstr ""
msgid "Invalid connected account ID."
msgstr "Ugyldigt ID for tilknyttet konto."
#. js-lingui-id: DH8Ofc
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
msgid "Invalid connected account input."
msgstr ""
#. js-lingui-id: 1PtrY0
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid country code {countryCode}"
@@ -2018,6 +2034,16 @@ msgstr "Ugyldigt etiketformat."
msgid "Invalid limit parameter."
msgstr "Ugyldig limit-parameter."
#. js-lingui-id: fNaXqL
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Invalid message channel input."
msgstr ""
#. js-lingui-id: zUyPBT
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Invalid message folder input."
msgstr ""
#. js-lingui-id: iBBCnf
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
@@ -2679,6 +2705,16 @@ msgstr "Maj"
msgid "Message channel not found"
msgstr "Meddelelseskanal ikke fundet"
#. js-lingui-id: vjIbbe
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Message channel not found."
msgstr ""
#. js-lingui-id: BZbxut
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Message folder not found."
msgstr ""
#. js-lingui-id: PslTPV
#: src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter.ts
msgid "Metadata validation failed"
@@ -554,6 +554,11 @@ msgstr ""
msgid "Calendar channel is misconfigured."
msgstr "Kalenderkanal ist falsch konfiguriert."
#. js-lingui-id: 3J2x1Q
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Calendar channel not found."
msgstr ""
#. js-lingui-id: 1t3DGy
#: src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts
msgid "Calendar event not found."
@@ -920,6 +925,7 @@ msgid "Connect is not allowed for {connectFieldName} on {objectMetadataNameSingu
msgstr "Verbinden ist nicht erlaubt für {connectFieldName} bei {objectMetadataNameSingular}."
#. js-lingui-id: /OJTzk
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
#: src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception.ts
msgid "Connected account not found."
msgstr "Verbundenes Konto nicht gefunden."
@@ -1774,6 +1780,11 @@ msgstr "Ungültiger Audit-Typ."
msgid "Invalid CALDAV credentials. Please check your username and password."
msgstr "Ungültige CalDAV-Anmeldedaten. Bitte überprüfen Sie Ihren Benutzernamen und Ihr Passwort."
#. js-lingui-id: cizVEW
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Invalid calendar channel input."
msgstr ""
#. js-lingui-id: X7+bC6
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid calling code {callingCode}"
@@ -1866,6 +1877,11 @@ msgstr ""
msgid "Invalid connected account ID."
msgstr "Ungültige ID des verbundenen Kontos."
#. js-lingui-id: DH8Ofc
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
msgid "Invalid connected account input."
msgstr ""
#. js-lingui-id: 1PtrY0
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid country code {countryCode}"
@@ -2018,6 +2034,16 @@ msgstr "Ungültiges Label-Format."
msgid "Invalid limit parameter."
msgstr "Ungültiger Limit-Parameter."
#. js-lingui-id: fNaXqL
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Invalid message channel input."
msgstr ""
#. js-lingui-id: zUyPBT
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Invalid message folder input."
msgstr ""
#. js-lingui-id: iBBCnf
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
@@ -2679,6 +2705,16 @@ msgstr "Mai"
msgid "Message channel not found"
msgstr "Nachrichtenkanal nicht gefunden"
#. js-lingui-id: vjIbbe
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Message channel not found."
msgstr ""
#. js-lingui-id: BZbxut
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Message folder not found."
msgstr ""
#. js-lingui-id: PslTPV
#: src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter.ts
msgid "Metadata validation failed"
@@ -554,6 +554,11 @@ msgstr ""
msgid "Calendar channel is misconfigured."
msgstr "Το κανάλι ημερολογίου είναι εσφαλμένα ρυθμισμένο."
#. js-lingui-id: 3J2x1Q
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Calendar channel not found."
msgstr ""
#. js-lingui-id: 1t3DGy
#: src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts
msgid "Calendar event not found."
@@ -920,6 +925,7 @@ msgid "Connect is not allowed for {connectFieldName} on {objectMetadataNameSingu
msgstr "Σύνδεση δεν επιτρέπεται για το {connectFieldName} στο {objectMetadataNameSingular}."
#. js-lingui-id: /OJTzk
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
#: src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception.ts
msgid "Connected account not found."
msgstr "Ο συνδεδεμένος λογαριασμός δεν βρέθηκε."
@@ -1774,6 +1780,11 @@ msgstr "Μη έγκυρος τύπος ελέγχου."
msgid "Invalid CALDAV credentials. Please check your username and password."
msgstr ""
#. js-lingui-id: cizVEW
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Invalid calendar channel input."
msgstr ""
#. js-lingui-id: X7+bC6
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid calling code {callingCode}"
@@ -1866,6 +1877,11 @@ msgstr ""
msgid "Invalid connected account ID."
msgstr "Μη έγκυρο αναγνωριστικό συνδεδεμένου λογαριασμού."
#. js-lingui-id: DH8Ofc
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
msgid "Invalid connected account input."
msgstr ""
#. js-lingui-id: 1PtrY0
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid country code {countryCode}"
@@ -2018,6 +2034,16 @@ msgstr "Μη έγκυρη μορφή ετικέτας."
msgid "Invalid limit parameter."
msgstr "Μη έγκυρη παράμετρος ορίου."
#. js-lingui-id: fNaXqL
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Invalid message channel input."
msgstr ""
#. js-lingui-id: zUyPBT
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Invalid message folder input."
msgstr ""
#. js-lingui-id: iBBCnf
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
@@ -2679,6 +2705,16 @@ msgstr "Μάιος"
msgid "Message channel not found"
msgstr "Δεν βρέθηκε το κανάλι μηνυμάτων"
#. js-lingui-id: vjIbbe
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Message channel not found."
msgstr ""
#. js-lingui-id: BZbxut
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Message folder not found."
msgstr ""
#. js-lingui-id: PslTPV
#: src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter.ts
msgid "Metadata validation failed"
@@ -549,6 +549,11 @@ msgstr "Calendar"
msgid "Calendar channel is misconfigured."
msgstr "Calendar channel is misconfigured."
#. js-lingui-id: 3J2x1Q
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Calendar channel not found."
msgstr "Calendar channel not found."
#. js-lingui-id: 1t3DGy
#: src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts
msgid "Calendar event not found."
@@ -915,6 +920,7 @@ msgid "Connect is not allowed for {connectFieldName} on {objectMetadataNameSingu
msgstr "Connect is not allowed for {connectFieldName} on {objectMetadataNameSingular}"
#. js-lingui-id: /OJTzk
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
#: src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception.ts
msgid "Connected account not found."
msgstr "Connected account not found."
@@ -1769,6 +1775,11 @@ msgstr "Invalid audit type."
msgid "Invalid CALDAV credentials. Please check your username and password."
msgstr "Invalid CALDAV credentials. Please check your username and password."
#. js-lingui-id: cizVEW
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Invalid calendar channel input."
msgstr "Invalid calendar channel input."
#. js-lingui-id: X7+bC6
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid calling code {callingCode}"
@@ -1861,6 +1872,11 @@ msgstr "Invalid configuration type for widget \"{widgetTitle}\". Expected {expec
msgid "Invalid connected account ID."
msgstr "Invalid connected account ID."
#. js-lingui-id: DH8Ofc
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
msgid "Invalid connected account input."
msgstr "Invalid connected account input."
#. js-lingui-id: 1PtrY0
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid country code {countryCode}"
@@ -2013,6 +2029,16 @@ msgstr "Invalid label format."
msgid "Invalid limit parameter."
msgstr "Invalid limit parameter."
#. js-lingui-id: fNaXqL
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Invalid message channel input."
msgstr "Invalid message channel input."
#. js-lingui-id: zUyPBT
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Invalid message folder input."
msgstr "Invalid message folder input."
#. js-lingui-id: iBBCnf
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
@@ -2674,6 +2700,16 @@ msgstr "May"
msgid "Message channel not found"
msgstr "Message channel not found"
#. js-lingui-id: vjIbbe
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Message channel not found."
msgstr "Message channel not found."
#. js-lingui-id: BZbxut
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Message folder not found."
msgstr "Message folder not found."
#. js-lingui-id: PslTPV
#: src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter.ts
msgid "Metadata validation failed"
@@ -554,6 +554,11 @@ msgstr ""
msgid "Calendar channel is misconfigured."
msgstr "El canal del calendario está mal configurado."
#. js-lingui-id: 3J2x1Q
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Calendar channel not found."
msgstr ""
#. js-lingui-id: 1t3DGy
#: src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts
msgid "Calendar event not found."
@@ -920,6 +925,7 @@ msgid "Connect is not allowed for {connectFieldName} on {objectMetadataNameSingu
msgstr "La conexión no está permitida para {connectFieldName} en {objectMetadataNameSingular}."
#. js-lingui-id: /OJTzk
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
#: src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception.ts
msgid "Connected account not found."
msgstr "Cuenta conectada no encontrada."
@@ -1774,6 +1780,11 @@ msgstr "Tipo de auditoría no válido."
msgid "Invalid CALDAV credentials. Please check your username and password."
msgstr "Credenciales de CALDAV no válidas. Por favor, verifica tu nombre de usuario y contraseña."
#. js-lingui-id: cizVEW
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Invalid calendar channel input."
msgstr ""
#. js-lingui-id: X7+bC6
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid calling code {callingCode}"
@@ -1866,6 +1877,11 @@ msgstr ""
msgid "Invalid connected account ID."
msgstr "ID de cuenta conectada no válido."
#. js-lingui-id: DH8Ofc
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
msgid "Invalid connected account input."
msgstr ""
#. js-lingui-id: 1PtrY0
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid country code {countryCode}"
@@ -2018,6 +2034,16 @@ msgstr "Formato de etiqueta no válido."
msgid "Invalid limit parameter."
msgstr "Parámetro de límite no válido."
#. js-lingui-id: fNaXqL
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Invalid message channel input."
msgstr ""
#. js-lingui-id: zUyPBT
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Invalid message folder input."
msgstr ""
#. js-lingui-id: iBBCnf
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
@@ -2679,6 +2705,16 @@ msgstr "mayo"
msgid "Message channel not found"
msgstr "Canal de mensajes no encontrado"
#. js-lingui-id: vjIbbe
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Message channel not found."
msgstr ""
#. js-lingui-id: BZbxut
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Message folder not found."
msgstr ""
#. js-lingui-id: PslTPV
#: src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter.ts
msgid "Metadata validation failed"
@@ -554,6 +554,11 @@ msgstr ""
msgid "Calendar channel is misconfigured."
msgstr "Kalenterikanava on määritetty virheellisesti."
#. js-lingui-id: 3J2x1Q
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Calendar channel not found."
msgstr ""
#. js-lingui-id: 1t3DGy
#: src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts
msgid "Calendar event not found."
@@ -920,6 +925,7 @@ msgid "Connect is not allowed for {connectFieldName} on {objectMetadataNameSingu
msgstr "Yhteyden muodostaminen ei ole sallittua kohteelle {connectFieldName} kohteeseen {objectMetadataNameSingular}."
#. js-lingui-id: /OJTzk
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
#: src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception.ts
msgid "Connected account not found."
msgstr "Yhdistettyä tiliä ei löytynyt."
@@ -1774,6 +1780,11 @@ msgstr "Virheellinen auditointityyppi."
msgid "Invalid CALDAV credentials. Please check your username and password."
msgstr "Virheelliset CALDAV-tunnistetiedot. Tarkista käyttäjänimi ja salasana."
#. js-lingui-id: cizVEW
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Invalid calendar channel input."
msgstr ""
#. js-lingui-id: X7+bC6
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid calling code {callingCode}"
@@ -1866,6 +1877,11 @@ msgstr ""
msgid "Invalid connected account ID."
msgstr "Virheellinen yhdistetyn tilin tunnus."
#. js-lingui-id: DH8Ofc
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
msgid "Invalid connected account input."
msgstr ""
#. js-lingui-id: 1PtrY0
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid country code {countryCode}"
@@ -2018,6 +2034,16 @@ msgstr "Virheellinen tunnisteen muoto."
msgid "Invalid limit parameter."
msgstr "Virheellinen raja-parametri."
#. js-lingui-id: fNaXqL
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Invalid message channel input."
msgstr ""
#. js-lingui-id: zUyPBT
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Invalid message folder input."
msgstr ""
#. js-lingui-id: iBBCnf
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
@@ -2679,6 +2705,16 @@ msgstr "Toukokuu"
msgid "Message channel not found"
msgstr "Viestikanavaa ei löytynyt"
#. js-lingui-id: vjIbbe
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Message channel not found."
msgstr ""
#. js-lingui-id: BZbxut
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Message folder not found."
msgstr ""
#. js-lingui-id: PslTPV
#: src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter.ts
msgid "Metadata validation failed"
@@ -554,6 +554,11 @@ msgstr ""
msgid "Calendar channel is misconfigured."
msgstr "Le canal de calendrier est mal configuré."
#. js-lingui-id: 3J2x1Q
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Calendar channel not found."
msgstr ""
#. js-lingui-id: 1t3DGy
#: src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts
msgid "Calendar event not found."
@@ -920,6 +925,7 @@ msgid "Connect is not allowed for {connectFieldName} on {objectMetadataNameSingu
msgstr "La connexion n'est pas autorisée pour {connectFieldName} sur {objectMetadataNameSingular}"
#. js-lingui-id: /OJTzk
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
#: src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception.ts
msgid "Connected account not found."
msgstr "Compte connecté introuvable."
@@ -1774,6 +1780,11 @@ msgstr "Type d'audit invalide."
msgid "Invalid CALDAV credentials. Please check your username and password."
msgstr ""
#. js-lingui-id: cizVEW
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Invalid calendar channel input."
msgstr ""
#. js-lingui-id: X7+bC6
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid calling code {callingCode}"
@@ -1866,6 +1877,11 @@ msgstr ""
msgid "Invalid connected account ID."
msgstr "Identifiant de compte connecté invalide."
#. js-lingui-id: DH8Ofc
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
msgid "Invalid connected account input."
msgstr ""
#. js-lingui-id: 1PtrY0
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid country code {countryCode}"
@@ -2018,6 +2034,16 @@ msgstr "Format de libellé invalide."
msgid "Invalid limit parameter."
msgstr "Paramètre de limite invalide."
#. js-lingui-id: fNaXqL
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Invalid message channel input."
msgstr ""
#. js-lingui-id: zUyPBT
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Invalid message folder input."
msgstr ""
#. js-lingui-id: iBBCnf
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
@@ -2679,6 +2705,16 @@ msgstr "Mai"
msgid "Message channel not found"
msgstr "Canal de messagerie introuvable"
#. js-lingui-id: vjIbbe
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Message channel not found."
msgstr ""
#. js-lingui-id: BZbxut
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Message folder not found."
msgstr ""
#. js-lingui-id: PslTPV
#: src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter.ts
msgid "Metadata validation failed"
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
@@ -554,6 +554,11 @@ msgstr ""
msgid "Calendar channel is misconfigured."
msgstr "ערוץ לוח השנה מוגדר שלא כראוי."
#. js-lingui-id: 3J2x1Q
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Calendar channel not found."
msgstr ""
#. js-lingui-id: 1t3DGy
#: src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts
msgid "Calendar event not found."
@@ -920,6 +925,7 @@ msgid "Connect is not allowed for {connectFieldName} on {objectMetadataNameSingu
msgstr "לא ניתן להתחבר עבור {connectFieldName} על {objectMetadataNameSingular}"
#. js-lingui-id: /OJTzk
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
#: src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception.ts
msgid "Connected account not found."
msgstr "חשבון מחובר לא נמצא."
@@ -1774,6 +1780,11 @@ msgstr "סוג ביקורת לא תקין."
msgid "Invalid CALDAV credentials. Please check your username and password."
msgstr "פרטי ההתחברות ל-CALDAV אינם תקינים. אנא בדוק את שם המשתמש והסיסמה שלך."
#. js-lingui-id: cizVEW
#: src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts
msgid "Invalid calendar channel input."
msgstr ""
#. js-lingui-id: X7+bC6
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid calling code {callingCode}"
@@ -1866,6 +1877,11 @@ msgstr ""
msgid "Invalid connected account ID."
msgstr "מזהה חשבון מחובר לא תקין."
#. js-lingui-id: DH8Ofc
#: src/engine/metadata-modules/connected-account/connected-account.exception.ts
msgid "Invalid connected account input."
msgstr ""
#. js-lingui-id: 1PtrY0
#: src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts
msgid "Invalid country code {countryCode}"
@@ -2018,6 +2034,16 @@ msgstr "פורמט תווית לא תקין."
msgid "Invalid limit parameter."
msgstr "פרמטר limit לא תקין."
#. js-lingui-id: fNaXqL
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Invalid message channel input."
msgstr ""
#. js-lingui-id: zUyPBT
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Invalid message folder input."
msgstr ""
#. js-lingui-id: iBBCnf
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
#: src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
@@ -2679,6 +2705,16 @@ msgstr "מאי"
msgid "Message channel not found"
msgstr "ערוץ הודעות לא נמצא"
#. js-lingui-id: vjIbbe
#: src/engine/metadata-modules/message-channel/message-channel.exception.ts
msgid "Message channel not found."
msgstr ""
#. js-lingui-id: BZbxut
#: src/engine/metadata-modules/message-folder/message-folder.exception.ts
msgid "Message folder not found."
msgstr ""
#. js-lingui-id: PslTPV
#: src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter.ts
msgid "Metadata validation failed"

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