7b682bced9be2e75a94db2bab09f4e19e6e85d3f
13303 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. --> |
||
|
|
ef7b480063 |
chore: bump version to 2.19.0 (#22363)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22363?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 Action Deploy <github-action-deploy@twenty.com> |
||
|
|
5a5c829129 |
fix(page-layout): render relation field widgets in table display mode (#22220)
Adding a to-many relation field as a **Table** on a record page rendered an empty widget (header only) in several cases. This fixes three independent defects behind that. - **Morph inverse relations crashed the table.** The host-scoping view filter (`IS current record`) is built on the relation's inverse field. When that inverse is a `MORPH_RELATION` (attachments, notes, tasks…), `getFilterTypeFromFieldType` fell through to `TEXT` and the GraphQL builder threw `Unknown operand IS for TEXT filter`, unmounting the table via the ErrorBoundary. `MORPH_RELATION` now classifies as `RELATION`, and the relation filter resolves the correct morph join column (e.g. `targetPersonId`) from the current record's object type. - **Stale `viewId` on field change.** Changing the bound field on a Table widget kept the previous relation's draft view (wrong object/fields/filter). Field selection now regenerates the draft view for the new relation, or clears the stale `viewId` when the new field can't back a table. - **Label identifier could be hidden or reordered.** Relation-table widget views now pin the label-identifier field first and visible on view creation and save. Deferred: morph relation filters with arbitrary selected record ids (not just "current record") — needs target-object identity in the filter value schema. **Test:** open a Person → edit layout → add a Field widget → bind a to-many relation → switch Layout to Table. Previously empty for `attachments` (morph) and for any field changed on an existing Table widget; now scoped to the host record. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22220?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. --> |
||
|
|
46ef8a8813 |
Update workflows documentation (#22356)
## Summary Documentation-only updates to the workflow and logic-function docs: - **Code action ↔ logic functions**: clarify that each Code action is backed by its own logic function, and document how to reuse logic across workflows via `workflowActionTriggerSettings` (Code/User Guide + Logic Functions/Developer docs cross-linked). - **`workflowActionTriggerSettings` example**: add a complete example (`label`, `icon`, `inputSchema`, `outputSchema`) and document the previously-undocumented `outputSchema` field. - **Iterator improvements** (docs for #22031): document the new **"Use the whole item"** (reference the whole current item) and **"Whole list"** (loop over a step's top-level array output) options across the Iterator and array-handling guides. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22356?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. --> |
||
|
|
3031891491 |
improve dry run logs: show entity names and changed fields (#22299)
## Summary Before this change, dry run logs showed raw UUIDs for `update` and `delete` actions, making it hard to understand what changed: ``` updated fieldMetadata 94265b02-25b4-4bd3-9dae-669f9e983c0f updated fieldMetadata 12920ff8-b04f-46d8-97a8-016390dfb2df ``` After this change, logs show human-readable names when available, plus which fields were modified: ``` updated fieldMetadata myField (94265b02-25b4-4bd3-9dae-669f9e983c0f) [label, description changed] updated fieldMetadata anotherField (12920ff8-b04f-46d8-97a8-016390dfb2df) [isActive changed] ``` ### Changes - **`twenty-shared`** — Extended `SyncUpdateAction` and `SyncDeleteAction` types to include an optional `flatEntity` (with `name`, `nameSingular`, `universalIdentifier`) and `diff` (map of changed field names to before/after values). These fields are already populated by the server-side workspace migration builder but were missing from the shared contract. - **`twenty-sdk`** — Updated `formatSyncActionsSummary` to: - Show `name (uuid)` for update/delete actions when a human-readable name is available via `flatEntity` - Append `[field1, field2 changed]` for update actions when a `diff` is present - Keep the existing behavior for create actions (name only, no uuid since there's no top-level identifier) - Updated and extended tests to cover the new display formats. |
||
|
|
aea6c3832a |
Credit workspaces for onboarding invite-team signups (#22309)
https://github.com/user-attachments/assets/6591cbb0-2b60-4f25-8b03-26b0da73f0d8 After the invite has been accepted: <img width="1606" height="286" alt="CleanShot 2026-06-30 at 11 24 47@2x" src="https://github.com/user-attachments/assets/7becf8a5-04dc-4512-ac7f-951a77e4c0ac" /> Adds a dedicated `ONBOARDING_INVITATION_TOKEN` app-token type so invitations sent during the onboarding invite-team step are distinguished from regular invites. When an invited person actually signs up, the inviting workspace is credited 0.5 credits. Reward eligibility is derived entirely server-side, with no public API parameter: an invitation is reward-eligible only while the workspace is in the onboarding invite-team step (`ONBOARDING_INVITE_TEAM_PENDING`), a flag set once at workspace creation that no public mutation can re-arm. Both token types stay valid invitations everywhere via a shared `INVITATION_APP_TOKEN_TYPES`, so invitees still join normally and appear in invite lists. Crediting is a best-effort direct call to `BillingCreditService.creditWorkspaceBalance` from the sign-in-up flow: it no-ops when billing is disabled and never blocks signup, and is bounded by a 10-invite-per-workspace cap. No DB migration needed: `appToken.type` is a text column. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22309?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. --> |
||
|
|
3d00dd4066 |
feat(server): add 2.18 recompute-search-vectors upgrade command (#22355)
## Summary closes https://github.com/twentyhq/core-team-issues/issues/2620 Adds the 2.18 `recompute-search-vectors` workspace upgrade command — Part 2 of https://github.com/twentyhq/core-team-issues/issues/2620 (Part 1, the GIN-index rebuild fix, merged in #22349). It uniformizes every workspace's `TS_VECTOR` (`searchVector`) columns onto the new derive-from-`searchFieldMetadata` model and drops the now-dead cached settings: - **Recomputes** every searchVector column (re-derives its generated expression from the `searchFieldMetadata` rows) and **recreates its GIN index** — relying on the Part 1 rebuild fix. - **Clears** the deprecated cached `TS_VECTOR` settings (`asExpression` / `generatedType`), which nothing reads anymore. ## How New command `RecomputeSearchVectorsCommand` (`@RegisteredWorkspaceCommand('2.18.0', 1799200001000)`), per active/suspended workspace: 1. Load `flatFieldMetadataMaps`, enumerate every `FieldMetadataType.TS_VECTOR` field. Skip (log) if none. 2. Support `--dry-run` (log the count, no writes). 3. Build one `update-field` action per TS_VECTOR field and run them in a **single migration** via `workspaceMigrationRunnerService.run(...)`: ```ts update: { universalSettings: null }, // clears the deprecated cached settings rebuildSearchVector: true, // re-derive column + recreate GIN index ``` `universalSettings: null` transpiles to `settings: null`, so one atomic action both clears settings and triggers the rebuild; `runner.run` invalidates the field-metadata cache afterward. ## Why these choices - **Single migration under the standard app** covers standard, custom, and installed-app search vectors at once — the runner operates per workspace-schema table regardless of a field's owning application, and the update-field handler doesn't use `flatApplication`. - **Ordering is safe**: the upgrade sequence runs fast-instance → slow-instance → workspace commands per version, so the 2.18 `tsVectorFieldMetadataId` backfill (which the expression derivation depends on) is guaranteed to have run first. - **Cost**: this is a deliberate full rebuild — it drops/re-adds every searchVector STORED column (table rewrite per searchable object) and recreates each GIN index, per workspace, under the workspace iterator. Intentional ("uniformize for everyone"), as noted in the issue. ## Test plan - [x] `npx nx typecheck twenty-server` - [x] `npx nx lint:diff-with-main twenty-server` Closes Part 2 of https://github.com/twentyhq/core-team-issues/issues/2620 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22355?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. --> |
||
|
|
82516d65e4 |
Credit the import-contacts onboarding reward on account connection (#22354)
Onboarding V2 shows a credit reward for connecting an email account, but the reward was only ever a frontend localStorage counter, never granted server-side. This applies it for real. When the connect-account step is actually completed via a Google or Microsoft connection, the workspace is credited `ONBOARDING_IMPORT_CONTACTS_CREDITS_REWARD`. Eligibility is derived server-side from the `ONBOARDING_CONNECT_ACCOUNT_PENDING` flag (set once at workspace creation), so the reward is one-time and is not granted when the step is skipped. Crediting is best-effort: it never blocks the OAuth flow and no-ops when billing is disabled. The invite-team reward is handled separately in #22309. The upgrade reward needs no grant: it is applied structurally through the trial resource-usage cap, so an explicit grant would double-count it. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22354?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. --> |
||
|
|
96e5d0f3ed |
Track total onboarding free credits in an atom (#22348)
The v2 onboarding header shows a "free credits" counter, but every page
fed it a hard-coded `0`, so it never reflected the credits the workspace
would actually receive. This tracks the running total based on the
choices made at each step.
- New `onboardingFreeCreditsState` atom (`{ importContacts, inviteTeam
}`, localStorage-backed) + `useOnboardingFreeCreditsTotal` to sum it
into the header.
- Connecting email sets the import-contacts reward (persisted so it
survives the OAuth redirect); inviting teammates sets `min(count ×
perUser, max)` on submit. Skipping a step contributes 0; the atom resets
at onboarding start.
- Counter scope is import-contacts + invite-team rewards only
(display-credit units already exposed via `onboardingConfigState`).
Plan/trial credits are out of scope.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22348?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. -->
|
||
|
|
e0ebe0e029 |
v1.2.0 — Client brief intake (marketplace B2) (#22290)
## Summary **Version:** 1.2.0 (`packages/twenty-apps/internal/twenty-partners/package.json`) Adds marketplace B2 client brief intake to the partners app: - `POST /s/client-briefs` logic function — validates payload, creates an unlisted Opportunity (`isListed = false`) with `— client brief` name suffix - **Client briefs** ops view for review before listing - Unit + integration tests Merge this **before** the website PR (`rk-client-brief-web`). ## Test plan - [ ] `yarn twenty dev --once` syncs schema - [ ] `yarn test` passes in `twenty-partners` - [ ] Smoke: `POST /s/client-briefs` with `x-application-secret` creates Opportunity with `isListed = false` - [ ] Client briefs view visible in nav <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22290?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. --> |
||
|
|
3e53a16b27 |
Clear orphan search field metadata backfill tsVectorFieldMetadataId (#22353)
Instead of invariant throw in instance slow, auto recover by deleting orphan search field metadata as in the end they would just end up as dead metadata <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22353?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. --> |
||
|
|
fe644a0630 |
fix(server): recreate searchVector GIN index on rebuild (#22349)
## Summary Fixes a pre-existing regression where rebuilding a `TS_VECTOR` (`searchVector`) generated column drops its GIN index without recreating it, leaving search correct but **unindexed** (sequential scan). Changing a generated column's expression requires `DROP COLUMN` + `ADD COLUMN` (Postgres can't `ALTER` a generated expression). The `searchVector`'s GIN index is a separate index-metadata entity built on that column, so the `DROP COLUMN` cascade-drops the physical index — and the rebuild branch never re-issued `CREATE INDEX`. This existed on `main` (triggered by `asExpression`/`generatedType` settings changes) and was inherited by the `rebuildSearchVector` refactor in #22287. This is the first, self-contained part of https://github.com/twentyhq/core-team-issues/issues/2620. The 2.18 recompute/backfill workspace command is intentionally left for a follow-up PR. ## What changed ### Runner loads the maps a rebuild needs `workspace-migration-runner.service.ts` — `fieldMetadata` declares neither `searchFieldMetadata` nor `index` as a related metadata name, so a `fieldMetadata`-only rebuild action had neither `flatSearchFieldMetadataMaps` (needed by the expression derivation) nor `flatIndexMaps` (needed to recreate the index) in context. The runner now detects `update` actions carrying `rebuildSearchVector === true` and loads those two maps — **only** when a rebuild is present, so ordinary field operations are unaffected. ### Handler recreates the index after re-adding the column `update-field-action-handler.service.ts` — in the rebuild branch, after `addColumns`, recreate the field's single GIN index: ```ts const [searchVectorFlatIndexMetadata] = findFieldRelatedIndexes({ flatFieldMetadata: optimisticFlatFieldMetadata, flatObjectMetadata, flatIndexMaps, }); if (isDefined(searchVectorFlatIndexMetadata)) { await createIndexInWorkspaceSchema({ flatIndexMetadata: searchVectorFlatIndexMetadata, ... }); } ``` - **Narrow lookup, not a workspace-wide scan.** The flat field has no index back-reference (`fieldMetadata.indexFieldMetadatas` is `null` in `ALL_ONE_TO_MANY_METADATA_RELATIONS`). The *object* does aggregate its indexes (`indexMetadataIds`), so we reuse the existing `findFieldRelatedIndexes` helper — already used by `handle-index-changes-during-field-update.util.ts` and the morph-rename path — which resolves only this object's indexes and filters to the one on the field. - A `TS_VECTOR` field has exactly one index (the standard `searchVectorGinIndex`), so we retrieve that single index rather than iterating. `createIndex` emits `CREATE INDEX IF NOT EXISTS` (idempotent). This makes the rebuild self-contained (column + index move together) and fixes every rebuild path: rename, label-identifier change, and `searchFieldMetadata` changes. ### Regression test Extends `update-one-field-metadata-search-vector-side-effect.integration-spec.ts` to query `pg_indexes` before and after the rename and assert the GIN index on the `searchVector` column persists (not just that search still returns the record). Fails without the fix, passes with it. ## Test plan - [x] `npx nx lint:diff-with-main twenty-server` — 0 warnings, 0 errors - [x] `npx nx typecheck twenty-server` — clean for changed files - [ ] Integration: extended rename-rebuild spec (GIN index present post-rebuild) Part of https://github.com/twentyhq/core-team-issues/issues/2620 |
||
|
|
f08b87c478 |
Fix v2 onboarding dropping to v1 after connecting email (#22351)
Connecting an email during v2 onboarding triggers a full-page OAuth round-trip that returns to `/` with no query param. `isOnboardingV2State` was an in-memory atom, so it reset to `false` on return and the navigation hook routed the user into the v1 onboarding (same break on a plain refresh). Fix: back the atom with `sessionStorage`. It survives the same-tab OAuth redirect and refresh, hydrates synchronously (`getOnInit`), and is auto-cleared by the existing `sessionStorage.clear()` on sign-out. The `onboardingV2=true` URL-param plumbing stays, since it carries the flag across the cross-subdomain signup hop. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22351?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. --> |
||
|
|
3752247b73 |
i18n - docs translations (#22352)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22352?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> |