573fd00ea7ffc6935a191ebf80d102033dfdd45e
10661 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
573fd00ea7 |
feat(workflow): add Pick Record action (1/3 — random selection) (#21899)
## Overview
Adds a new workflow action, **Pick Record**, that selects **one** record
from a configured candidate pool and exposes the chosen record as the
step's output. Downstream steps can then reference it through the normal
variable picker — e.g. assign an owner in an _Update Record_ step by
setting **Account Owner = `{{step.<pickRecordId>.id}}`**.
This is the foundation for building **assignment workflows**
(round-robin / load-balanced owner assignment, reviewer rotation, etc.)
in Twenty.
## This is PR 1 of a 3-PR stack
| PR | Strategy | Adds |
|----|----------|------|
| **1 (this one)** | `RANDOM` | The whole `PICK_RECORD` action,
end-to-end, stateless |
| 2 | `ROUND_ROBIN` | A persistent, atomically-incremented per-step
cursor + the strategy selector UI |
| 3 | `LOAD_BALANCED` | "fewest related records wins" via an aggregate
count |
Each PR widens the `strategy` enum (a backward-compatible change), so no
data migration is needed between them.
## How it works
- **Editor**: pick an Object, then pick the candidate records (a
multi-record selector). A random record is selected from that pool at
run time.
- **Output**: a single record of the chosen object — the same output
shape as `CREATE_RECORD`/`UPDATE_RECORD` — so it drills into
`{{step.x.id}}`, `{{step.x.name}}`, … in the variable picker.
- **Execution**: reuses `FindRecordsService` to fetch the pool (`id IN
(recordIds)`, which also transparently drops any deleted candidates),
then returns one at random.
## Design decisions & tradeoffs
1. **Standalone step that outputs a variable, not an inline "random"
mode on the relation field.** This mirrors Attio's round-robin block.
The decisive reason is composition: the chosen record is almost always
reused (assign owner **and** create a follow-up task for them **and**
email them). A variable is chosen once and reused everywhere; an inline
per-field value would re-roll independently in each place. It also keeps
the (stateful) round-robin/load-balanced logic out of the field inputs.
Tradeoff: one extra step to wire up vs. an inline control — accepted for
the composability win. An inline "Assign automatically" entry point can
still be layered on later as sugar that inserts this step.
2. **Co-located in the `record-crud` action module and reuses
`FindRecordsService`.** Avoids duplicating module wiring (auth context,
permissions, object-metadata resolution) and the data-access path.
Tradeoff: "Pick" is a selection rather than a CRUD op, so the folder
name is slightly broad; chose reuse + low risk over a separate module.
Can be extracted if the family grows.
3. **`strategy` exists in the schema (defaulted `RANDOM`) but the
selector is hidden in this PR.** A dropdown with a single option would
be UX slop, and adding the field only in PR 2 would force a data
backfill for any `PICK_RECORD` steps created in between. Keeping the
field now (hidden) avoids both. PR 2 introduces the selector once
there's a real choice.
4. **Pool is an explicit static list (`recordIds`) for v1.** Matches the
most common assignment case ("rotate among these N people") and reuses
the existing `FormMultiRecordPicker`. A filter-based pool (reusing the
Find Records filter UI) and a list-from-a-previous-step pool are natural
follow-ups, intentionally out of scope here to keep the stack focused on
the three strategies.
5. **Output schema is computed on the frontend** (like `CREATE_RECORD`),
derived from `input.objectName` — so it is **not** added to
`PERSISTED_OUTPUT_SCHEMA_TYPES` and needs no server-side schema
computation.
6. **Validation**: `PICK_RECORD` is added to object-name metadata
validation (so a deleted/invalid target object is flagged) via a
dedicated `OBJECT_TARGETING_ACTION_TYPES` set — deliberately **not** to
`VARIABLE_CONSUMING_ACTION_TYPES`, because a static pool legitimately
references no upstream variable and would otherwise raise a spurious "no
variable reference" warning.
7. **Empty pool → step error** at run time (respecting the step's
error-handling options) rather than a silent no-op, since an empty pool
is a misconfiguration or fully-deleted set.
8. **`Math.random`** is used for selection — no cryptographic guarantee
is needed for assignment fairness.
## Testing
Per our testing convention (integration test over service/`.spec`
tests): added `pick-record-workflow.integration-spec.ts`, which builds a
workflow with a manual trigger + a `PICK_RECORD` step, configures a
known two-record pool, runs it, and asserts the run completes and the
picked record is **always** within the configured pool (verifying the
pool filter) across repeated runs.
Local verification (typecheck + lint for shared/server/front) is green;
running the integration suite and attaching editor screenshots in a
follow-up comment.
## Follow-ups
- PR 2: `ROUND_ROBIN` + persistent atomic cursor (Redis `incrBy` vs. a
Postgres counter table — tradeoff to be documented on that PR) +
strategy selector.
- PR 3: `LOAD_BALANCED`.
- Later (not in this stack): filter-based / variable-list pools, an
inline "Assign automatically" entry point on relation fields, OOO-skip /
weighting.
https://claude.ai/code/session_01MuPWZsqf2bmQSevk6fRbX8
---
_Generated by [Claude
Code](https://claude.ai/code/session_01MuPWZsqf2bmQSevk6fRbX8)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21899?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. -->
|
||
|
|
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.
|
||
|
|
a0689d1577 |
feat(workflow): condition filter on database-event triggers (#21868)
## Problem
Connecting a mailbox bulk-creates contacts via the email/calendar sync,
and each `person.upserted` fires the seeded **"Create company when
adding a new person"** workflow. The trigger enqueues one run per record
(no batching) and each run bills several `WORKFLOW_NODE_RUN` events — so
a single mailbox connect can rack up tens of thousands of runs and
exhaust credits on a brand-new workspace. The workflow is also redundant
on that path: the sync already creates the company from the email domain
and links the person to it.
## What this does
Adds an optional, user-defined **filter** to database-event (listener)
triggers, evaluated in the listener **before a run is enqueued**.
Non-matching events never create a run, so they consume zero execution
credits. This is the Filter node's capability, lifted to the trigger
level, and available for all event types (created / updated / upserted /
deleted).
The seeded "Create company when adding a new person" workflow now
carries a visible trigger filter — `Created by → Source is not Email`
**and** `is not Calendar` — so it no longer runs for sync-created
contacts, while still running for manually / API / CSV-added people.
## How (reuse)
- **Backend:** extracted `evaluateStepFilters()`, shared by the Filter
action and the trigger listener's new `eventMatchesRecordFilter` gate.
The record is exposed under the `trigger` key so filters reference it
exactly like steps do (`{{trigger.properties.after.…}}`).
- **Shared:** one optional `filter` added to the database-event trigger
zod schema; the front-end type derives from it (settings stay JSON — no
codegen).
- **Frontend:** extracted `WorkflowStepFilterBuilder` from the Filter
action's body; both the Filter action and the trigger editor render it.
The field picker needed no changes — at the trigger it already resolves
to the record's own fields via `TRIGGER_STEP_ID`.
## Scope / decisions
- **No migration for existing workspaces** (by request) — only newly
created workspaces get the filtered default; already-created workspaces
keep the always-on workflow.
- Deliberately did **not** add relation-enrichment to the upsert path
(it would add a DB lookup to the very bulk-sync path we're relieving).
Trigger filters work on the record's own scalar/composite fields (e.g.
`createdBy.source`); relation-based filters work on created/updated
where enrichment already runs.
## Verification
- Typecheck: `twenty-shared`, `twenty-server`, `twenty-front` all green.
- Lint (diff, autofix): 0 warnings / 0 errors across all three.
- Unit tests: a new `evaluate-step-filters` spec exercising the exact
`createdBy.source IS_NOT` seed mechanism, plus new listener specs
proving non-matching events are not enqueued. All backend
filter/listener suites pass.
- Not run here: integration tests (need a DB) and Storybook.
https://claude.ai/code/session_013k36vfekDppwRCgM6Ha7De
---
_Generated by [Claude
Code](https://claude.ai/code/session_013k36vfekDppwRCgM6Ha7De)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21868?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>
|
||
|
|
f8db73598c |
Fix dangling relation fields crashing records after deleting a custom object (#21874)
Fixes https://github.com/twentyhq/twenty/issues/21706 ## Context Deleting a custom object that has relation/junction fields pointing to it (e.g. a junction object linked from Person and Company) crashes record pages with `Target object metadata item not found for <field>`. The backend cascade correctly deletes the related relation fields, view fields and page-layout widgets, but the frontend metadata store only removed the deleted object itself, leaving dangling relation fields (and stale UI-layer references) behind. ## Fix After a successful deletion, `useDeleteOneObjectMetadataItem` now calls `invalidateMetadataStore()`, triggering the existing reconcile path that refetches objects, fields, indexes, views, view fields and page-layout widgets. This removes the dangling relations and cleans up the UI layers in one consistent pass (also replacing the previous manual command-menu refetch). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21874?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. --> |
||
|
|
544c89119c |
fix: hide restricted objects and views nested in navigation folders (#21914)
Closes #20141 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21914?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. --> |
||
|
|
602acf7a16 |
fix(twenty-website): fill feature-card visual frame on wide viewports (#21876)
## Problem On the home feature cards, the visual frame is capped at `max-width: 411px` (the scene's design width) and centered. Below ~411px-wide cards this is invisible, but once a card grows past 411px (wider viewports) the dark scene stops filling and the **card's light background shows on both sides** of the visual. At 1200px everything looks correct because the cards are narrower than 411px and the cap is never engaged; the issue only appears as the viewport widens. ## Fix `FeatureCard.tsx`, one file: - **Remove the `max-width: 411px` cap** (and the now-dead `margin: 0 auto`) from `CardImageFrame` so the frame fills the card width at every breakpoint. `useScaleToFit` then scales the 411×508 scene up to match — it's a CSS transform on DOM, so it stays crisp; no raster upscaling. - **Even out the card gutter** — `CardImage` padding `8px → 16px` (top + sides) so the visual's inset matches the content's 16px inset instead of stepping in. Bottom stays `0` (the content block's 16px provides the bottom gutter). The visual scenes themselves are untouched — this is purely the frame/container. ## Before <img width="1477" height="681" alt="image" src="https://github.com/user-attachments/assets/731f1ef7-e761-468e-b7aa-a5a06f8ac790" /> ## After <img width="1473" height="705" alt="image" src="https://github.com/user-attachments/assets/99d036a6-d3ad-4682-99c8-f283b5b95171" /> |
||
|
|
a99d380175 |
feat(website): spotlight visual on top and uniform tile background (#21906)
Two tweaks to the product-feature tiles section: - **Spotlight visual moved to the top** — the spotlight tile now renders its visual above its content, matching the layout of the regular grid cells. - **Uniform tile background** — every tile now uses the same neutral gray background; removed the per-tile `TILE_MUTED` alternating toggle. |
||
|
|
e90fb4b55c |
fix(security): bump dompurify to 3.4.11 (config/hook pollution) (#21905)
## fix(security): bump dompurify to 3.4.11 (config/hook pollution) Resolves [Dependabot Alert #1520](https://github.com/twentyhq/twenty/security/dependabot/1520) and [#1509](https://github.com/twentyhq/twenty/security/dependabot/1509). ### What `dompurify` is affected by: - **Permanent `ALLOWED_ATTR` pollution via `setConfig()`** ([#1520](https://github.com/twentyhq/twenty/security/dependabot/1520), Moderate, `<= 3.4.10`) - **Trusted Types policy survives `clearConfig()`** ([#1509](https://github.com/twentyhq/twenty/security/dependabot/1509), Low, `< 3.4.9`) Both patched in `3.4.11`. Bumps the direct `twenty-server` dep `^3.4.0 -> ^3.4.11`. ### Compatibility Both advisories are about config/hook state pollution via `setConfig`/`clearConfig`/hooks. All four of our call sites use plain `DOMPurify(window).sanitize(...)` with **default config** — no `setConfig`, `clearConfig`, `addHook`, `ALLOWED_ATTR`, or `RETURN_TRUSTED_TYPE` — so we are not on the affected path, and the fix does not change default-`sanitize` behavior. Verification: `typecheck twenty-server` passes; the `prepare-file-for-storage`, `create-html-to-text-converter`, and `email-composer` suites pass (28 tests). ### Verification - `dompurify` resolves to `3.4.11` (no `<= 3.4.10` remains). - Lockfile + single package.json pin change; `yarn install --immutable` passes. |
||
|
|
d74b6aeadf |
fix(security): bump nodemailer to 9.0.1 (raw-option SSRF / file read) (#21903)
## fix(security): bump nodemailer to 9.0.1 (raw-option SSRF / file read) Resolves [Dependabot Alert #1518](https://github.com/twentyhq/twenty/security/dependabot/1518) and [#1519](https://github.com/twentyhq/twenty/security/dependabot/1519). ### What `nodemailer` `<= 9.0.0` lets the message-level `raw` option bypass `disableFileAccess`/`disableUrlAccess`, enabling **arbitrary file read** and **full-response SSRF** in the delivered message ([GHSA advisory](https://github.com/twentyhq/twenty/security/dependabot/1518), High). Patched in `9.0.1`. ### How — direct bump, no resolution - **twenty-server:** `nodemailer ^8.0.5 -> ^9.0.1` (major bump). - **seed-dependencies:** the application-package template `nodemailer ^8.0.5 -> ^9.0.1`; both `DEFAULT_PACKAGE_JSON_CHECKSUM` and `DEFAULT_YARN_LOCK_CHECKSUM` regenerated to match the recomputed seed files (the deps-layer cache key). ### Compatibility — verified nothing breaks It is a major upgrade, so the 9.0 breaking change was checked against the current tree. The only behavior change is **stricter TLS validation when nodemailer fetches remote content** (attachment `href`/`path` URLs, built-in OAuth2 token endpoints, HTTP/HTTPS proxy `CONNECT`). None of those paths are reachable here: - Attachments are passed as **content buffers**, never `path`/`href`. - Gmail OAuth uses **googleapis**, not nodemailer's built-in OAuth2. - No proxy on any transport. - The SMTP socket TLS is governed separately (unchanged). Verification: `typecheck twenty-server` passes (with `@types/nodemailer ^7.0.3`), and the `email-sender`, `gmail-message-outbound`, and `imap-smtp-caldav-connection` suites pass (10 tests). ### Not covered (follow-up) Root alert **#1521** will stay open: `imapflow@1.3.6` exact-pins `nodemailer@8.0.10`. The clean fix is `imapflow 1.4.2` (which pins nodemailer `9.0.1`), but it published 2026-06-19 and is **age-gated until ~2026-06-22** — it will land then as a parent-bump (no resolution). ### Verification - `nodemailer` resolves to `9.0.1` for twenty-server; seed lockfile has `9.0.1`; both seed checksums match the canonical recompute. - `yarn install --immutable` passes. |
||
|
|
7eafbd91c6 |
test(server): make timeline integration test self-seed its data (#21896)
## Problem
`timeline-from-object-record.integration-spec.ts` is flaky depending on
Jest shard composition. Its `beforeAll` scans the dev-seeded people for
one with message threads and one with calendar events, and throws when
none is found:
```
Expected the seeded workspace to contain a person with message threads and calendar events
```
This was observed as a deterministic failure of `server-integration-test
(2)` (failed on re-run too), while the other 15 shards were green.
## Root cause
The suite depends on **mutable shared fixture state** under two fragile
assumptions:
1. **That no sibling suite wiped the seeded people.**
`deleteAllRecords('person')` is a common pattern across the REST/GraphQL
suites — `rest-api-core-find-many`, `rest-api-core-find-one`,
`all-people-resolvers`, `search-resolver`, etc. — each hard-deletes
every person (`DELETE FROM "...".person`) and leaves only its own
handful behind, without restoring the seed. Within a shard, Jest runs
files serially (`maxWorkers: 1`) ordered by file size descending (no
timing cache in CI). `rest-api-core-find-many` (~16 KB, runs 2nd)
executes **before** `timeline-from-object-record` (~12 KB, runs 5th), so
by the time the timeline `beforeAll` runs, only 4 company-linked test
people remain — none with threads or events.
2. **That the seeder's `Math.random` participant assignment** happened
to land a thread and an event on a company-linked person within the
first 100 results — itself non-deterministic across DB resets.
It surfaced now because an unrelated PR added a new integration test
file, which changed the total file set and therefore Jest's shard
distribution, moving `timeline-from-object-record` and
`rest-api-core-find-many` into the **same shard** for the first time. It
is a latent test-isolation issue, not a product regression.
### Reproduced locally
Against a DB where `rest-api-core-find-many` had already run (person
count = 4), the timeline suite fails with the exact CI error; on a
freshly seeded DB it passes. So the failure is purely order/seed
dependent.
## Fix
Make the suite self-contained: in `beforeAll` it now provisions its own
graph via the GraphQL API and tears it down in `afterAll`:
```
company → person → messageThread → message → messageParticipant(personId)
↘ calendarEvent → calendarEventParticipant(personId)
```
The timeline resolvers count threads via `messageThread → messages →
messageParticipants.personId` and events via `calendarEvent →
calendarEventParticipants.personId`, so this graph is sufficient and
minimal. The suite no longer reads any ambient seeded data, making it
independent of execution order and seeding randomness.
## Validation
- Self-seeding suite passes against the **polluted** DB (4 people, no
seeded threads/events) — the exact CI failure condition.
- Idempotent across repeated runs and leaves **no residue** (all
fixtures destroyed in `afterAll`).
- Full `--shard=2/16` run green except a pre-existing environmental
failure (`successful-save-imap-smtp-caldav-account`, fails locally with
no mail server, identical before/after this change).
https://claude.ai/code/session_013k36vfekDppwRCgM6Ha7De
---
_Generated by [Claude
Code](https://claude.ai/code/session_013k36vfekDppwRCgM6Ha7De)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21896?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. -->
|
||
|
|
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. -->
|
||
|
|
2e1da86535 |
i18n - docs translations (#21884)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21884?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
6423c4cd3c |
Add recall io webhook endpoint (#21879)
## Context
Bot-recording integrations (e.g. the Recall.ai meeting bot) receive
webhooks from a third-party provider that delivers **every
tenant's events to a single URL**. Our existing `route-trigger` (`/s/…`)
resolves the workspace from the request host, which can't
work for one shared multi-tenant webhook URL. We need an instance-scoped
ingress that identifies the target workspace from the payload
instead.
## Strategy
Add a new **`ingress-trigger`** logic-function trigger, mirroring
`route-trigger`:
- A public endpoint keyed by the app's identifiers: `POST
/webhooks/ingress/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier`.
- The logic function declares an `ingressTriggerSettings` block in its
manifest describing how to find the workspace in the payload
(`workspaceId: { source: 'body' | 'query' | 'header', path }`).
- Core only **resolves the workspace** (declarative, fail-closed,
prototype-safe path getter), verifies the app is installed in that
workspace, then runs the function **synchronously** so the provider sees
the response (status codes / retries).
- **Signature verification stays in the logic function** (it gets
`rawBody` + forwarded headers), keeping core provider-agnostic.
- Shared execution logic (`build event → execute → map response`)
extracted into `LogicFunctionTriggerService`, now reused by both
`route-trigger` and `ingress-trigger`.
## Major changes
- **twenty-shared**: new `ingressTriggerSettings` on
`LogicFunctionManifest` (`IngressTriggerSettings` type).
- **twenty-server**: new `ingress-trigger` module (controller, service,
exception + filter, workspace-id resolver util).
- **twenty-server**: extracted `LogicFunctionTriggerService` +
`route-trigger-response.util` (response builder + sender); refactored
`RouteTriggerService` and both controllers to reuse them.
- **twenty-docs**: documented the ingress trigger (endpoint, workspace
resolution, signature responsibility, provider HMAC examples).
- Unit tests for the resolver and the ingress service.
|
||
|
|
dd6fbbe854 |
Rename meeting bot app variables from RECALL_BOT_* to MEETING_BOT_* (#21878)
sorry recall :) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21878?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. --> |
||
|
|
c2e3296d85 |
Add transcript tab to calendar record page (#21792)
Adds a read-only "Transcript" tab to the CalendarEvent record page, contributed by the Twenty Meeting Bot app. Renders the diarized transcript stored on `CallRecording.transcript`, with placeholder states for pending and failed transcription. Front-end only — the transcription pipeline (request/download/reconcile + PENDING/FAILED markers) already landed on main. Deferred: live-mount verification via `yarn twenty dev`. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21792?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. --> |
||
|
|
f9084fd208 | Clear stale parent-view filters on front-component cross-object navigation (#21869) | ||
|
|
8899360ebe | fix(security): refresh undici across lockfiles (6.x → 6.27.0, 7.x → 7.28.0) (#21870) | ||
|
|
1578cc2562 |
i18n - docs translations (#21873)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
fecf699bc5 |
Fix broken CSV import grid layout (#21867)
## What Import `react-data-grid/lib/styles.css` in `SpreadsheetImportTable`, the single component that renders the import grid (used by the Validate Data and Select Header steps). ## Why The React 19 migration (#21531) bumped `react-data-grid` from `7.0.0-beta.13` to `7.0.0-beta.59`. The old beta auto-injected its layout CSS; beta.59 ships it as a separate `react-data-grid/lib/styles.css` export that must be imported manually. It was never imported, so the grid lost its base layout (grid template, row heights, cell positioning): rows stacked at full height and columns no longer aligned. The library scopes its styles under `@layer rdg`, so the existing Linaria theme overrides still take precedence. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21867?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. --> ## Before <img width="2540" height="1448" alt="CleanShot 2026-06-19 at 17 43 58@2x" src="https://github.com/user-attachments/assets/a208b518-9088-4988-8245-4fdc4f8bc8de" /> ## After <img width="2454" height="1392" alt="CleanShot 2026-06-19 at 18 02 36@2x" src="https://github.com/user-attachments/assets/e0b30d71-8244-4363-86aa-60b12a2dfdd9" /> |
||
|
|
ee5b3a65f4 |
Workspace migration post transaction commit side effect (#21845)
# Introduction Avoid side effect in transaction to reduce lock duration As discussed we're going to introduce in migration runner side effect later through jobs and metadata boolean state tracker in db <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21845?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. --> |
||
|
|
a98e3a6df9 |
i18n - website translations (#21866)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21866?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
4e64a38e1e |
i18n - docs translations (#21865)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
9695d77252 |
i18n - website translations (#21864)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21864?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
a870e034a6 |
Add twenty slack to internal application ci (#21849)
- adds `twenty-slack` to internal application ci - unify config with twenty-last-contact app - add base oxlint config to show error twenty-shared is used in internal app <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21849?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. --> |
||
|
|
15fd236ad0 |
fix(workflow): add tooltip explaining why the variable picker is disabled (#21862)
## Context Closes #21773 <img width="448" height="301" alt="Capture d’écran 2026-06-19 à 16 33 56" src="https://github.com/user-attachments/assets/4efc637e-3361-4108-86b6-92ffc2e84252" /> When a workflow's variable picker (the `+` button next to a field) is disabled — e.g. on a step whose only trigger is a global manual trigger that produces no record variables — the button just shows a `not-allowed` cursor with no explanation of *why*. ## Change Add an `AppTooltip` to the disabled state of `WorkflowVariablesDropdown` explaining the reason: > No variables are available yet. Variables come from the workflow trigger and previous steps. The disabled state is reached via `disabled === true || noAvailableVariables`. In practice the callers hide the picker entirely in read-only mode (it's rendered only when `!disabled`/`!readonly`), so the meaningful trigger is **no available variables** — hence a single message rather than separate copy per reason. The tooltip is anchored with a `data-*` attribute selector instead of an `#id`, because the picker's `instanceId` comes from React's `useId()` (values like `:r1:`) which are invalid in a CSS `#id` selector that `AppTooltip` runs through `querySelectorAll`. ## Testing - `nx lint:diff-with-main twenty-front` — passes (lint + format). - Verified the component resolves/renders on a local instance running this branch. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21862?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. --> |
||
|
|
973b35989e |
Add standard record page layout for calendar events (#21857)
Moves calendar event details from the bespoke side-panel page to the standard record page layout system. - Adds standard calendar event record page metadata, fields view, widgets, tests, snapshots, and upgrade command for existing workspaces. - Opens calendar events through the generic ViewRecord side-panel path. - Adds participants and call recordings as standard field widgets. - Removes the old custom calendar event side-panel page and related side-panel enum/config entry. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21857?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. --> https://github.com/user-attachments/assets/c1f88cac-1615-478c-a3dd-87d0c61ab9a8 <img width="3024" height="1658" alt="CleanShot 2026-06-19 at 19 01 27@2x" src="https://github.com/user-attachments/assets/c3df5705-ff08-446e-ac3c-6ccb11cf21ec" /> <img width="3024" height="1658" alt="CleanShot 2026-06-19 at 19 01 19@2x" src="https://github.com/user-attachments/assets/633525db-310c-4462-8458-a72068cc1432" /> |
||
|
|
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> |
||
|
|
1f6c2b89fd |
Accessibility guardrails and component hardening for twenty-ui (#21848)
Builds on twenty-ui's existing runtime axe gate by adding a static
enforcement layer and fixing accessibility gaps in shared components.
Color contrast is intentionally out of scope (still deferred via
`A11Y_DEFER_COLOR_CONTRAST`).
## What changed
- **Static guardrails:** enabled oxlint's `jsx-a11y` plugin
(keyboard-operability rules at `error`), and added a custom
`twenty/no-storybook-a11y-disable` rule that blocks `a11y: { test: 'off'
| 'todo' }` so the axe gate can't be silently disabled again.
- **Focus visibility:** wired the existing `focus-ring` mixin into all
buttons for real `:focus-visible` rings (was `outline: none`).
- **Decorative icons:** `aria-hidden` on icons inside labeled buttons
(added to `IconComponentProps` + render sites).
- **Inputs:** accessible-name support on `SearchInput` and `Checkbox`.
- **Interactive components:** `Tag` renders a real `<button>` when
clickable; the non-semantic clickable `div`s (`Avatar`, `Status`,
`ColorSchemeCard`, `NavigationBarItem`, etc.) are now keyboard-operable
via a shared `handleClickableElementKeyDown` helper, role and accessible
name.
## Notes for reviewers
- Two `oxlint-disable` lines remain on genuine non-interactive capture
wrappers (`CodeEditor`, `OverflowingTextWithTooltip`).
- 8 lint warnings remain by design: conditional-interactivity
`no-static-element-interactions` and legitimate `autoFocus` on
`SearchInput`.
- `NavigationBarItem` gained a required `ariaLabel`; its only consumer
(`MobileNavigationBar`) is updated with translated labels.
## Follow-ups (separate PRs)
- Enforced accessible names on icon-only buttons
(`IconButton`/`LightIconButton`) — breaking, ~128 call sites.
- `aria-activedescendant` wiring for the dropdown/listbox keyboard
layer.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21848?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. -->
|
||
|
|
3473410d64 |
fix(website): restore deploymentId, security headers and redirects in next config (#21851)
The rebuilt-site cutover dropped several `next.config.ts` blocks the previous twenty-website had: - **`deploymentId`** — required by `open-next.config` skew protection; deploys were failing with *"Deployment ID should be set in the Next config when skew protection is enabled"*. - **Security headers** (HSTS, CSP `frame-ancestors`, X-Frame-Options, …) + immutable asset `Cache-Control`. - **Redirects** — www→apex canonicalization and content redirects (docs, legal, case-studies, partners, why-twenty). Legacy raw locale-code redirects were intentionally not restored (the site only serves en/fr/es; those URLs never existed for other locales). Pairs with twentyhq/twenty-infra#741, which builds twenty-ui before the worker build. |
||
|
|
8712ae754d |
i18n - docs translations (#21859)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21859?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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. --> |
||
|
|
0758a4fcef |
reset filter search input on field select (#21850)
### Before https://github.com/user-attachments/assets/3e5d2193-c638-4898-a11b-a9a1b9607206 ### After https://github.com/user-attachments/assets/99eab3e2-8475-46dd-915b-0324ada53e5a <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21850?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. --> |
||
|
|
98c35ea804 |
App uninstall lambda, layers cleanup (#21749)
# Introduction On a logic function deletion also remove the driver entry On a app uninstall also remove the sdk layer ( keeps the dep one as it can be shared across several lambda ) | Resource | Scope | Before this PR | After | |---|---|---|---| | DB metadata (functions, objects, fields…) | per-app | deleted | deleted | | Source folder (`FileFolder.Source`) | per-function | deleted | deleted | | Built handler file (`FileFolder.BuiltLogicFunction`) | per-function | deleted | deleted | | **Lambda function** | per-function | **leaked** | **deleted** (driver `delete`) | | **SDK layer** `sdk-<wsId>-<appId>` (all versions) | per-app | **leaked** | **deleted** (driver `deleteApplicationResources` → `deleteSdkLayer`) | | Deps layer `deps-<checksum>` | shared across apps/workspaces | not deleted | **intentionally not deleted** (content-addressed, GC'd) | ## What I don't like about all that Right now there's some non reversible side effect inside the workspace migration transaction - If the transaction fails we're facing data loss - It also slows down everything I'm about to create a new PR that allow population post transaction commit side effect / cleanup to be run later <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21749?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> |
||
|
|
705caab2b0 |
fix(onboarding): refresh stale workspace in currentWorkspace field resolver (#21839)
## What & why
After sign-up, users are redirected to `/sync/emails` and the page loads
forever; a full refresh fixes it. This blocks production deploy.
**Root cause** — a GraphQL field resolver returns an unrefreshed (stale)
workspace:
- `@AuthWorkspace()` (`request.workspace`) is read from the per-instance
core entity cache and can still be `PENDING_CREATION` /
`ONGOING_CREATION` right after `activateWorkspace`.
- The `currentUser` query resolver and the `onboardingStatus` field
already guard against this by calling
`refreshWorkspaceIfPendingOrOngoingCreation(...)`.
- But the `currentWorkspace` `@ResolveField` returned the raw
`@AuthWorkspace()` workspace. Because a field resolver takes precedence
over any value the query resolver attaches to its returned object, the
client receives that stale workspace.
So right after activation the client got an inconsistent payload:
- `onboardingStatus: SYNC_EMAIL` (fresh — computed from a direct DB
read)
- `currentWorkspace.activationStatus: ONGOING/PENDING_CREATION` (stale)
On the frontend, metadata loading is gated on
`isWorkspaceActiveOrSuspended(currentWorkspace)`, and
`MinimalMetadataGater` does **not** exclude `/sync/emails`. So the
workspace looked inactive → metadata never loaded (no metadata GraphQL
request was even issued) → the gater's loader showed indefinitely. A
full refresh worked because the cache had since refreshed to `ACTIVE`.
## Why it surfaces on staging but isn't caught by tests
The stale window only opens on a real fresh sign-up followed by
immediate activation, against a workspace cache that hasn't refreshed
yet (multi-instance / cache TTL). Single-instance local dev and the
existing `successful-user-and-workspace-creation` integration test
exercise `activateWorkspace` + `getCurrentUser` against one consistent
cache, so `currentWorkspace` already looks `ACTIVE` and they pass —
which is why this reproduces on staging/production but not locally, and
why a manual refresh recovers.
## How
Refresh the workspace in the `currentWorkspace` field resolver too, so
it is consistent with `onboardingStatus`. For active workspaces this is
a no-op (no extra DB read).
```ts
async currentWorkspace(@AuthWorkspace({ allowUndefined: true }) workspace) {
if (!isDefined(workspace)) return workspace;
return this.userService.refreshWorkspaceIfPendingOrOngoingCreation(workspace);
}
```
This is preferred over the frontend alternative (excluding
`/sync/emails` from `MinimalMetadataGater`), which would only hide the
symptom while every other consumer still received a wrong
`activationStatus`.
## Verification
- `nx typecheck twenty-server` and `nx lint:diff-with-main
twenty-server` (oxlint + oxfmt) are green.
https://claude.ai/code/session_018c1X6CwDgttMXA5tB797yS
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
88967a6e47 |
Add Update fields select to People Data Labs enrichment functions (#21801)
## What The People Data Labs enrichment logic functions (`enrich-person`, `enrich-company`, `enrich-people`, `enrich-companies`) now expose an **`Update fields`** select instead of the `overrideExistingValues` boolean, and always return the enriched data in their output. `Update fields` options: - **Yes and overwrite**: persist, overwriting existing standard fields - **Yes and don't overwrite** (default): persist, filling standard fields only when empty - **No**: write nothing to the record (no CRM fields, no PDL metadata, no company creation) ## Why The functions previously only persisted data. With `No`, they can now fetch from PDL and return the result without modifying the record, so downstream workflow steps can consume it. Every matched result now carries a `data` object with the mapped record fields (standard + `pdl*` values), and the bulk functions also declare their `results[]` array in the output schema. Billing is unchanged: a successful PDL match is still charged in all modes, since the API cost is incurred regardless of persistence. ## Notes - Default behavior is preserved (unset input means fill-empty + persist). - Typecheck, lint, and the full unit suite (368 tests) pass. |
||
|
|
d19b7f8485 |
Enable getting started translations (#21842)
## Summary The Getting Started pages on the docs site (docs.twenty.com) were only ever available in English, never translated into the other supported languages. **Root cause:** The Getting Started section (added in #19728) was never added to the Crowdin source config (`crowdin-docs.yml`), so its `.mdx` files were never uploaded for translation. Only `user-guide`, `developers`, and `twenty-ui` were configured. This also surfaced a related bug: because the pages had no translations, the navigation generator fell back to the English page path for every language, duplicating paths like `getting-started/introduction` across all 14 language navs. Mintlify treats duplicate cross-language paths as undefined behavior, which broke the language switcher (it always redirected to `/getting-started/introduction`). ## Changes - `.github/crowdin-docs.yml` — add `getting-started/**/*.mdx` as a translation source so the pages get sent to Crowdin. - `packages/twenty-docs/scripts/fix-translated-links.sh` — add `getting-started` link-rewriting rules to match the other sections. - `packages/twenty-docs/scripts/generate-docs-json.ts` — only include a page in a non-default language when its translated file exists; drop empty groups/tabs (removes the duplicate cross-language paths that broke the switcher). - `packages/twenty-docs/docs.json` — regenerated. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21842?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: Cursor <cursoragent@cursor.com> |
||
|
|
f61522b56a |
i18n - docs translations (#21841)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21841?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
0064ff6741 |
fix(ai): validate AI agent output field names against schema-key constraint (#21834)
## Problem
On a self-hosted instance, an AI Agent workflow action fails at run time
with an opaque model error:
```
The model returned the following errors: tools.0.custom.input_schema.properties:
Property keys should match pattern '^[a-zA-Z0-9_.-]{1,64}$'
```
This is Anthropic's validation on tool `input_schema` **property keys**.
An AI Agent's structured **Output** fields are turned into a JSON schema
and passed to the model as a tool; each output **variable name** becomes
a property key. Anthropic rejects any key that does not match
`^[a-zA-Z0-9_.-]{1,64}$` — most commonly a name containing a **space**
(e.g. `meetings brief`), but also names over 64 characters or with other
symbols.
Until now nothing validated this: `fieldsToSchema` writes
`properties[field.name]` verbatim, so a bad name only failed once the
workflow executed, with an error that gives the user no idea what to
fix. It doesn't reproduce on every instance — it depends purely on how
the workflow's output variables happen to be named.
## Fix
Introduce a single shared check,
`isValidAgentResponseSchemaPropertyKey`, and enforce it in two places:
- **Backend** — `validateAgentResponseFormat` now rejects invalid output
field names at agent **save time** with a clear `userFriendlyMessage`,
instead of letting the broken schema reach the model. This also gates
agents created via the API and re-saves of existing bad data.
- **Frontend** — the output schema builder shows an inline error on the
Variable Name field as soon as an invalid name is entered.
## Tests
- Unit test for the shared validity check (valid + invalid cases:
spaces, leading space, empty, > 64 chars, symbols, unicode).
- Unit test for `validateAgentResponseFormat` covering text/json
formats, valid names, a space in a name, an over-length name, and
reporting multiple invalid names at once.
## Notes for the reporter
The immediate unblock for an affected workflow is to rename the output
variable to remove the space (e.g. `meetings brief` → `meetings_brief`)
and retry the run. With this change the bad name is caught up front with
an explanation rather than failing mid-run.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21834?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. -->
|
||
|
|
576f88b5c5 |
Update ci internal applications (#21837)
Fix `twenty-last-contact` ci - add tests - add tsgo - update package commands <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21837?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. --> |
||
|
|
1bd7be36e0 |
fix(front): recompute ExpandableList visible chips on resize (#21139)
## Summary
Relation field cells in the record table render their chips through
`ExpandableList`, which measured how many chips fit only once (during
the
children ref pass) and cached the cutoff. It recomputed on item-count
and
hover changes, but never when the cell's available width changed — so a
cell
measured while narrow stayed stuck on that count even after the column
grew
wider. This is the "only ~3 items shown even when the cell is larger"
bug.
This PR adds a `ResizeObserver` on the outer container that resets the
first
hidden child index whenever the available width changes, so the list
reveals
as many chips as fit (and re-trims when narrowed). The outer container
is
observed because its width tracks the available width independently of
how
many chips are currently rendered, which avoids a
measure → trim → shrink → re-measure feedback loop. The observer is
cleaned
up on unmount.
## Test plan
- [x] Added a Storybook interaction test
(`RecomputesVisibleChipsOnResize`)
that renders the list in a narrow container, widens it, and asserts more
chips become visible.
- [x] Verified the test fails without the fix and passes with it.
- [x] `oxfmt` and `oxlint` pass on the changed files.
- Manual: open a record table with a to-many relation field that has
several
linked records, widen the column, and confirm more chips appear.
Fixes #12039
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
26db3f5735 |
Deprecate legacy encryption (#21831)
# Introduction Still preserving the cross-upgrade flow close https://github.com/twentyhq/core-team-issues/issues/2465 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21831?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. --> |
||
|
|
4de9f45015 |
fix: Edit Layout keeping the command menu open (#21161)
## Summary - Fixes https://github.com/twentyhq/core-team-issues/issues/2460 - Engine/headless commands always skipped closing the menu (`closeSidePanelOnCommandMenuListExecution: false`), even when the item was not pinned. Edit Layout is `isPinned: false`, so the menu should close like other list-only actions. ## Approach - Option 1 (I chose this one): Derive close behavior from `item.isPinned` -> pinned commands keep the menu open; non-pinned ones close it. - Option 2 (not chosen): remove the engine command override totally and use the default close behavior for all commands. Option 1 is more targeted: it fixed Edit Layout without changing pinned commands (e.g. Export progress in the menu list). Option 2 is simpler but widens the blast radius to every engine command clicked from the side panel list. ## Screenshots ### Before https://github.com/user-attachments/assets/70b8dc75-af00-4917-81a1-646381f571d5 ### After https://github.com/user-attachments/assets/f733d696-9330-4e6c-993b-8a8133c53e0d --------- Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
1ec59beb8b |
docs(user-guide): remove unsupported Between operator for Number filters (#21838)
## What The [Filters & Sorting user guide](https://docs.twenty.com/user-guide/views-pipelines/capabilities/filters-and-sorting) lists a **Between** operator for **Number** fields, but it does not exist in the product and we do not plan to implement it. This removes it from the operators table. | Field Type | Before | After | |---|---|---| | Number | Equals, Greater than, Less than, **Between**, Is empty | Equals, Greater than, Less than, Is empty | The **Date** row is intentionally left untouched. ## Why Avoids documenting a capability that does not exist (the same gap that prompted #20932). Closing the external PR that attempted to add it for numbers; the docs should reflect what we actually ship. ## Notes - Only the canonical English source is edited; localized copies under `packages/twenty-docs/l/*` are regenerated by the existing i18n docs-translation workflow. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21838?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. --> |
||
|
|
5674f693d7 |
fix: prevent Create Workspace redirect from being cancelled (#21835)
## Summary
Clicking **Create Workspace** in the multi-workspace dropdown did
nothing.
The handler closed the dropdown right before redirecting:
```ts
const createWorkspace = () => {
closeDropdown(MULTI_WORKSPACE_DROPDOWN_ID); // unmounts this component
redirectToDefaultDomain({ ... }); // schedules window.open ~1ms later
};
```
`redirectToDefaultDomain` → `useRedirect` wraps the navigation in
`useDebouncedCallback(..., 1)`. `closeDropdown` flips the dropdown
content to `{isDropdownOpen && ...}` → `false`, unmounting
`MultiWorkspaceDropdownDefaultComponents` — the component that owns that
debounced callback. `use-debounce` drops the pending call on unmount, so
the queued `window.open` never fires. React commits the unmount before
the 1ms timer, so it loses every time. Regression from #21723, which
added the `closeDropdown` call.
## Fix
Remove the `closeDropdown` call. The redirect navigates the whole page
away, so closing the dropdown first is unnecessary — and it mirrors the
sibling "switch workspace" handler, which already redirects without
closing.
## Why not reorder, or drop the debounce?
The 1ms debounce in `useRedirect` is intentional (#9079, "sleep before
redirect"). Callers set cookie-backed state immediately before
redirecting — e.g. `redirectToDefaultDomain` clears the
`lastAuthenticateWorkspaceDomain` cookie via `useCookieStorage`.
Deferring the hard navigation by one macrotask lets that cookie write
flush before the page tears down; removing it risks dropping the write.
Reordering wouldn't help either, since the unmount still beats the
timer. So the debounce is left untouched.
## Logout is not affected
`signOut` → `clearSession` navigates with `window.location.assign(...)`
directly (synchronous, not debounced) and never calls `closeDropdown`,
so it can't hit this race.
## Testing
- Before: clicking Create Workspace → `window.open` called 0 times, page
unchanged.
- After: navigates to
`<defaultDomain>/welcome?action=create-new-workspace` and renders the
"Create your workspace" form.
- Switch-workspace and Log out both still work.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21835?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. -->
|
||
|
|
4075018834 |
fix(workflow): label manual trigger record output as Record/Records (#21832)
## What
The manual trigger output schema exposed the triggering record(s) under
a node labeled **Payload**. Relabels it to match what the node actually
contains:
- **Single-record** availability → **Record**
- **Bulk-records** availability → **Records**
## Why
"Payload" was a misnomer — the node holds the record(s) that triggered
the workflow. This is a display-label-only change.
## Notes for reviewers
- **No migration.** The persisted output schema key stays `payload`, so
existing variable references (`{{trigger.payload.x}}`) are unaffected.
- The front recomputes the output schema on the fly
(`computeStepOutputSchema`), so the variable picker shows the new labels
immediately, including for existing triggers.
- The backend (`workflow-schema.workspace-service`) is updated to match
for newly persisted/re-saved schemas. Previously persisted schemas keep
"Payload" until re-saved.
- Added `WORKFLOW_TRIGGER_RECORD_LABEL` /
`WORKFLOW_TRIGGER_RECORDS_LABEL` and removed the now-unused
`WORKFLOW_TRIGGER_PAYLOAD_LABEL`.
- Unit tests updated for both single and bulk cases (55/55 passing).
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21832?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. -->
|
||
|
|
8f7d7101ff |
Add application to test input in workflow (#21830)
https://github.com/twentyhq/twenty/pull/21791 follow up |
||
|
|
ceecae30db |
fix(workflow): show fields for system objects in record-updated trigger (#21826)
## Problem On the **Record is updated** (and **upserted**) workflow trigger, picking a record type under the **Advanced** submenu (i.e. a *system* object) showed an empty "Fields (Optional)" list — you couldn't select any field to watch. ## Root cause The trigger's field picker (`WorkflowFieldsMultiSelect`) was called with `actionType="UPDATE_RECORD"`, which runs each field through `shouldDisplayFormField`. For `UPDATE_RECORD` that predicate requires `(isUIEditable ?? true)` — correct for the *Update Record action* (you can't write to a read-only field), but wrong for a *trigger*, where you're choosing which fields to **watch for changes** and editability is irrelevant. System objects define their fields with `isUIEditable: false`, so every field failed the gate and the list rendered empty. ## Fix Add DATABASE_EVENT trigger type to separated from action type. The `UPDATE_RECORD` / `UPSERT_RECORD` action paths are untouched. |
||
|
|
8ff494e5e8 |
fix(security): bump tar to 7.5.16 in seed-dependencies (PAX file smuggling) (#21829)
## fix(security): bump tar to 7.5.16 in seed-dependencies (PAX file smuggling) Resolves [Dependabot Alert #1500](https://github.com/twentyhq/twenty/security/dependabot/1500). ### What `tar` (`node-tar`) `<= 7.5.15` applies a PAX size override to intermediary GNU long-name/long-link headers, causing a tar-parser interpretation differential (file smuggling) — [GHSA-vmf3-w455-68vh](https://github.com/advisories/GHSA-vmf3-w455-68vh). Patched in `7.5.16`. This is the **seed-dependencies holdout** deferred from the main tar PR (#21813): that lockfile + its checksum constants were also touched by the form-data PR, so it was carved out to avoid a conflict. The form-data PR has since merged, unblocking it. ### How - Refreshed `tar` `7.5.13 -> 7.5.16` in `seed-dependencies/yarn.lock` (transitive via `^7.5.4`, which already permits it) — an in-range refresh, no override. - Regenerated `DEFAULT_YARN_LOCK_CHECKSUM` in `get-default-application-package-fields.util.ts` so the row-stored checksum matches the value recomputed from file content in `application.service.ts` (the deps-layer cache key; `logicFunctionCreateHash` = SHA-512, first 32 hex). `package.json` is unchanged, so `DEFAULT_PACKAGE_JSON_CHECKSUM` is unaffected. ### Verification - No `tar <= 7.5.15` remains in the seed lockfile. - Both checksum constants verified to match the canonical recompute of the current seed files. - Lint + format pass on the changed `.ts` file. |