38fbff465f218e46e65c66679c572819db1f57bf
13316 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
38fbff465f |
chore(server): ship the 2.20 standardOverrides drop as a dormant command (#22448)
Follow-up to #22417, per [this thread](https://github.com/twentyhq/twenty/pull/22417#discussion_r3512187719): migrate the `2-20/README.md` placeholder into a real command using the `TWENTY_NEXT_VERSIONS` mechanism. ### What - Add `DropMetadataStandardOverridesColumnFastInstanceCommand`, registered against `2.20.0`. It boots (`2.20.0` is in `TWENTY_ALL_VERSIONS`) but stays **dormant** — the upgrade sequence only runs `TWENTY_CROSS_UPGRADE_SUPPORTED_VERSIONS` (previous + current), so it never executes during the 2.19 deploy and activates automatically when `nx version:bump` promotes 2.20 to current. - Name constant + unit test (SQL parity, registration against `2.20.0`, name-constant parity). - Register it in `instance-commands.constant.ts`. - Update the `standardOverrides` `@deprecated` comments on object/field metadata to point at the shipped command. - Delete `2-20/README.md`. - Document the "ship a command for a future version" flow in `docs/UPGRADE_COMMANDS.md` and `.cursor/rules/server-migrations.mdc` (the mechanism was previously undocumented). ### Note / correction to the README's plan The old README implied both the command **and** `@WasRemovedInUpgrade` could be added at 2.20 time. Only the command can ship now: the decorator's validator runs against the active sequence, so referencing a still-dormant 2.20 step fails boot with `unknown-step-name`. So the entity keeps its `WasRemovedInUpgrade<T>` type wrapper for now; the decorator gets wired (one line, via the name constant) once 2.20 is current — same deferred-drop shape as `isUIReadOnly`. ### Verification Could not run `jest`/`typecheck`/`lint` in this environment: `yarn install` is blocked by egress policy on a git-based transitive dep (`github.com/electron/node-gyp.git`). Verified by review against the sibling 2-19 add-column and 2-12 drop commands. **Please let CI run before merge.** https://claude.ai/code/session_01KMArJvdEmsX3eAmJLbS1b6 --- _Generated by [Claude Code](https://claude.ai/code/session_01KMArJvdEmsX3eAmJLbS1b6)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22448?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
7a5896ff5d |
feat(ai) - add delete_workflow tool (#22432)
## Summary - Add a new `delete_workflow` agent tool that soft-deletes a workflow and cleans up its sub-entities (versions, runs, triggers) via `WorkflowCommonWorkspaceService.handleWorkflowSubEntities` - Update the workflow skill system prompt to document the new capability and instruct the agent to always confirm with the user before deleting - Wire `WorkflowCommonModule` / `WorkflowCommonWorkspaceService` into the workflow-tools dependency graph ## Test plan - [x] Unit tests added (`delete-workflow.tool.spec.ts`) covering successful deletion and error handling - [ ] Verify the agent can resolve a workflow by name via `list_workflows` then delete it with `delete_workflow` - [ ] Confirm the agent asks for user confirmation before executing the deletion - [ ] Confirm sub-entities (versions, runs, triggers) are removed after deletion <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22432?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
080014c970 |
fix(ai) - Fix date stripped from agent tool output (#22443)
https://discord.com/channels/1130383047699738754/1522138489238458569 **Summary** Fix a bug where Date objects (returned by TypeORM for createdAt, updatedAt, deletedAt columns) were silently dropped from AI agent tool responses stripEmptyValues treated Date instances as empty objects because Object.entries(new Date()) returns [], causing the function to discard them Add instanceof Date guard before the generic object branch so Date values pass through unchanged **Root cause** TypeORM marks createdAt/updatedAt/deletedAt as special columns (createDate/updateDate/deleteDate) and returns them as JavaScript Date objects rather than strings. The stripEmptyValues utility checked typeof value === 'object' (true for Date), then called Object.entries() on it -- which yields an empty array since Date has no own enumerable properties -- and concluded the value was "empty". The existing tests used string dates ('2024-01-01') instead of actual Date objects, so the bug was never caught. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22443?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
5a4ebca226 |
refactor(server): unify the two metadata override mechanisms into one (#22417)
## Unify the two metadata override mechanisms into one Twenty had **two** override mechanisms: - **`standardOverrides`** — a bespoke JSONB column on `objectMetadata`/`fieldMetadata` with typed DTOs and a per-locale `translations` map, resolved by two i18n-aware resolvers. - **`OverridableEntity.overrides`** — a flat, registry-driven JSONB blob on view / view-field / view-field-group / command-menu-item / page-layout-tab / page-layout-widget, resolved by a plain spread. This PR collapses them into **one** concept: a single `overrides` blob, one registry-driven overridable set, one i18n-aware read path, and one write path (`computeMetadataOverridesBlob`, extracted in #22404). Object/field **stay on `SyncableEntity`** (not reparented to `OverridableEntity`) so their `isActive` default stays **FALSE** — this sidesteps the `isActive` default conflict entirely. ### GraphQL breaking change (accepted) The `standardOverrides` field is **removed** with no deprecation alias — `overrides` (a `JSON` scalar) is exposed instead on `Object` and `Field`. Product confirmed negligible external usage; the front-end has no hand-written consumer (only generated types), which are regenerated here. ### Commit structure (reviewable commit-by-commit) 1. **Unified resolver + parity harness** — `resolveEffectiveEntityProperty` is a strict superset of the three legacy resolvers; a corpus parity spec compares it against a *frozen reference* of the old logic across every locale, `isStandardApp` branch and override shape. 2. **Registry-driven** — object/field presentation props tagged `isOverridable` + `translatable`; the overridable/translatable sets are derived from the registry (a test asserts they equal the legacy hardcoded lists). 3. **Rename + swap + delete** — `standardOverrides` → `overrides` across entities, DTOs, flat/universal types, producers, the ~12 resolve/write/create/sync call sites, mocks and specs; the reconciler's two compare entries collapse to one; the three legacy resolvers, both DTOs and the hardcoded constants/types are deleted. 4. **Migration (zero-downtime, two-phase)** — split across two releases so a rolling deploy never drops a column a previous-release pod still `SELECT`s: - **2.19 fast** — add the `overrides` column (schema only). - **2.19 slow** — backfill `overrides` from `standardOverrides` in `runDataMigration` (kept out of the schema transaction so the bulk write doesn't hold the ACCESS EXCLUSIVE lock; skipped on fresh installs, which have no data to copy). - **2.20 fast** — drop the legacy `standardOverrides` column (gated by `TWENTY_NEXT_VERSIONS`, so it stays dormant until the instance reaches 2.20). 5. **Front/client-SDK regen** — regenerated metadata GraphQL types. 6. **Integration specs + i18n** — updated the standard object/field update integration specs + snapshots, and the reworded validator message catalog entry. ### Rolling-deploy safety `standardOverrides` is retained through 2.19 and only dropped in 2.20, mirroring the codebase's deferred-drop convention (`isUIReadOnly`/`isCustom`). During the 2.19 rollout both columns exist, so old and new pods coexist without "column does not exist" errors. The backfill lives in a slow `runDataMigration` (per the `no-data-mutation-in-fast-instance-command` rule) so it doesn't stall reads. ### `isActive` guard The migration never reads or writes `isActive`; the backfill asserts the active-row count is unchanged and aborts otherwise. Verified on a real DB: apply + revert preserves the blob **and** the nested `translations` map, with `isActive` counts identical before/after. ### Verification (local) - `nx typecheck twenty-server` + `nx typecheck twenty-front` — green - `nx lint:diff-with-main twenty-server` (oxlint `--type-aware` + oxfmt) — green - `nx test twenty-server` — green (unit + parity + registry + migration tests) - `nx run twenty-server:test:integration:with-db-reset` — green - `database:reset` applies the 2.19 phases and leaves **both** columns present (2.20 drop stays dormant); backfill + revert round-trip verified on a real DB - Metadata integration suites (standard object/field update, application sync) pass end-to-end against the two-column schema - Metadata GraphQL types regenerated against a booted server; zero `standardOverrides` references remain in application code (only the migration commands + the legacy schema baseline) --------- Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
63d092a31b |
fix(website): center the User Guide nav preview image (#22445)
## What The **User Guide** item in the Resources dropdown rendered its preview image off-center (anchored top-left with a gap below the halftone). ## Fix Set `imagePosition: 'center'` on the User Guide preview so the halftone book is centered and fills to the bottom of the frame, matching the others. ## Testing - `nx lint twenty-website` ✓ (check-conventions + oxlint + oxfmt) - `nx typecheck twenty-website` ✓ |
||
|
|
3bbc08d41f |
refactor(schema): reorganize IndexField and related types (#22439)
## Summary
Querying `indexMetadatas { indexFieldMetadatas { ... } }` on the
`/metadata` GraphQL endpoint fails with a 500:
> Nest could not find IndexFieldMetadataDTOAuthorizer element (this
provider does not
> exist in the current context)
The `@CursorConnection('indexFieldMetadatas', ...)` decorator on
`IndexMetadataDTO` makes nestjs-query auto-generate a relation resolver
that injects an authorizer for `IndexFieldMetadataDTO`. That authorizer
is never provided, because the DTO was never registered as a resolver in
`IndexMetadataModule` — so the field has been broken since it was
introduced in #7162.
Since the working, DataLoader-backed `indexFieldMetadataList` field
already exposes the same data (and is what the frontend uses), this PR
removes the dead connection instead of wiring up the authorizer.
## Changes
- Remove `@CursorConnection('indexFieldMetadatas', ...)` from
`IndexMetadataDTO`
- Regenerate frontend metadata GraphQL types
(`twenty-front/src/generated-metadata`)
- Regenerate client SDK metadata schema/types
(`twenty-client-sdk/src/metadata/generated`)
## Notes
- Not a breaking change in practice: the removed field always threw, so
no consumer can have been relying on it. Callers now get a standard
GraphQL validation error suggesting `indexFieldMetadataList` instead of
an internal server error.
- Verified locally: the failing query now returns `Cannot query field
"indexFieldMetadatas" on type "Index". Did you mean
"indexFieldMetadataList"?` and `indexFieldMetadataList` continues to
work.
Fixes [sonarly issue #54098](https://sonarly.com/issue/54098?type=bug)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22439?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
a99431902d |
fix: bump json-2-csv 5.5.10 -> 5.5.11 (Dependabot) (#22438)
## Summary Bumps **json-2-csv 5.5.10 → 5.5.11** to clear **1 medium Dependabot alert** ([alert 1575](https://github.com/twentyhq/twenty/security/dependabot/1575) — GHSA-g27c-q7cp-mhx6 / CVE-2026-9673, CSV Injection via the `preventCsvInjection` option, vulnerable `>= 3.15.0, < 5.5.11`). json-2-csv is a **direct dependency** of `twenty-front` (`"json-2-csv": "^5.4.0"`). The caret already permits 5.5.11, so this is a **lockfile-only** bump — no `package.json` change (matches Dependabot's `versioning-strategy: lockfile-only`). ## Verification - `yarn install --immutable` passes (CI parity). - Diff is `yarn.lock`-only; json-2-csv resolves to 5.5.11. - 5.5.11 is the latest and cleared twenty's 3-day npm age gate (published 2026-05-26). |
||
|
|
717b297bd1 |
Add Last contact app to onboarding v2 installable apps (#22433)
Adds the Last contact app to the list of installable apps shown in the onboarding v2 install-apps step, alongside Call recorder and Enrichment. Wired in both the frontend list (label + description) and the backend reward/install allow-list so it can be selected, installed server-side, and credited. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22433?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
8b6bd34a17 |
i18n - website translations (#22436)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22436?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
3474d75b56 |
feat(website): add Product to menu + footer nav, move Why into Resources dropdown (#22429)
## What Restores the **Product** link to the site nav (removed in #21794), reversing that commit's nav-structure change: - **Menu** — Product is the first top-level item (in place of Why); **Why** moves back into the **Resources** dropdown with its `why.webp` preview (`IconBulb`, "Why teams choose Twenty"). - **Footer** — Product added to the **Sitemap** group (after Home). The `/product` and `/why-twenty` routes already exist and are in the sitemap; only the nav data changed. The rest of #21794 (dropdown frame height, preview assets, current-page highlight) is untouched. ## Notes - New `msg` strings (`Product`, and Why's restored strings) are left to CI / the i18n bot to extract + translate — no catalog changes here. ## Testing - `nx lint twenty-website` ✓ (check-conventions + oxlint + oxfmt) - `nx typecheck twenty-website` ✓ |
||
|
|
1b06532cb1 |
fix(website): tighten product-feature spotlight height and align bento spacing (#22428)
## What Design polish on the product page's `ProductFeature` bento, from designer review: - **Spotlight height** — the first (spotlight) card's visual was `min-height: 420px` on desktop vs the grid cards' `340px` (80px taller), so it towered over the rest. Now **340px**, matching the grid cards. - **Spacing consistency** — the spotlight visual used a uniform `margin` (bottom margin included), unlike the other cards' `CardVisualFrame` (`… 0` bottom). Removed it so the visual→content gap is consistent across every card. - **Gap** — bumped the visual→content gap to `spacing(6)` (**24px**) on desktop for all cards. ## Testing - `nx lint twenty-website` ✓ (oxlint + oxfmt + check-conventions) - `nx typecheck twenty-website` ✓ |
||
|
|
8182b2a07d |
fix(billing) - invalidate activationStatus after billing event (#22414)
Issue with workspaces still blocked after being re-activated <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22414?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
3c13dc1ab8 |
feat(twenty-sdk): include app readme in published package (#22431)
## Context When running `yarn twenty app:publish`, no README was included in the npm-published app package. Closes twentyhq/core-team-issues#2632 ## What changed - Added `copy-readme-to-output.ts`, which finds the app's root readme file (matched case-insensitively, preferring the markdown variant, mirroring how npm ranks README candidates) and copies it into the build output directory (`.twenty/output/`). - Wired `copyReadmeToOutput` into `buildApplication` — the shared build path used by `publish`, `build`, and `dev` — so the readme is present when `npm publish`/`npm pack` runs from the output directory. npm only ships a README when the file lives in the package root, which for published apps is `.twenty/output/`. The readme is not tracked in the manifest checksums; it is a pure npm packaging artifact, so it is only copied into the output directory and does not affect app installation/validation. ## Tests - Added unit tests for `findReadmeFileName` (case-insensitivity, markdown preference, ignoring unrelated files) and `copyReadmeToOutput` (copies the readme into the output dir; no-ops when the app has no readme). https://claude.ai/code/session_01Qje6VemuMk8nunn6yVJNtL --- _Generated by [Claude Code](https://claude.ai/code/session_01Qje6VemuMk8nunn6yVJNtL)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22431?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
7b682bced9 |
feat(shared): require defaultValue on non-nullable field manifests (#22419)
## Context Follow-up to #22362, which made `isNullable` manifest changes actually apply (including a nullable → non-nullable backfill). This models the `isNullable` / `defaultValue` relationship directly in the `FieldManifest` type. ## Rule - A **non-nullable** field (`isNullable: false`) must declare a `defaultValue`, so the column always has a value to fall back on (e.g. for the backfill on the nullable → non-nullable transition). - A **nullable** or **unspecified** field may omit `defaultValue`. ## Changes - Split `RegularFieldManifest` into a base shape plus a discriminated nullability union. The union keeps `isNullable` free once a `defaultValue` is supplied, so helpers that always provide one can still pass a dynamic `boolean` `isNullable`. - `defaultValue` keeps its rich per-type `FieldMetadataDefaultValue<T>` (POSITION → number, ACTOR → composite) rather than a bare `string`. - `RelationFieldManifest` is rebased on the shared base and keeps `isNullable` / `defaultValue` optional, since relation join columns are always nullable by design. - Narrowed `buildEstimateFieldManifest` in the manifest-update integration test to satisfy the stricter type. ## Verification Environment couldn't install the monorepo deps (registry connections aborting), so `nx typecheck` wasn't run here. Validated the union structure with standalone `tsc` synthetic tests mirroring every construction pattern in the codebase: - ✅ nullable/no-default, no-`isNullable`, non-nullable with string/number/composite defaults, dynamic-boolean-with-default, and the `DistributiveOmit` path into `ObjectFieldManifest` - ✅ non-nullable **without** a default is correctly rejected with a clear "defaultValue is missing but required" error Recommend a full `nx typecheck twenty-shared twenty-sdk twenty-server` in CI to confirm against full project resolution. https://claude.ai/code/session_01VnbrgBB3kNGP876qaKPYDL --- _Generated by [Claude Code](https://claude.ai/code/session_01VnbrgBB3kNGP876qaKPYDL)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22419?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
7b5d313dd1 |
test(server): fix DPA Annex C test broken by sub-processor sync action (#22422)
## Fix flaky DPA Annex C test broken by the sub-processor sync action `resolveDpa`'s Annex C test hard-coded Amazon Web Services' processing locations: ``` Amazon Web Services (https://aws.amazon.com) — Processing location(s): United States, Germany, France. ``` But `subprocessors.json` is overwritten by the **trust-center sync GitHub action** (#22403). AWS is now listed with `processingLocations: ["DE"]`, so the DPA renders `Processing location(s): Germany.` and the hard-coded assertion fails on `main` (`twenty-server:test:ci`). This makes the test derive its expectations from `subprocessors.json` — asserting that every synced sub-processor renders an Annex C entry (`<name> (<vendorUrl>) — Processing location(s):`) and that Annex C is tied to §6.1 — instead of hard-coding vendor locations the sync action controls. The sibling `expands the sub-processor sentinel into exactly the synced entries` test already follows this data-derived pattern. No production code changes — test only. ### Verification - `resolve-dpa.util.spec.ts` — 18/18 pass (was 1 failing on `main`) - oxlint + oxfmt clean <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22422?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
128abcc433 |
i18n - docs translations (#22420)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
795785653d |
chore: sync DPA sub-processors from trust center (#22403)
Automated weekly sync of `subprocessors.json` from Twenty's Trust Center (OneLeet). This keeps the DPA's Annex C (the SCC Annex III list of Sub-Processors) in lockstep with the canonical list at https://trust.twenty.com — the Trust Center is the single source of truth; this file is generated from it. **Please review before merging** — confirm the added/removed Sub-Processors are expected, and that customers were notified per Section 6.2 where required. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22403?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
f7f224aa7a |
Fix lint (#22416)
as title <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22416?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
05ce08ddba |
Add twenty-app keyword (#22415)
as title, bump version <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22415?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
15c0c3b773 |
i18n - docs translations (#22413)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22413?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
d8cc81cb91 |
Improve billing settings UI (#22377)
## Summary - Refresh the settings billing subscription and credits cards with clearer status, usage, and action states. - Add the credit-package picker flow and route past-due/cancellation actions to billing management instead of credit modals. - Clean up related billing UI helpers and formatting. ## Screens ### regular <img width="1007" height="781" alt="image" src="https://github.com/user-attachments/assets/dc9c0c59-8cda-422a-a0d3-56292421a744" /> ### downgrading <img width="1049" height="818" alt="image" src="https://github.com/user-attachments/assets/0dc3dea8-5b55-4cf2-bc92-997cf6e9bebc" /> <img width="1048" height="901" alt="image" src="https://github.com/user-attachments/assets/fba805ad-f523-444b-93a0-2011b6b43443" /> ### Trialing without card <img width="1008" height="903" alt="image" src="https://github.com/user-attachments/assets/d5dd11b6-4c92-4102-ac22-ad1ad4e9fbfb" /> with card <img width="1008" height="806" alt="image" src="https://github.com/user-attachments/assets/0dbeb00e-890e-4448-94fe-0cc0ef411e8d" /> ### Past due & Unpaid <img width="1052" height="860" alt="Past due" src="https://github.com/user-attachments/assets/25ba53ef-6e74-4b4c-bfe1-d6c74e165917" /> <img width="1138" height="860" alt="Unpaid" src="https://github.com/user-attachments/assets/bb16fe65-84e4-40a7-8951-90b01030aded" /> --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
55ed4b7adb |
feat(sdk): translate front-component strings with t()/Trans/useTranslate (#22301)
## What
Lets app **front components** localize the strings they render,
extending the
existing application-translation pipeline (which today only covers
manifest
labels) to component source. App authors mark strings with a small,
familiar
API; the build extracts and bakes them; the runtime resolves them for
the
user's locale.
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
<Trans>Loading postcard…</Trans>
<Trans context="card-title">Untitled</Trans> // disambiguation
const empty = t('No content yet…'); // works outside JSX
<p>{t('Saved {count} cards', { count })}</p> // interpolation
const STATUSES = [{ id: 'draft', label: msg('Draft') }]; // lazy descriptor
```
## How
- **Runtime** (`twenty-sdk/front-component`): `t()` (eager, usable
anywhere —
event handlers, helpers, module scope), `msg()` (lazy descriptor),
`<Trans>`
(reactive JSX), `useTranslate()` / `useLocale()`. Source-string
fallback,
`{name}` interpolation, and `context` disambiguation. No build-time
macro —
these are plain runtime functions.
- **Extraction**: a `ts-morph` scan collects `t()`/`msg()`/`<Trans>`
strings
from component source into the same `locales/*.json` catalogs the
manifest
pipeline already writes (`twenty dev:translations-extract`).
- **Delivery**: `twenty dev:build` bakes the compiled per-locale catalog
into
each front-component bundle via an esbuild banner, so the runtime
resolves
with **no server or renderer changes**. Locale comes from the execution
context that already flows to the worker.
The catalog key and `generateMessageId` hashing are shared between the
node
extractor and the browser runtime; `<Trans>` text whitespace is
normalized
identically on both sides so multi-line elements resolve.
## Design notes
- Reuses the existing `extract → compile → manifest.translations`
contract and
`generateMessageId`, so component strings flow through the same
machinery as
manifest labels.
- Self-contained in `twenty-sdk` + a shared pure helper; the server is
untouched.
## Scope / follow-ups
- `twenty dev` (watch) does not bake catalogs yet — preview shows source
strings; use `twenty dev:build` (documented). Wiring the watcher is a
follow-up.
- Usage is documented in twenty-docs under **Apps → Translations**
(`developers/extend/apps/translations`).
## Tests
Unit tests for the catalog-key/interpolation helpers, the runtime
resolver
(hit/miss/context/fallback/interpolation), and the ts-morph extractor
(static `t`/`msg`/`<Trans>`, dynamic-skip, dedup, multi-line
whitespace), plus a
compile test for context→messageId. Verified with an adversarial review
pass.
https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA
---
_Generated by [Claude
Code](https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA)_
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22301?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a>
---------
Co-authored-by: github-actions <github-actions@twenty.com>
|
||
|
|
efd600c12b |
Update app name (#22410)
as title <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22410?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
ad31227f7c |
Fix BlockNote placeholder alignment + paddings (#22409)
## Before <img width="660" height="958" alt="image" src="https://github.com/user-attachments/assets/017e73c3-cd55-47e3-9470-3f6c31152663" /> ## After <img width="1347" height="995" alt="file-c55b9d7bc3bd212f18f8c77a0318eaef" src="https://github.com/user-attachments/assets/3f96af62-6509-4b58-a740-cd87856422cd" /> |
||
|
|
2e7441380f |
refactor(server): unify metadata override-blob computation (step 1 of override unification) (#22404)
## Context
Twenty currently has **two override mechanisms** for metadata:
- `standardOverrides` (a bespoke JSONB column on `objectMetadata` /
`fieldMetadata`) — i18n-aware, typed DTO with a per-locale
`translations` map, resolved via
`resolve-object/field-metadata-standard-override.util.ts`.
- `OverridableEntity.overrides` (base class: `view`, `view-field`,
`view-field-group`, `command-menu-item`, `page-layout-tab`,
`page-layout-widget`) — a flat, i18n-free `{...entity, ...overrides}`
spread, registry-driven via `isOverridable`.
"One concept, two code paths → drift & confusion; reconciliation has to
special-case." This PR is **step 1** of collapsing them.
## What this PR does (small, behavior-preserving)
The add / remove / null-collapse **override-blob write logic was
triplicated** across:
- `sanitizeOverridableEntityInput` (`overrides`)
- `sanitizeRawUpdateObjectInput` (object `standardOverrides`)
- `sanitizeRawUpdateFieldInput` (field `standardOverrides`)
This extracts it into a single `computeMetadataOverridesBlob` helper
that all three now call. This is genuine **cross-mechanism convergence
of the write path** — the first concrete reduction of the "two code
paths".
- Behavior-preserving: same diff semantics. The object/field paths used
strict `===` on their string standard-override props; `isEqual` subsumes
that for strings, and the overridable path already used `isEqual`.
- Type-casts are contained **inside** the one helper; the three call
sites stay clean and type-preserving.
- 4 files: 1 new util + 3 refactors.
## ⚠️ Draft — verification status
I could **not** run `typecheck` / `lint` / tests locally: the sandbox
this was authored in cannot complete `yarn install` (network aborts
mid-install, no `node_modules`). The change is small and reasoned, but
**please let CI validate it** — that's why this is a draft. If CI flags
a type/lint nit in the contained casts, it's isolated to
`compute-metadata-overrides-blob.util.ts`.
Per request: no code comments were added; the design/tradeoff discussion
lives here.
## The full unification plan (this PR is step 1)
The remaining steps are deliberately **not** in this PR because they
need a live DB (migration) and the front-end codegen pipeline to verify
— neither is available in the authoring sandbox. Documented here for
review before we proceed:
| Step | Change | Why staged |
|------|--------|-----------|
| **(this PR)** | Unify the write-path blob logic | Safe,
behavior-preserving, no DB/FE |
| Read path | One i18n-aware `resolveEffectiveEntity` (superset of the
flat spread + the two i18n resolvers) | The i18n resolvers are entangled
with typed translation-key narrowing; merging cleanly needs the
storage/i18n generalization below |
| Registry | Make object/field presentation props registry-driven
(`facet` + `translatable`), like the overridable set already is |
Depends on the facet annotation |
| Storage | Object/field extend `OverridableEntity`; `standardOverrides`
→ `overrides` (translations preserved); **one data migration** | Needs
DB verification; changes schema |
| GraphQL + FE | Remove the `standardOverrides` field, expose
`overrides`; regen `twenty-front` / client-SDK types; update the
Settings → Data-Model rename UI | Needs codegen; see tradeoff below |
## Key tradeoffs / decisions to confirm
1. **GraphQL break on `standardOverrides` — accepted.** Per product
call, external usage is negligible, so the later step will **remove**
the field outright (no deprecated alias). The one real consumer is the
Settings → Data-Model rename-label UI, updated in the same step. This
drops the most complex part of the original plan (a virtual-alias
resolver + deprecation window).
2. **`isActive` default.** `OverridableEntity` defaults `isActive` to
`true`; object/field default it to `false`. The storage step must
**explicitly override the default** and assert in the migration that no
existing row's `isActive` changes.
3. **Overrides stay anonymous single-slot blobs** (no per-app
attribution / multi-contributor 3-way merge). That limitation is
unchanged here and is only worth revisiting if a concrete use case needs
owner-tagged layering (real schema work, sized separately).
4. **Parity harness is the safety net for the storage step.** Because
the read-path/storage merge touches the hot object/field resolve path
and i18n precedence, that PR should land a golden-corpus parity gate
(all locales, `isStandardApp`, empty/partial/full overrides) proving the
unified resolver reproduces today's output byte-for-byte, before any
switch.
## Not included (per request)
- No service tests added.
- No code comments added (rationale/tradeoffs are here, in the PR).
## Test plan
- CI: `typecheck` + `lint` + the existing
`sanitize-overridable-entity-input.util.spec.ts` (which exercises the
shared logic through `sanitizeOverridableEntityInput`).
- The object/field write paths have no dedicated unit spec; they're
covered by the metadata integration suites
(`successful-update-one-standard-object/field-metadata`).
https://claude.ai/code/session_01E1pGBDLC3gEBs1w45G2W5Z
---
_Generated by [Claude
Code](https://claude.ai/code/session_01E1pGBDLC3gEBs1w45G2W5Z)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22404?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
27dea0ed0b |
Add installed workspaces view to application registration (#22359)
## After <img width="895" height="344" alt="image" src="https://github.com/user-attachments/assets/33591753-f248-45ce-b32d-cc1112f50579" /> <img width="889" height="425" alt="image" src="https://github.com/user-attachments/assets/469ee228-9abb-486f-b2ec-9efb490bb2c8" /> <img width="766" height="343" alt="image" src="https://github.com/user-attachments/assets/2d88444a-6d98-4f97-8e5d-109197cfad27" /> ## Summary Add a new "Installed workspaces" section to the application registration settings page that displays all workspaces that have installed a given application, with pagination support. ## Key Changes - **Backend Service**: Added `getInstalledWorkspaces()` method to `ApplicationRegistrationService` that queries installed applications across workspaces with pagination support - **Backend DTO**: Created `ApplicationRegistrationInstalledWorkspacesDTO` and `InstalledWorkspaceDTO` to structure the response with workspace details (id, displayName, logo, version), total count, and hasMore flag - **GraphQL Resolver**: Added `findApplicationRegistrationInstalledWorkspaces` query resolver with pagination (page parameter, default page size of 10) and proper authorization guards - **Frontend Component**: Created `SettingsApplicationRegistrationInstalledWorkspaces` component that: - Displays installed workspaces in a table with workspace logo, name, and version - Shows initial 3 workspaces with "Show all" button to expand - Implements pagination with "Show more" button to load additional pages - Handles empty state (returns null if no workspaces installed) - **GraphQL Query**: Added `FindApplicationRegistrationInstalledWorkspaces` query document for frontend data fetching - **Integration**: Integrated the new component into `SettingsApplicationRegistrationGeneralTab` ## Implementation Details - Pagination uses offset-based approach with configurable page size (10 workspaces per page) - Query results are ordered by workspace displayName and id for consistent ordering - Soft-deleted applications and workspaces are excluded from the list and counts - Apollo Client's `fetchMore` with `updateQuery` merges paginated results into the cache - Component respects existing authorization (API_KEYS_AND_WEBHOOKS permission required) - Uses existing UI components (Table, Card, Avatar, Button) from twenty-ui library - Supports internationalization with Lingui ## Screenshots The new "Installed workspaces" section on the app registration General tab (admin app detail page), captured against a local instance with a demo app installed in 14 workspaces. The three PNGs are committed under `.github/assets/screenshots/installed-workspaces/` and render inline in the **Files changed** tab of this PR: - `1-first-3-show-all.png` — Collapsed: the first 3 installed workspaces (avatar + name + installed version) with a "Show all" button. - `2-expanded-show-more.png` — "Show all": the first page of 10 workspaces, with a "Show more" button (more remain). - `3-all-paginated.png` — "Show more": all 14 workspaces loaded, button gone. Review in cubic: https://cubic.dev/pr/twentyhq/twenty/pull/22359?utm_source=github https://claude.ai/code/session_012nWtviSBdfFeHEASTtwvJ7 |
||
|
|
b8a3399230 |
fix(billing): link billing emails to the workspace subdomain (#22401)
## Problem Billing and workspace-suspension emails hardcoded a `BILLING_SETTINGS_URL` constant pointing at `https://app.twenty.com/settings/billing`. A user in `myworkspace.twenty.com` therefore received a CTA that bounced through the central `app` domain instead of landing on their own workspace. Those cross-subdomain redirects are unreliable, so it's better to link straight to the workspace. The invite, password-reset and email-verification emails already do this correctly by building a workspace-specific URL server-side with `WorkspaceDomainsService.buildWorkspaceURL(...)`; the billing/suspension senders had the `workspace` entity in scope but never used it. ## Fix Build the billing settings URL server-side and pass it into the templates as a `link` prop, mirroring the existing pattern: - **Templates** now take a `link` prop instead of the hardcoded constant: `billing-trial-ending`, `billing-trial-converting`, `billing-subscription-renewing`, `warn-suspended-workspace`. - **`BillingReminderService`** and **`CleanerWorkspaceService`** build `buildWorkspaceURL({ workspace, pathname: getSettingsPath(SettingsPath.Billing) })` and thread it through. - Wired `WorkspaceDomainsModule` into both NestJS modules; deleted the now-unused `billing-settings-url.constant.ts`; updated the reminder unit test. This also fixes **self-hosted** deployments, which previously got the same wrong hardcoded `app.twenty.com` link. ### Intentionally unchanged - `clean-suspended-workspace` keeps its central-domain "start a new workspace" CTA — that workspace is already deleted, so its subdomain no longer resolves. - `password-update-notify` (not a billing email) still uses `getBaseUrl()`; the workspace entity isn't readily loaded there. Can be a follow-up. ## Testing Extended `billing-reminder.service.spec.ts` to assert the workspace-specific `link` is threaded into the email. Note: local `typecheck`/tests could not be run because the sandbox proxy repeatedly dropped `yarn install` mid-fetch; the diff was reviewed line-by-line and import paths verified against the actual `twenty-shared` exports and module wiring. CI will provide the authoritative check. https://claude.ai/code/session_01QsgNd4SWdcRkPFyrnCgj2b --- _Generated by [Claude Code](https://claude.ai/code/session_01QsgNd4SWdcRkPFyrnCgj2b)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22401?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
72bcc78e36 |
Let the sign-in screen scroll when its content overflows the viewport (#22397)
On the sign-in screen, when a step's content is taller than the viewport (e.g. many workspaces to choose from), it grew past the fixed-height background and overflowed the page. Make the shared background scroll instead, so every sign-in step scrolls when its content overflows and stays centered when it fits. ## Before https://github.com/user-attachments/assets/0226daee-0cd9-454c-9f4b-257cfab61bfb ## After https://github.com/user-attachments/assets/7a535099-f0f0-431b-86ad-9fa8121638db <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22397?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
1b9ba48e34 |
Update onboarding v2 credit reward amounts (#22399)
Adjusts the free-credit rewards shown and granted across onboarding v2: - Import contacts: 2 → 1 credit - Install app: 1 → 0.5 credit per app - Invite user: 0.5 credit per user (unchanged) - Upgrade free trial: 5 → 1 credit All values live as defaults in `config-variables.ts` (micro-credits) and reach the frontend via ClientConfig, so nothing else needed changing. Note: the upgrade reward maps to `BILLING_FREE_WORKFLOW_CREDITS_FOR_TRIAL_PERIOD_WITH_CREDIT_CARD`, which is also the actual with-credit-card trial grant, so that grant drops from 5 → 1 credit too (intentionally the same number the onboarding advertises). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22399?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
868ae4cbfd |
feat(last-contact): design for Last contact by + Last contact item (#22308)
To test, go to https://twenty-applications.twenty.com/settings/applications/6aa2ca76-fdbe-456d-89ab-c622452ef055 ## After <img width="1512" height="697" alt="image" src="https://github.com/user-attachments/assets/5b17f94e-6e0e-400a-8291-a39ad796e423" /> ## What Adds the design spec for the next version of the `twenty-last-contact` public app, extending it from a single `lastContactAt` date column to the three-column experience in the app's cover image on the All People view: 1. **Last contact by** — the team member who last interacted with the person (`ACTOR` field). 2. **Last contact** — the existing `lastContactAt` field, unchanged. 3. **Last contact item** — the email or meeting that was the last contact, as a clickable record (`MORPH_RELATION` → message | calendarEvent). All three columns always describe the same single most-recent interaction (atomic "newer wins" update). This PR contains the **design doc only** — `docs/superpowers/specs/2026-06-29-last-contact-by-and-item-design.md`. Implementation follows. ## Why The app today only answers *when* you last talked to someone. These fields also answer *who* on your team and *through which* email/meeting, matching the product vision in the cover. ## Key design decisions - **`lastContactBy` is an ACTOR**, with the team member resolved from the interaction's participants (`messageParticipant` / `calendarEventParticipant` both carry `workspaceMemberId` + `workspaceMember`). - **No provider (Gmail/Outlook) logo.** That data lived on `connectedAccount`, which v2.7 (`drop-connected-account-standard-object`) removed from the app-queryable workspace schema. Confirmed acceptable; the actor still shows the member + an email/calendar source. - **`lastContactItem` is a MORPH_RELATION** following the SDK pattern used by `attachment` / `noteTarget` / `taskTarget` (shared `morphId`, one field per target, reverse relation on each target object). ## Reviewer notes - **Load-bearing open risk** documented in the spec: how to *write* a morph relation through the app's GraphQL API — no app in the repo writes morph yet. The plan starts with a spike on this; if morph writes aren't supported from an app, the fallback is two nullable `RELATION` fields (`lastContactMessage` / `lastContactCalendarEvent`). - No code/behavior change yet — safe to merge or hold as the design of record. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22308?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
f2e7009baa |
i18n - docs translations (#22400)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22400?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
4d96ec489b |
Add smooth page transitions to onboarding v2 (#22392)
## Before https://github.com/user-attachments/assets/d2fcd5ce-7e34-4f07-9a52-cac8acdc37cd ## After https://github.com/user-attachments/assets/5c245949-cff9-41b2-802d-3deeb562efa6 On a full-page load of a v2 onboarding URL (the post-signup workspace-subdomain redirect), Lingui's `I18nProvider` renders `null` until the locale chunk async-activates, so the app is blank for ~2s before the verify step appears. Steps also hard-cut and flashed a loader between each other. - Show a pulsing-logo loader until the locale activates (a gate above `I18nProvider`), scoped to onboarding v2 paths so every other page is unchanged. - Cross-fade between steps and preload their chunks on entry, so navigating never flashes the loader. Frontend-only; i18n loading itself is untouched. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22392?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
2b1417770e |
SearchVector derivation via migration-scoped index (alt to #2622 __warmedUpCache) (#22389)
## What this is close https://github.com/twentyhq/core-team-issues/issues/2622 A **POC / discussion branch** implementing the runner-scoped alternative to the `__warmedUpCache` design in [core-team-issues#2622](https://github.com/twentyhq/core-team-issues/issues/2622). Not for merge as-is — meant to diff against that plan. ## Problem `deriveSearchVectorAsExpressionForTsVectorField` scans the entire `flatSearchFieldMetadataMaps` (`Object.values(...).filter(...)`) once per object created in a migration. On install that's `O(objectsCreated × totalSearchFields)` — the quadratic #2622 targets. Only **one** of the three call sites is actually hot: - `create-object` (runner) — global maps, called per created object → the quadratic - export DDL — maps already built **per object** (O(k)) - `update-field` rebuild — one field, gated on `rebuildSearchVector` ## Approach Instead of a private `__warmedUpCache` side-channel on `FlatEntityMaps<T>` + drain-on-hydration, this keeps the index in the **consumer**: 1. `derive` now takes `targetSearchFieldMetadatas` (already scoped to the tsVector field) instead of scanning the map itself. 2. The runner builds a `Map<tsVectorFieldMetadataId, searchFields[]>` **once per migration**, lazily, and threads it through the action context. Safe because `searchFieldMetadata` creates are ordered before `objectMetadata` creates (`computeOrderedMigrationActions`), so the map is complete on first use. → `O(totalSearchFields)`. 3. `getTargetSearchFieldMetadatasForTsVectorField` (O(total) filter) stays as the fallback for the one-off callers (export, field-update) and when the accessor isn't provided. ## Why this over `__warmedUpCache` - **No `FlatEntityMaps<T>` type widening**, no convention-only privacy, no id/universalIdentifier drain to keep in sync. - **No referential-integrity obligation.** The index only ever contains entities present in the map, so the "search field created-then-deleted before its object hydrates" case (deferred as an edge in #2622) can't put a stale id into an aggregator and crash `derive` via the `-orThrow` lookup. - **One `derive` path**, not "aggregator + direct-filter fallback for export". - Blast radius: ~220 lines, mostly a new util + test. ## Benchmark (micro, isolated function) Median of 7 trials, 10 search fields per object, running the real shipped utils — old = `getTargetSearchFieldMetadatasForTsVectorField` once per object (identical to the old inline scan), new = `buildSearchFieldMetadatasByTsVectorFieldId` once + N lookups (both assert they resolve the same fields): | objects | total search fields | old (scan/obj) | new (index once) | speedup | |--------:|--------------------:|---------------:|-----------------:|--------:| | 50 | 500 | 2.08 ms | 0.06 ms | 33× | | 100 | 1,000 | 9.26 ms | 0.12 ms | 77× | | 200 | 2,000 | 36.2 ms | 0.23 ms | 160× | | 400 | 4,000 | 151 ms | 0.40 ms | 379× | | 800 | 8,000 | 701 ms | 0.92 ms | 766× | Confirms the old path is quadratic (~4× per doubling of object count) and the new path is linear (sub-ms throughout). **Caveats — read these before trusting the speedup:** - This is the **isolated derivation function**, no DB / DDL / inserts. In a real `create-object` action the derive is a small fraction of per-action cost, so the end-to-end win is far smaller than the ratios above. - A default workspace has ~20–30 objects, where the **old** code already costs only ~1–2 ms total across the whole install. The quadratic only becomes material (>50 ms, the runner's slow-action threshold) around **200–400 objects**. - The measurement that should actually gate this — `[install-perf] create:objectMetadata` on a real install against a real DB with a few hundred objects — has **not** been run yet. The micro-benchmark bounds the upside and locates the knee of the curve; it does not prove end-to-end payoff. ## Not done on purpose - **No end-to-end benchmark yet** — step 0 should still be measuring `[install-perf] create:objectMetadata` on a real large install to confirm the quadratic is worth removing at all. - Relies on the ordering invariant (commented at the build site). The fully self-contained variant is to put the object's search fields on `FlatCreateObjectAction` (builder change) — deliberately left out to keep this runner-scoped. ## Checks `nx typecheck twenty-server`, `nx lint:diff-with-main twenty-server`, new util spec + existing `generate-workspace-schema-ddl` spec all green. |
||
|
|
1a475d0edd |
feat(twenty-sdk): terraform-style plan/apply for app metadata sync (#22372)
## What & why Syncing a Twenty app's metadata is destructive (removing a field/object drops the backing column/table), but the only preview was `dev --once --dry-run`, which collapsed every change into one line per entity — no before/after, no color, no destructive warning, and no confirmation before a real sync. This introduces a `terraform plan`-style flow. The server's `syncApplication(manifest, dryRun)` already returns a complete `SyncAction[]` (create/update/delete with per-attribute `before`/`after`), so this is a CLI-only change — **no server changes**. ## Command surface `plan` previews, `apply` applies; `dev` is the watch wrapper over the same engine. | Command | Behavior | | --- | --- | | `twenty plan [appPath]` | Render the full plan, read-only | | `twenty apply [appPath]` | Plan → confirm on destructive → apply | | `twenty dev --once` | **Deprecated** alias of `twenty apply` (still works, warns) | | `twenty dev --once --dry-run` | **Deprecated** alias of `twenty plan` (still works, warns) | | `twenty dev` (watch) | Compact summary; inline `[y/N]` confirm on destructive saves | | `-f, --force` | Skip the destructive gate (on `apply` and `dev`) | ## Plan output ``` Twenty will perform the following actions: # objectMetadata "rocket" will be created + nameSingular = "rocket" + labelSingular = "Rocket" # fieldMetadata "name" will be updated in-place ~ label = "Name" -> "Launch name" ~ isNullable = true -> false # fieldMetadata "legacyCode" will be destroyed - name = "legacyCode" Plan: 1 to add, 1 to change, 1 to destroy. Warning: 1 destructive change(s) will permanently delete data. - fieldMetadata "legacyCode" — drops the column and its data Destroys are irreversible. Review carefully before applying. ``` Grouped by metadata type, ordered create → update → destroy, `=` aligned per block. Internal keys (`id`, `workspaceId`, `*Id`, timestamps, nulls) are filtered; updates show only changed keys via the server `diff`. ## Destructive safety gate The server applies the manifest diff atomically, so every apply path computes the plan read-only first, then decides whether to apply: - **`twenty apply` / `dev --once`** — interactive `y/N` prompt when the plan deletes metadata; `--force` skips; **fails closed** (exit 1) in CI / non-TTY. - **`dev` (watch)** — creates/updates auto-apply with the compact summary; a save that deletes metadata shows an inline `y/N` prompt in the Ink UI. **Declining cleanly stops the watch** (exit 1) rather than leaving the session in a nagging/blocked state — since the atomic apply would otherwise also block the additive changes on every subsequent save until resolved. `dev --force` applies deletions without asking. ## Notes - `twenty apply` / `dev --once` now do one extra **read-only** dry-run before applying (to compute the plan + gate). `--force` skips it. - The watch sync step now skips API-client regeneration on any non-synced outcome (error or decline), avoiding a partial client write during shutdown. - The Ink watch UI keeps its existing compact summary; the full plan renders only on the plain-console surfaces — `dev` watch output is unchanged in the common case. ## Test plan - `npx nx typecheck twenty-sdk` ✓ - `npx nx lint twenty-sdk` ✓ - Unit tests (vitest): renderer (`format-sync-actions-plan.spec.ts`) + confirm gate (`confirm-destructive-apply.spec.ts`); existing summary / sync-step specs still green. - Manual against `simple-app` + a local server: `plan`, `apply` (destructive prompt + `--force` + non-TTY fail-closed), and the `dev` watch inline confirm (incl. decline → stop). |
||
|
|
2e6077383b |
Add install your first apps onboarding V2 step (#22347)
https://github.com/user-attachments/assets/5326d48f-1842-4db1-bc7c-94852145c035 <img width="838" height="754" alt="CleanShot 2026-06-30 at 16 25 05@2x" src="https://github.com/user-attachments/assets/5c7d53d7-4d65-4e35-aed1-edf0c104e140" /> Adds an "Install your first apps" step to the V2 onboarding, shown right after import-contacts. It lets users opt into installing marketplace apps (Call recorder and People Data Labs for now) during onboarding. - New backend `OnboardingStatus.APPS_INSTALLATION` (between SYNC_EMAIL and PROFILE_CREATION); V1 auto-skips it. - The primary button sends the selected app ids to the server via `triggerInstallAppsOnboardingStep`, which enqueues a dedicated job that installs them asynchronously so onboarding isn't blocked. Skip continues without installing. - The workspace is credited per app on successful installation. Credits are env-driven via `ONBOARDING_INSTALL_APPS_CREDITS_REWARD_PER_APP`, shown as "Earn +N free credits (1 per tool)". <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22347?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
93b8f66794 |
fix(dpa): correct US processor entity to Twenty.com PBC (#22393)
As title. |
||
|
|
fab0358df5 |
Handle field isNullable update (#22362)
## Context Setting isNullable on a field via the app SDK manifest was silently ignored when re-syncing an existing field. The first sync that creates a field honored isNullable correctly, but any later manifest change to isNullable had no effect, neither on the field metadata nor on the underlying Postgres column. Two compounding gaps caused this: The diff never detected the change. isNullable was configured with toCompare: false, so compareTwoFlatEntity excluded it from the diff and no update action was ever generated. There was no DDL to apply it. Even if detected, the update field action handler only altered name, options, defaultValue, and settings. The column manager had no way to alter a column's NOT NULL constraint. ## Fix - Set isNullable.toCompare: true so manifest changes are detected and persisted to the field metadata (via the existing executeForMetadata path). - Add WorkspaceSchemaColumnManagerService.alterColumnNullable(): emits SET NOT NULL / DROP NOT NULL, with an optional pre-serialized backfill (UPDATE … WHERE col IS NULL) applied only on the nullable → non-nullable transition. - Add handleFieldNullableUpdate() to the update field action handler, dispatched after the defaultValue block so the default is in place before NOT NULL is enforced. It is composite-aware (mirrors the per-sub-column parentIsNullable || !property.isRequired rule used at column creation) and skips relation/morph join columns and TS_VECTOR, which are always nullable by design. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22362?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
49a80c72d2 |
feat: show group-by context in record show breadcrumb (#22247)
Fixes [#837](https://github.com/twentyhq/core-team-issues/issues/837) On the record show page, extend breadcrumb pagination when the current view is grouped: (`rank/total in {viewName} -> {groupValue}`) Example: `Tasks / Schedule follow-up call (1/1,800 in By Status -> To do)` https://github.com/user-attachments/assets/7038d1f5-57e5-4e85-a3e6-09ac46c5b824 https://github.com/user-attachments/assets/88134fa4-e038-4520-a970-ce058a4444b0 <img width="1427" height="173" alt="Screenshot 2026-06-27 202807" src="https://github.com/user-attachments/assets/842d911e-b4bc-4443-afcd-4c67ed007ae0" /> <img width="1426" height="183" alt="Screenshot 2026-06-27 202851" src="https://github.com/user-attachments/assets/f8d9ee31-b1cb-46fa-9e7c-8876d4b099ff" /> <img width="1427" height="178" alt="Screenshot 2026-06-27 203423" src="https://github.com/user-attachments/assets/196ed694-0edb-4b52-934c-0938d3fd4da2" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22247?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com> |
||
|
|
4fef02394f |
Backfill webhook subscriptions for existing connected accounts (#22314)
Add command iterating workspaces, enqueuing staggered per-channel jobs for Google/Microsoft channels still on polling <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22314?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
59d708e324 |
fix(workflow): stop relative date filter from crashing on empty/invalid date values (#22384)
## Problem
A workflow **Filter** step on a `DATE`/`DATE_TIME` field using
`IS_RELATIVE` crashes with a `RangeError` when the referenced step
output is empty or invalid. The empty value is coerced into an `Invalid
Date` (`new Date("undefined")`),
whose `.getTime()` is `NaN`, and
`Temporal.Instant.fromEpochMilliseconds(NaN)` throws, failing the
affected workflow runs.
This is a latent regression from the Date → Temporal migration (#16544):
the previous `date-fns` implementation silently returned `false` on an
invalid date, but Temporal is strict and throws. The guard was never
carried over.
## Fix
Validate the coerced date once at the boundary in `evaluateDateFilter` —
the single place arbitrary/empty step output is turned into a `Date`. An
unparseable date now resolves to "does not match" for every comparison
operand (`IS`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`,
`IS_AFTER`, `IS_RELATIVE`), restoring the pre-migration contract.
`IS_EMPTY` / `IS_NOT_EMPTY` are intentionally excluded so emptiness is
still evaluated on the raw operand.
## Tests
Added a parameterized regression test covering every date comparison
operand with empty and missing step output, asserting no throw and a
`false` result.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22384?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
18c0d117a3 |
chore: sync AI model catalog from models.dev (#22387)
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev). This PR updates pricing, context windows, and model availability based on the latest data. New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically. Deprecated models are detected based on cost-efficiency within the same model family. **Please review before merging** — verify no critical models were incorrectly deprecated. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22387?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
06515408d1 |
fix: drop domain from computed remote name when subdomain present (#22376)
## Context Closes twentyhq/core-team-issues#2619 When adding a remote with `yarn twenty remote:add` and a URL like `https://martin-s-workspace.twenty.com`, the computed remote name ended up as `martin-s-workspace-twenty-com`. The apex domain (`twenty.com`) should be dropped when a subdomain is present, so the name should be `martin-s-workspace`. ## What changed - Extracted `deriveRemoteName` from `remote/index.ts` into its own module `remote/derive-remote-name.ts`. - When the host has a subdomain (more than two labels), only the subdomain labels are used (joined with dashes) — the apex domain is dropped. - Hosts with no subdomain keep the full host (`twenty.com` → `twenty-com`). - Single-label hosts like `localhost` are preserved. - IPv4 addresses are kept intact (`127.0.0.1` → `127-0-0-1`). - Invalid URLs still fall back to `remote`. ## Tests Added `derive-remote-name.spec.ts` covering subdomain, multi-label subdomain, apex-only host, `localhost`, IPv4, and invalid-URL cases. All 6 pass. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22376?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
1df00698cf |
feat(server): make workspace Custom application carry an applicationRegistration so custom labels are translatable (#22378)
## Why Custom objects/fields belong to a per-workspace **Custom** application (`workspace.workspaceCustomApplicationId`). That application was created with `applicationRegistrationId = null`. Because the metadata label resolver loads a translation catalog from `core.applicationTranslation` **keyed by `applicationRegistrationId`** (`ApplicationTranslationCacheService.getCatalog` → `applicationTranslationCatalogLoader` → `resolveObjectMetadataStandardOverride` / `resolveFieldMetadataStandardOverride`), the Custom app had no catalog and custom labels always resolved to the raw source string. This is the foundational slice: it wires up the missing key so custom labels can be translated **exactly like any installed third-party app**. The read/resolve path already works once a catalog exists — confirmed end-to-end. `flatApplicationMaps` carries `applicationRegistrationId` straight from the entity column, so setting it + recomputing that cache is all that's needed. ## What changed - **`ApplicationService.createWorkspaceCustomApplication`** now creates a workspace-scoped `applicationRegistration` and links it to the Custom application. This covers both production creation sites (sign-in-up and the dev-seeder), which are the only callers. - **New idempotent workspace upgrade command** `upgrade:2-18:backfill-workspace-custom-application-registration` creates a registration for each existing workspace's Custom application that lacks one and links it. It delegates the registration lifecycle (create + link + `flatApplicationMaps` recompute) to `ApplicationService`, so the command only decides *which* workspaces need it. - New `WORKSPACE_CUSTOM_APPLICATION_NAME` constant; the registration creation lives in `ApplicationService.createWorkspaceCustomApplicationRegistration`. ## Design decisions - **Per-workspace registration (not a shared "custom" registration).** `applicationTranslation` is keyed *only* by `applicationRegistrationId` (cross-workspace). A shared registration would force every workspace's custom translations into one catalog keyed by `generateMessageId(sourceText)`, guaranteeing cross-workspace collisions and leakage (two workspaces both naming an object "Project" would clash). Each workspace's Custom app gets its own registration (`ownerWorkspaceId = workspaceId`, `universalIdentifier = the Custom app's per-workspace uuid`) and thus an isolated catalog — matching installed-app behaviour, where `application.universalIdentifier === registration.universalIdentifier`. - **Source-label keying kept** (`generateMessageId(sourceLabel)`). The resolve path and the third-party manifest pipeline both key catalogs this way. Re-keying by a stable `universalIdentifier` would require changing the shared resolver/dataloader for *all* apps and would break marketplace manifest translations — out of scope for this slice. Consequence: renaming a label orphans its catalog entry (it falls back to the source label until re-translated) — the same behaviour an installed app has when it changes a source string. Re-keying on rename can be handled later by the interactive write path. - **Workspace command (not instance command)** for the backfill: it is per-workspace data logic that must recompute the per-workspace `flatApplicationMaps` cache the resolver reads from. It is idempotent (skips Custom apps that already have a registration), supports `--dry-run`, and is forward-only by design. - **Interactive write path deferred** as an explicit follow-up. This slice proves the read/resolve path; an editor that writes custom translations into `applicationTranslation` (+ cache invalidation) is the natural next step. ## Tests - **Unit test** for the backfill command: creation + linking, idempotency, dry-run, and the skip paths. - **Integration test** (`custom-application-translation.integration-spec.ts`): on a freshly created workspace (so the registration's translation cache is guaranteed cold), it asserts the Custom application is created with a registration, seeds an `applicationTranslation` row, and verifies a custom object's label resolves from that catalog while a label with no catalog entry falls back to its source label. ## Notes for reviewers - No new entity columns or migrations beyond the workspace command — `ApplicationRegistrationEntity` already supports a workspace-scoped `workspaceId`. - The backfill follows the established upgrade-command pattern: it imports `ApplicationModule` and delegates to `ApplicationService` (consistent with the other version-command modules). https://claude.ai/code/session_018heTgu4ew4AJ99VVz4bjqd --- _Generated by [Claude Code](https://claude.ai/code/session_018heTgu4ew4AJ99VVz4bjqd)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22378?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
52d3735147 |
[REQUIRED FOR 2.18 RELEASE] Show the upgrade-plan step at the end of V1 onboarding (#22368)
## What On V1 onboarding the upgrade-plan step (`ChooseYourPlan`) only appeared later, once the user happened to create a record, instead of right after Invite team. ## Why The frontend advances the onboarding status optimistically in `getNextOnboardingStatus()` without refetching, and it never emitted `PLAN_REQUIRED`. So after Invite team the user was locally marked `COMPLETED` and dropped into the app; the backend's real `PLAN_REQUIRED` only surfaced on a later `GetCurrentUser` refetch. ## Fix Make `getNextOnboardingStatus()` billing-aware so it mirrors the backend: return `PLAN_REQUIRED` in the terminal branches when `isBillingEnabled && billingSubscriptions.length === 0` (using `billingSubscriptions` to match the backend's any-subscription check). The navigate hook already routes `PLAN_REQUIRED` to `/plan-required`, so no routing change is needed. Self-hosted and existing-subscription flows are unchanged. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22368?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
e82a47c9a2 |
chore: auto pre-translate untranslated docs strings before Crowdin pull (#22334)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22334?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
36e89c04ad |
Front fallback flat object search field metadata (#22369)
## Fix: crash on Settings → Object → Search after changing label identifier ### Problem Opening the Search section (or changing an object's label identifier) threw `t.searchFieldMetadatas is not iterable` in `SettingsObjectSearchSection`. ### Root cause `EnrichedObjectMetadataItem.searchFieldMetadatas` is typed as a non-optional array, but at runtime it can be `undefined`. `useLoadMinimalMetadata` stores the minimal objects with `objectMetadataItems as unknown as FlatObjectMetadataItem[]`. The minimal query doesn't select `searchFieldMetadataList`, so the double-cast hides that the property is missing. Until the full metadata reload lands, the object has no `searchFieldMetadatas`, and `objectMetadataItemsWithFieldsSelector` spreads that `undefined` straight through to the component, which spreads it (`[...searchFieldMetadatas]`) and crashes. (`fields`/`indexMetadatas` never hit this because they come from `Map.get()`, which is honestly typed as `| undefined` and already falls back to `[]`.) ### Fix Guarantee the array contract in `objectMetadataItemsWithFieldsSelector`, matching how `fields`/`indexMetadatas` are already defaulted: `searchFieldMetadatas: flatObject.searchFieldMetadatas ?? []`. ### Tradeoff considered The "clean" alternative is promoting `searchFieldMetadatas` to its own metadata-store entity (like `indexMetadataItems`), which would make the `?? []` type-mandated via `Map.get`. Rejected for now: it's a medium cross-package refactor (new store key, type, selectors, split/reload wiring, plus a server-side collection hash for staleness) for an entity that is never independently mutated — it only changes as a side effect of label-identifier/field updates, so independent caching buys nothing. The selector default fixes the crash with minimal surface area; the deeper cleanup (making the `as unknown as` cast honest, or splitting the store) can be deferred until search-field metadata becomes directly editable. |
||
|
|
9d361c8bb0 |
[FIX_TYPECHECK_ON_MAIN] Add missing inviteTeamMaxCreditsReward to OnboardingConfig type (#22370)
## Context The `twenty-front` typecheck is broken on `main`: ``` src/modules/onboarding/hooks/useInviteTeam.ts:154:27 - error TS2551: Property 'inviteTeamMaxCreditsReward' does not exist on type 'OnboardingConfig'. ``` This is a merge race: one PR started consuming `onboardingConfig.inviteTeamMaxCreditsReward` in `useInviteTeam.ts`, while the frontend `OnboardingConfig` type only declared `inviteTeamCreditsRewardPerUser`. The backend already returns both fields (`client-config.entity.ts` declares `inviteTeamMaxCreditsReward` and the service populates it), so this is purely a missing frontend type field. ## Changes - Add `inviteTeamMaxCreditsReward: number` to the frontend `OnboardingConfig` type. - Add the field to the config mock so `mock-data/config.ts` satisfies the type. ## Test `npx nx typecheck twenty-front` passes. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22370?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
1d7767dbc7 |
i18n - docs translations (#22371)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22371?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
101b85db7b |
messaging remove dead workspace entities (#22366)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22366?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
d55ef3063b |
Fix draft send: read messageChannel from core, not workspace ORM (#22365)
messageChannel moved to a core-schema entity, so resolving it via the workspace ORM by name throws 'object metadata missing'. Query the core MessageChannelEntity repository scoped by workspaceId instead. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22365?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |