1cdede89deaeee16b708bcd79f2e28e5fd02e4b0
6248 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1cdede89de |
fix(email-settings): enable independent message folder and subfolder selection (#21853)
## Summary Fixes #21840 Currently, selecting a root folder in **Settings → Accounts → Emails → Folders** automatically selects all of its subfolders, and selecting a subfolder automatically selects all of its ancestor folders. Users have no granular control over individual folder sync. This PR replaces the cascade selection logic with fully independent per-node selection, matching standard tree-select UX patterns used in file explorers and permission trees. ## Changes ### Bug Fix - **`computeFolderIdsForSyncToggle.ts`**: Removed `collectChildren` and `collectParents` cascade helpers. The function now returns only the toggled folder's ID, enabling fully independent selection. - **`SettingsAccountsMessageFoldersCard.tsx`**: Updated call site to match simplified function signature (removed unused `allFolders` and `isSynced` args). ### Tests - **`computeFolderIdsForSyncToggle.test.ts`**: Rewrote tests to reflect new per-node behavior. Removed tests asserting old cascade behavior; replaced with tests verifying only the toggled folder is affected. - **`isFolderTreePartiallySelected.test.ts`** *(new)*: Added 9 tests for `isFolderTreePartiallySelected`, which is now the primary mechanism driving the indeterminate checkbox state on parent folders. ## Behavior Before / After | Action | Before | After | |--------|--------|-------| | Check a root folder | Checks root + all subfolders | Checks root only | | Check a subfolder | Checks subfolder + all ancestors | Checks subfolder only | | Uncheck a root folder | Unchecks root + all subfolders | Unchecks root only | | Parent with partial children | No indeterminate state (broken) | Shows `–` indeterminate correctly | ## What Was Already Correct The indeterminate checkbox UI was already fully implemented: - `isFolderTreePartiallySelected` correctly detects mixed sync states in subtrees - `SettingsMessageFoldersTreeItem` already passes `indeterminate` to the `Checkbox` component - The `Checkbox` component in `twenty-ui` already supports the `indeterminate` prop Only the toggle cascade logic needed fixing. ## Testing ```bash # Unit tests cd packages/twenty-front && yarn jest --testPathPattern="computeFolderIdsForSyncToggle|isFolderTreePartiallySelected" # Lint npx nx lint:diff-with-main twenty-front # Type check npx nx typecheck twenty-front <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21853?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> Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com> Co-authored-by: neo773 <neo773@protonmail.com> |
||
|
|
614bc7b7e6 |
feat: serve HTTP logic functions on isolated *.withtwenty.com domain (#22045)
## Summary Implements [core-team-issues#2473](https://github.com/twentyhq/core-team-issues/issues/2473): serve HTTP-triggered logic functions from a dedicated, **cookieless** public domain (`{workspaceSubdomain}.withtwenty.com`) instead of the same-site `/s/` route, so functions can safely return **arbitrary headers** — custom headers, `Permissions-Policy` (camera/mic/geolocation), `Cross-Origin-Opener-Policy: same-origin`, `Cross-Origin-Embedder-Policy: require-corp`, `Set-Cookie`, etc. The `/s/` route stays the strict, same-site path it is today. **Self-hosting is unchanged** — everything new is gated on `PUBLIC_DOMAIN_URL` being set. ### Why Today user-authored function responses are served same-site with the Twenty app, so the response-header allow-list is restricted to 5 safe headers and request headers are limited to a per-function allow-list. Serving from an origin that shares nothing with `*.twenty.com` removes that constraint safely — the same "user content domain" pattern as GitHub (`*.githubusercontent.com`) and CodeSandbox (`*.csb.app`). ## What's in here **Routing** - The **root-path → `/s` rewrite happens at the nginx ingress**, not in app code. The existing `api-ingress.yaml` already rewrites root paths onto `/s` (host-agnostically) when the edge sets `X-Twenty-Public-Domain: true`, so `*.withtwenty.com` and registered custom public domains are handled by the same mechanism. (An earlier in-app middleware was removed as a redundant, wrong-layer duplicate.) - `WorkspaceDomainsService.resolveWorkspaceAndPublicDomain` recognizes `*.` subdomains, resolves the workspace by subdomain, and returns `isIsolatedOrigin`. Explicitly registered public-domain rows still take precedence and keep their application scoping. The ingress preserves the `Host` header, so this resolution still fires. **Headers (server)** - Isolated origin → all response headers pass through and all request headers are forwarded. Same-site `/s/` keeps the strict allow-lists. (Global CORS already handles preflight/ACAO.) **`/s/` deprecation for new routes (cloud only)** - New `LOGIC_FUNCTION_LEGACY_ROUTE_CUTOFF` config var (ISO date, optional). When `PUBLIC_DOMAIN_URL` is set, functions created on/after the cutoff return **410 Gone** on `/s/` with the new URL. Existing routes and self-hosted instances are untouched. **Frontend education** - `publicFunctionDomain` added to `ClientConfig` (from `PUBLIC_DOMAIN_URL`). - The logic-function **Live URL** now resolves to `https://{workspaceSubdomain}.{publicFunctionDomain}{path}` on cloud, falling back to `/s/` for self-hosting. - Front components call their functions through the SDK (`RestApiClient`), which now targets the isolated domain via the injected `TWENTY_FUNCTIONS_URL`. - New **"Public URL"** section on the application **Settings** tab explaining the isolated domain (shown when the app exposes HTTP-triggered functions). **Docs**: note the `withtwenty.com` domain for external callers in the apps guide. ## Infra prerequisites (not code — needs dashboard work) - Wildcard DNS `*.withtwenty.com` (proxied) + wildcard TLS in the public-domain Cloudflare zone. - Edge (Cloudflare) sets `X-Twenty-Public-Domain: true` for `*.withtwenty.com` requests, so the existing nginx ingress rewrites them onto `/s` (same header the custom-domain flow already relies on). - Set `PUBLIC_DOMAIN_URL=https://withtwenty.com` on cloud. - Submit `withtwenty.com` to the **Public Suffix List** (required for cross-tenant cookie isolation before relying on `Set-Cookie`). ## Test plan - [x] `nx typecheck twenty-server`, `nx typecheck twenty-front` - [x] `lint:diff-with-main` + oxfmt clean (server + front) - [x] `npx jest route-trigger public-function-domain domain-server-config workspace-domains build-logic-function-event client-config` → server unit tests passing (resolution tiers, header passthrough vs allow-list, `/s/` cutoff 410) - [x] `npx jest getLogicFunctionHttpUrl` (front) and `nx test twenty-client-sdk` (RestApiClient routing) passing - [x] CI green (server, front, sdk, renderer, ui, zapier, example apps) - [ ] Manual: hit `{subdomain}.withtwenty.com/` end-to-end once infra is provisioned <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22045?utm_source=github" rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a> |
||
|
|
b5a1aed24b |
feat(server): run server-exposed logic functions in the owner workspace (#22002)
## Summary Implements the server-level logic-function tier in the simplest shape: a logic function is "server-exposed" iff its manifest entry carries `serverWebhookTriggerSettings`. Execution delegates to the owner-workspace copy of that function — billing, throttling, env vars, and the existing executor all apply uniformly against that workspace. Supersedes #21971 with the simplified design from that discussion (no `applicationRegistrationLogicFunction` registry, no dedicated manifest type, no separate SDK helper, no special throttling). ## Design - **Manifest**: `LogicFunctionManifest` gains `serverWebhookTriggerSettings?`. The declarative `workspaceIdResolver` shape is dropped. - **Materialization**: those settings become two new jsonb columns on `LogicFunctionEntity`. The manifest → flat converter and the create-from-source DTO/util forward them; the property-config map and editable-properties list are extended. - **Lookup**: a single QB query joins `logicFunction → application → applicationRegistration` and filters on `lf.workspaceId = reg.workspaceId` to get only the owner workspace's copy. - **Webhook**: `POST /webhooks/server/:logicFunctionUniversalIdentifier` → `ServerWebhookTriggerService.handle` → join lookup → `LogicFunctionTriggerService.run`. No registry table, no `:applicationRegistrationUniversalIdentifier` segment, no resolver. - **Gate**: `IS_SERVER_LOGIC_FUNCTION_ENABLED` config var (disabled by default). ## Test plan - [x] `npx jest server-webhook-trigger` — 9 unit tests across the webhook service. - [x] `npx jest logic-function` — 88 existing tests stay green. - [x] `npx nx typecheck twenty-server`. - [x] `npx nx lint:diff-with-main twenty-server`. - [x] Reset DB → init → run `database:migrate:prod` → run `database:migrate:generate --name pending-migration-check` → no drift. - [ ] Manual: hit `/webhooks/server/<uid>` end-to-end against a manifest carrying `serverWebhookTriggerSettings`. https://claude.ai/code/session_01GgsnCGmYJ26xRirx8va1Yh --- _Generated by [Claude Code](https://claude.ai/code/session_01GgsnCGmYJ26xRirx8va1Yh)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22002?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. --> |
||
|
|
ad3c82bd15 |
fix(front): prevent lingui extract crash in buildCrudToolStatusMessage (#22080)
## Problem
The `build-front / s3-build` CD job fails during the `Build frontend`
step, in the `twenty-front:lingui:extract` target (`lingui extract
--overwrite --clean`):
```
Cannot process file .../build-crud-tool-status-message.util.ts:
Cannot read properties of undefined (reading 'name')
at @lingui/babel-plugin-extract-messages/dist/index.cjs:88:22
at extractFromObjectExpression (...index.cjs:87:18)
at extractFromMessageDescriptor (...index.cjs:121:19)
at PluginPass.CallExpression (...index.cjs:189:11)
```
## Root cause
`buildCrudToolStatusMessage` called `i18n._()` with an inline object
literal containing a spread:
```ts
i18n._({ ...verbs.loading, values: { objectLabel } })
```
Lingui's `extract-messages` babel plugin fires on every `i18n._(...)`
call. When the first argument is an `ObjectExpression`, it runs
`extractFromObjectExpression`, which reads `key.name` for **every**
property. The spread element `...verbs.loading` has no `key`, so
`key.name` throws `Cannot read properties of undefined (reading
'name')`, crashing `lingui extract` and failing the whole S3 publish
job.
## Fix
Hoist the descriptors into variables so `i18n._()` receives an
identifier rather than an inline object expression. The plugin then
skips extraction (no statically-extractable id), so no crash. Runtime
behavior is unchanged — the translatable strings are still extracted
from the `msg` macros in `CRUD_TOOL_OPERATION_VERBS`.
## Testing
- Reproduced the **exact** CI crash locally on `main` by running `lingui
extract --overwrite --clean` (same file, message, and stack frames).
- After the fix, `lingui extract --overwrite --clean` runs clean (exit
0).
- `build-crud-tool-status-message.util.test.ts` passes (2/2).
- `nx lint:diff-with-main twenty-front` passes (0 warnings, 0 errors,
formatting clean).
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22080?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. -->
|
||
|
|
9e57ec3153 |
Update data model object settings labels (#22070)
## Summary - Update Data Model object list copy: rename the section to **Objects** and the count column to **Records**. - Improve relation rows by showing the related object name with the field name as a secondary label, including morph relation-specific labels/icons. - Hide relations to system objects unless Advanced mode is enabled, and default the System objects filter to on while Advanced mode is on. - Reuse a shared secondary-label component for the light subtitle/deactivated text treatment. ## Screenshots ### Before Mix of field name & object name. Not all relations are navigable <img width="1642" height="950" alt="image" src="https://github.com/user-attachments/assets/e04fb710-e333-4dd7-a29f-82c226202e77" /> ### After <img width="1690" height="1112" alt="image" src="https://github.com/user-attachments/assets/f7d75974-a5cc-401b-bbd2-ab82a2006cf9" /> |
||
|
|
0df83eceb2 |
fix(front): restore loading state on third-party app command menu actions (#22073)
## Problem Headless command-menu actions provided by third-party applications (e.g. the "Twenty Eng" app actions like *Fetch Pull Requests*, *Recompute Build Tasks*) no longer show a loading/progress indicator while they run, so users can't see that the action is in progress. ## Root cause In `CommandMenuItemSelectableRenderer`, [#21020](https://github.com/twentyhq/twenty/pull/21020) added an early-return branch for third-party application actions that renders `AppMenuItem`: ```tsx if (isThirdPartyApp) { return ( <SelectableListItem ...> <AppMenuItem ... /> // no loader passed </SelectableListItem> ); } ``` This branch returns **before** the `listItem` path that builds the `loaderComponent` (spinner + progress %), and `AppMenuItem` had no way to render a right-side loader. So `progress` / `showDisabledLoader` from `useCommandMenuItemClick` were computed but dropped for third-party app actions. Native (non third-party) actions kept their loader because they go through the `listItem` path. ## Fix - Add an optional `RightComponent` prop to `AppMenuItem`, forwarded to the underlying `MenuItem` (which already renders it). - Hoist the `loaderComponent` computation in `CommandMenuItemSelectableRenderer` above the branches and pass it to both the third-party `AppMenuItem` path and the existing `listItem` path (no behavior change for the latter). The loader now appears for third-party app actions exactly as it does for native ones — `<CommandListItemLoader progress={progress} />` once progress is reported, falling back to a `<Loader />` spinner before the first progress update. ## Verification - `oxlint --type-aware` clean on both changed files. - `typecheck` clean for the changed files. - Manual browser repro requires a third-party application with a progress-reporting headless action installed in the workspace (as in the reported screenshot), which isn't available in a stock dev workspace. The fix mirrors the already-working native `listItem` loader path. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22073?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. --> |
||
|
|
558e2e4107 |
Add new onboarding login screen at /welcome-v2 (#22027)
Stands up the new onboarding login screen at a new route `/welcome-v2`, as the foundation for the new onboarding flow (future PRs build the post-login steps on top of it). There is no feature flag: feature flags are per-workspace and read from `currentWorkspaceState`, which is null on the pre-auth welcome screen, so they can't cleanly gate it. A dedicated route is used instead. `/welcome` is untouched and stays the default for logged-out users; `/welcome-v2` is reachable only by navigating to it directly (nothing links or redirects to it yet), so this is fully non-breaking. The new page reuses all existing auth logic and behavior components (`useSignInUp`, `useSignInUpForm`, step state, the Google/Microsoft/credentials forms, `Logo`, `Title`, `ModalContent`) and mirrors `SignInUp.tsx` almost exactly. The only intentional design delta from today's screen is the footer wording, per Figma: "Data Processing Agreement" (linking to `/legal/dpa`) instead of "Privacy Policy". Notable: - Added an optional `to` prop to the shared `Logo` (defaults to `AppPath.SignInUp`, backward-compatible) so the logo on `/welcome-v2` doesn't bounce users back to `/welcome`. - The remaining changes are single-line additions to the pre-auth allowlists next to the existing `AppPath.SignInUp` entries (router, redirect guard, auth modal, metadata gater, captcha, page title, focus). https://github.com/user-attachments/assets/abfc96ec-a87d-4608-b92a-87e2322e4874 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22027?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. --> |
||
|
|
1589b9b912 |
Add search to new sidebar item picker (#22041)
## Summary - Add search to the custom layout “New menu item” side panel. - Group search results by Objects, Views, and Records. - Reuse the existing record search behavior through a shared hook and preserve add-to-navigation drag/select flows. ## Video - Recording: https://gist.githubusercontent.com/Bonapara/c78107650efd94b580e38426b9fc2dbd/raw/755c87fab253281a9c68e5a24cbfdff6c9248af1/search-nav-item-custom-layout.webm ## Verification - Browser plugin: opened layout customization, clicked `Add menu item`, searched `o`, and verified `Objects`, `Views`, and `Records` result groups with object/view/record results. - `npx oxlint --type-aware -c packages/twenty-front/.oxlintrc.json packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemPage.tsx packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordSubPage.tsx packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemSearchResults.tsx packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useAvailableNavigationMenuItemSearchRecords.ts` - `npx nx typecheck twenty-front` - `npx nx lint twenty-front` <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22041?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. --> |
||
|
|
5ca41d55fb |
feat(ai): humanize tool-call (#21976)
# Humanize tool-call labels cc: https://github.com/twentyhq/twenty/pull/21462 ## Preview <img width="459" height="156" alt="Screenshot 2026-06-22 at 19 13 11" src="https://github.com/user-attachments/assets/e7a2f5f5-cd09-4ec6-920b-5eb16b98285c" /> <img width="461" height="156" alt="Screenshot 2026-06-22 at 19 14 54" src="https://github.com/user-attachments/assets/c2114d2e-2aa8-499a-9801-68e3bb7c45f8" /> <img width="461" height="505" alt="Screenshot 2026-06-22 at 19 15 01" src="https://github.com/user-attachments/assets/ee9ca5d0-8e79-4c63-a2ff-ed5e359a9a9c" /> ## Why In the AI chat, tool steps were displayed using raw tool identifiers (`find_many_companies`, `create_one_task`, `send_email`...) and labels were partially reconstructed/humanized on the frontend. This was hard to localize and inconsistent across tool categories. This PR makes the **backend the single source of truth for human-readable, localized tool labels**, exposes them through `getToolIndex`, and reduces the frontend to a thin resolver that picks the right label for the current status (in-progress / completed). ## What changed ### Backend - `ToolIndexEntry` (and the `getToolIndex` GraphQL DTO) now carry `label`, `inProgressLabel?`, `completedLabel?`. - New `getCrudToolLabels(operation, objectLabel, i18nService, locale)` builds CRUD labels from a verb table (Search / Find / Group / Create / Update / Upsert / Delete × imperative / in-progress / completed) + the (translated, lowercased) object label. - New `translate-tool-label.util.ts` translates a source label via `I18nService` (`generateMessageId` → fallback to source when no translation exists). - Action tools: labels extracted to the `ACTION_TOOL_LABELS` constant (`msg` + `i18nLabel`) and translated in `ActionToolProvider.buildDescriptor`. - Logic-function tools use the function name as label; `toolSetToDescriptors` (workflow / view / metadata / dashboard) accepts an optional `labels` map and falls back to a humanized tool name. - Labels are localized server-side using the request locale (`@RequestLocale` → `buildToolIndex` → `context.locale`, threaded through `ToolContext` / `ToolProviderContext`). - `code_interpreter` schema now asks the model for `loadingMessage` (present tense) and `completedMessage` (past tense), so its status text is model-generated. - Removed the old generic `loadingMessage` injection mechanism (`wrap-tool-for-execution.util.ts` deleted; `wrapJsonSchemaForExecution` / `stripLoadingMessage` no longer wrap every tool). ### Frontend - New `useToolLabelMap()` hook builds a `Map<name, { label, inProgressLabel, completedLabel }>` from `getToolIndex`. - `getToolDisplayMessage` → `resolveToolDisplayMessage({ input, toolName, isFinished, labelMap, output })`: a small resolver registry keyed by tool name (`execute_tool`, `web_search`, `learn_tools`, `load_skills`, `code_interpreter`, default). - Default resolver prefers backend `completedLabel` / `inProgressLabel`, falling back to `Ran X` / `Running X`. - `learn_tools` / `load_skills` resolve their inner tool/skill names to labels (label map → tool output labels via `getToolOutputLabelEntries` → raw name). - `code_interpreter` step is now expandable to show the code even while running. ## How tool labelling flows (BE → FE) ```text BACKEND ┌───────────────────────────────────────────────────────────────────────────┐ │ Tool providers (per category) → ToolIndexEntry │ │ │ │ DatabaseToolProvider │ │ getCrudToolLabels(operation, object.labelPlural/Singular, i18n, locale) │ │ verb table (Search/Create/Update/Delete…) + translateToolLabel(object) │ │ → { label, inProgressLabel, completedLabel } │ │ │ │ ActionToolProvider │ │ ACTION_TOOL_LABELS[toolId] (msg) → translateToolLabel(…, locale) │ │ → { label, inProgressLabel?, completedLabel? } │ │ │ │ LogicFunctionToolProvider → label = logicFunction.name │ │ toolSetToDescriptors → label = labels[name] ?? humanize(name) │ │ (workflow / view / metadata / dashboard) │ └───────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────────────────────┐ │ GraphQL Query getToolIndex : [ToolIndexEntry] │ │ { name, label, inProgressLabel, completedLabel, description, │ │ category, objectName, icon } │ └───────────────────────────────────────────────────────────────────────────┘ │ ▼ FRONTEND ─ resolve the right label for the current status ┌───────────────────────────────────────────────────────────────────────────┐ │ useGetToolIndex() → useToolLabelMap() │ │ Map<name, { label, inProgressLabel?, completedLabel? }> │ └───────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────────────────────┐ │ resolveToolDisplayMessage({ input, toolName, isFinished, labelMap, output })│ │ │ │ TOOL_LABEL_RESOLVERS[toolName] ?? defaultResolver │ │ ├─ execute_tool → unwrap { toolName, arguments } then re-resolve │ │ ├─ web_search → "Searching/Searched the web for <query>" │ │ ├─ learn_tools → "Learning/Learned <labels>" │ │ ├─ load_skills → "Loading/Loaded <labels>" │ │ │ inner names resolved via: labelMap → output labels → raw name │ │ ├─ code_interpreter → model's loadingMessage / completedMessage │ │ └─ default → isFinished │ │ ? completedLabel ?? "Ran <label>" │ │ : inProgressLabel ?? "Running <label>" │ └───────────────────────────────────────────────────────────────────────────┘ │ ▼ Rendered by ThinkingStepsDisplay / ToolStepRenderer ``` ## Localization notes - Standard object labels and action/CRUD verbs are translated server-side via `I18nService` using the requester's locale. - Custom object labels are not translated unless a workspace custom translation exists (matched by `generateMessageId`); otherwise the source label is used as-is. ## Tests - **FE:** `resolveToolDisplayMessage` / `getToolOutputLabelEntries` (status selection, inner-name resolution, `code_interpreter` model labels, fallbacks). - **BE:** `toolSetToDescriptors` (label map + humanized fallback) and `database-tool.provider` label generation. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21976?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. --> |
||
|
|
680e4a712b |
feat(ui): additional social providers to link components (#21716)
The current link component matches only to linkedin, twitter and facebook. It is currently missing the x handle. In addition to this, we should also accomodate for instagram, bluesky and tiktok. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21716?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> |
||
|
|
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. --> |
||
|
|
3943898642 |
Remove custom widget from calendarEvent page layout (#22046)
## Before <img width="608" height="759" alt="image" src="https://github.com/user-attachments/assets/e75f6cc9-aef8-4247-a88d-6992b3936d3d" /> ## After <img width="844" height="658" alt="image" src="https://github.com/user-attachments/assets/446a13f2-e40c-4e1c-a1ed-0920fe5065b3" /> remove https://github.com/twentyhq/twenty/pull/22016 custom widget and replace with regular field widget does not match figma design but avoid introducing specific behavior for calendarEvent <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22046?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. --> |
||
|
|
2e099c91e1 |
fix(domains): show custom domain DNS records and activation status without a page reload (#22037)
## Problem
Setting up a custom domain had two confusing UX issues, both caused by
local state not being refreshed after the relevant mutation:
1. **DNS records didn't appear after saving.** After hitting save you
got the green "Custom domain updated" snackbar, but the "Domain Setup"
section (the Cloudflare/DNS records to configure) stayed empty. You had
to leave the page and come back for the records to show up.
2. **The "Custom Domain" card stayed "Inactive"** even after the DNS
records validated as "Success". Only a full page reload flipped it to
"Active".
## Root cause
**Issue 1 — stale closure.** In `useSettingsCustomDomain.handleSave`,
the `updateWorkspace` `onCompleted` callback called
`setCurrentWorkspace({ ...currentWorkspace, customDomain })` and then
`checkCustomDomainRecords()`. But `checkCustomDomainRecords` guarded on
the closed-over `currentWorkspace.customDomain`, which was still `null`
at that render. The `setCurrentWorkspace` call doesn't synchronously
update that captured value, so the guard returned early and the records
were never fetched. Remounting the page (navigate away/back) ran the
on-mount effect with a fresh workspace, which is why the trip "fixed"
it.
**Issue 2 — `isCustomDomainEnabled` never refreshed locally.** The
Active/Inactive badge is driven by
`currentWorkspace.isCustomDomainEnabled`. The backend flips this flag
inside `checkCustomDomainValidRecords`
(`custom-domain-manager.service.ts`), but the mutation didn't return it,
so the local `currentWorkspaceState` stayed stale until a full reload
re-ran the bootstrap query. The green "Success" DNS rows read from a
different source (`record.status`), which is why the rows and the badge
disagreed.
## Changes
**Issue 1**
- `checkCustomDomainRecords` now accepts the domain explicitly
(defaulting to the workspace value), so the freshly-saved domain can be
passed straight from `handleSave` instead of relying on the stale
closure. No new `useEffect` introduced.
- Fixed the Reload button so it no longer passes its click event as the
domain argument.
**Issue 2**
- Added a nullable `isCustomDomainEnabled` field to the
`DomainValidRecords` GraphQL type, populated only by the custom-domain
check (the shared public-domain flow leaves it null, so it's backward
compatible).
- The frontend now writes that value back into `currentWorkspaceState`
when the check completes, using a **functional** Jotai update so a
concurrent `customDomain` update is never clobbered. The badge flips to
"Active" as soon as validation passes — on mount, on Reload, and right
after save.
I deliberately kept this targeted rather than introducing real-time
workspace sync: `isCustomDomainEnabled` only changes server-side during
the on-demand DNS check (mount/Reload/cron), so returning it from that
mutation is sufficient and far lower risk.
## Notes
- `packages/twenty-front/src/generated-metadata/graphql.ts` was updated
to match what `graphql:generate` produces for the new schema field
(codegen requires a running backend, which isn't available in this
environment). Worth re-running codegen in CI to confirm it's
byte-identical.
- No existing unit or integration tests reference these paths.
## Test plan
- [ ] Set a custom domain → DNS records appear immediately (no
navigation needed).
- [ ] Once DNS validates, the "Custom Domain" card flips to "Active"
without a reload.
- [ ] Reload button still refreshes records.
- [ ] Public domain validation flow is unaffected.
https://claude.ai/code/session_01BB6C6bpPZMUbMzKSCydEaj
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BB6C6bpPZMUbMzKSCydEaj)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22037?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. --> |
||
|
|
d4e4e2612b |
feat(front): render Instagram URLs as @handles in link fields (#21642)
LinkedIn and X links already show a readable handle in Twenty's link fields. Instagram doesn't — it just shows `instagram.com`, which isn't much help when you're scanning a record. This adds the same handling for Instagram. `instagram.com/ptcrash` now shows as `@ptcrash`, in tables, on record pages, and in the edit menu. Post and reel links (`/p/...`, `/reel/...`) have no handle, so they fall back to `Instagram`. How it works: - `Instagram` added to the `LinkType` enum - `checkUrlType` detects `instagram.com` - `getDisplayValueByUrlType` pulls the handle and prefixes `@` - a shared `isSocialLinkType` helper keeps the three display components in sync Tested with unit tests for both helpers, the updated story, and manually against a record whose Instagram field is `http://instagram.com/ptcrash`. Closes #21644 Co-authored-by: Johnny Martin <ptcrash@users.noreply.github.com> |
||
|
|
855664daa2 |
feat(timeline): activity kind registry (Layer A) (#21950)
## What & why
The timeline-activity system's contract is a magic `name` string
(`"company.updated"`, `"linked-note.created"`, `"message.linked"`)
decoded by `String.split('.')` in **four** different frontend spots and
produced by a hardcoded `if`-ladder + two listeners. It is not
extensible and it already harbored a latent bug.
This PR replaces that stringly-typed protocol with an explicit,
persisted **`kind`** contract consumed through registries on both ends.
Adding a new timeline activity type becomes: add a producer + register a
presenter — no edits to a central switch.
This is **Layer A** of a larger plan (see
`packages/twenty-server/docs/TIMELINE_ACTIVITIES_REFACTOR.md` and
`TIMELINE_ACTIVITIES_PR_A.md`). Layer B (timeline projection /
"inheritance") and Layer C (user-defined aggregation rules) are
intentionally **out of scope** here.
## 🐛 Bug fixed along the way
`calendar-event-participant.listener.ts` was writing calendar-event
timeline rows with `name: 'message.linked'` (copy-paste from the message
listener). It rendered "correctly" only by luck — the frontend routed on
`linkedObjectMetadataId → nameSingular`, never on `name`. This PR fixes
it at the source (`calendarEvent.linked` / `kind:
'linkedCalendarEvent'`), and the shared resolver also corrects
historical rows that carry the wrong `name`.
## Changes
**`twenty-shared`** — new `timeline` module
- `TimelineActivityKind` (`recordChange | linkedNote | linkedTask |
linkedMessage | linkedCalendarEvent | linkedRecord`) +
`resolveTimelineActivityDescriptor`, the **single** place that decodes
an activity into `{ kind, action }`. Reads the persisted `kind` when
present and falls back to legacy `name`/`linkedObjectMetadataId` parsing
(back-compat shim). Unit-tested (20 cases).
**`twenty-server`**
- Persist a nullable `kind` field on the `timelineActivity` standard
object (entity shape + field-metadata builder + universalIdentifier).
- Producers (`timeline-activity.service.ts`, the two participant
listeners) set `kind` explicitly; dev seeder populates it.
- Fix the `calendarEvent.linked` mislabel.
**`twenty-front`**
- Static `TIMELINE_ACTIVITY_PRESENTERS` registry replaces the render
`switch`, the icon `if`-chain, the diff-validation name-parsing, and the
`name.match(/note|task/i)` title-prefetch hack.
- New `EventRowGenericLinked` so an unknown linked object type renders a
real "linked a {object}" row instead of falling through to the wrong
(main-object) renderer.
## Migration / compatibility
- The `kind` column on this **workspace** standard object is created by
the normal workspace metadata sync — no hand-written migration. It is
**nullable**, so pre-upgrade rows degrade gracefully through the
resolver shim (they resolve correctly from `linkedObjectMetadataId` +
`name`). An optional backfill workspace command could populate `kind` on
old rows later; not required for correctness.
- No GraphQL breaking change — `kind` is additive, `name` is retained
for display/search.
## Test plan
- `twenty-shared` unit tests (resolver) ✅
- `typecheck` + `lint:diff-with-main` green on `twenty-front`,
`twenty-server`, `twenty-shared` ✅
- Reset + reseed a workspace: `kind` is populated for all seeded rows
(recordChange / linkedMessage / linkedNote / linkedTask /
linkedCalendarEvent) with no nulls ✅
- Manual end-to-end verification via Playwright on person / company
record timelines — screenshots in a follow-up comment.
Screenshots attesting the rendering (incl. the calendar fix) are posted
as a comment below.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01YRueWMo4UyaX2em8R2cdio)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21950?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. -->
|
||
|
|
e9d5d71cd3 |
Wire up search field metadata (#21964)
## Part 1 - Exact scope of the current PR (#21964) close https://github.com/twentyhq/core-team-issues/issues/2586 This PR introduces `searchFieldMetadata` as a first-class flat metadata entity and migrates the existing search surface onto it, with **no change to which records are searchable** (ISO with `main`). In scope (what the PR does): - New flat entity `searchFieldMetadata` (universalIdentifier, applicationId, **`position`**, maps, conversions), registered in the central flat-entity constants and the migration build orchestrator. - `searchVector.asExpression` is **derived server-side** from `searchFieldMetadata` rows (validated by `isSafeTsVectorExpression`); never trusted from client input. - **Derivation order is deterministic, driven by each row's `position`** ([compute-search-vector-as-expression-from-search-field-metadatas.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-search-field-metadata/utils/compute-search-vector-as-expression-from-search-field-metadatas.util.ts)), replacing the previous non-deterministic `(createdAt, id)` sort. That sort collapsed to random UUIDs for standard fields (same `createdAt`), so any rename/relabel rewrote the `STORED` generated column to a logically-identical-but-textually-different expression and produced a permanent per-workspace diff vs the standard definition. Ordering now equals provisioning order; ties break on `universalIdentifier`. - Provisioning at object creation mirrors the existing surface exactly **and seeds `position`**: - custom objects -> the `name` field only, at `position: 0` ([build-default-search-field-metadatas-for-custom-object.util.ts](packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-search-field-metadatas-for-custom-object.util.ts)) - standard objects -> their curated `SEARCH_FIELDS_FOR_*` sets, `position` = the curated index - Backfill (instance + workspace commands in `2-16`) provisions rows for existing workspaces with the same surface **and the same positions** (standard from the curated standard maps, custom `name` = `0`), scoped to the workspace's own custom application ([build-search-field-metadata-backfill-operations.util.ts](packages/twenty-server/src/database/commands/upgrade-version-command/2-16/utils/build-search-field-metadata-backfill-operations.util.ts)). The `position` column is added in the same `2-16` fast instance command as `universalIdentifier`/`applicationId`. - Field rename of an already-indexed field recomputes `asExpression` (positions preserved, so order is stable) ([recompute-search-vector-on-field-rename.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-search-vector-on-field-rename.util.ts)). - Field delete drops the matching row(s) and recomputes; remaining rows keep their relative order (no renumber) ([from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts)). - Object relabel is **additive** and ISO/regression-fix only: it indexes the new label identifier **appended last (`position = max(existing) + 1`)** without dropping `name` ([recompute-search-vector-on-label-identifier-update.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-on-label-identifier-update.util.ts)). This is a deliberate, temporary bridge. Explicitly OUT of scope (deferred): - No API to edit `searchFieldMetadata` (no user-facing search-field configuration, including `position` — it is internal and only written by provisioning/backfill/recompute). - No auto-indexing of arbitrary searchable fields. Creating a custom TEXT/EMAILS/etc. field does NOT add it to search (the `computeSearchFieldMetadataCreationForFields` behavior was removed in `e6820ad`). - No field-type-transition handling (field type is immutable - not in `FLAT_FIELD_METADATA_EDITABLE_PROPERTIES`, so that path was dead code). - No `position` validation (uniqueness/range) and no multi-vector / per-field `weight` config — deferred to the configurable-search follow-up (#1428). Net: `searchFieldMetadata` becomes the source of truth for the *same* surface as `main`. The only intentional divergences from `main` are "relabel preserves `name`" (additive) and the deterministic `position`-ordered `asExpression` (a correctness/perf fix that is byte-identical to provisioning order, so it does not change the searchable surface). --------- Co-authored-by: Félix Malfait <felix@twenty.com> |
||
|
|
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> |
||
|
|
47b48d83f7 |
fix(front): include isUIEditable/isRemote in CreateOneObjectMetadataItem so new objects aren't read-only until refresh (#21796)
## Problem
After creating a custom object in **Settings → Data Model**, the new
object's **"New Field"** (and **"Add relation"**) buttons are missing.
They only appear after a hard page refresh.
## Root cause
`CreateOneObjectMetadataItem`
(`packages/twenty-front/src/modules/object-metadata/graphql/mutations.ts`)
selected only a subset of object-level fields and omitted
`isUIEditable`, `isRemote`, `isSystem`, `isUICreatable`,
`universalIdentifier`, `shortcut`, and `duplicateCriteria` — all of
which are present in the shared `ObjectMetadataFields` fragment used by
the bootstrap query.
`useCreateOneObjectMetadataItem` writes the mutation response into the
metadata store via `addToDraft`. Because the mutation resolves *after*
the SSE create event and `addToDraft` replaces entries by `id`, the
reduced mutation response overwrites the fuller record that arrived over
SSE. The stored object then has `isUIEditable === undefined`, so
`isObjectMetadataReadOnly` returns `true` (`!undefined`), and
`ObjectFields` hides the action buttons via its `{!readonly && …}`
guard.
A hard refresh "fixes" it only because the bootstrap query repopulates
the store from `ObjectMetadataFields`, which includes the missing
fields.
## Fix
Add the missing object-level fields to the `CreateOneObjectMetadataItem`
selection so a newly created object matches the bootstrap shape, and
regenerate the metadata GraphQL types. No other code changes required.
## How to test
1. Go to **Settings → Data Model** and create a new custom object.
2. Open the new object's **Fields** tab.
3. ✅ The **"New Field"** button is visible immediately — no refresh
needed.
Before this change, the button was hidden until a manual refresh.
🤖 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/21796?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. -->
|
||
|
|
0e6d96bb5e |
fix(workflow): stop trigger/action filter Conditions from flashing on edit (#21952)
## Problem
Adding a condition to a database-event **trigger** (the new Conditions
section), the **Filter** action, or the **If/Else** action causes the
just-added condition to flash out and back in.
## Root cause
`WorkflowEditActionFilterBodyEffect` seeds the builder's local jotai
atoms from the persisted `defaultValue` through an effect that
**resynced whenever the live atoms differed from `defaultValue`** (the
atoms were in the effect deps and the equality check compared atoms vs
`defaultValue`).
A local edit writes the atoms **synchronously**, then persists through
an **async** mutation — and for an *active* workflow that mutation first
creates a draft version over the network. During that window the atoms
are ahead of the still-stale `defaultValue`, so the effect treated it as
"out of sync" and overwrote the edit back to the stale value, then wrote
it again once the save landed. That round-trip is the flash.
The resync existed for a real reason: the atoms are module-cached per
`instanceId` and persist across mounts, and the trigger shares a single
**constant** `instanceId` (`'trigger'`), so a previous trigger's filters
must be overwritten when a different one is opened. (This is also why
the `?? { stepFilterGroups: [], stepFilters: [] }` fallback was added in
#21868 — to reset builder state deterministically between trigger
edits.) So a naive "init-once" fix would reintroduce that stale-state
leak.
## Fix
Resync from `defaultValue` **only when `defaultValue` itself changes**,
tracked via the last-synced value in `useState` (not the live atoms).
This:
- never clobbers an in-flight local edit → no flash;
- still re-seeds when switching the trigger/action being edited → no
stale-state leak;
- preserves reflecting genuine external `defaultValue` changes.
The `hasInitialized*` flags are no longer needed and are removed (along
with the now-unused `stepId` prop on the effect).
## Tests
Adds a regression test covering: seeding from `defaultValue` on mount,
the **no-clobber-while-stale** invariant (the flash), and resync on a
genuine `defaultValue` change. Verified the no-clobber test **fails**
against the old "resync against live atoms" behavior and passes with the
fix.
## Verification
- `nx typecheck twenty-front` ✅
- `nx lint:diff-with-main twenty-front` ✅ (0 warnings / 0 errors)
- New unit test: 3 passing ✅
## Known residual / follow-up
On an *active* workflow, the first edit creates a draft version over the
network; making a second edit before that round-trip completes leaves a
narrow window where the optimistic echo of the first value could
momentarily win. Far narrower than the current flash-on-every-edit.
Eliminating it entirely (and resolving the still-open HIGH-severity
"constant `instanceId`" review flag from #21868) would mean giving the
trigger a unique `instanceId` per workflow version + a React `key` to
reset on remount — proposed as a separate, scoped follow-up.
https://claude.ai/code/session_01XnjtzFepMJX2VFwnQVcQbV
---
_Generated by [Claude
Code](https://claude.ai/code/session_01XnjtzFepMJX2VFwnQVcQbV)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21952?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. -->
|
||
|
|
9e31ffdf68 |
feat(messaging): webhook push sync for Gmail, Calendar and Microsoft (#21970)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21970?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. --> |
||
|
|
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> |
||
|
|
46642c81c9 |
fix(front): unblock email verification on the central domain (blank modal) (#21980)
## Problem
After clicking the email-verification link on the central domain (e.g.
`app.twenty.com/verify-email?...`), a new user is left staring at a
**blank white auth modal** and onboarding never continues. The email is
actually verified — the user is just never moved off the verify-email
page.
## Root cause
`VerifyEmailEffect` (mounted on `/verify-email`) handles the
central/workspace‑agnostic domain like this:
```tsx
if (!isOnAWorkspace) {
await verifyEmailAndGetWorkspaceAgnosticToken(emailVerificationToken, email);
return enqueueSuccessSnackBar(successSnackbarParams);
}
```
It renders nothing of its own in this branch (`return <></>`) and relies
entirely on the auth hook to navigate.
The onboarding workspace-creation refactor (**#21641** "Let users pick
their workspace subdomain during sign-up", refined by **#21723**)
changed `navigateAfterMultiWorkspaceSignInUp`:
- **Before:** a user with `0` workspaces was sent through
`createWorkspace()`, which created the workspace and **redirected to the
workspace subdomain** — navigating away from `/verify-email`.
- **After:** for multi-workspace it now only does
`setSignInUpStep(SignInUpStep.WorkspaceCreation)` (the new
name/subdomain/logo form) — **no navigation**.
`signInUpStepState` is read **only by the `SignInUp` page**
(`/sign-in-up`), which renders `SignInUpWorkspaceCreationForm` for that
step. But the user is on `/verify-email`, whose route renders only
`VerifyEmailEffect` — which knows nothing about the step state and
returns an empty fragment. Nothing bridges the gap
(`usePageChangeEffectNavigateLocation` also won't redirect, because
`/verify-email` is whitelisted in `ONGOING_USER_CREATION_PATHS`), so the
user is stuck on an empty modal.
### Scope of the breakage
- **Broken:** new user, multi-workspace instance (Twenty Cloud central
domain), email verification enabled, signing up to create a workspace
(`0` workspaces). The `2+`-workspaces case (`WorkspaceSelection`) is the
same.
- **Not affected:** the single existing-workspace case (still does a
real `redirectToWorkspaceDomain`), the workspace-subdomain verification
path (`verifyEmailAndGetLoginToken` → `verifyLoginToken`), and
single-workspace self-host.
## Fix
After a successful workspace-agnostic verification, hand off to the
`SignInUp` page so it mounts and renders whatever step the hook just
set:
```tsx
if (!isOnAWorkspace) {
await verifyEmailAndGetWorkspaceAgnosticToken(emailVerificationToken, email);
enqueueSuccessSnackBar(successSnackbarParams);
return navigate(AppPath.SignInUp);
}
```
This is intentionally scoped to `VerifyEmailEffect` (the only entry
point that lives on a route which doesn't host the sign-in-up step UI).
The in-app sign-in/sign-up callers of
`navigateAfterMultiWorkspaceSignInUp` are already on `/sign-in-up`, so
they're untouched — keeping their query params (invite tokens, billing
checkout, returnToPath) intact. For the single existing-workspace edge
case, the hook's redirect still wins.
## Testing
- New `VerifyEmailEffect.test.tsx`:
- central-domain success → navigates to `AppPath.SignInUp` + shows the
success snackbar;
- failure → does **not** hand off to `SignInUp` (error state is shown);
- workspace subdomain → workspace-scoped path is untouched (no
workspace-agnostic call, no `SignInUp` hand-off).
- `nx typecheck twenty-front` ✅, `oxlint --type-aware` + `oxfmt` on
changed files ✅.
https://claude.ai/code/session_017oVwW12hC42RdCgSKK8dFP
---
_Generated by [Claude
Code](https://claude.ai/code/session_017oVwW12hC42RdCgSKK8dFP)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21980?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. -->
|
||
|
|
242b989c0e |
fix(emails): stop reply composer infinite re-render (#21935)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21935?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. --> |
||
|
|
d2083e7a1b |
Set OpenAI Responses store false for AI chat and agents (#20888)
## Summary This PR sets `openai.store = false` for Twenty's `@ai-sdk/openai` AI calls. This follows the approach discussed in #20877: instead of adding a new Twenty-specific Zero Data Retention config variable, OpenAI Responses calls no longer rely on OpenAI-stored response/item references. This should help Zero Data Retention organizations and may also avoid stale persisted-item replay errors for non-ZDR OpenAI users. Changes included: - Adds a shared OpenAI provider-options helper that merges `openai.store = false` for `@ai-sdk/openai` models. - Applies the helper to AI chat `streamText` calls. - Applies the helper to workflow/agent `generateText` calls. - Preserves OpenAI encrypted reasoning metadata through DB/UI message mappers so reasoning context can be replayed without stored OpenAI item references. - Does not add a new env/config variable. Related to issue #20877. ## Behavior / Tradeoffs This changes OpenAI Responses behavior for all Twenty OpenAI users, not only ZDR users. The intended benefit is that Twenty no longer depends on OpenAI-stored response/item references. The main tradeoff is reduced provider-side item-reference reuse for non-ZDR OpenAI users. To reduce the impact for reasoning models, this PR preserves `providerMetadata.openai.reasoningEncryptedContent` through message persistence/replay so reasoning context can still be provided without stored OpenAI item references. ## Tests - Focused server Jest tests for OpenAI provider-options merging and reasoning metadata mapping. - Focused frontend Jest test for reasoning metadata mapping. - `oxlint` and `oxfmt --check` on changed files. - `git diff --check`. --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.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. |
||
|
|
96a2987610 |
Fix/sanitize chart filters on save (#21958)
# Fix: sanitize chart filters referencing deactivated/deleted fields ## Summary When a field used in a chart (graph) widget filter was later **deactivated or deleted**, saving the page layout failed with a backend error such as: > Chart "...": One of the chart filters uses "...", but it was deleted. Please remove or replace this filter rule. This happened even after the user tried to remove the offending filter rule, because the invalid filter could still end up in the saved configuration. This PR makes invalid chart filters get cleaned up reliably — both as the user edits filters and, as a safety net, at save time. ## Root causes - **Edit-time persistence kept invalid filters.** `handleFiltersUpdate` persisted the current filter state to the page layout draft without sanitizing it against the object's active fields. An invalid filter (referencing a deactivated/deleted field) was re-saved on every update, blocking the configuration from being accepted. - **Save never enforced the cleanup.** `useSavePageLayout` serialized the draft as-is. The "filters referencing deactivated/deleted fields will be automatically removed on save" promise shown in the warning banner was only honored reactively (when the filter panel was actively edited), never at the actual save boundary. A chart whose filter panel wasn't touched kept its stale invalid filter in the payload. - **Query time treated inactive fields as valid.** `useGraphWidgetQueryCommon` considered all fields (including inactive ones) valid, so deactivated-field filters were never dropped when running the chart query. ## Changes ### Edit-time (keeps draft and UI in sync as you edit) - `ChartFiltersSettings` — sanitize filters in `handleFiltersUpdate` before writing to the draft, dropping any filter whose `fieldMetadataId` is not in the active-fields set. - `dropChartRecordFiltersWithDeletedFields` — enhanced to also clean up filter groups left orphaned once invalid filters are removed (iteratively removing empty groups and re-parenting checks). - `useGraphWidgetQueryCommon` — restrict valid field IDs to `isActive` fields so deactivated-field filters are silently dropped at query execution. - `ChartFiltersDeletedFieldsWarning` — updated copy to mention both deactivated and deleted fields. ### Save-time safety net (guarantees no invalid filter is ever persisted) - New `sanitizeChartFiltersInPageLayoutDraft` util — walks every chart widget in the draft and drops record filters (and now-orphaned groups) whose `fieldMetadataId` is not in the widget object's set of active fields. It leaves non-chart widgets untouched and leaves filters intact when the object metadata can't be resolved (avoids wiping valid filters during metadata loading). - `useSavePageLayout` — builds a `Map<objectMetadataId, Set<activeFieldId>>` from `useObjectMetadataItems()` and sanitizes the draft before converting it to the update input. This layer only ever removes filters whose field is genuinely deactivated/deleted — the exact set the backend rejects — and never removes filters pointing at valid fields. ## Tests - `dropChartRecordFiltersWithDeletedFields.test.ts` — extended coverage for orphaned filter-group cleanup. - `sanitizeChartFiltersInPageLayoutDraft.test.ts` — new: drops deactivated/deleted-field filters on save, keeps valid filters, cleans up orphaned groups, leaves non-chart widgets alone, and leaves filters untouched when object metadata is unresolved. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21958?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> |
||
|
|
6ce7d04f9d |
Fix field widget textarea focus reset (#21959)
Short description: Keeps the field widget textarea on a local draft value while focused so record-store rewrites do not reset the active caret. # Before Typing in an editor-mode text field can lose caret position when the global record store is rewritten by an external record update/refetch. https://github.com/user-attachments/assets/1ee7a819-2c27-4d09-aae5-814c2cf27181 # After The focused textarea should preserve the in-progress draft and caret while still updating sibling previews optimistically and flushing the final value on blur. https://github.com/user-attachments/assets/41832493-021f-46e8-bc75-722bbb1cd7b7 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21959?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. --> |
||
|
|
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. --> |
||
|
|
c608792aea |
feat: real-time email & calendar tabs on record pages (#21953)
emails and calendar tabs only refreshed on reload, unlike timeline. this subscribes to the participant object (messageParticipant / calendarEventParticipant) for the record's related people over the existing sse stream and refetches on change. relatedPersonIds is resolved server-side so any object with the tab inherits it, no per-object code. resolver stays the source of truth so visibility masking is untouched. |
||
|
|
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>
|
||
|
|
eeca9cd42e |
fix(front): isolate record table dashboard widget filters on duplicate (#21936)
closes https://discord.com/channels/1130383047699738754/1518291134382608394 https://github.com/user-attachments/assets/931e4e88-44e8-4634-a7f9-e0564bd80fff <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21936?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. --> |
||
|
|
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> |
||
|
|
2abf9c2930 |
feat(workflow): Pick Record load balanced strategy (3/3) (#21902)
## Overview Final PR in the Pick Record stack. Adds the **Load Balanced** strategy: pick the candidate that currently has the *fewest related records*. This is the "fair assignment" mode — e.g. assign a new company to the account owner who currently owns the fewest companies, or route a lead to the rep with the fewest open opportunities. **Stacked on #21900** (which is stacked on #21899) — merge in order. This PR's diff against `main` includes PRs 1 & 2 until they merge. ## What changed - Widened the `strategy` enum to add `LOAD_BALANCED`, and added an optional `loadBalance: { objectNameSingular, fieldName }` to the action input. - Editor: selecting **Load balanced** reveals a **Balance by** object picker and a **Count by** field picker (the related object's many-to-one relation fields). - Executor: for each candidate, counts records of the chosen related object whose chosen relation points at that candidate, then selects the least-loaded one. ## How it works Given pool = workspace members and config `{ objectNameSingular: "opportunity", fieldName: "pointOfContact" }`, the executor counts, per member, the opportunities whose `pointOfContact` is that member, and picks the member with the lowest count. ## Design decisions & tradeoffs 1. **No persistent state — computed live each run.** Unlike round robin, load balancing reads current data, so there's no cursor to store. Correct by construction even under concurrency (each run recomputes counts); the only caveat is two simultaneous runs can both see the same "least loaded" candidate before either assignment lands (a small, self-correcting skew), which is inherent to load-balancing and acceptable. 2. **Count via per-candidate queries.** One filtered count per candidate (`{ [relationField]: { id: { eq: candidateId } } }`), run in parallel. For the realistic pool sizes this targets (a team), this is simple and clear. A single `group_by` aggregate would scale better for very large pools — noted as a future optimization, deliberately not done to keep the logic obvious. 3. **Deterministic tie-break.** Candidates are pre-sorted by id (shared with round robin), and the first minimum wins — so equal-load ties resolve deterministically rather than arbitrarily. 4. **`Count by` lists all many-to-one relations of the chosen object** (not filtered to those targeting the pool object). Keeps the editor simple; picking an unrelated field just yields zero counts, which is visibly wrong. Filtering options to relations that target the pool object is a nice follow-up. 5. **Filter on the counted set** (e.g. only *open* opportunities) is intentionally out of scope for this first cut — documented as a follow-up. ## Testing Added `pick-record-load-balanced-workflow.integration-spec.ts`: creates two fresh companies (0 related opportunities each), attaches one opportunity to the second, configures `LOAD_BALANCED` counting opportunities by `company`, and asserts the step picks the **first** company (0 < 1). Passes locally alongside the random and round-robin tests (3 suites / 4 tests). `typecheck` + `lint:diff-with-main` green for shared/server/front. ## The full stack 1. #21899 — Random (the action + the whole scaffold) 2. #21900 — Round robin (atomic Redis cursor) 3. this — Load balanced Together these enable round-robin / load-balanced / random **assignment workflows** in Twenty, composed via the standard variable picker (assign the chosen record downstream with `{{step.<id>.id}}`). 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/21902?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. --> |
||
|
|
fa6d1394af |
feat(workflow): Pick Record round robin strategy (2/3) (#21900)
## Overview Second PR in the Pick Record stack. Adds a **Round Robin** selection strategy alongside Random, so an assignment workflow can distribute records *evenly* across a candidate pool (e.g. rotate company ownership across a set of workspace members) rather than just randomly. **Stacked on #21899** — review/merge that one first. This PR's diff against `main` includes PR 1's commits until #21899 merges. ## What changed - Widened the `strategy` enum (`RANDOM` → `RANDOM | ROUND_ROBIN`) in the shared schema and the server input type. - Editor now shows a **Strategy** selector (Random / Round robin). The candidate-pool label changed from "Pick at random from" to the neutral "Pick from" since random is no longer the only mode. - Executor implements round robin. ## Design decisions & tradeoffs 1. **State store: Redis `incrBy` (atomic), keyed `pick-record:round-robin:{workspaceId}:{stepId}`.** Round robin needs a persistent cursor, and workflow runs are **not** serialized — two runs can execute the same step concurrently — so the increment must be atomic. `CacheStorageService.incrBy` (workflow cache namespace) is a single atomic Redis op, needs no schema change, and is already injectable. Index = `(cursor - 1) % poolSize`. **Tradeoff — durability:** a Redis flush/eviction resets the cursor, which restarts the cycle from an offset. That causes a one-time *fairness drift*, never a *correctness* bug (no double-assignment, since each increment is atomic). If strict durability is ever required, the cursor can move to a Postgres counter table with `INSERT … ON CONFLICT … DO UPDATE SET cursor = cursor + 1 RETURNING cursor` (atomic + durable) — deliberately **not** done here to avoid a migration for what is, in practice, an acceptable reset. 2. **Deterministic pool ordering.** The resolved pool is sorted by `id` before the cursor is applied, so position→record mapping is stable run-to-run regardless of fetch order. Without this, round robin wouldn't reliably cycle. 3. **Cursor key uses `stepId`.** Stable across runs of a published version. Republishing a version may mint new step ids, which resets the cursor — acceptable and documented here. 4. **Slot-on-increment.** The cursor increments when the step runs (reserving a position); if a later step in the run fails, that position is effectively skipped. Minor, acceptable unfairness — flagged rather than adding cross-step compensation. ## Testing Added `pick-record-round-robin-workflow.integration-spec.ts`: builds a workflow with a 3-record pool and `ROUND_ROBIN`, runs it 4 times sequentially, and asserts the picks are exactly `[p0, p1, p2, p0]` (full cycle + wraparound) against the deterministically-ordered pool. Passes locally alongside PR 1's random test (2 suites / 3 tests). `typecheck` + `lint:diff-with-main` green for shared/server/front. ## Follow-up - PR 3: `LOAD_BALANCED` (fewest related records wins). 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/21900?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. --> |
||
|
|
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. --> |
||
|
|
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. -->
|
||
|
|
f9084fd208 | Clear stale parent-view filters on front-component cross-object navigation (#21869) | ||
|
|
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" /> |
||
|
|
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. -->
|