26491ecdc62be3d3d542f90c4ca8633e8e691675
2022 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
26491ecdc6 |
fix(front): don't blank record title when the title cell is untouched (#22293)
## Problem
On a record show page, the record title (label identifier) can be
silently blanked. Repro:
1. Create a record, set its name, set a relation field (e.g. a
one-to-one/many relation).
2. Reload the page.
3. Click the **edit** affordance on the relation field.
→ The record's name clears to “Untitled”, and it persists (an
`updateOne` fires with `input: { name: "" }`).
## Root cause
`RecordTitleCellTextFieldInput` registers `onClickOutside` / `onEnter` /
`onTab` / `onShiftTab` and always forwards `draftValue ?? ''` to be
persisted. When the title cell is in edit mode but the user never typed
in it, `draftValue` is `undefined`, so the forwarded value is `""`.
Opening another field's input counts as a click-outside on the title
cell, which then persists `{ name: "" }` over the existing label
identifier.
## Fix
Skip persisting when the title draft is untouched (`draftValue ===
undefined`) by passing `skipPersist` on the blur-style events. An
unedited title can no longer overwrite the existing label identifier;
genuine edits set the draft and persist exactly as before.
## Test plan
- Repro above: name no longer clears when editing a relation after
reload.
- Creating a new record and naming it still works.
- Renaming an existing record via its title still works.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22293?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: Charles Bochet <charles@twenty.com>
|
||
|
|
e609320666 | Squirclesssss 🟦🔵 (#22535) | ||
|
|
1270054d35 |
feat(ai): dashboard & view building (#22411)
## Why
Building a dashboard through AI chat used to cost ~9 sequential LLM
round-trips
(~160K input tokens for a single request): the agent had to resolve
object/field UUIDs and assemble views through many granular tool calls,
each
step replaying the full cached context.
## What changed
### 1. Reference objects & fields by name (fewer round-trips)
The agent no longer needs to resolve UUIDs before acting.
- `get_object_metadata`: filter by `objectName` (singular/plural) and a
new
`includeFields` flag returning each object's fields (`{id, name, type,
label}`)
inline — object + field IDs in one call.
- `get_field_metadata`: accepts `objectName` as an alternative to
`objectMetadataId`.
- All three dashboard write tools (`create_complete_dashboard`,
`add_dashboard_widget`, `update_dashboard_widget`): accept `objectName`
and
`*FieldName` variants (`aggregateFieldName`,
`primaryAxisGroupByFieldName`,
`secondaryAxisGroupByFieldName`, `groupByFieldName`, ratio `fieldName`),
resolved to UUIDs server-side by `resolveWidgetFieldNamesToIds`. UUID
variants
still win when both are given.
### 2. `upsert_complete_view` — one atomic call to build/reconfigure a
view
- New `upsert_complete_view` tool + `ViewService.upsertCompleteView`:
create or
update a view together with its fields, filters, and sorts.
- Children are **declarative**: a provided array replaces all existing
entries of
that kind, `[]` clears them, omitting leaves them untouched. Fields are
referenced by name or UUID; no child-row IDs needed.
- Runs as a **single workspace migration** (`view` + `viewField` +
`viewFilter` +
`viewSort` in one `validateBuildAndRunWorkspaceMigration` matrice)
instead of
chained per-entity service calls. New
`buildCompleteViewChildrenFlatOperations`
util assembles the child create/delete operations.
- Granular tools (`create_view_filter`, `update_view_sort`, …) are
retained for
surgical single-entry edits.
### 3. Chart filters on dashboard widgets (end-to-end)
- Added `chartFilterSchema` (`recordFilters` + optional
`recordFilterGroups` for
AND/OR logic) to the four chart configs, with field-by-name or -UUID
references
and documented operands/value formats.
- **Relative dates supported** — e.g. `PAST_7_DAY`, `THIS_1_MONTH`,
`NEXT_3_WEEK`,
plus open-ended `IS_IN_PAST` / `IS_IN_FUTURE` / `IS_TODAY`. Filters
route
through the same read pipeline (`computeRecordGqlOperationFilter`) as
view
filters, so they resolve and apply correctly.
- `resolveChartFilterFieldNamesToIds` resolves filter `fieldName` → id
against the
widget object.
### 4. Re-enable AI-assisted dashboards
- Removed the "coming soon" gating (`isActive: false` on the dashboard
skill and
the "not available yet" copy in the MCP server + chat prompts) and
registered
`DashboardToolProvider`.
- Rewrote the dashboard skill prompt: confirmation gate (present a plan,
wait for
confirmation), completion guard (once confirmed, emit the create tool
in-turn —
no "now let me…" preambles), default-and-proceed (pick sensible defaults
for
missing fields instead of stalling), and an intent gate so informational
dashboard questions are answered directly without loading skills.
### 5. Frontend: clearer advanced-filter labels
- `useRecordFilterField` now derives the filter label from field
metadata and
appends the relation target field (e.g. `Company → Name`), so
relation/target
filters — including those set by the AI — display correctly instead of
showing
a stale/blank stored label.
## Fixes
- **`get_object_metadata({ objectName })` crash.**
`ObjectMetadataService.findManyWithinWorkspace`
spread an array-form (`OR`) `where` into a plain object, producing
`{ "0": {...}, "1": {...}, workspaceId }` → `Property "0" was not found
in
"ObjectMetadataEntity"`. Now injects `workspaceId` into each OR clause,
so name
lookups work.
- **Invalid SELECT/MULTI_SELECT filter options silently produced broken
charts/views.**
Chart-configuration validation and the migration-layer
`FlatViewFilterValidator`
now reject filters that reference options that don't exist, with a clear
`Allowed values: …` message at creation time (shared
`getInvalidSelectFilterOptionValues` util + tests).
- **Non-atomic view assembly.** The previous multi-call view build could
leave a
half-built view on failure; `upsert_complete_view` now runs as a single
transaction (one validation pass, one cache recompute, rollback on
error).
- **Blank RECORD_TABLE widgets from UNLISTED views.** Guidance + the
upsert
ownership check steer widget-backing views to `WORKSPACE` visibility; an
UNLISTED view created without an owner renders a blank widget.
- **Extra discovery round-trip removed.** Deleted the skill→tool bundle
mechanism
(`SKILL_TOOL_BUNDLES`, `getBundledToolNamesForSkills`, and the
`load_skills`
schema-loading path) that forced a second `learn_tools` call.
- **Type-safety of widget resolution.** Reworked the widget resolver to
build a
properly typed `WidgetWithMetadataIds` (dedicated input/output types)
instead of
returning an untyped, cast-heavy object.
## Notes
- Backend changes are in `twenty-server`; one small `twenty-front`
change to the
advanced-filter label hook. No entity/schema changes, so no migration.
- Tests added: `getInvalidSelectFilterOptionValues`,
`resolveWidgetFieldNamesToIds`
(incl. filter/relative-date resolution), `update_dashboard_widget`, and
expanded
view-tools factory specs.
- Design decisions: dedicated composite tool over code-interpreter
orchestration
(atomicity + validation + consistency with `create_complete_dashboard` /
`create_complete_workflow`); name-or-UUID but no child-row IDs on
`upsert_complete_view`; name→id resolution kept as stateless utils, not
services.
## Test plan
- [ ] `npx nx run twenty-server:typecheck`
- [ ] `npx nx lint:diff-with-main twenty-server` and `twenty-front`
- [ ] `npx nx test twenty-server` (view tools factory,
`getInvalidSelectFilterOptionValues`,
`resolveWidgetFieldNamesToIds`, `update_dashboard_widget`)
- [ ] AI chat: "Create a dashboard with a chart of deal value by
pipeline stage
and a table of the top 10 open opportunities" → plans, waits for
confirmation, then builds with fewer round-trips
- [ ] AI chat: add a chart widget filtered by a relative date (e.g.
deals created
in `PAST_7_DAY`) and confirm the chart is actually filtered
- [ ] Filter on a non-existent SELECT option is rejected with a clear
error
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22411?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. -->
|
||
|
|
25fe66565c |
feat(applications): add type and options to application variables (#22157)
## Before <img width="1452" height="709" alt="image" src="https://github.com/user-attachments/assets/cd384ffa-cbe6-49d5-a807-ca8d580f55a9" /> <img width="1074" height="452" alt="image" src="https://github.com/user-attachments/assets/720d38db-3495-4032-8831-17d24ec6a7e7" /> ## After <img width="1421" height="865" alt="image" src="https://github.com/user-attachments/assets/2275c996-c895-4800-8324-2aa2ddfddd43" /> <img width="1348" height="870" alt="image" src="https://github.com/user-attachments/assets/3e1a891d-6db0-4cbd-870a-2a5bbde4929d" /> ## Summary Adds typed application variables with optional select **options**. This is the other half of #22059, split out from the custom-settings-tab removal. ## Changes - **Shared types**: `ApplicationVariable` / `ServerVariables` gain an optional `type` (a `FieldMetadataType` subset — `TEXT`, `BOOLEAN`, `NUMBER`, `DATE`, `SELECT`, `MULTI_SELECT`, `RAW_JSON`, `RICH_TEXT`, `ARRAY`, …) and select `options`. New `serializeApplicationVariableValue` / `deserializeApplicationVariableValue` helpers convert typed values to/from the encrypted string storage. - **Server**: `type`/`options` columns on `applicationVariable` and `applicationRegistrationVariable` (entities + DTOs), a fast `2-17` instance command, manifest processing via the serialization helpers, and a `QueryDeepPartialEntity` cast where the manifest JSON column is persisted. - **Frontend**: a polymorphic `SettingsApplicationVariableInput` that renders the native `Form*` field component for each type (boolean, number, date/date-time, select, multi-select, array, raw JSON, rich text, text); fragment/query updates to fetch `type`/`options`. - **SDK**: `defineApplication` validates that `SELECT`/`MULTI_SELECT` variables declare non-empty `options` at build time (since `options` is kept structurally optional for TypeORM/SDK compatibility). Variables default to `TEXT` when no type is given, so existing manifests are unaffected. ## Notes The generated GraphQL artifacts (`type`/`options` on the variable types) are regenerated by codegen; that change accompanies this PR. https://claude.ai/code/session_013Z7UB35V2mvUozh55QHG23 --- _Generated by [Claude Code](https://claude.ai/code/session_013Z7UB35V2mvUozh55QHG23)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22157?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. --> |
||
|
|
3aeb2b0d5d |
Self-host Inter and DM Mono fonts (#22506)
Replaces the two Google Fonts stylesheets with self-hosted fonts via `@fontsource`, imported in the app entry (and Storybook preview), and removes the duplicate Inter that BlockNote was shipping. ## Why - **The caching argument for Google Fonts is dead.** Browsers partition the HTTP cache by top-level site (Chrome 86+, Firefox 85+, Safari even earlier), so a font cached from another website is never reused on ours. Every first-time visitor downloads the fonts either way — Google just adds a detour. - **Faster first paint.** This removes two render-blocking cross-origin stylesheets from `index.html` (DNS + TLS to `fonts.googleapis.com`, then a second connection to `fonts.gstatic.com`, with no preconnect today). The fonts now ship from our own `/assets` alongside the rest of the app, behind the same CDN and cache policy. - **Privacy.** Visitor IPs are no longer sent to Google on every page load. A German court ruled in 2022 that Google Fonts embedding violates GDPR, and privacy-conscious self-hosters currently have no way to opt out of the dependency. - **Air-gapped / offline self-hosted instances** currently render fallback system fonts; they now get the real ones. ## Font unification We were actually loading Inter from two places: the Google stylesheet, plus `@blocknote/core/fonts/inter.css` (8 weights, latin-only, woff+woff2) imported by the two rich-text editors — whichever loaded last won the cascade. Both are gone; `@fontsource` is now the single source: - Inter 400/500/600 (theme weights) + 700 (rich-text bold, previously only covered by the BlockNote copy) - DM Mono 400/500 (DM Mono has no 600 upstream; the old Google link requested one anyway) Fontsource ships the same `unicode-range` subsets as the Google CSS, so browsers still only download the subset they need (~60KB of woff2 for latin), and non-latin locales keep full coverage — which the latin-only BlockNote copy didn't provide. ## Notes - The BlockNote PDF export (`exportBlockNoteEditorToPdf.ts`) still fetches Inter TTFs from `fonts.gstatic.com` at export time — react-pdf needs TTF files, left unchanged here. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22506?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. --> |
||
|
|
d8ea406b80 |
fix(front): skip unknown fields in SSE optimistic updates instead of dropping the event (#22474)
## Rationale When an SSE record-update event carries a field the tab's metadata cache doesn't know (someone added a custom field after this tab loaded), `computeOptimisticRecordFromInput` throws `Should never occur, encountered unknown fields …`. The catch in `useTriggerEventStreamCreation` swallows the throw, so the **entire event is discarded** — the tab silently stops reflecting that update. **Production evidence (Sentry):** the `Error while processing SSE message` family — ~860 events / ~480 users in the last 30 days, ongoing ([TWENTY-FRONT-7PD](https://twenty-v7.sentry.io/issues/TWENTY-FRONT-7PD) et al.), with the sampled stack landing exactly on this throw. Related: [TWENTY-FRONT-633](https://twenty-v7.sentry.io/issues/TWENTY-FRONT-633) (903 users). ## Why this is the root cause, not a symptom patch The throw is an assertion that unknown fields "should never occur". That's the correct contract for the 4 local-mutation callers (`useUpdateOneRecord`, `useCreateOneRecord`, `useCreateManyRecords`, `useRunWorkflowVersion`) — there, an unknown field is a programming bug. But for the SSE caller the input comes from the **server**, which can legitimately be ahead of the tab's metadata. Schema convergence is the metadata-event pipeline's job (it flows over the same SSE channel); the record pipeline's job is to tolerate the window. So the fix moves the decision to the right caller instead of weakening the assertion for everyone: - `getUnknownRecordInputFields` — detection logic extracted, shared - mutation callers: still throw (behavior unchanged) - SSE update path: filters unknown fields and applies the rest of the event Dropping the *fields* loses nothing: the tab couldn't render them anyway without the metadata, and the metadata event that follows triggers the proper refresh. ## User impact ~480 users/month currently get silently stale tabs (list/kanban rows not reflecting teammates' updates) whenever any custom field is added while they have Twenty open. After this fix, updates keep flowing; only the not-yet-known field is skipped until metadata converges. ## Test plan - [x] Unit tests for `getUnknownRecordInputFields` (known fields, `__typename`, unknown fields, relation join columns) - [x] Existing `computeOptimisticRecordFromInput` tests cover the unchanged throw path - [ ] CI green https://claude.ai/code/session_01Lyi6zTema2FMVVh8MD6c38 --- _Generated by [Claude Code](https://claude.ai/code/session_01Lyi6zTema2FMVVh8MD6c38)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22474?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. --> |
||
|
|
429e8c4b84 |
fix: align email validation between front and server and roll back optimistic value on failed save (#22490)
## Summary
Inline edits of EMAILS fields could leave the UI in a misleading state:
the frontend validated with Zod's default `z.email()` while the server
used the stricter `z.regexes.unicodeEmail` pattern (which caps the local
part at 64 characters). A very long email passed client validation and
was optimistically written to the UI; the server then rejected the
mutation. An error snackbar was shown, but the field kept displaying the
unsaved value until a page reload.
## Changes
- **Single source of truth for email validation**: added a shared
`emailSchema` (`z.email({ pattern: z.regexes.unicodeEmail })`) in
`twenty-shared/utils`, now used by:
- the server-side EMAILS field validator
(`validate-emails-primary-email-subfield-or-throw.util.ts`)
- the `EmailsFieldInput` inline editor
- spreadsheet import validation
- **Rollback on failed save**: `useUpdateOneRecord` now restores the
optimistically updated fields in the record store when the mutation
fails, mirroring the store upsert already done in the success path.
Previously the catch block only rolled back the Apollo cache — which
stopped reverting the UI after table virtualization, since the record
store (the render source of truth) is no longer synced reactively from
the cache. The error is still rethrown, so the existing global
promise-rejection handler keeps showing the error snackbar. This fixes
the stale-value-until-reload behavior for all field types and all
callers, not just EMAILS fields.
- **Regression tests**: added unit tests for the shared schema,
including the >64-character local part case.
Fixes [sonarly issue
#54034](https://sonarly.com/issue/54034?share=eyJ0aWQiOjMzMCwidHlwIjoiYnVnIiwicmlkIjo1NDAzNCwiZXhwIjoxNzgzNTI1OTQzfQ.9e7639034a677301512fceeafab764b1)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22490?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> |
||
|
|
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. --> |
||
|
|
b0d7516951 |
Deprecate asExpression from field metadata search_vector (#22287)
## Summary Fully deprecates the cached `asExpression` / `generatedType` settings on `TS_VECTOR` (searchVector) fields. Previously the generated-column expression was stored in `FieldMetadataSettings` and kept in sync via imperative recompute side-effects. It is now **derived at DDL time** from the `searchFieldMetadata` rows that describe which fields feed the search vector, making `searchFieldMetadata` the single source of truth and removing a whole class of cache-drift bugs. This is delivered across the milestones tracked in #2587 and coordinates with the frontend migration (#1428). ## Why - The searchVector expression lived in two places (stored `settings.asExpression` + the actual generated column), kept consistent by bespoke side-effects (`recompute-search-vector-on-field-rename`, label-identifier recompute, etc.). - The frontend reconstructed the searchable-fields list by **regex-parsing** the stored `asExpression`. - Both are brittle. Deriving the expression from `searchFieldMetadata` rows at build/run time removes the cache and the parsing. ## What changed ### Server - data model & derivation - Introduce the `tsVectorFieldMetadata` relation on `searchFieldMetadata` (`tsVectorFieldMetadataId` / universal identifier) linking each searchable-field row to its target `TS_VECTOR` field. - New runtime derivation `deriveSearchVectorAsExpressionForTsVectorField` (`flat-search-field-metadata/utils/...`) used by the create-object and update-field handlers to generate the column expression from `searchFieldMetadata` rows. - Remove `asExpression` / `generatedType` from stored settings: `FieldMetadataSettings.TS_VECTOR` is now `null`; the column builder (`generate-column-definitions.util.ts`) hardcodes `generatedType: 'STORED'` and requires the derived expression. - Delete the imperative recompute side-effects and the `compute-search-vector-universal-settings-from-object-manifest` path; drop the `settings` block from all 28 standard `compute-*-standard-flat-field-metadata` utils. ### Server - migration runner - New `rebuildSearchVector` marker on `update-field` actions: the orchestrator synthesizes targeted column rebuilds (`compute-search-vector-rebuild-target-universal-identifiers.util.ts` + the deprioritize aggregator) only when a searchFieldMetadata change or indexed-field rename actually requires it - instead of rebuilding on every settings touch. - Deferrable FKs + in-flight ID resolution so a `searchFieldMetadata` row and its `TS_VECTOR` field can be created in the same transaction (deterministic UUIDs). ### Frontend (contract change, #1428) - New `SearchFieldMetadataDTO` + dataloader exposing `searchFieldMetadataList` on object metadata. - `SettingsObjectSearchSection` now reads `objectMetadataItem.searchFieldMetadatas` instead of parsing `asExpression`; new `SearchFieldMetadataItem` type, fragment, and mapping updates. ### Upgrade commands (2.18) - `2-18-instance-command-fast-...-add-ts-vector-field-metadata-id-to-search-field-metadata` - `2-18-instance-command-fast-...-make-search-field-metadata-fks-deferrable` - `2-18-instance-command-slow-...-backfill-ts-vector-field-metadata-id-on-search-field-metadata` (These were relocated from 2.16 to 2.18 and re-timestamped into an ordered block - add column -> make FK deferrable -> backfill data - since 2.16/2.17 are released.) ### Tests - Updated search-vector side-effect integration specs to assert behavior (search works) rather than the now-removed `asExpression`; removed the obsolete expression-validation specs; refreshed the application-sync snapshot (`universalSettings: null`). ## Upgrade / compatibility notes - Existing workspaces keep their stored `settings` until a later cleanup; nothing reads it anymore. The new derivation drives all DDL going forward. - Schema changes are gated behind the 2.18 instance commands above. ## Known follow-up (separate PR) https://github.com/twentyhq/core-team-issues/issues/2620 - The column rebuild (`DROP`/`ADD` of the `searchVector` STORED column) cascade-drops its GIN index and does not recreate it - a pre-existing regression on `main` inherited here. A follow-up PR will fix the rebuild handler to recreate the GIN index and add a 2.18 workspace command to recompute every search vector + strip the deprecated settings. (Planned.) ## Test plan - [ ] `npx nx typecheck twenty-server` / `twenty-front` - [ ] `npx nx lint:diff-with-main twenty-server` / `twenty-front` - [ ] Server integration: create/update/delete field, rename indexed field, update object - search returns expected records - [ ] Run the 2.18 instance commands on a seeded DB; verify `tsVectorFieldMetadataId` backfilled and FKs deferrable - [ ] Frontend: object Search settings tab lists the correct searchable fields (no `asExpression` parsing) close https://github.com/twentyhq/core-team-issues/issues/2587 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22287?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. --> |
||
|
|
fb800e7a2d |
fix: improve native print output for dashboards and record tables (#22272)
## Summary - Adds print-specific snapshots for dashboards so native browser print/PDF captures charts and front components instead of the app shell. - Adds record-table print snapshots for object index pages so printable tables are generated from visible rows and native print avoids virtualized blank pages. - Preserves rendered chart layers by rasterizing canvas/SVG content for print. ## AI-generated disclosure This pull request was AI-generated by Hermes Agent on behalf of Vittorio Alfieri. The changes were reviewed and tested locally before submission. ## Screenshots ### Dashboard print **Before**  **After**  ### Table records print **Before**  **After**  ## Test plan - [x] `yarn nx typecheck twenty-front` - [x] `yarn nx build twenty-front` - [x] Generated dashboard PDFs from the preview build and rasterized pages to PNG for visual verification. - [x] Generated Tasks/table-record PDFs from the preview build and verified the final page contains table content instead of blank pages. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22272?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: Félix Malfait <felix@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
2a21eb46c0 |
perf: cap to-many relation records per parent and inline chips in table (#22206)
## Problem Record table views that show a to-many relation column (e.g. Workflows with a "Runs" column) get slow and janky to scroll when some records have many related records. Two root causes, found by profiling the page live: 1. **Backend over-fetch + unfairness.** Nested one-to-many relations were loaded with a single flat limit of `QUERY_MAX_RECORDS_FROM_RELATION * parentCount` shared across *all* parents in the page (`WHERE parentColumn IN (ids) LIMIT 60*N`, no per-parent cap). A single hot parent can consume the entire budget — returning thousands of rows for one cell, and potentially starving sibling parents of records they actually have. The `limit * parentCount` shape shows the original intent *was* a per-parent budget; it was just implemented as a global limit. 2. **Frontend DOM explosion.** `ExpandableList` mounts the *entire* child array inline (clipped with `overflow: hidden`) when unfocused, and mounts all children for measurement when focused. A cell with 2,000+ relation chips mounts ~14k DOM nodes — one observed page reached ~55k nodes for 43 rows, producing 100–300 ms main-thread long tasks on every scroll. ## Fix - **Backend:** load one-to-many relations with a true **per-parent** cap via a `LATERAL` join — each parent runs its own indexed, `LIMIT`-ed scan that stops after the per-parent budget. This is `O(perParentLimit × parentCount)` and never reads or sorts a parent's full relation set. The per-parent query is built through the workspace query builder (so it stays schema-qualified and keeps the soft-delete predicate) and wrapped as a `FROM` subquery; read/row-level permissions are enforced when records are hydrated by id, as elsewhere in the relation loader. Many-to-one is unchanged. - **Frontend:** add an opt-in `maxInlineCount` to `ExpandableList` so to-many relation cells mount only a small inline preview; the expand dropdown still renders the full fetched set. Fully backward compatible (no cap → identical behavior). ## Why LATERAL over a window function A windowed `ROW_NUMBER() OVER (PARTITION BY parent) <= limit` is correct and fair too, but a window function **cannot stop early within a partition** — it must read every matching row (and sort it). Measured on skewed data (one parent with ~4k children, on the existing single-column join index, PG16): | Approach | Time | Buffers | Rows read from the hot partition | |---|---|---|---| | Pre-PR (`LIMIT 60×N`) | 1.6 ms | 91 | ~180 total, early-stops, but **unfair** (starves siblings) | | Window (`ROW_NUMBER`) | 3.7 ms | 128 | **all ~4k + sort** | | **LATERAL (`per-parent LIMIT`)** | **0.5 ms** | **57** | **~60, index early-stop** | LATERAL matches the pre-PR read cost while being fair, needs no new index, and scales independently of how large any single relation is. ## Verification - Backend integration test (`nested-relation-per-parent-limit`): a parent with 65 children is capped at 60 while a sibling with 3 keeps all 3 — passes. - `EXPLAIN ANALYZE` on the generated SQL: Index Scan with the `LIMIT` pushed into the per-parent lateral (early-stop). - Frontend unit test for the `ExpandableList` cap. - Manual check on a table cell with 40 related records: exactly 10 chips mount inline (down from 40), no console errors, chips still clickable and the overflow count reflects the true total. |
||
|
|
9b57c5dfac |
Set record card header height and calendar spacing (#22187)
## Summary Sets the shared `RecordCardHeaderContainer` height to `32px`, so board and calendar card headers use the same common header size. Updates the board fetch-more card-height estimate to use the same `32px` header value. Reduces the calendar card header/content gap by removing the body top padding while keeping the existing side and bottom padding. ## Screenshots Before:  After:  ## Validation - Browser verification on `http://apple.localhost:3001/objects/opportunities?viewId=0433d066-dda7-4b2c-89e5-04d4792d193c`: visible calendar card body padding computes to `0px 4px 4px` - Browser verification on board view: visible board card headers compute to `32px` - Browser console errors: none - `git diff --check` - `prettier --write` on changed files - `oxlint --type-aware` on changed files in the running checkout |
||
|
|
7e48d36c98 |
fix(twenty-front): apply object type translations to details panel relation labels (#22090)
## Problem When users customize or translate object type names (e.g. "Company" → "Unternehmen" in German), the translated/customized names do **not** appear in the details panel. The default English names still show instead. This is because the frontend's relation metadata only carried `nameSingular`/`namePlural` (internal API identifiers), not `labelSingular`/`labelPlural` (user-facing display labels). Components that display relation names had no choice but to use the internal identifiers. Fixes #19790 ## Changes ### Data layer — add labels to the pipeline - **GraphQL fragment** (`fragment.ts`): Added `labelSingular`/`labelPlural` to `sourceObjectMetadata` and `targetObjectMetadata` in both `relation` and `morphRelations` - **Type** (`FieldMetadataItemRelation.ts`): Extended the `Pick` type to include `labelSingular`/`labelPlural` - **Field metadata type** (`FieldMetadata.ts`): Added `relationObjectMetadataLabelSingular`/`LabelPlural` to `FieldRelationMetadata` - **Mapping** (`formatFieldMetadataItemAsFieldDefinition.ts`): Maps the new label fields with `label ?? name` fallback for backwards compatibility ### Display layer — use labels for user-facing text - **RecordDetailRelationRecordsListItem**: Uses `relationObjectMetadataLabelSingular` for delete confirmation dialog title, subtitle, and button text (falls back to `nameSingular`) - **RecordDetailRelationRecordsList**: Threads `objectLabelSingular` prop through - **RecordDetailRelationSection**: Passes `labelSingular ?? nameSingular` from the looked-up object metadata - **FieldWidgetRelationCard**: Passes `relationObjectMetadataLabelSingular` from field metadata - **FieldWidgetJunctionRelationCard**: Passes `labelSingular ?? nameSingular` from object metadata lookup - **FieldWidgetMorphRelationCard**: Passes label from morph relation hook result - **useGetMorphRelationRelatedRecordsWithObjectNameSingular**: Carries `labelSingular` from matched morph relation ### Test data - Updated story/mock files with `relationObjectMetadataLabelSingular`/`LabelPlural` fields - Updated `SettingsDataModelRelationFieldPreview` with label fields in morph relation objects ## Design decisions - **Backwards compatible**: All new props are optional. Every display usage uses `label ?? name` fallback, so if `labelSingular` isn't available yet (e.g. before GraphQL regeneration), it falls back to the old behavior - **Lookup vs display separation**: `nameSingular` continues to be used for lookups, routing, and GraphQL queries (it's the identifier). `labelSingular` is only used for user-facing display text - **Minimal scope**: Only changes the display paths identified in the bug report — confirmation dialogs and relation labels in the details panel ## Test plan 1. Set workspace language to a non-English locale (e.g. German) 2. Navigate to a record with relation fields 3. Verify relation section titles and labels show translated names 4. Try to delete a related record — verify the confirmation dialog uses the translated name 5. Switch language back to English — verify everything still works correctly <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22090?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: Charles Bochet <charles@twenty.com> |
||
|
|
b3e39e2198 |
fix: relative date picker calendar display (#21895)
Part of https://github.com/twentyhq/twenty/issues/19739#issuecomment-4652034526 (Bug 1-3). Maybe it feels like theses bugs are not actually bugs, but we can maybe say it as UX improvements: specially needed in case when an user will choose any past options. ### Bug 1: calendar open on wrong month With Is Relative (e.g. Past 1 Quarter), the calendar opened on today’s month instead of the range start. After the fix, it now opens on the first month of the filtered range. **Testing:** View filter → Date field → Is Relative → Past 1 Quarter. Calendar opens on January (range start), not today’s month https://github.com/user-attachments/assets/8849d00a-4d5c-4f8a-8d31-3a62535eb311 ### Bug 2: Dates not highlighted Ranges older than ~2 months (e.g. Q1 when today is June) showed no highlighted days. Highlighting now covers the full resolved range. **Testing:** Same setup: past 1 Quarter on a date when Q1 is outside the old 2‑month window. Jan 1 - Mar 31 will highlight. https://github.com/user-attachments/assets/d21e2272-c923-4493-80ff-bdf4228842b1 ### Bug 3: No month navigation Relative mode only showed Past - 1 - Quarter controls with no way to browse months. Now see the new arrows move through months without changing the filter. <img width="377" height="455" alt="Screenshot 2026-06-20 181107" src="https://github.com/user-attachments/assets/eb51feb9-af10-489a-b166-8b8d6c642e05" /> > [!NOTE] > 1. We can't do the fixes by one by one, i have to fix them within one PR because all the fixes are inter-related, like we can't test the bug 1 fix alone without implementing bug 3. > 2. Bug 4 will be done in a separate PR which is actually the issue #19739. See https://github.com/twentyhq/twenty/issues/19739#issuecomment-4652034526 for better understanding. > 3. If you see the screen recordings, they are actually done with the alignment fixes from #21881 . So without that changes you will see the alignmemt issues in the calendar grid in your local. --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
d8cb4aa15b |
fix(filter): resolve filter-derived date defaults with Temporal (fixes create/update crash in date-filtered views) (#22124)
## Symptom
Creating or updating an Opportunity on a board view crashes with:
```
Uncaught TypeError: e.split is not a function
at splitDateString (date-fns) → parseISO → isMatchingDateFilter
→ isRecordMatchingFilter → <group-by optimistic effect> → createOneRecord
```
`e` is a non-string (a `Date` object, shown as `{}` in the debugger).
Distinct from the `null` case fixed in #22029.
## Root cause
Reproduced on a live board filtered by **Close date — Is relative —
"This quarter"** (a `DATE_TIME` field).
When you create a record in a filtered view, `useCreateNewIndexRecord`
derives default field values from the view's filters via
`buildRecordInputFromFilter` → `buildValueFromFilter`. For a date field,
`computeValueFromFilterDate` returned a JS **`Date` object** (`new
Date()` / `new Date(value)`), assigned to the new record verbatim. The
optimistic record's `closeDate` was then a `Date`, not an ISO string,
and the group-by optimistic effect matched it via `parseISO(dateObject)`
→ `dateString.split is not a function`, crashing every create/update in
the view.
## Fix
`computeValueFromFilterDate` now returns timezone-aware **ISO strings**
via Temporal, mirroring what `turnRecordFilterIntoGqlOperationFilter`
produces for the same filters — so a record created in a date-filtered
view actually satisfies its own filter:
- **`DATE_TIME`** → an instant. Date-only filter values (a `DATE_TIME`
"is" filter stores `yyyy-MM-dd`, no time) are resolved to the start of
day in the user's time zone, matching how the filter operands are built
— `Temporal.Instant.from()` alone would `RangeError` on them.
- **`DATE`** → a plain date `yyyy-MM-dd` resolved in the **user's time
zone** (`Temporal.Now.plainDateISO(timeZone)`). A bare `new
Date().toISOString()` would use the UTC date, which near midnight is the
wrong calendar day for non-UTC users, so the new record could miss its
own `IS_TODAY`/relative filter. The time zone is threaded from
`useUserTimezone` (same source the filter side uses).
- `IS_BEFORE` subtracts 1 day for `DATE` / 1 minute for `DATE_TIME` (the
`-1 minute` special-case moved out of `buildRecordInputFromFilter`,
which now just assigns the string).
No `Date` object ever reaches `parseISO`, and the value matches the
filter operand, so the optimistic card lands in the right place.
## Tests
- `buildValueFromFilter.spec.ts`: every date operator returns an ISO
**string** (round-tripped to the expected instant); a `DATE` block
asserts date-only `yyyy-MM-dd` output and that `IS_TODAY` resolves to
the correct calendar day **per time zone** at a UTC day boundary
(`2024-03-20` UTC vs `2024-03-21` Asia/Tokyo); a date-only `DATE_TIME`
`IS` value resolves to start-of-day in the user time zone (UTC vs
America/New_York) without throwing.
- `buildRecordInputFromFilter.test.ts`: filter-derived date values are
ISO strings, not `Date` objects.
## Verified locally (Chrome)
Reproduced the exact prod scenario on the **By Stage Opportunities
Kanban board** (the group-by optimistic effect) with a **Close date — Is
relative** filter:
- Created a card in a column → **no `split is not a function` crash**;
the card got a valid Close date (`now` for the relative filter, e.g. `25
Jun 2026 13:10`). Console clean.
- Also verified a **table view + Close date — Is** filter: created
record gets a valid start-of-day value (`25 Jun 2026 00:00`).
Both `IS_RELATIVE` (→ now instant) and the date-only `IS` (→
start-of-day in the user tz) paths produce string values that the
optimistic matcher handles without throwing.
|
||
|
|
db5338cf32 |
Feat: group records by many to one relation (#22123)
## Group records by relation (Kanban + Table)
Adds grouping by `MANY_TO_ONE` relation fields on both board and table
views, reusing the existing `ViewGroup` storage (`fieldValue = related
record id`).
- **New group** record picker to create relation-backed groups (board
column + table row)
- Relation-aware group headers (name/avatar), filtering, and drag-drop
(writes the FK join column) — all using one canonical `${name}Id` column
- Sort menu hides alphabetical options when grouping by a relation (no
comparable title)
- A group whose backing record no longer exists renders a "Deleted" chip
instead of a blank header
- **Backend:** allow `MANY_TO_ONE` relations as the Kanban
`mainGroupByField` in the flat-view validator
https://github.com/user-attachments/assets/267077a6-2667-4506-b178-eee420a16f20
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22123?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. -->
|
||
|
|
33d2441a93 |
fix(ux): input error messages no longer overlap adjacent fields in forms (#21886)
## Context `InputErrorHelper` used `position: absolute` which took the error message out of normal document flow. In multi-field grid layouts (e.g. the HTTP Request workflow node editor), this caused the error text to render on top of the input below it instead of pushing it down. ## Solution Removed `position: absolute` from `InputErrorHelper`. The error message now participates in normal document flow and pushes subsequent content downward, as expected in modern forms. ## Test plan - [x] Open the HTTP Request node editor, fill in a field incorrectly — error message appears below the field without overlapping the next input - [x] Verify single-field forms still display error messages correctly 🤖 Generated with [Claude Code](https://claude.ai/claude-code) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21886?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: Emmanuel Hernandez <emmanuel.hernandez@clickbalance.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
269c8ef400 |
feat(front): allow advanced relation fields in FieldWidget selector (#22005)
## After <img width="706" height="760" alt="image" src="https://github.com/user-attachments/assets/d118285f-baab-4187-988c-d0180d61a629" /> <img width="707" height="372" alt="image" src="https://github.com/user-attachments/assets/5676d829-2ec1-494e-a8f0-5998f1f0c3c8" /> ## Summary The FieldWidget field-selection dropdown currently filters out relation fields whose target is a system object, so users can't pick fields like `calendarEventParticipants` on the CalendarEvent record page. The widget itself can render them just fine as boxed relations — the restriction only lives in the picker. This unblocks the consistency story from #22003 (revert of #21857): once shipped, participants can be added to the calendar event record page via the existing FieldWidget mechanism instead of a bespoke side-panel page. ## Changes - `isFieldCellSupported`: adds an opt-in `includeSystemObjectRelations` option that skips the `isObjectMetadataAvailableForRelation` system check. - `useFieldListFieldMetadataItems`: forwards the option through to `isFieldCellSupported`. Default is `false`, so all existing callers keep current behavior. - `useFieldWidgetEligibleFields`: turns the option on, so the FieldWidget selector now surfaces fields like `calendarEventParticipants`, `messageParticipants`, etc. ## Test plan - [x] `nx typecheck twenty-front` - [x] `nx lint:diff-with-main twenty-front` - [ ] CI - [ ] Manually verify the FieldWidget dropdown now lists `calendarEventParticipants` on a CalendarEvent record page, and that selecting it renders a participants list via the existing relation card/field widget. https://claude.ai/code/session_01RnMcjL35wdCRzpXN257RLJ --- _Generated by [Claude Code](https://claude.ai/code/session_01RnMcjL35wdCRzpXN257RLJ)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22005?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. --> |
||
|
|
f24be8eacb |
fix: keep AI chat open when opening a record full-page (e.g. workflows) (#22036)
## Problem
When the AI chat is open in the side panel and you click a workflow from
the Workflows list, the AI chat closes as you navigate to the workflow
page. Navigating between other index/list pages, or to settings, keeps
the chat open — only opening a record full-page closes it.
## Root cause
`useOpenRecordFromIndexView` unconditionally calls
`closeSidePanelMenu()` before navigating to a full-page record:
```ts
} else {
closeSidePanelMenu();
navigate(AppPath.RecordShowPage, { ... });
}
```
Workflows (and other objects excluded by `canOpenObjectInSidePanel` —
`workflow`, `workflowVersion`, `dashboard`) can't open in the side
panel, so they *always* take this branch and close the panel, including
the AI chat.
This is inconsistent with `PageChangeEffect`, which already lets the AI
chat survive navigation by exempting `SidePanelPages.AskAI`. That
exemption is why navigating between index pages or to settings doesn't
close the chat.
## Fix
Skip the close when the side panel is showing the AI chat, mirroring the
exemption already used in `PageChangeEffect`. Any other side panel page
still closes as before.
## Testing
- `nx lint:diff-with-main twenty-front` (file lints clean)
- `nx typecheck twenty-front` passes
https://claude.ai/code/session_01LtBBAxjn32FQVduyAi6B37
---
_Generated by [Claude
Code](https://claude.ai/code/session_01LtBBAxjn32FQVduyAi6B37)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22036?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. -->
|
||
|
|
766d90af7e |
Remove framer-motion from twenty-ui (#22021)
## What Removes the `framer-motion` dependency from `twenty-ui` and replaces every usage with pure CSS animations, reaching for Base UI primitives where one fits: - **Collapse/expand** (`AnimatedEaseInOut`, `AnimatedExpandableContainer`): rebuilt on Base UI `Collapsible` (CSS-animated `--collapsible-panel-height/width` + transition states). Public props unchanged, so the ~28 call sites are untouched. - **ProgressBar**: rebuilt on Base UI `Progress` (proper `role`/`aria-valuenow`). The snackbar auto-dismiss countdown now uses a CSS keyframe + `animation-play-state` (pause on hover), removing a per-frame React re-render; `useProgressAnimation` is deleted. - The remaining `Animated*` components, the circular spinner, checkmark, and the placeholder pointer parallax move to plain CSS (SCSS modules + the `duration()` helper + theme tokens). - Deletes 3 unused components (`AnimatedTranslation`, `AnimatedTextWord`, `AnimatedFadeOut`). ## Why `twenty-ui` is a publicly published library with a size budget, so dropping framer-motion shrinks what consumers ship. `twenty-front` keeps its own framer-motion; that is out of scope here. ## Notes for reviewers - A few `twenty-ui` components received framer props from `twenty-front` call sites; those were migrated (e.g. `AnimatedLightIconButton` gained a CSS `rotate` prop, and the `EMPTY_PLACEHOLDER_TRANSITION_PROPS` spreads were removed). - Behavior change: Base UI `Collapsible` animates only on open/close transitions, so the old "animate in on first mount while already open" case no longer plays (the `initial` prop is kept for API compatibility). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22021?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. --> |
||
|
|
c18787350f |
fix: month and year dropdowns in settings logs date picker (#21529)
### Summary - Fixes #21514 and Issue 2 - **Issue 1**: when opening the calendar and choosing a month or year, those lists could appear underneath the calendar, making them impossible to see and use. (Issue #21514) - **Issue 2**: after opening the calendar icon menu, clicking the month or year controls don't work, so you couldn’t actually change the month or year. Before: <img width="355" height="434" alt="607363028-0d3a302e-9dba-4d9a-b354-ad7cbcd1fba5" src="https://github.com/user-attachments/assets/b1c357d1-a7cf-4572-8737-721cf4e2597a" /> After: https://github.com/user-attachments/assets/ffc26447-ff34-4f11-a3b4-4c329e446ec4 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21529?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: Charles Bochet <charles@twenty.com> |
||
|
|
6e7d8ef96c |
[Twenty-front]: Record table Header drag and drop functionality (#21304)
Closes #21303 and https://github.com/twentyhq/core-team-issues/issues/151 https://github.com/user-attachments/assets/45cee1be-464f-467e-a1c0-cf5354ff87db --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
5f22908588 |
Decouple twenty-ui Avatar from app server-URL config (#21968)
Makes `twenty-ui`'s `Avatar` render the `avatarUrl` it receives instead of building it from `window._env_`/`window.location` at module load, so the library no longer depends on the app environment. URL resolution moves to `twenty-front` via a `getAbsoluteImageUrl` helper applied at the call sites. Part of making twenty-ui a standalone library. |
||
|
|
b1ada79d72 |
fix(front): scope relation table widget via currentRecordId (#21965)
## Context Follow-up to #21293 (merged). That PR added a bespoke `relationTableFilter` to keep a relation field rendered as a record-table widget scoped to the host record. It turns out to be redundant. ## Why it's redundant The relation table widget's view already carries an `isCurrentRecordSelected` relation filter on the inverse field — it's baked in by `useAddDraftViewForFieldRelationTableWidget` when the widget is configured. That filter is resolved through the `currentRecordId` that `FieldWidgetRelationTable` provides via `RecordFilterValueDependenciesContext`, and `turnRecordFilterIntoGqlOperationFilter` turns it into exactly `{ `${inverseField}Id`: { in: [recordId] } }`. So the hand-built `relationTableFilter` duplicated a filter the existing mechanism already produces from `currentRecordId`. ## Changes - Remove `relationTableFilter` from `RecordFilterValueDependenciesContext` - Stop reading/applying it in `useFindManyRecordIndexTableParams` and `useAggregateRecordsForRecordTableColumnFooter` - Delete the `getRelationTableFilter` util and its test - `FieldWidgetRelationTable` provides `currentRecordId` only Net −263 lines; relies on the existing `isCurrentRecordSelected` + `currentRecordId` scoping path. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21965?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. --> |
||
|
|
068a8d4efe |
fix(front): keep relation field record tables scoped to the host record (#21293)
## Problem
When a relation field is added to a record page as a record **table**
widget
(Page Layouts → a `FIELD` widget with `fieldDisplayMode: TABLE` and a
`viewId`),
the table renders the **global** list of the related object instead of
only the
records related to the current record.
Steps to reproduce:
1. On a Company record page layout, add a to-many relation field (e.g.
`Opportunities`) as a widget and set its display mode to **Table** with
a view
(so it shows columns).
2. Open a Company record.
3. The Opportunities table lists *all* opportunities in the workspace,
not just
the ones linked to that company.
Note: when the same relation widget has **no** `viewId`, it is correctly
scoped
to the record — but then it can't render custom columns. So custom
columns and
relation-scoping were effectively mutually exclusive.
## Root cause
`FieldWidgetRelationTable` renders the related records through
`RecordTableWidgetRendererContent` using the widget's `viewId`. That
path loads
the view's filters and fetches the related object's records, but **never
applies
the relation filter** that constrains the table to the host record. With
a
`viewId` present, the table therefore shows the whole object.
The relation filter itself already exists elsewhere —
`RecordDetailRelationSection` builds
``{ `${inverseRelationFieldName}Id`: { in: [recordId] } }`` for its
aggregate.
It just isn't applied on the table path.
## Fix
- Add a pure helper `getRelationTableFilter()` that builds the
host-relation
filter for a to-many relation field (morph-aware, mirroring
`RecordDetailRelationSection`).
- `FieldWidgetRelationTable` computes this filter and passes it down via
the
existing `RecordFilterValueDependenciesContext` (new optional
`relationTableFilter`).
- `useFindManyRecordIndexTableParams` (rows) and
`useAggregateRecordsForRecordTableColumnFooter` (footer aggregates) AND
this
filter into their queries.
The filter is scoped to the relation-table instance through the context
and
defaults to `undefined`, so **every other table (record index, kanban,
dashboards, …) is unaffected** — `combineFilters` / object spread treat
the
absent filter as a no-op. No backend changes.
## Tests
- New unit tests for `getRelationTableFilter` (to-many → foreign-key
filter;
to-one → none; unresolved relation type / field → none; morph relation;
missing morph target names → none).
- `nx typecheck twenty-front`, `nx lint twenty-front`, and the new
`nx test twenty-front` suite pass locally.
## Screenshots
Same record (a "Centre" with 0 related theory allocations and 34 related
orders), same page-layout (relation fields shown as Table widgets with a
view).
**Before** — with a `viewId`, the relation tables show the *global*
lists: the
Theory Allocations table is full of allocations belonging to *other*
records,
and Collateral Orders shows 60 (the whole object's first page) instead
of 34.
<!-- drag the BEFORE screenshot here -->
**After** — the same tables are scoped to the record: Theory Allocations
is
empty (this record has none) and Collateral Orders shows exactly its 34
orders,
with the view's columns (Status / Total Value / Date).
<!-- drag the AFTER screenshot here -->
## Verification
Verified on a self-hosted instance running the equivalent change (the
four
touched files are byte-identical on `main` and the latest release tag):
a
relation table widget with a `viewId` now shows only the host record's
related
rows **with** the view's columns, the footer aggregates match the
visible rows,
and the global record index is unchanged. Confirmed across records with
different related-record counts (e.g. a record with 34 related orders
shows 34;
a record with 1 shows 1; records with 0 show an empty table).
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
6237598a30 |
Fix meeting bot CalendarEvent field visibility and editability (#21883)
- Add the meeting bot preference field to the CalendarEvent record page fields view. - Use a Standard-app ownership gate for record field read-only logic. - Allow app-owned and workspace-custom fields on system objects to follow isUIEditable and permissions. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21883?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. --> |
||
|
|
4c966bfc32 |
[Twenty-front]: Bunch of View Picker Fixes and improvements. (#21290)
While working on #21208, I found a few related improvements and fixes that were worth including in this PR. 1. Improved View Picker UX: - Added optimistic updates when selecting a view from both the drag-and-drop view picker - Added optimistic updates when editing view. Before it used to close the whole dropdown. - Added highlighting for the currently selected view. - Before: https://github.com/user-attachments/assets/469fc60c-e65f-4452-a5a4-7df6188ab19d - After: https://github.com/user-attachments/assets/d3b151c1-0c10-45e7-a796-b5e6061c898d 2. Remove Favorites from the View Picker - Added support for removing a favorite directly from the view picker without needing to open additional menus. - Before: https://github.com/user-attachments/assets/70437fb9-d4c1-488b-aab9-0ea92d1bad99 - After: https://github.com/user-attachments/assets/442546bd-24ae-43d5-abe1-268ef3ff6475 --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
f4219449db |
fix(front): prevent AI agent output field error message from overlapping the Type field (#21921)
## Problem Follow-up to #21834, found during QA. That PR added an inline validation error on the AI Agent **Output → Variable Name** field. The error is rendered with `InputErrorHelper`, which is `position: absolute`. When the message wraps to two lines (which it does at the side-panel width), it is taken out of the layout flow and **overlaps the "Type" selector** directly below it: ``` Variable Name [ sdlfkj sdlkj ] Use only letters, numbers, underscores, dots or hyphens (max 64 Type <-- overlapped by the error message [ Text ▾ ] ``` ## Fix Render the error with `InputHint danger` instead of `InputErrorHelper`, matching how the sibling `FormNumberFieldInput` already shows its errors. `InputHint` flows in the column (`margin-top`, not absolute), so the error reserves its own space and pushes the following fields down instead of overlapping them. This is a one-line behaviour change in `FormTextFieldInput`; no new component or styling is introduced. ## After The `Type` field is pushed below the wrapped error message with correct spacing:  ## Tests - Added a `WithError` story to `FormTextFieldInput` (mirrors the existing `FormNumberFieldInput` `WithError` story) asserting the error message is visible. ## QA Reproduced and verified in Storybook against the real `WorkflowOutputSchemaBuilder` (throwaway story, not committed): before the fix the error overlapped `Type`; after the fix the `Type` field is pushed below the wrapped message with correct spacing. |
||
|
|
334e962ab5 |
fix: cannot create record from table view — empty morph to-many relation returns null (#21846)
## Problem
Creating a record from the table view (reproduced on **People**) crashes
the client even though the `createOne…` mutation succeeds server-side,
so the record never appears:
```
Cannot read properties of null (reading 'map')
getRecordConnectionFromRecords → getRecordNodeFromRecord → optimistic cache effect → createOneRecord
```
## Root cause
An empty **morph** to-many relation comes back as `null`, while every
other to-many relation comes back as `{ edges: [] }`. The frontend then
runs `null.map` while building the optimistic cache node; the error
escapes the mutation `update`, the rollback evicts the record, and it
never lands in the table.
## Fix
**Server** — plain to-many relations are hydrated to `[]` and formatted
to `{ edges: [] }` by `ObjectRecordsToGraphqlConnectionHelper`; an empty
morph to-many was left undefined and the field was skipped (→ `null`).
Default an unset to-many value to `[]` so it goes through the **same
connection path as plain to-many relations**.
**Frontend** — defensive guard in `getRecordNodeFromRecord`: a to-many
relation whose value isn't an array is skipped instead of crashing,
mirroring the existing guard in `extractTargetRecordsFromRelation`.
Needed regardless, since cached data / SSE / older servers still send
`null`.
## Tests
- Unit: `getRecordNodeFromRecord` skips a null to-many (reproduces the
exact crash without the guard).
- Integration: an empty morph `ONE_TO_MANY` read returns `{ edges: []
}`, not null.
|
||
|
|
b8ea742a88 |
fix(front): respect user number format for counts and aggregates (#21894)
## Problem
Several user-facing numbers were rendered raw (e.g. `153909`) instead of
honoring the workspace member's **Number format** preference (e.g. `153
909` with `Spaces and comma`). The formatting utilities already existed
(`formatNumber` / `useNumberFormat`) but were not applied on these
surfaces.
## Root cause
`transformAggregateRawValueIntoAggregateDisplayValue` — the shared
helper behind every table/board/chart aggregate — returned the `COUNT`
branch as a raw string and never threaded the user's locale format into
`formatNumber` for the other branches (so they silently fell back to
`COMMAS_AND_DOT`).
Its existing `numberFormat` param actually held the chart `SHORT`/`FULL`
abbreviation setting, so it is renamed to `chartNumberFormat`, and a new
`numberFormat: NumberFormat` now carries the locale separators.
## Surfaces fixed
- Record table footer aggregates, including the raw **"Count all"**
total
- Record board column / group-section aggregates
- Aggregate chart and pie-chart center metric (including their raw
`COUNT` early-returns)
- View picker `<view> · <count>` total
- Record show breadcrumb pagination `(x/y)`
- Record index header and side panel `N selected` counts
The board-column header needs no change — it now receives an
already-formatted string from the transform.
## Out of scope (intentionally left raw)
The editable `SettingsCounter` input (formatting would break parsing),
the advanced-filter pill, the `+N` overflow badge, and the AI routing
debug display.
## Testing
- New + existing unit tests pass
(`transformAggregateRawValueIntoAggregateDisplayValue`, `formatNumber`,
`useNumberFormat`), with added locale-aware coverage (`SPACES_AND_COMMA`
→ `153 909`, `DOTS_AND_COMMA` → `153.909`).
- `nx typecheck twenty-front`, oxlint and oxfmt on the diff all pass.
> Note: two i18n strings change placeholder shape (`{count} selected` →
`{0} selected`); a `lingui:extract` will refresh the catalogs (runtime
falls back to source text meanwhile).
https://claude.ai/code/session_013XNL2Xa11Bw7fsnPFQgsGX
---
_Generated by [Claude
Code](https://claude.ai/code/session_013XNL2Xa11Bw7fsnPFQgsGX)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21894?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. -->
|
||
|
|
23f5ba9ebf |
feat: add resizable kanban column width (#21828)
## What & why Lets users resize the columns of a Kanban (record board) view. Requested by a user; the design avoids the "ragged board" problem by making the width a **single shared value**. ## Behaviour - A drag handle appears on the right edge of every column header. - Because all columns read **one** width value, dragging any handle resizes **every** column together — they can never end up mismatched. - Width is clamped between **150px** and **400px** (default **200px**). - The width is **persisted per view** and restored on reload. ## Approach **Backend** — a new nullable `View.kanbanColumnWidth` field, threaded through the existing view-level setting pattern (the same one `kanbanAggregateOperation` / `shouldHideEmptyGroups` use), so it gets create/update/manifest/override support for free: - entity column + `ViewOverrides` + `@WasIntroducedInUpgrade` - `CreateViewInput` / `UpdateViewInput` (`Int`, `@Min(150)`/`@Max(400)`) + `ViewDTO` - flat-view editable properties, entity-properties config, compare-type, standard-view + manifest converters - a fast instance command adding the `core.view` column **Frontend** — the value hydrates into a view-scoped atom and drives a single CSS variable set on the board container, which both column headers and bodies read. Live dragging only writes that CSS variable (no per-move React re-render); the final width is committed to the atom and persisted via `updateView` on pointer-up. ## Nullability / defaults `kanbanColumnWidth` is nullable — `null` means "never resized" and the UI falls back to the 200px default, so existing rows need no backfill. ## Validation - `nx typecheck twenty-server` ✅ and `nx typecheck twenty-front` ✅ - `nx lint:diff-with-main twenty-server` ✅; frontend lint fixes applied (split constants to one-per-file, removed `useRef`-for-state in favour of `useState`). - Draft pending a final green CI run (the dev container reclaimed `node_modules` mid-session; re-running locally). ## Test plan - [ ] Drag a kanban column edge → all columns resize together, clamped 150–400px - [ ] Reload → width persists for that view; other views unaffected - [ ] A view that was never resized still renders at 200px https://claude.ai/code/session_016Qe6oDBkhVbrq2QkXBJ5nE --- _Generated by [Claude Code](https://claude.ai/code/session_016Qe6oDBkhVbrq2QkXBJ5nE)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21828?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: Claude <noreply@anthropic.com> |
||
|
|
34a2037b65 |
fix(front): keep record table footer visible below information banner (#21852)
## Context Fixes #21765. <img width="1439" height="961" alt="Capture d’écran 2026-06-19 à 15 03 46" src="https://github.com/user-attachments/assets/07104099-19de-45f9-9cca-eaaacbc326af" /> When a page-level information banner is visible on a record table (e.g. the mailbox **"Sync lost with mailbox … Please reconnect"** banner), the table footer / bottom edge was hidden behind the card boundary. As a side effect, drag-select **auto-scroll never triggered** near the bottom, because the cursor could not reach the scroll wrapper's real bottom edge. ## Root cause In `PageCardLayout`, the `InformationBannerWrapper` and the page children are siblings in a flex column. The record index child (`StyledIndexContainer`) used `height: 100%`, so it demanded the **full** body height regardless of the banner. With a banner present, banner height + 100% exceeded the card, and since the container's content (the table) has a large min-content height it would not shrink — so the bottom (the footer) was pushed past `StyledCard`'s `overflow: hidden` and clipped. `useDragSelectWithAutoScroll` only scrolls when the cursor is within `AUTO_SCROLL_EDGE_THRESHOLD_PX` (20px) of `containerRect.bottom`. With the bottom edge clipped off-screen, that zone was unreachable, so auto-scroll appeared broken. ## Fix Replace `height: 100%` with `flex: 1; min-height: 0;` so the container takes the space **remaining** after the banner — the same flex idiom its parent `StyledBodyContent` already uses. When no banner is shown, the banner wrapper collapses to `height: 0`, so the table fills the full height exactly as before (no behaviour change in the common case). ## Testing - Verified locally: with the mailbox reconnect banner forced visible on the Companies table, the footer (aggregate row) stays visible and drag-select auto-scroll reaches the bottom. - No change when no banner is present. This is a layout fix, not a drag-select threshold change — as suggested in the issue, raising the threshold would only mask the layout problem. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21852?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. --> |
||
|
|
a59ed80422 |
fix(front): allow null subfields in Phones default value so Save enables (#21847)
## Problem Closes #21780. When editing a **Phones** field in Settings → Data Model, changing the **Default Country Code** does not enable the Save button — the form becomes dirty but never valid, so the change can't be saved. ## Root cause The settings form validates `defaultValue` with the record-value `phonesFieldValueSchema`, which requires non-null strings: ```ts primaryPhoneNumber: z.string(), primaryPhoneCountryCode: z.string(), ``` But a Phones default value can legitimately have **null** subfields — a default country code with no default number. The backend normalizes empty subfields to `null` (`nullify-empty-phones-default-value.util.ts`), and the shared contract `FieldMetadataDefaultValuePhones` is `string | null`. So an existing field whose stored default has `primaryPhoneNumber: null` makes the form **permanently invalid**: changing the country code preserves the null number → `isValid` stays `false` → `canSave = isDirty && isValid` keeps Save disabled. The sibling **address** field doesn't have this bug because `addressFieldValueSchema` already makes every subfield `.nullable()`. Phones was simply inconsistent. ## Fix Add a dedicated `phonesFieldDefaultValueSchema` with nullable subfields (mirroring the address pattern and matching `FieldMetadataDefaultValuePhones`) and use it in the Phones settings form. The stricter record-value `phonesFieldValueSchema` is left untouched, so record input/persistence/empty-checks are unaffected. ## Test plan - [x] Unit test covering the partial-null default value (and asserting the record-value schema still rejects it) - [x] `nx typecheck twenty-front` clean - [x] `nx lint:diff-with-main twenty-front` clean - Manual: open a Phones field, set a Default Country Code and save, re-open, change the country code → Save now enables. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21847?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: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
1e744a761d |
fix: reorder table columns against visible fields only (#20940) (#21084)
## Fixes #20940 ### Problem The "Move Left" / "Move Right" actions in the table column header menu were unreliable. Clicking them often produced no visible change, or appeared to move the column an inconsistent number of positions. ### Root cause `useMoveRecordField` computed the swap target from **all** record fields (`currentRecordFieldsComponentState`) sorted by position — including hidden and non-readable columns. As a result, a move frequently swapped positions with an *invisible* neighbor, leaving the visible column order unchanged. This was also inconsistent with the drag-and-drop reorder path (`useReorderVisibleRecordFields`), which already operates only on the visible field set, and with the dropdown's own Move enable/disable logic, which is based on `visibleRecordFields`. ### Fix `useMoveRecordField` now sources the neighbor from `visibleRecordFieldsComponentSelector` — the same selector that drives the table display and the Move menu items (`isVisible && isReadable && isActive`, sorted by position). The real `position` values are still swapped, so hidden columns keep their positions and only the visible order changes. ### Tests Added `useMoveRecordField.test.tsx`, which seeds real object metadata with a hidden column interleaved between visible ones (by position) and asserts that the visible selector reorders correctly after a move. The test fails against the previous implementation and passes with this change. ### How to verify 1. Open any table view. 2. Open a column header menu and click "Move Right" / "Move Left". 3. The column now moves reliably by one visible position each click, regardless of hidden columns. --------- Co-authored-by: Harsh Singh <harsh@Harshs-MacBook-Air.local> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
3675f264f1 |
Infer record pickers for record-typed logic function workflow inputs (#21494)
## Context Logic functions can declare workflow inputs typed as records or arrays of records (e.g. the People Data Labs enrichment functions), but the workflow builder rendered those as a plain text input with a variable picker, which is not usable. ## What this does - Adds an `objectUniversalIdentifier` link on input schema properties, so a record-typed input is tied to a workspace object. - The SDK build infers it from a `TwentyRecord<'objectUniversalIdentifier'>` marker type in the handler signature, reading the object's universal identifier straight from the source; explicit input schemas can still set the field directly. - The workflow builder renders these inputs as a single record picker or a record multi-select with the variable picker on the right. Selected records are stored as record ids; `TwentyRecord<UID>` is a branded `string`, so the handler signature reflects that it receives ids (a bound variable resolves to whatever the referenced step produced). - The multi-select collapses overflowing chips into a `+N` badge (reusing `ExpandableList`) and its variable picker offers both record objects and fields. - Updates the People Data Labs enrichment inputs as the reference implementation. <img width="802" height="824" alt="CleanShot 2026-06-12 at 16 54 10@2x" src="https://github.com/user-attachments/assets/a0896d74-0aab-49bd-a173-14c578a2e533" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21494?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. --> |
||
|
|
1b3d933900 |
fix(workflow): hide empty option for non-nullable select fields (#21075)
## Summary Closes #20139. In a workflow **Create / Update / Upsert Record** action, the field input for a `SELECT` field always offered a **"No value"** option. Selecting it clears the field — even when that field is **not nullable** (for example an opportunity `Stage`). The maintainer asked in the issue to *"not allow to put a 'No stage' if the field is not nullable"*. ### Root cause `FormSelectFieldInput` unconditionally builds a `defaultEmptyOption` and passes it as the `<Select>`'s `emptyOption`, regardless of the field's nullability: ```tsx const defaultEmptyOption = { label: label ? t`No ${label}` : t`No value`, value: '', icon: IconCircleOff, }; // ... <Select ... emptyOption={defaultEmptyOption} /> ``` The nullability is already available on the field metadata (`FieldSelectMetadata.isNullable`), and the regular (non-workflow) record input already gates its clear affordance on it in `SelectFieldInput.tsx` (`fieldDefinition.metadata.isNullable && canSelectEmpty ? handleClearField : undefined`). The workflow form input just wasn't using it. ### Fix - Add an optional `isNullable` prop to `FormSelectFieldInput` and only build the empty option when the field is nullable (`isNullable !== false`). `emptyOption` is already optional on `<Select>`, so omitting it cleanly removes the choice. - Pass `isNullable={field.metadata?.isNullable}` from `FormFieldInput`'s `isFieldSelect` branch. The prop is optional and defaults to the previous behaviour, so the other `FormSelectFieldInput` call sites (country, calling code, currency, cron, etc.) are unaffected. Scope is intentionally limited to single `SELECT`: `FieldMultiSelectMetadata` has no `isNullable` field and the multi-select input has no equivalent empty option, so it is left untouched. ## Test plan - [x] `npx oxlint --type-aware` — passes on the changed files - [x] `npx oxfmt --check` — passes - [x] `tsc --noEmit` — no type errors in the changed files - [x] Added a `NonNullable` Storybook interaction test asserting the "No value" option is not rendered when `isNullable={false}` - [ ] Manual: in a workflow Update Record step on Opportunity, the **Stage** field no longer shows a "No value" option; a nullable select still does --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
de9af38a67 |
Fix view type label casing (#21772)
## Summary Fix the view type label shown in the object Options menu by introducing a shared `ViewType` label map instead of formatting raw enum values at each call site. I chose to fix the root cause instead of only patching the symptom: the menu was displaying the generated enum value `TABLE`, and `capitalize()` only uppercased the first character without lowercasing the rest. The new mapping gives each view type an explicit translated UI label, so the parent Options menu, the Layout submenu, the view picker, and application content rows all use the same casing source. ## Before / After | Before | After | | --- | --- | | The Options menu showed `TABLE` in uppercase. | The Options menu now shows `Table`, and the Layout submenu still shows `Table`, `Calendar`, and `Kanban`. | |  |  | ## Tests - `git diff --check` - Browser smoke test on `http://apple.localhost:3001/objects/companies` - default view Options menu still opens - custom view Options menu shows `Layout` contextual text as `Table`, not `TABLE` - Layout submenu still shows `Table`, `Calendar`, and `Kanban` - no browser console errors Not run: package lint/test commands, because this checkout has no `node_modules` installed. |
||
|
|
40e386bfb4 |
Fix(record table): Drag select position (#21579)
Fixes: #21578 https://github.com/user-attachments/assets/8a1f3aa1-b436-4ce0-a9ec-c49ab61e214c <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21579?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: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
9f30915f6f |
fix(metadata): remove deprecated isCustom from Objects and Fields (#21799)
## Context Follow-up to #21228, which deprecated `isCustom` on object/field metadata but kept it exposed because the frontend still relied on it. This removes it from the GraphQL API and the frontend entirely. ## Implementation ### Server - Remove `isCustom` `@Field` from the `Object`, `Field`, and `MinimalObjectMetadata` GraphQL types - Remove the `isCustom` `@ResolveField` resolvers and the `isCustomLoader` dataloader (+ payload/interface) - Remove `isCustom` as an internal `@HideField()` on the Object/Field DTOs used by the i18n standard-override gate > Use an explicit isStandard instead (which is the correct gating) ### Frontend - Add `getIsMetadataItemCustom` helper + `useGetIsMetadataItemCustom` hook: an item is custom when `applicationId === currentWorkspace.workspaceCustomApplication.id` - Migrate all consumers off `objectMetadataItem.isCustom` / `fieldMetadataItem.isCustom`; `isRecordFieldReadOnly` now takes a precomputed `isFieldCustom` - Drop `isCustom` from the metadata fragment/mutations/minimal query, FE types, zod schemas, and mock generators; regenerate GraphQL types ## Notes - Breaking change on the (already-deprecated) `Object.isCustom` / `Field.isCustom` GraphQL fields and the `isCustom` filter - FE semantic is "belongs to the workspace custom app" (third-party-app objects/fields are treated as non-custom) - `isCustom` on IndexMetadata / View / Skill / Agent is a separate column and is untouched - Breaking changes on REST metadata API |
||
|
|
9bc0db5666 |
fix: exclude non-groupBy date fields (deletedAt) from calendar field selection (#21764)
Closes #21608 The Tasks Calendar renders an empty grid because `GroupByTasks` fails with `Field "deletedAt" is not supported in groupBy` while the header count (`AggregateTasks`) still succeeds. The calendar renders by grouping records on the selected date field. Calendar-field eligibility only checked `isFieldMetadataDateKind`, so `deletedAt` (a system DATE_TIME field) could be picked or auto-defaulted as the calendar field — and the groupBy engine correctly rejects it (only `createdAt`/`updatedAt` are groupable system date fields). Fix: gate calendar-field eligibility on `isFieldMetadataSupportedInGroupBy` (the same authority the backend groupBy validator uses), so non-groupable date fields can no longer be selected. - `useGetAvailableFieldsForCalendar` — add the groupBy-support check alongside the date-kind filter - `ObjectOptionsDropdownCalendarFieldsContent` — reuse the hook's list instead of re-filtering raw fields |
||
|
|
8034c7725f |
Reorganize twenty-ui into best-practice component domains and per-component folders (#21745)
Reorganizes `twenty-ui`'s component organization to follow how the best
UI libraries (MUI, Mantine, Base UI, Polaris) structure their source,
now that the package has stabilized.
**Taxonomy** — dissolves the meaningless `components/` junk-drawer and
the 107-file `display/` mega-category. New domains/subpaths:
`data-display`, `typography`, `icon`, `surfaces`; `feedback` and
`layout` absorb the rest (banners/callout/info + placeholders →
feedback; modal/card → surfaces; motion + separators → layout).
**Per-component layout** — every component is now
`<domain>/<ComponentName>/<ComponentName>.tsx` with colocated
styles/stories/types, `internal/` for private helpers and `parts/` for
re-exported compound sub-parts. The redundant inner `/components/` is
gone. `icon` and `json-visualizer` are kept as cohesive subsystems.
**Also:** adds a tree-shakeable root barrel (`import { Button } from
'twenty-ui'`), the generator now owns `individual-entry.ts`, and a real
barrel-leak bug is fixed (private `internals/` parts were leaking into
the public API).
Consumer imports (~1.2k files) and the `twenty-sdk` UI aggregator were
updated by codemod. The change is **export-neutral** except 16
intentionally-removed private internals symbols (all verified
unconsumed). Gates green: typecheck, lint, build, size-limit, storybook.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21745?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. -->
|
||
|
|
102c530d0f |
Add limit on view widget (#21718)
<img width="1345" height="463" alt="image" src="https://github.com/user-attachments/assets/a5d9ac2f-6375-4956-895d-3675aa9bebc1" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21718?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: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
8130fa1c45 |
feat(workflow): use workspace member as variable sender for emails (#21582)
## Summary
Lets the email workflow node sender be driven by a variable: the
connected-account field accepts a `{{variable}}`, and the backend
resolves it to a connected account at run time.
<img width="436" height="195" alt="Capture d’écran 2026-06-16 à 11 59
27"
src="https://github.com/user-attachments/assets/18eee21e-aed6-4447-9bf4-5cb0e2cfc371"
/>
### Email sender by variable
- The connected-account field now accepts a `{{variable}}` via the
variable picker (uses `FormSelectFieldInput` with
`WorkflowVariablePicker`), with a hint to pick a connected account or
set a workspace member as a variable.
- The email workflow action resolves the stored sender value explicitly:
if it is a `workspaceMemberId` (a UUID matching a workspace member), it
resolves that member's first connected account; otherwise the value is
used directly as a `connectedAccountId`.
- Resolution lives in `EmailWorkflowActionBase` and applies to both
`SEND_EMAIL` and `DRAFT_EMAIL`. If a matching member has no connected
account, the run fails fast with a clear message (no silent fallback).
- `DRAFT_EMAIL` also fails fast when the resolved connected account is
missing the required OAuth scopes (`gmail.compose` / `Mail.Send`), via a
server-side `getMissingDraftEmailScopes` util that mirrors the front-end
check.
- Existing workflows with a hardcoded `connectedAccountId` keep working
unchanged (no migration needed).
> Note: exposing the running workspace member as a manual-trigger
variable (`_metadata.workspaceMemberId`) is split into a follow-up PR.
## Test plan
- [x] Backend unit tests for `draft-email-tool`,
`get-missing-draft-email-scopes`, and the `send-email` / `draft-email`
workflow actions (incl. workspace-member sender resolution)
- [x] Lints clean on all changed files
- [x] Manual: configure an email node with a workspace-member variable
sender and confirm it resolves and drafts/sends
- [x] Manual: confirm a member lacking compose permission fails the run
with the permission message
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21582?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. -->
---
### Update — scoped to Draft Email only
The sender variable picker is now exposed **only on the Draft Email
node**. The Send Email node keeps a plain account select (no variable
picker, no variable hint) until we enable it there in a follow-up.
Backend resolution still lives in `EmailWorkflowActionBase` and remains
generic, so enabling the picker for Send Email later requires no backend
change.
|
||
|
|
9c9c34fccf |
Remove twenty-ui-deprecated and migrate frontend to twenty-ui (#21596)
Migrates `twenty-front`, `twenty-sdk`, and `twenty-front-component-renderer` from `twenty-ui-deprecated` to `twenty-ui` (mechanical import swap — the packages have API parity) and deletes the deprecated package along with its workspace/CI/config wiring. Also adds `@linaria/react`/`@linaria/core` as direct deps of `twenty-front` (it used them transitively via the deprecated package). Note: move the required status check from `ci-ui-status-check` to `ci-new-ui-status-check`. Argos: the Storybook box-model/button-reset baseline shift (the bulk of the visual diffs) is isolated in #21665 — Storybook now loads twenty-ui's global `reset.scss`, which the production app already ships. Once #21665 merges and this branch is rebased, the remaining Argos diffs are component-level visual-parity items only. |
||
|
|
14d8105f22 |
fix(front): remove runtime default-view creation fallback (#21652)
## Problem `useCreateDefaultViewForObject` was a temporary runtime fallback that created a view + one view field per field (each with a fresh `v4()` id) whenever `RecordIndexLoadBaseOnContextStoreEffect` found no view for the current view id. Because the created view got a fresh id that never matched the requested `contextStoreCurrentViewId`, the next load missed again and re-created another duplicate — leaking `core.view` / `core.viewField` rows without bound (notably during the 2.13.0 cache-first bootstrap window). #21592 made the fallback idempotent as a stop-gap, but the mechanism is no longer needed at all: standard/index views are created server-side at object creation and during standard app installation, so the client never needs to mint them. ## Change Remove the fallback entirely: - Delete `useCreateDefaultViewForObject`. - In `RecordIndexLoadBaseOnContextStoreEffect`, when no view resolves for the current id, do nothing and let the loaded views settle (the effect re-runs once the view is present and loads it). ## Note This removes the leak at the source for any client running the new bundle. Clients still on old cached JS will keep creating duplicates until they reload; the already-leaked rows are being cleaned up separately via SQL. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21652?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. --> |
||
|
|
504eaa5600 |
Fix relation-traversal filters showing no options on select fields (#21616)
Relation-traversal filters (e.g. *Company → Industry*) on `SELECT`/`MULTI_SELECT` fields rendered an empty option picker, so the filter couldn't be configured. The value inputs resolved the select options from the **source relation field** (which has no options) instead of the **relation target field**. Fixed in both filter UIs: - Dashboard chart filters and workflow "Find Records" (`AdvancedFilterSidePanelValueFormInput`) - Record-index simple & advanced filters and the role-permission filter builder (`ObjectFilterDropdownOptionSelect`) Both now resolve the value-input field and its options from `relationTargetFieldMetadataId` when a filter traverses a relation, falling back to the source field otherwise. ## Before <img width="802" height="702" alt="CleanShot 2026-06-15 at 17 27 35@2x" src="https://github.com/user-attachments/assets/032b0875-8a5a-4e09-a2ad-4c4a8a319f49" /> ## After <img width="804" height="796" alt="CleanShot 2026-06-15 at 17 27 05@2x" src="https://github.com/user-attachments/assets/88248409-5781-49c7-aa9e-3502ff8c68e4" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21616?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. --> |
||
|
|
4be76e3fd1 |
Support morph relations in workflow record nodes (#21403)
## Support morph (polymorphic) relations in workflow record nodes
Morph relations (e.g. a polymorphic `Owner` on `Pet` targeting `Person`
or `Company`) were not selectable in the workflow **Create / Update /
Upsert Record** nodes. This PR adds full support for setting them.
### What changed
**Frontend**
- `shouldDisplayFormField`: allow `MORPH_RELATION` (many-to-one) so
morph fields appear in record forms.
- New `FormMorphRelationToOneFieldInput`: a polymorphic record picker
across the morph's target objects, storing a self-describing value `{
targetObjectMetadataId, id }`.
- Wired the morph branch into `FormFieldInput`.
**Backend**
- New `formatWorkflowRecordMorphRelationFields` util: resolves the form
value (stored under the base field name, e.g. `owner`) into the correct
per-target join column (`ownerCompanyId`), nulling siblings to keep
exactly one target referenced.
- Wired into the create / update / upsert workflow actions (update also
expands `fieldsToUpdate` to the concrete join columns).
### Permissions handling
- The picker's search is scoped to only the morph targets the user can
read (`canReadObjectRecords`), so it no longer breaks when a target
object is inaccessible.
- If an existing value points to an object the user can't read, the
field shows the reused **"Not shared"** lock display instead of an empty
field, while remaining editable when other targets are readable.
### Notes
- No data schema / migration changes — reuses the existing per-target
morph columns and stores the selection in the existing workflow step
JSON settings.
<img width="607" height="717" alt="Screenshot 2026-06-10 at 14 57 40"
src="https://github.com/user-attachments/assets/496442a1-04a5-40f8-8b56-b28e38b00d5a"
/>
Also handles the case where the selected record is not readable
<img width="596" height="737" alt="image"
src="https://github.com/user-attachments/assets/c5ffb94e-3838-4db5-853e-f8e490331f23"
/>
|
||
|
|
fb4608e437 |
chore(deps): upgrade Tier-1 deps (googleapis 173, gaxios 7, express 5, jsdom 29, date-fns 4, stripe 20) (#21570)
## What Security-driven upgrade of the biggest-drift Tier-1 dependencies (staying on latest = staying patched). Bundled because they share the lockfile and the googleapis/gaxios pair must move together. | Package | From | To | Gap | |---|---|---|---| | googleapis | 105.0.0 | **173.0.0** | 68 majors | | gaxios | 5.1.3 | **7.1.5** | 2 majors | | express | 4.22.2 | **5.2.1** | 1 major | | jsdom | 26.1.0 | **29.1.1** | 3 majors | | date-fns | 2.30.0 | **4.4.0** | 2 majors | | date-fns-tz | 2.0.0 | **3.2.0** | 1 major | | stripe | 19.3.1 | **20.4.1** | 1 major | `yarn npm audit` reports **0 high/critical** advisories before and after. ## Code changes - **gaxios v7** — `GaxiosError.code` is now `string | number` (guard the calendar network-error check by `typeof`); `GaxiosError` config/response use `URL` + `Headers`; and crucially the v7 constructor drops `response.data` unless `bodyUsed` is set — updated the synthetic gmail error mocks accordingly (production gaxios sets it, so real error parsing is unaffected). - **google-auth-library / gaxios dedup** — `googleapis-common@8.0.2` exact-pins `google-auth-library@10.5.0` + `gaxios@7.1.3` while `googleapis` pulls `^10.2.0`; the two copies made `OAuth2Client`/`GaxiosError` type-identities diverge across every gmail/calendar service. Added two singleton `resolutions` (documented inline in root `package.json`). - **express 5** — no source changes. `@nestjs/platform-express@11.1.24` already resolves `express@5.2.1` internally; the old `4.22.2` pin was the override. - **jsdom 29** — no source changes, but it now pulls ESM-only transitive deps (`@csstools/*` `.mjs`, `parse5`, `entities`, `tough-cookie`, `@exodus/bytes`). Extended the server jest `transformIgnorePatterns` allowlist and added `.mjs` to the transform/extensions so jest can load jsdom. - **stripe 20** — `Subscription` gained a required `customer_account` field; added to mocks. No runtime changes. - **date-fns v4** — `Locale` is no longer ambient (import explicitly in 5 files); per-locale entrypoints dropped the typed `default` export (the locale loader now reads the single named export); fixed the default locale import in `formatTimeZoneLabel`. ## Tests - Full suites green locally: **twenty-server 5709 passed**, **twenty-front 4937 passed**, twenty-ui / twenty-ui-deprecated green; typecheck + builds (swc + vite) + lint all pass. - Added regression tests for the two runtime behaviors these upgrades touch and that had no coverage: - `getDateFnsLocale` — named-export locale resolution (date-fns v4). - `sanitizeFile` — jsdom 29 + DOMPurify still strips `<script>`/event handlers from uploaded SVGs (security guard). ## Deliberately deferred (not in this PR) - **stripe → 21/22**: stripe **21** bundles a runtime `Decimal` type for money fields **and** jumps the pinned API version to `2026-03-25.dahlia` (changes webhook/billing payload behavior) — too risky to fold into a deps bump on billing code. stripe **22** additionally drops the node10-resolvable `types` entry, which would force a repo-wide `moduleResolution` change. Capped at the latest clean **20.x**. - **openid-client → 6**: v6 is a full functional rewrite and its passport strategy manages the OAuth `state` internally, but our SSO flow uses `state` to carry `identityProviderId` across the shared `/auth/oidc/callback`. That needs an auth-flow redesign (session-carried provider id) on Enterprise SSO code with no integration harness — it deserves its own focused PR rather than riding along here. ## Tier-1 source Originated from a dependency-drift audit; remaining Tier-1 items (date-fns done here) plus Tier-2/3 follow-ups tracked separately. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21570?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. --> |
||
|
|
53de4c557b |
Fix record index sync when view fields arrive via SSE (#19069)
Fixes #19023 ## What changed This updates the record index/view field state flow so the current view can react to late-arriving `viewFields` coming from SSE without requiring a page refresh. Changes: - extracted a narrower `syncRecordIndexViewFields` path in `useLoadRecordIndexStates` - kept the initial full record-index load for first entry into a view - added a follow-up sync in `RecordIndexLoadBaseOnContextStoreEffect` when the same view receives updated `viewFields` - updated `ViewBarRecordFieldEffect` so it re-syncs current record fields when `currentView.viewFields` changes instead of only initializing once ## Why There is a race when a user navigates to a custom object while AI is still creating metadata. In that case, the record index can initialize from a partial view, and later SSE `viewFields` updates were not being applied to the active view state. That could leave the table visually empty or incomplete until a refresh. ## Impact This should allow: - record index columns to update live when view fields arrive via SSE - view bar field state to update live as well - the current view to stay usable without a refresh while AI-created metadata is still streaming in ## Validation Validated locally with: - `npx prettier --check` on modified files - `npx oxlint --type-aware` on modified files Manual verification: - confirmed live record creation appeared without refresh - manual AI/SSE testing was partially limited by Groq TPM/token caps on the selected model, but the state-sync path was verified in code and local behavior checks <img width="3024" height="1964" alt="image" src="https://github.com/user-attachments/assets/f71c7490-bf57-4357-9d5f-087b2424b53b" /> --------- Co-authored-by: Charles Bochet <charles@twenty.com> |