eddb56d26ec4365e712310e582df40b2cf863375
5180 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
eddb56d26e |
Fix uneven padding in the onboarding trust badge (#23174)
The "+10k" trust badge on the onboarding import contacts step had 2px left padding vs 10px right, so the PwC logo hugged the left edge. The badge now has equal padding on both sides so its content is centered. <img width="347" height="168" alt="image" src="https://github.com/user-attachments/assets/05e18541-827e-4ace-99ce-7e8c4201cd1b" /> |
||
|
|
de3889e04a |
fix: use aria-current instead of aria-selected on navigation drawer items (#23160)
## Fixes Closes #23129 ## Problem Navigation drawer items render an `<a>` (or React Router `Link`) with `aria-selected="true"`. `aria-selected` is only valid on roles such as `option`, `tab`, `row`, or `gridcell`, not on links, so axe flags WCAG 4.1.2 (aria-allowed-attr): "ARIA attribute is not allowed: aria-selected=true". ## Fix Use `aria-current="page"` instead. It is the WAI-ARIA recommended way to mark the current item in a navigation, it is a global attribute valid on any role (link, button, or div), and it correctly communicates the active page to screen readers. `packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx`: - aria-selected={active} + aria-current={active ? 'page' : undefined} <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23160?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. --> |
||
|
|
0c964aee10 |
Fix blank pages and add error recovery in onboarding (#23069)
Onboarding showed a blank page in two places: before the plan/payment step, and briefly after the welcome animation. `ChooseYourPlan` returned `null` while its `ListPlans` query loaded, leaving the step empty during the crossfade. It now renders the step loader, and the plans query is warmed from an earlier onboarding step so the content is usually already there. The post-completion redirect lands transiently on `/`, whose route element was `<></>`, so the welcome animation could reveal an empty page. It now renders a skeleton, and the `null` Suspense fallbacks on payment-success and book-call are replaced too. A failed `ListPlans` query was worse than a blank frame: `PLAN_REQUIRED` redirects every route back to itself, so the user was locked out of the product with no way to retry. That step and the billing settings page now show a retryable error state. One related fix found on the way: `BlankLayout` had no error boundary, so a render-time throw anywhere in sign-in or onboarding took down the whole app. `DefaultLayout` already had one. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23069?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. --> |
||
|
|
f5ab8a236c |
fix: add aria-label to record table selection checkboxes (#23147)
## Fixes Closes #23130 ## Problem On record tables, the select-all header checkbox and each row's selection checkbox render as `role="checkbox"` with no accessible name, failing WCAG 4.1.2 (aria-toggle-field-name). Screen-reader users cannot tell what the checkbox selects. ## Fix The shared `Checkbox` component already accepts and forwards `aria-label`, but the record-table callers weren't passing one. Added translated labels: - Row checkbox: `aria-label={t\`Select row\`}` in `RecordTableCellCheckbox.tsx` - Header checkbox: `aria-label={t\`Select all rows\`}` in `RecordTableHeaderCheckboxColumn.tsx` Both use `const { t } = useLingui()` from `@lingui/react/macro`, following the existing i18n convention in this module. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23147?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. --> |
||
|
|
b4aa323889 |
Record Board & Record Calendar Drag Drop dnd-kit rewrite. (#23071)
Closes: #23070 Record Board: https://github.com/user-attachments/assets/26df63e2-fbbd-4339-88b3-eed73af283b4 Record Calendar: https://github.com/user-attachments/assets/cb209545-695e-44a1-8dfb-a92b3c8650a6 ### Technical Inputs - Each column is a `Droppable`. This differs from other implementations (Record table header and Record board header), where only the gap between headers is `Droppable`. - Individual cards are `Sortable`. - Uses `DragOverlay` to display a cloned version of the dragged card along with a `+N` chip when dragging multiple cards. - Updated `DragDropColumnDropTarget` to handle `vertical` and `horizontal` orientation of drop target. - File name changes: - `DragDropColumnDropTarget` → `DragDropItemDropTarget` - `DragDropColumnDroppableSlot` → `DragDropItemDroppableSlot` - `DragDropColumnSortableCell` → `DragDropItemSortableCell` - `DragDropColumnSortableHandle` → `DragDropItemSortableHandle` - `DragDropColumnDndContext` → `DragDropItemDndContext` - `DragDropColumnData` → `DragDropItemData` <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23071?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
71a1ff7ac8 |
Cache twenty-client-sdk modules host-side via content-addressed URLs (#22981)
## Context Front component sources are fetched host-side and integrity-verified by the SHA-256 checksum embedded in their URL, cached in Cache Storage — a layer that exists specifically because their download URLs are presigned and rotate. The `twenty-client-sdk` modules (`core` and `metadata`) were re-fetched on every render and could not be cached safely: their URLs carried no checksum and the server exposed no freshness signal. This PR makes the SDK module URLs **content-addressed** and relies on the **browser HTTP cache** for immutability, and it keys the checksums on their real owners: the **application** for `core`, the **instance** for `metadata`. The checksum does double duty: cache invalidation (regeneration changes the checksum → the URL changes → guaranteed cache miss) and a server-side cacheability guard (the server only grants `immutable` when the checksum in the URL matches the authoritative checksum it knows for that module — persisted at generation time for `core`, hashed once at bootstrap for `metadata` — so no per-request hashing of the served bytes). Note this is **not** an end-to-end integrity guarantee: there is no client-side hash verification, and on a fingerprint mismatch the server still serves the current bytes with `no-store` (self-healing for stale URLs) rather than failing. <img width="2412" height="926" alt="image" src="https://github.com/user-attachments/assets/d97935d2-0fdb-4c44-89ac-596b7ca8ca64" /> Closes twentyhq/core-team-issues#2688. ## Routes | Module | URL | Scope | | --- | --- | --- | | `core` | `/rest/sdk-client/{applicationId}/core[/{checksum}]` | Per application (generated bundle) | | `metadata` | `/rest/sdk-client/metadata[/{checksum}]` | **Instance-wide**: no application segment, so every application converges on one URL and the browser downloads the module once per release instead of once per application | The previous application-scoped metadata path (`/rest/sdk-client/{applicationId}/metadata[/{checksum}]`) is **kept for backward compatibility**, new clients just stop generating those URLs. The instance-wide route is declared before the parameterized route so `metadata/{checksum}` is not swallowed as `:applicationId/:moduleName`. ## Caching model | Request | `Cache-Control` | Effect | | --- | --- | --- | | Fingerprinted URL, checksum matches the known module checksum | `immutable` | Cached indefinitely by the browser HTTP cache; a new checksum is a new URL | | Fingerprinted URL, checksum does not match | `no-store` | Current bytes served uncached (self-healing for stale URLs) | | Bare URL (pre-generation fallback, `core` only in practice) | `no-store` | Never cached | - Both responses also set `X-Content-Type-Options: nosniff` and `Content-Type: application/javascript`. - SDK modules are intentionally **not** placed in Cache Storage. That layer stays reserved for the presigned/rotating component-source URLs; SDK modules are served directly and authenticated, so the browser HTTP cache (keyed by the content-addressed URL) is their single cache layer. ## Checksum provenance - **core** — per **application**, persisted on `application.sdkClientCoreChecksum` at generation time and read back from `flatApplicationMaps` (never re-hashed per request). - **metadata** — **instance-wide**, hashed once from the installed `twenty-client-sdk/dist/metadata.mjs` package (warmed at bootstrap, memoized per process) and served straight from that package, so it is fresh from the first request after a release with no archive dependency. ## Server (twenty-server) - Hash `dist/core.mjs` at SDK generation and persist `sdkClientCoreChecksum` via `applicationRepository.update`. Adds the nullable text column to `application.entity.ts` (mirroring `packageJsonChecksum`) plus a fast instance command with up/down; `FlatApplication` picks it up automatically. - New **application-scoped** query `applicationSdkClientChecksums(applicationId: UUID!): SdkClientChecksums` on `ApplicationResolver` (metadata schema, `WorkspaceAuthGuard` + `NoPermissionGuard`). `SdkClientChecksums.core` is **nullable** and stays `null` until the SDK has been generated at least once; `metadata` is **always present** (bootstrap-warmed), so the metadata module is cacheable from the very first render of any app. The query itself returns `null` only for unknown applications. - `SdkClientChecksumsDTO` now lives in the shared `core-modules/sdk-client/dtos/`. `FrontComponentDTO` and the `frontComponent` resolver no longer carry checksums (decoupled from the front-component row). - `sdk-client` controller: instance-wide `metadata[/:checksum]` route (no workspace-cache or application lookup, serves the memoized installed module) + application-scoped `:applicationId/:moduleName[/:checksum]` route (serves `core` from the per-application archive, `metadata` kept for back-compat). Cacheability compares the URL checksum against the **known** checksum — persisted `sdkClientCoreChecksum` for `core`, memoized package hash for `metadata` — instead of hashing the served bytes on every request: `immutable` on match, `no-store` otherwise (bare URL or stale fingerprint), plus `nosniff`. A persisted checksum out of sync with the archive only downgrades to `no-store` until the next regeneration. ## Front (twenty-front) - New metadata query `GetApplicationSdkClientChecksums`, keyed by `applicationId`; removed the `sdkClientChecksums` selection from `FindOneFrontComponent`. - `getSdkClientUrls` builds the two module URLs independently: `/sdk-client/{applicationId}/core/{checksum}` and the **instance-wide** `/sdk-client/metadata/{checksum}` (no application segment → one shared browser cache entry per release across all applications). Each falls back to its bare URL when its checksum is absent — since `core` is nullable, a never-generated app still gets a content-addressed metadata URL and only `core` falls back. The checksum type is sourced from the codegen `SdkClientChecksums` type rather than a hand-maintained duplicate. - `FrontComponentRenderer` is split into a gating outer component (runs `FindOneFrontComponent`, renders nothing while loading) and a content component that receives a guaranteed-non-null `frontComponent`. Following project conventions, the side effects live in dedicated effect components: `FrontComponentLoadErrorSnackBarEffect` (query error → snackbar) and `FrontComponentApplicationTokenPairEffect` (mirrors the query-derived token pair into component state unconditionally, `null` included, so revoked credentials can never be retained or refreshed). The content component fetches checksums via the application-keyed query and **gates the mount of SDK-using components on that query**, so the very first module fetch is always the content-addressed (`immutable`) URL instead of the bare `no-store` one. Non-SDK components skip the query and are never blocked. - **Live invalidation without reload:** SDK regeneration updates the application row, and the server broadcasts an `application` metadata event carrying the new core checksum. `useOnApplicationSdkClientChecksumsUpdated` / `useUpdateSdkClientChecksumsApolloCache` patch the application-keyed checksum query cache (core only; the instance-wide metadata is preserved), so every mounted component of that application picks up the new URL at once. This replaces the previous frontComponent-derived field and closes the earlier "known gap" (a mounted component staying on a session-old checksum until a full reload). The cache-patching callback is memoized (`useCallback`) so the window listener is registered once per application, and the listener is **skipped entirely** for non-SDK components (`useListenToMetadataOperationBrowserEvent` gained a `skip` option) — they register no listener and never refetch a query they don't consume. ## Renderer (twenty-front-component-renderer) - SDK sources are fetched through a dedicated plain authenticated fetch, `fetchJavaScriptModuleSourceText` (Bearer header, `credentials: 'omit'`), instead of the Cache Storage `fetchComponentSource` path; `fetchSdkClientSources` uses it. Execution stays exclusively in the opaque-origin worker via blob URLs; the host only fetches and forwards source strings (no hashing host-side). Staleness self-resolves through the checksum: new checksum → new URL → cache miss. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22981?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. --> |
||
|
|
8f704e87e3 |
fix(ai-chat) - fix AI record references when display names contain markdown characters (#23113)
<img width="516" height="86" alt="Screenshot 2026-07-21 at 16 41 16" src="https://github.com/user-attachments/assets/6e14b933-b48e-49ab-856b-400efdeba53a" /> ## Summary - Switch record references from `[[record:object:id:label]]` to `[[record:object:id:label[[/record]]` so labels can include `]`, backticks, brackets, and other markdown-significant characters - Parse references with an explicit close tag (still accepting legacy `]]`), escape labels before markdown lexing, and serialize mentions through a shared formatter - Update the AI chat system prompt so the model emits the new format ## Test plan - [ ] Ask AI about a record whose name contains `` ` ``, `[`, `]`, or `]]` and confirm it renders as a chip, not broken markdown - [ ] Confirm legacy `[[record:...]]` references still chip correctly - [ ] Mention a record in the chat editor and verify serialized text uses `[[/record]]` - [ ] Run: - `npx jest src/modules/ai/utils/__tests__/findRecordReferences.test.ts src/modules/ai/utils/__tests__/formatRecordReference.test.ts src/modules/ai/utils/__tests__/protectRecordReferencesForMarkdown.test.ts src/modules/ai/components/__tests__/TextWithRecordLinks.test.tsx --config=packages/twenty-front/jest.config.mjs` - mention extension tests for `MentionTag` / `MentionSuggestion` <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23113?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. --> |
||
|
|
e14c32015f |
Make upgrade applications batch size a job parameter defaulting to 5 (#23101)
Makes the batch size used when upgrading applications a parameter instead of a hardcoded constant, defaulting to 5, and lets admins set it from the upgrade confirmation modal. Backend: - `UpgradeApplicationsJobData` gains an optional `batchSize` field, passed through by `UpgradeApplicationsJob` to the service. - `ApplicationUpgradeService.upgradeAllApplications` accepts an optional `batchSize` parameter, defaulting to `UPGRADE_APPLICATIONS_DEFAULT_BATCH_SIZE = 5` (previously a fixed batch size of 20). The value is sanitized to a positive integer to avoid an infinite batching loop. - The `upgradeRegistrationApplications` admin mutation accepts an optional `batchSize: Int` argument and forwards it to the job. Frontend (admin panel): - The "Upgrade existing installations" confirmation modal now includes a "Batch size" number input, defaulting to 5, sent with the mutation. - Updated the admin GraphQL document and generated types. ## Screenshots Upgrade section on the admin app registration page:  Confirmation modal with the new batch size input (defaults to 5):  --------- Co-authored-by: Martin <martin@twenty.com> |
||
|
|
3ad3e8bd1a |
feat: kanban, calendar and group-by table layouts for dashboard view widgets (#22963)
## Context Dashboard view widgets previously only rendered flat tables. This PR ships the full feature: **Table with group-by**, **Kanban**, and **Calendar** layouts for dashboard view widgets — server API + frontend, end-to-end. (Originally staged as a 4-PR stack — #22966, #22967, #22968 — consolidated here per review.) ## Server / API - **View typing.** Adds `KANBAN_WIDGET` and `CALENDAR_WIDGET` to `ViewType` (following the `TABLE_WIDGET` precedent) so widget-backing views keep their layout in `view.type` while staying excluded from record-index pickers. Shared `getViewLayoutFromViewType()` maps widget types to their base layout; `isWidgetViewType()` centralizes the exclusions that were previously hardcoded per-site. - **Migrations.** Two fast instance commands (**2.23**): `ALTER TYPE core.view_type_enum ADD VALUE` for both values, and a widened `CHK_VIEW_CALENDAR_INTEGRITY` constraint covering `CALENDAR_WIDGET` (entity `@Check` updated for fresh installs). - **Validation.** `FlatViewValidatorService` keys kanban/calendar validation on the mapped layout, so widget views get the same invariants as index views (kanban needs a groupable group-by field; calendar needs a date field + layout). Calendar widget views default to month; a non-month (DAY/WEEK) layout is rejected at the API level **unless** the `IS_CALENDAR_WEEK_VIEW_ENABLED` feature flag is enabled for the workspace — the same flag that gates day/week on index calendars. - **API.** `upsertViewWidget` (LAYOUTS permission) accepts a nested `view` settings input (`type`, `mainGroupByFieldMetadataId`, `shouldHideEmptyGroups`, kanban aggregate/column-width, calendar layout/fields). Routes through the standard update path, so `viewGroups` auto-generate from SELECT options exactly like index views. Only widget view types accepted; only `RECORD_TABLE` widgets can change view settings. - **AI tools.** `create-complete-dashboard` + `create_view` now use/allow the `*_WIDGET` types (previously they created plain `TABLE` views that leak into index pickers). ## Frontend **Settings panel.** The **Source** (object) row comes first, since which layouts are available depends on it. The **Layout** row below is a working dropdown (Table / Kanban / Calendar); layouts the source object can't support are **disabled with a hint** ("Needs a Select field" / "Needs a Date field") rather than hidden. Group-by row (select fields; searchable) with a **Hide empty groups** toggle while grouped; **Date field** row replaces Group by while Calendar is active, and — when the `IS_CALENDAR_WEEK_VIEW_ENABLED` flag is on — a **Calendar view** row (Day / Week / Month) appears beside it; **Limit** row hidden while grouped (only the flat virtualized loader enforces it). Kanban keeps its group-by locked (no `None` option). **Instant edit-mode preview.** Draft snapshots carry `viewGroups`; picking a group-by synthesizes them client-side (`buildDraftViewGroupsForFieldMetadataItem`, mirroring the server's generation), so grouped tables/boards preview immediately before dashboard save. On save, `upsertViewWidget` responses hand back the server-generated groups, which replace the client-generated ones in the persisted snapshot. **Renderers.** `RecordTableWidgetRendererContent` branches on the backing view's layout: `RecordBoardWidget` (wraps the standard `RecordBoardContainer`) and `RecordCalendarWidget` (mounts the existing `RecordCalendar`, which renders month / day / week) inside the same per-widget provider sandbox the table uses. **Read-only semantics.** Two flags with distinct scopes, each documented on its state: - `isRecordBoardViewSettingsReadOnlyComponentState` — locks the board chrome that edits view settings (add group, column reorder/resize/menu, aggregates); **card drag still updates records** under object permissions. - `isRecordCalendarReadOnlyComponentState` — widget calendars are read-only by default (no drag, no add-new, no in-calendar layout switch); cards open the side panel. The one exception, behind `IS_CALENDAR_WEEK_VIEW_ENABLED`: a **live (non edit-mode) day/week** widget calendar allows drag-to-reschedule and record creation under object permissions. Month calendars and edit-mode previews stay read-only. **Calendar state componentization.** The calendar module's three settings move from global atoms to component states keyed on `RecordCalendarComponentInstanceContext` (same pattern as record-board), so several calendar widgets and an index-page calendar can coexist without leaking state. All readers resolve the ambient instance; calendar unit tests updated. **Multi-instance fixes that also fix index pages:** record drag states were written against a different instance than every reader resolves (now use the ambient instance); the board sticky-header DOM id is namespaced per board; dragged board cards portal to `document.body` while dragging so react-grid-layout's transforms can't offset the clone from the pointer. ## Scope (v1) - Widget calendars are month-only and read-only by default. With `IS_CALENDAR_WEEK_VIEW_ENABLED` enabled, day/week layouts become selectable (UI + API) and live day/week widget calendars support drag-to-reschedule and record creation under object permissions. - Widget group-by offers SELECT fields only (server auto-generates groups from options; widgets have no per-record add-group flow). ## Tests - Integration: `upsert-view-widget-view-settings.integration-spec.ts` (9 tests — group auto-creation, invalid type/field rejections, non-month calendar widget rejected while the week/day flag is off and accepted once it's enabled, combined settings+fields call); pre-existing `upsert-view-widget` suite (20) green. - Front: new suites for draft view-group generation and snapshot clone/build utils; calendar suites componentized; full `twenty-front` jest, typecheck, oxlint green; `twenty-server` typecheck + lint green. - Browser-verified end-to-end (real dev server + seeded workspace): configure → live edit-mode preview → save → reload for all three layouts; measured drag with pointer inside the card; index-page calendar re-verified (with the week/day flag enabled). https://claude.ai/code/session_01E5N87kwwZWhDtEQaP72cMf |
||
|
|
bc3112a999 |
Fix: allow API key creation without Roles permission (#23102)
## Problem A user with the **API keys & webhooks** permission but **without** the **Roles** setting permission cannot create an API key through the UI. The role selector relies on the `getRoles` query, which is guarded by the `ROLES` permission, so the roles list comes back empty, `SettingsDevelopersRoleSelector` early-returns, and no role can be selected — leaving the form unsavable. <img width="1058" height="408" alt="Screenshot 2026-07-21 at 13 38 34" src="https://github.com/user-attachments/assets/fe97ba78-e116-458d-af10-11c5969c4636" /> ## Fix Expose the assignable roles through the API-key permission scope so users can **pick** a role to assign to an API key without being able to **edit** roles. - **Backend**: add `getApiKeyRoles` query on `ApiKeyResolver` (already guarded by `API_KEYS_AND_WEBHOOKS`), backed by `ApiKeyRoleService.getApiKeyAssignableRoles` which returns roles where `canBeAssignedToApiKeys = true`. - **Frontend**: add a `GetApiKeyRoles` query and use it in the API key create and detail pages instead of `getRoles`. The role selector prop type is narrowed to the fields it actually uses. <img width="1025" height="455" alt="Screenshot 2026-07-21 at 13 45 01" src="https://github.com/user-attachments/assets/f1be8f97-5a30-4afc-9eee-c928f4607471" /> |
||
|
|
6d38b14520 |
fix(ai-chat) - fix record chips in AI ask-questions card (#23106)
## Summary - Ask-questions cards rendered question text and option labels as plain strings, so `[[record:...]]` showed up raw instead of as chips - Extracted `TextWithRecordLinks` from `LazyMarkdownRenderer` and reuse it in `AiChatQuestionCard` for question text and option labels - Added unit coverage for plain text, single, and multiple record references <img width="532" height="242" alt="Screenshot 2026-07-21 at 14 37 43" src="https://github.com/user-attachments/assets/a4bbd386-7757-4f09-a74d-c7eb3d0f74b9" /> ## Test plan - [ ] Open an AI chat ask-questions card whose question/options include `[[record:...]]` mentions - [ ] Confirm mentions render as record chips (not raw markup) - [ ] Confirm normal assistant text replies still chip mentions as before - [ ] Run `npx jest packages/twenty-front/src/modules/ai/components/__tests__/TextWithRecordLinks.test.tsx --config=packages/twenty-front/jest.config.mjs` fixes: https://discord.com/channels/1130383047699738754/1526887613867360347 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23106?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. --> |
||
|
|
9abf76f5f2 |
fix(ai): show answered ask_questions as a card in chat history (#23075)
## Context Feedback on the `ask_questions` (Ask AI) tool: - When answering a select prompt with a free-form message, the answer wasn't surfaced as expected in the conversation history. - The selected value also looked dropped once picked. Root cause: answered `ask_questions` parts were caught by the thinking-steps grouping and rendered as a generic collapsible "Ran ask_questions" tool step (JSON output), so the dedicated renderer was never reached. ## Changes - **Render answered questions as a card** (`AiChatQuestionStatusRenderer`): an "Answers" card that shows each full question with the chosen option label(s) or the free-text answer beneath it, instead of the faint inline `header: value` line. - **Free-text keyboard navigation** (`AiChatQuestionCard`): pressing Enter in the free-text area now advances to the next question, or submits when on the last question (mirroring the option-select flow). Shift+Enter still inserts a newline. ## Notes - No schema/GraphQL changes; display + interaction only. - Existing `thinkingStepsDisplayState` grouping test is unaffected (only `web_search`/`create_task`/`code_interpreter` are used there). <img width="421" height="301" alt="Screenshot 2026-07-20 at 17 48 04" src="https://github.com/user-attachments/assets/3845055d-7061-40c0-b263-aa1c670329cd" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23075?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. --> fixes https://discord.com/channels/1130383047699738754/1526871110300209282 |
||
|
|
05132d262b |
fix(workflow): show account select in Send Email node when no account is connected (#23066)
# Why In the workflow **Send Email** (and Draft Email) node, when the workspace has no eligible connected account, the **Account** field disappears entirely — only the variable picker icon remains. The "Add account" call-to-action is unreachable, so there is no way to connect an account from the node. ## Root cause Regression from #21075. `FormSelectFieldInput` used to always pass a default empty option ("No Account") to `<Select>`; since #21075 it only prepends it when `isNullable` is set, and the email account field doesn't set it. With zero connected accounts, `<Select>` then has no option to resolve a selected option from and bails out rendering an empty fragment — even though a `callToActionButton` is configured: ```tsx // Select.tsx if (!isDefined(controlSelectedOption)) { return <></>; } ``` # What changed `FormSelectFieldInput` now prepends the empty option whenever the field is nullable **or there are no options at all**. A populated non-nullable select still offers no clearing choice (the #21075 behavior is preserved); an empty one renders its "No X" state so the control — and its call-to-action — stay visible and clickable. # Test plan - New story `FormSelectFieldInput > NoOptionsWithCallToAction`: zero options + CTA renders the "No Work Policy" control, the dropdown opens, and the CTA is clickable. - New story `WorkflowEditActionEmailBase > NoConnectedAccounts`: with `MyConnectedAccounts` mocked to `[]`, the Account field renders "No Account" instead of vanishing. The meta's msw handlers move to the keyed-object form so the story can override a single query (story-level handler arrays get concatenated after the meta's, and msw's first match wins), and the story evicts the module-singleton Apollo client's cached accounts so it actually hits the empty mock. - `npx nx typecheck twenty-front` and `npx nx lint:diff-with-main twenty-front` pass; both story files pass under the storybook vitest project (13 stories total). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23066?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. --> |
||
|
|
3e14dcbb05 |
Fix record board column header action accessibility (#22496)
## Summary - Keep Record Board column header actions mounted instead of rendering them only on mouse hover - Show actions on hover and focus-within so keyboard users can reach them - Avoid header layout shifts when actions appear ## Context This is a small follow-up found while reviewing #22323. It does not duplicate the Kanban column drag-and-drop implementation. ## Testing - git diff --check - Not run: package lint/typecheck because this checkout still has no node_modules and Yarn is not available on PATH <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22496?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: bosiraphael <raphael.bosi@gmail.com> |
||
|
|
8d84a0b9f3 |
feat(app): allow non-admin developers to claim and list marketplace apps (#22621)
## Context Follow-up to #22609. Lets a non-admin developer claim ownership of a public Twenty app they published to npm, then request a marketplace listing that a server admin reviews. Marketplace state is per-instance for now. ## Claiming - Developer tab gets a **Claim an application** section: look up an unclaimed npm app by package name or universal identifier. - Ownership is proven with GitHub OAuth against the package's npm provenance (trusted publishing): the connected account must own the GitHub account or organization the package was published from. - Errors from the GitHub callback come back as a code and are shown inline with a link to the relevant documentation. - The old one-click claim stays admin-only. - A **Sync catalog** button triggers a catalog refresh instead of waiting for the hourly cron. - Gated behind the `IS_APP_CLAIMING_ENABLED` feature flag. ## Listing requests - Catalog-synced apps are created **unlisted**; a data migration unlists previously auto-listed unclaimed npm apps (owned or vetted rows are left untouched). - Owners request a listing from the Distribution tab (logo + description required); a server admin approves or rejects it from a **Listing requests** section in the Admin Panel. ## Screenshots <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/788d4362-97c4-4e42-810c-ef1f11517bec"/> <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/d6246190-c82a-4f64-87be-3bb668527645"/> <img width="1512" height="828" alt="image" src="https://github.com/user-attachments/assets/21a8dad4-610b-4d1f-8948-b9acab40d373"/> <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/58246130-41f7-451e-ae7f-57bd21d04bb6"/> --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
45b319ef2d |
Add autofocus to 2FA OTP inputs (#23067)
as title <img width="854" height="533" alt="image" src="https://github.com/user-attachments/assets/777c2d83-8318-4337-865d-67aebc9186c2" /> |
||
|
|
baa84bb2e0 |
Add auto-upgrade-in-apps (#23001)
We need to auto upgrade application, lets add this column in application entity, and add an admin button to autoupgrade all applications to latest app registrration version manually <img width="1131" height="372" alt="image" src="https://github.com/user-attachments/assets/4e755abc-38ad-4895-a2e8-d55ee1948ac2" /> <img width="906" height="533" alt="image" src="https://github.com/user-attachments/assets/ce002057-0341-4581-bf9a-66ac2bd84a9b" /> |
||
|
|
dd57842187 |
Show the onboarding welcome animation on sign-up only (#23057)
The welcome overlay replayed for existing users signing in, because it had no signal for "this user just signed up" and inferred it from a coincidence: a COMPLETED user standing on an onboarding URL while being redirected away. This drops that inference and triggers explicitly at the only two places onboarding reaches COMPLETED: `useSetNextOnboardingStatus` and the Stripe return. |
||
|
|
5bedd5b8cc |
feat(email-group): communications UX, per-record DNS status (#23002)
- Rename Communications label to singular, remove docs-home banner - Provision unsubscribe Cloudflare records at domain creation and surface per-record status badges; skip Cloudflare when not configured - Move sending-domain status into the section header and only show the records table when a record is unverified - Reply to the original recipients when replying to your own message - Fix DNS records table column/badge alignment; emit synthetic records in the log driver for local testing <img width="1496" height="849" alt="Screenshot 2026-07-17 at 7 47 24 PM" src="https://github.com/user-attachments/assets/a5a59adb-2df4-4154-98b2-acf87a8008da" /> |
||
|
|
7e133a4930 |
Converge email recipient fields on existing patterns: shared parser/formatter, search-index members, one display-name rule (#22997)
# Why Follow-up to #22668, addressing @charlesBochet's five post-merge review comments. They all point the same direction: the recipient fields rebuilt things the codebase already had. This PR converges on the existing patterns where that holds up, and answers on the threads where it deliberately does not. # What changed, per comment **Parser duplication ([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064242))**: `parseEmailAddressList` now lives in twenty-shared (addressparser, group flattening, try/catch). The server's `safeParseEmailAddresses` delegates to it, the front wrapper keeps only paste normalization (newlines to commas) and invalid-token preservation for red chips. The `addressparser` dependency moves from twenty-front to twenty-shared. Side effect worth knowing: RFC 5322 group members in inbound To/Cc headers were previously dropped entirely (group entries have no top-level address, so the filter removed them); flattening now imports those participants. Covered by a new regression test. **Formatter duplication ([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064243))**: `formatEmailAddress` (quote only when specials require it) lives in twenty-shared. The composer chips and the server's `formatMessageFromHeader` both delegate to it. The Gmail From header output is byte-identical: the name is mime-encoded first and encoded words never contain characters that trigger quoting. CodeQL then caught that the quoting (ported from the original front util) escaped quotes but not backslashes, letting a crafted name close the quoted string early; escaping now covers both as RFC 5322 quoted-pairs, with a containment test proving a hostile name cannot split into extra recipients on reparse. **Member search divergence ([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064231))**: suggestions now search WorkspaceMember through the search index in the same `useObjectRecordSearchRecords` call as Person (one ranked query), and enrich hits from `currentWorkspaceMembersState`, exactly like `SettingsRoleAssignmentWorkspaceMemberPickerDropdown`. The client-side `filterBySearchQuery` pass is gone. The hook is now what the comment described: the merge of context people, searched people, and members into one ranked list, rendered with the same `SelectableList`/`MenuItemAvatar` primitives the pickers use. Also fixed while in there: searched person ids are sliced to the suggestion limit before hydration, so top-ranked people can no longer be crowded out of the hydration page. **Chip resolution duplication ([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064236))**: the display-name preference is now one rule, `getEmailIdentityDisplayName`, used by both `getDisplayNameFromParticipant` (threads) and the composer chip/menu, so the same address renders identically everywhere. The order is workspace member, then person, then display name, then handle: when an address belongs to both a teammate and a Person record, the internal identity wins (product call from Felix). `BaseChip.maxLabelWidth` is renamed `maxWidth` to match the twenty-ui `Chip` API. `ParticipantChip` itself is not used inside the field: it renders a navigating `RecordChip` when a person is linked, and navigation from the composer destroys the draft (no draft persistence yet), plus the field chips need remove/selected/danger/edit affordances it does not have. **Rebuilding on MultiItemFieldInput ([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064221))**: answered on the thread rather than in code, deliberately. `MultiItemFieldInput` is a dropdown-panel list editor (vertical rows, one input at a time, bound to record-field contexts and `FieldMetadataType`), and its own TODO says the API should be refactored into a hook before growing. The inline wrapping chip row commits batches (paste), dedupes with a flash, and keeps a persistent inline input with suggestions; layering that through `renderItem`/`renderInput` would strain both components. On the menu overlap: after comparing side by side, the shared surface between `MultiItemFieldMenuItem`'s dropdown and the chip menu is three `MenuItem` rows with different copy, order, and neighbors; `MenuItem` is already the shared primitive, and a config-driven fragment would be indirection without deduplication. If deeper convergence is wanted, the honest path is the existing TODO (extract the multi-item state machine into a hook, rebase both editors on it); that touches the Links/Phones/Emails/Array/Files cell editors and deserves its own PR. # Verification - New twenty-shared suites for the parser and formatter (16 tests), including parse/format round-trips, the encoded-word case, and the backslash-escaping containment case. - Server messaging util specs all pass (70 tests), including new group-flattening regression tests; From-header spec output unchanged. - Front email module suites all pass (59 tests) with the slimmed wrappers. - Typecheck and lint green on twenty-shared, twenty-front, twenty-server; oxfmt clean on all three. - Playwright smoke against the seeded dev stack passes end to end: context suggestions on the Google company, typed search showing people and the workspace member row (now served by the search index), Enter picking the top suggestion, duplicate merge, keyboard delete, chip menu with clipboard copy, Ctrl+Enter committing the buffer then triggering send. |
||
|
|
dc0bb7760f |
fix(front): only sign out when token renewal is rejected by the server (#22983)
## Context Users are frequently signed out when coming back to Twenty. The console shows `Failed to renew token after retries, triggering unauthenticated error`: the access token has expired and the `renewToken` call fails. Today any renewal failure wipes the stored token pair and redirects to sign-in, even when the refresh token is still valid, for example when the renewal request hits a transient network failure (laptop waking up, VPN reconnecting) or a server restart during a deploy. Since the token pair state is synced across tabs, one failing tab signs out every tab. ## What this does - Only triggers the unauthenticated flow when the server definitively rejects the refresh token. The `renewToken` mutation maps those cases to `UNAUTHENTICATED` (expired or invalid JWT), `FORBIDDEN` (revoked) and `BAD_USER_INPUT` (unknown or malformed token). - Keeps the session on any other renewal failure (network errors after retries, server errors): the token pair stays in place and the next request triggers a fresh renewal attempt, so the session recovers once the server is reachable again. - Signs out immediately when the stored pair has no refresh token instead of attempting a renewal that cannot succeed. - Logs the renewal error, which was previously swallowed and made this class of logouts hard to diagnose. ## Tests - renews and replays the operation after an access token rejection - signs out when the server rejects the refresh token - keeps the session on a network error (asserts all retry attempts ran) and on a server error - signs out without attempting renewal when the stored pair has no refresh token Test mocks now reset between tests so per-test overrides cannot leak into other tests. [[Review in cubic](https://www.cubic.dev/buttons/review-in-cubic-dark.svg)](https://cubic.dev/pr/twentyhq/twenty/pull/22983?utm_source=github) |
||
|
|
5e27e04c0a |
Add mostly-empty field hints to data model settings (#22962)
## What
Fields that are empty in almost all records now show a subtle `Mostly
empty` hint next to their name in the object's Fields settings table
(same visual treatment as `Deactivated`), with a tooltip explaining the
signal and a matching **Mostly empty** toggle in the search filter
dropdown. The goal is to nudge admins to clean up and deactivate fields
nobody uses, while keeping the page untouched when the data model is
healthy.
## How
**No table scans.** Emptiness is read from Postgres planner statistics,
so the cost is a catalog lookup regardless of table size:
- `pg_class.reltuples` gates the feature on an approximate row count (≥
100 records; never-analyzed tables mean no hints). Reuses the shared
helper extracted from `ObjectRecordCountService`.
- `pg_stats.null_frac` plus the sampled frequency of the column type's
empty sentinel (`''` for text columns, `'{}'` for arrays, `'{}'`/`'[]'`
for json — matched per physical column type) gives a per-column empty
fraction. A value dominating ≥ 95% of a column is guaranteed to appear
in the most-common-values list, so the approximation is reliable exactly
at the threshold we care about.
**Decision rules** (pure util, unit-tested):
- Flag when every relevant column is ≥ 95% empty and the object has ≥
100 records.
- Skip system fields, the label identifier, relations, booleans, and
actor fields (exhaustive switch — a new `FieldMetadataType` fails to
compile until classified).
- Composite fields must have all their columns empty, with column sets
derived from `compositeTypeDefinitions`; only default-bearing code
columns (`currencyCode`, phone country/calling codes) are excluded so
stamped defaults don't mask emptiness.
- Anything unknown (missing stats, new column since last ANALYZE)
degrades to silence — no hint is ever shown on missing data.
**API:** one `mostlyEmptyFieldMetadataIds(objectMetadataId)` query on
the metadata schema, guarded by the `DATA_MODEL` settings permission,
fetched lazily when the fields page opens.
**UI:** exception-based — no new columns, no persistent controls. The
badge and the filter toggle only materialize when at least one field
qualifies, and disappear once things are cleaned up.
## Test
- Unit tests for the decision util (threshold,
system/label-identifier/inactive exclusion, missing statistics,
composite all-columns rule, links label/secondary data, currency
narrowing, excluded types).
- Catalog SQL validated against Postgres 16 with a table mimicking
Twenty's column shapes (text `''` defaults, enums, arrays, jsonb,
currency pairs), including the type-aware sentinel matching (a text
column full of literal `"{}"` strings does not count as empty).
- End-to-end on a seeded dev instance: 899 companies with a mix of
filled/empty fields — the API returned exactly the five fields predicted
by the raw statistics (`annualRevenue`, `employees`, `introVideo`,
`tagline`, `workPolicy`) and correctly excluded `address` (city 33%
filled), actor/system fields, and the label identifier.
- UI driven with Playwright: badge, tooltip copy, filter toggle, and
filtered table all verified visually.
- `lint:diff-with-main`, `typecheck` (server + front), and all three
`graphql:generate` configurations + SDK metadata client regenerated and
committed.
|
||
|
|
c6f0380070 |
Reuse onboarding container width for workspace selection (#22974)
Fix in https://github.com/twentyhq/twenty/pull/22965 was wrong, 440px wide is the new intended width for both signup forms. This PR reverts + does the correct fix. See figma as source of truth https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=1633-94880&p=f&m=dev ## Before <img width="431" height="450" alt="Screenshot 2026-07-16 at 18 14 26" src="https://github.com/user-attachments/assets/3f8788e1-3764-4326-867a-973a98e48007" /> ## After <img width="1030" height="898" alt="Screenshot 2026-07-17 at 08 24 38" src="https://github.com/user-attachments/assets/42e03215-f32c-4f61-8f2d-1b8ff966d31c" /> <img width="1028" height="900" alt="Screenshot 2026-07-17 at 08 24 25" src="https://github.com/user-attachments/assets/3a49142a-dc52-4097-96d2-05a4e641f574" /> |
||
|
|
6a1de47a17 |
Fix signup visual regression in workspace selection layout (#22965)
## Summary - Split the sign-in/up onboarding container styles so workspace selection can keep its wider layout without affecting the other auth states. - Reuse the base onboarding container for the non-selection flow to restore the intended visual structure. https://github.com/twentyhq/twenty/commit/566c3b662954de932677a3fefe69735a45fe55ae commit accidentally reused the 440px workspace-selection container for the global credential form ## Before <img width="643" height="496" alt="Screenshot 2026-07-16 at 18 14 35" src="https://github.com/user-attachments/assets/abff0779-a236-424f-9503-9182dab5fa3f" /> ## After <img width="510" height="509" alt="Screenshot 2026-07-16 at 18 11 58" src="https://github.com/user-attachments/assets/8c4693b2-827f-4bc8-a9ce-10171bbb7d0b" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22965?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. --> |
||
|
|
2a94736ece |
fix(metadata): resync metadata store via collection hashes on SSE reconnect (#22956)
## Context Follow-up to #22562 (merged), which fixed the SSE-gap durability hole from #22504 by calling `invalidateMetadataStore()` on SSE reconnect. In review, @Weiko and a second reviewer flagged a performance concern: firing a full invalidation on every reconnect can spam the backend, and reconnect frequency is unbounded (`retryAttempts: Infinity`). The steer was to lean on the per-collection hashes that `FindMinimalMetadata` already returns and refetch only what actually changed. ## Problem `invalidateMetadataStore()` sets `currentCollectionHash: undefined` for every entity key. The staleness check in `useLoadMinimalMetadata` is `entry.currentCollectionHash !== hash`, so nulling the hash makes **every** collection compare as stale. Result: each reconnect forces a full refetch of every metadata collection (objects, fields, views, ...), even when nothing changed during the gap. That defeats the collection-hash mechanism built to avoid exactly this. ## Change Add `useResyncMetadataStore`, which only bumps `metadataLoadedVersionState` without clearing collection hashes. `MinimalMetadataLoadEffect` already re-runs on a version change, so this triggers one `FindMinimalMetadata` query; the existing hash comparison then marks only genuinely-changed collections stale. `SSEClientEffect` now calls `resyncMetadataStore()` instead of `invalidateMetadataStore()` on reconnect. Net: same durability guarantee (changes missed during a disconnect are caught on reconnect), but cost per reconnect drops from "refetch everything" to "one lightweight hash query + refetch only what changed." ## Testing 1. Open a record page in a workspace. 2. Create a field / page-layout tab via the metadata API while the SSE stream is dropped (background the tab, kill the network briefly, or restart the server). 3. On reconnect the new metadata appears without a manual reload. 4. Reconnect with no metadata change triggers only a `FindMinimalMetadata` query and no collection refetch. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22956?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. --> |
||
|
|
8e3ec5b43d |
Propagate record card background to inline hover content (#22957)
## Summary - Introduce a shared `--record-card-background-color` CSS variable on record cards - Reuse that variable for hovered inline cell content so the hover portal matches the card background state - Preserve selected, focused, and active background transitions without duplicating background logic ### Before <img width="196" height="337" alt="Screenshot 2026-07-16 at 16 03 40" src="https://github.com/user-attachments/assets/b74bfb24-0144-4a8c-b8a4-b56768e84d66" /> <img width="219" height="357" alt="Screenshot 2026-07-16 at 16 03 25" src="https://github.com/user-attachments/assets/19d052cb-8c8c-49c9-b3af-0178c53c0c0a" /> ### After <img width="189" height="372" alt="Screenshot 2026-07-16 at 16 03 52" src="https://github.com/user-attachments/assets/289e4186-d418-44dc-93d4-70fa47e50cf2" /> <img width="180" height="333" alt="Screenshot 2026-07-16 at 16 03 00" src="https://github.com/user-attachments/assets/b874e0c4-5840-4b7e-918c-d441c50fa487" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22957?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. --> |
||
|
|
5588ddf829 |
Fix delete/destroy/restore record commands on pages without a record index (#22952)
## Bug On a standalone page (`/page/:pageLayoutId` — a custom app page or standalone page layout), opening a record in the side panel and running **Delete** from the Options menu fails with an error toast: > Record index ID and object metadata are required to delete records The record is not deleted. The same guard breaks **Destroy** and **Restore**. ## Root cause `buildHeadlessCommandContextApi` only derives `recordIndexId` when the context store holds a `currentViewId`. On standalone pages there is no view, and `useOpenRecordInSidePanel` copies that null view id into the side panel context, so the delete/destroy/restore commands throw at mount — before executing anything. The throw is caught by `CommandMenuItemErrorBoundary` and surfaces as the toast (also reported to Sentry). The commands only use `recordIndexId` to reset table row selection and remove records from the record board — cleanup that is meaningless when no record index is on screen. The mutation itself only needs `objectMetadataItem` and the graphql filter, which are both available. ## Fix - Keep throwing when `objectMetadataItem` is missing (genuinely required). - Make `recordIndexId` optional: pass the existing `PLACEHOLDER_RECORD_INDEX_ID` to the selection hooks (they must be called unconditionally) and skip the selection cleanup at execute time when there is no record index — same pattern `useResetRecordIndexSelection` already uses. The constant is extracted to a shared file. ## Verified - **Bug path**: on a standalone page, opened a record in the side panel via search, ran Delete Task from the Options menu → record soft-deleted (checked `deletedAt` in DB), side panel closed, no error toast, no console error. - **Regression**: on the tasks index table, selected a row and ran Delete Task from the command menu → record deleted, row removed, table selection reset, no errors. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22952?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. --> |
||
|
|
a7de3ce3a5 |
Allow non-compact all-day calendar cards to show full content (#22953)
## Summary - Render all-day calendar items as full `RecordCalendarCard` content in non-compact views, while keeping compact cards clickable as a whole. - Rework the all-day time grid layout so the label and day cells align cleanly in a dedicated grid row. - Add coverage for the new card behavior and for filtering out `DATE_TIME` records from the all-day lane. ### Week (compact) <img width="1308" height="812" alt="Screenshot 2026-07-16 at 15 30 02" src="https://github.com/user-attachments/assets/24f74f22-86c1-4326-8c65-92ee2c3e8c92" /> ### Week (non compact) **NEW** <img width="1311" height="789" alt="Screenshot 2026-07-16 at 15 29 52" src="https://github.com/user-attachments/assets/8445c8c5-c952-47e9-ba24-c21d63352e79" /> ### Month <img width="1310" height="822" alt="Screenshot 2026-07-16 at 15 29 41" src="https://github.com/user-attachments/assets/aa33cf48-c602-49e6-bfb1-b9ab1c798bcb" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22953?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. --> |
||
|
|
18b746d525 |
fix(metadata): invalidate metadata store on SSE reconnect (#22504) (#22562)
## Fixes Fixes #22504 ## Description This PR fixes a bug where metadata updates (like creating a new field or a page layout) were missed if an open tab was temporarily disconnected from the server (e.g., tab backgrounded or a network blip). Because the frontend's metadata store relies on live SSE deltas for freshness, any gap in the connection meant the new metadata would never reach the client unless a full reload occurred. This often resulted in "No Data" states for newly created layouts or widgets. **Changes:** - Updated `SSEClientEffect.tsx` to call `invalidateMetadataStore()` upon a successful SSE reconnection. - Re-syncing the metadata store on reconnect ensures that any events missed during the disconnected gap are retrieved durably without requiring a manual page refresh. ## Testing 1. Open a record page tab in a workspace. 2. From an app front component or DevTools, trigger a field creation via the metadata API (`createOneField` / `page-layout` mutations). 3. Briefly disconnect the network or restart the server so the SSE stream drops during the mutation. 4. Re-establish the connection. 5. The tab should automatically refetch the metadata and reflect the new field/layout without needing a manual reload, instead of getting stuck in a "No Data" state. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22562?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. --> |
||
|
|
7ec7774087 |
Fix infinite redirect loop when logging out of a suspended workspace (#22949)
## Problem
Clicking **Log out** on a workspace suspended for a past-due
subscription locks the tab into an infinite redirect loop that hammers
the server with unauthenticated GraphQL requests until the tab is
closed.
Reproduced on cloud: workspace with `Pro plan • Past due` (activation
status `SUSPENDED`), user forced onto `/settings/billing`, click Log out
→ tab freezes, URL flip-flops between `/welcome` and
`/settings/billing`, requests stream out continuously.
## Root cause
`clearSession` nulls the `tokenPairState` atom and removes the persisted
session localStorage keys, but leaves the **in-memory**
`currentWorkspaceState`/`currentUserState` atoms populated, relying on
the subsequent `window.location.assign('/welcome')` reload to reset
them.
`PageChangeEffect` keeps running until that reload commits, and the
intermediate state (no token + suspended workspace) makes
`usePageChangeEffectNavigateLocation` ping-pong:
- on `/settings/billing`: no token → navigate to `/welcome`
- on `/welcome`: the no-token guard is skipped (`SignInUp` is in
`ONGOING_USER_CREATION_PATHS`), then `isWorkspaceSuspended` reads the
**stale** workspace atom → navigate back to `/settings/billing`
The synchronous navigation loop pegs the main thread, so the pending
full-page navigation never commits and the loop never resets. Every
bounce remounts pages whose queries refire without a token (each one
erroring `UNAUTHENTICATED`), plus Sentry envelopes — the server spam.
Verified during repro: mid-loop the tab had `tokenPairState="null"` and
no `currentWorkspaceState` in localStorage (the loop runs fully
unauthenticated off the in-memory atom), and an injected
`localStorage.setItem` wrapper survived the whole loop, proving the page
never reloaded.
## Fix
Clear the same in-memory auth atoms in `clearSession` that
`onUnauthenticatedError` (useApolloFactory) already clears:
`currentUserState`, `currentWorkspaceState`,
`currentWorkspaceMemberState`, `currentUserWorkspaceState`. With the
workspace atom gone, the suspended guard can't fire after logout, the
ping-pong never starts, and the redirect to `/welcome` commits normally.
## Test
Extended the `useAuth` sign-out test: seeds a suspended
`currentWorkspaceState` and a `currentUserState` before `signOut()` and
asserts both are null afterwards.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22949?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. -->
|
||
|
|
8cf462d5f7 |
Add day view support to record calendar (#22922)
## Summary - Add a calendar day view and wire it into the record calendar layout selection - Update the top bar, time grid, and week/day drag and drop handling to support the new view - Extend supported layout logic and public feature flags for calendar day view access - Add coverage for calendar view content, calendar container behavior, top bar behavior, day view rendering, supported layout resolution, and week event drop handling <img width="1276" height="852" alt="Screenshot 2026-07-15 at 17 48 33" src="https://github.com/user-attachments/assets/b1d9d255-2d64-4adb-82b9-3e500cb0d561" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22922?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. --> |
||
|
|
2dcf53619f |
fix(front): keep kanban header columns full-width when board overflows viewport (#22926)
## Problem Before <img width="1340" height="542" alt="Capture d’écran 2026-07-15 à 18 26 22" src="https://github.com/user-attachments/assets/472dfe13-336a-43c7-8986-6cdcb04e5217" /> After <img width="1340" height="542" alt="Capture d’écran 2026-07-15 à 18 26 12" src="https://github.com/user-attachments/assets/e393a3fa-2245-42cc-b965-f16f3feaeff6" /> The record board (Kanban) header breaks when the screen is narrower than the total column width. The stage header columns squish together to fit the viewport while the cards below keep their fixed 220px width and scroll horizontally, so the header labels no longer line up with their columns. Regression from #22323. ## Cause #22323 wrapped each column header in `DragDropColumnSortableCell`. In `fill` mode its `StyledSortableRoot` uses `min-width: 0` with the default `flex-shrink: 1`, so the header cells collapse below their column width when the board is wider than the viewport, instead of overflowing into horizontal scroll like the body. ## Fix Pin `flex-shrink: 0` in `fill` mode so header cells keep their column width and overflow in step with the body. `fill` is board-only; the record table header uses non-fill mode and is unaffected. ## Testing Opportunities → "By Stage" Kanban, narrow the window below the total column width: header stays aligned with the cards and scrolls horizontally with them. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22926?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. --> |
||
|
|
8e03921372 |
Add CREATED workspace activation status (read path + enum migration) (#22904)
## Context Since v2 onboarding (#22303), workspaces are activated **before** the billing plan step (now the last onboarding step). Users abandoning at the plan step leave ACTIVE workspaces with a Stripe customer but no subscription (~60–110/day on cloud, 935+ so far), and no cleanup mechanism ever touches them: billing webhooks never fire (no subscription), the suspended-workspaces cron only handles SUSPENDED, the onboarding cron only handles PENDING_CREATION/ONGOING_CREATION. Target lifecycle (across two PRs): `PENDING_CREATION → ONGOING_CREATION → CREATED → ACTIVE → SUSPENDED → deleted`. **`CREATED`** = the workspace schema is provisioned but onboarding is not complete — no billing subscription yet. It is **not** considered active: | Concern | CREATED behavior | |---|---| | Sign-in / invited teammates joining | allowed (invite-team step precedes the plan step) | | Member + metadata loading (app shell) | allowed (user must finish onboarding) | | Permissions | real permission checks (no PENDING-style bypass) | | Version upgrades / workspace migrations | **included** (schema must not drift) | | Messaging/calendar/workflow/etc. crons | **excluded** — no background processing until a plan is chosen | | PLAN_REQUIRED onboarding lock | unchanged (still derived from subscription existence) | ## What this PR does (read path only) The enum addition ships as a **slow** instance command, which can run after deploy — so nothing in this PR ever **writes** `CREATED`. The write path (setting it at activation, the cleanup sweep, the backfill of the existing zombie cohort) is a follow-up PR that ships once this migration has run everywhere. - **twenty-shared**: `CREATED` enum value; `PROVISIONED_WORKSPACE_ACTIVATION_STATUSES` + `isWorkspaceProvisioned` ("schema exists": CREATED | ACTIVE | SUSPENDED), replacing `isWorkspaceActiveOrSuspended` — all call sites (server member loading, access-token workspace-member lookup, front metadata-store gates) meant "has schema/members". - **Slow instance command** (2.22.0): swaps `core.workspace_activationStatus_enum` using the rename→recreate→alter-column idiom. The CHECK constraints on `core.workspace` embed casts to the enum type and would break the swap — the command captures them from `pg_constraint`, drops them, swaps the type, and restores them. - **Pre-migration-safe queries**: Postgres rejects `IN ('CREATED', ...)` when the enum value does not exist yet — even for reads, and the instance-command runner itself queries provisioned workspaces before migrating (a fresh database could never initialize). All provisioned-status filters go through a new `activationStatusIn` util comparing on `"activationStatus"::text`, valid before and after the migration. - **Upgrade path**: workspace iterator, command runner, upgrade-status and workspace-version services iterate CREATED workspaces. Since they now cover more than ACTIVE/SUSPENDED, the stale names were renamed to `ProvisionedWorkspaceCommandRunner`, `hasProvisionedWorkspaces`, `getProvisionedWorkspaceIds`, `loadProvisionedWorkspaces` (the mechanical import rename in old version-command dirs is why this PR carries the `ci:allow-previous-version-upgrade-mutation` label). - **Sign-in**: `throwIfWorkspaceIsNotReadyForSignInUp` accepts CREATED so invited members can join during onboarding (join authorization itself is unchanged — enforced upstream in `checkAccessForSignIn`); `activateWorkspace` idempotent-retry accepts CREATED as a terminal state. - **Transitions out of CREATED** (only write ACTIVE — safe to ship now, dead until the write path lands): the Stripe webhook reactivation branch also promotes CREATED, and `syncSubscriptionToDatabase` promotes synchronously; both gated on `WORKSPACE_ACTIVATING_SUBSCRIPTION_STATUSES` (Active/Trialing — extracted from `shouldReactivateWorkspace`, behavior-preserving) so an `incomplete` subscription created by the payment-intent flow before payment never promotes the workspace. - Deliberately untouched: all background crons, permission guards, JWT strategy, PLAN_REQUIRED logic, admin panel (renders the raw status string). ## Follow-up PR (after this migration has run) 1. `activateWorkspace` sets `hasWorkspaceAnySubscription ? ACTIVE : CREATED` (billing disabled → always ACTIVE, self-hosted unchanged). 2. Cleanup: suspend CREATED workspaces older than N days (config var), handing them to the existing suspended pipeline (warn → soft-delete → destroy). 3. Backfill: cloud-only slow command moving ACTIVE workspaces with no billingSubscription row (created since Jul 1) to CREATED. ## Verification - Migration exercised against a real database via the command class: up → down → up; `enum_range` and `pg_get_constraintdef` checked after each step (constraints restored against the new type, `DEFAULT 'INACTIVE'` preserved). - Pre-migration safety exercised for real: with the migration rolled back (enum without CREATED), `run-instance-commands` — the exact fresh-database CI path that failed before the `::text` fix — completes cleanly. - End-to-end with a workspace manually set to CREATED and the branch server+front running: sign-in issues tokens, `currentUser` loads workspaceMember(s), the full app loads with no console errors; GraphQL returns `activationStatus: CREATED`. - Workspace creation ran end-to-end locally in **both billing modes** on this branch: - billing disabled: signup → workspace creation → ACTIVE immediately → onboarding completes with no plan step → app loads (unchanged behavior); - billing enabled (Stripe test mode): signup creates the Stripe customer eagerly → activation ends ACTIVE → subscription-less workspace is pinned to the plan-required page → no-card trial checkout creates a `trialing` subscription via `createDirectSubscription`/`syncSubscriptionToDatabase` → app loads. - `twenty-shared` unit tests, server specs on touched services, `lint:diff-with-main` and `typecheck` for shared/server/front all green; full CI green. |
||
|
|
25bd2897a3 |
Add weekly layout to record calendar (#22819)
## Summary - Add a week layout to record calendar views and persist the selected layout. - Render `DATE` calendars as an all-day week and `DATE_TIME` calendars as an hourly week. - Add an optional end date field across calendar configuration, metadata, persistence, and complete-view upserts. - Use configured end values for ranged and multi-day events, with a one-hour fallback when a `DATE_TIME` end is absent or invalid. - Keep calendar cards consistent with the existing compact view, including checkbox selection and whole-card record opening. - Gate the weekly layout and end-date behavior behind the public Labs `IS_CALENDAR_WEEK_VIEW_ENABLED` workspace feature flag. ## Week interactions - Show overlapping timed events side by side and cap the visible records at two per day. - Display start and end times on timed cards, enforce a readable 30-minute minimum height, and keep today’s text contrast stronger. - Drag timed events between days and times with 30-minute snapping while preserving their duration, including zero-duration events. - Show a create button when hovering a 30-minute slot; keyboard users can focus a day, move the slot with the arrow keys, and reach the same contextual action. - Initialize new records with the selected slot time and a compatible writable end value one hour later. - Show the workspace time zone and current-time indicator in timed weeks; date-only weeks keep the all-day section without an hourly grid. ## Configuration and data loading - Only allow end fields that match the start field type, and prevent selecting the same field for both boundaries. - Load records whose ranges overlap the visible period so month and week layouts display the same relevant records. - Resolve and persist calendar end fields when updating existing views through `upsert_complete_view`. - Fall back to Month and ignore the configured end field while the flag is disabled, without overwriting either persisted setting, so re-enabling restores the previous configuration. - Expose the flag in Labs and keep it default-off for workspaces without a stored value; enable it in the development seeder. <img width="1285" height="808" alt="Screenshot 2026-07-15 at 15 50 17" src="https://github.com/user-attachments/assets/b7e3f7f1-ca77-492f-8cce-cca186ebca0b" /> |
||
|
|
0dbae2eda3 |
Address #22827 review comments and converge application file endpoints (#22868)
Follow-up to #22827, addressing the review comments left around merge time and applying the endpoint convergence discussed afterwards. ## Review comments from #22827 - **Swallowed error in dev sync asset read** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571513265)): the swallow is intentional (a missing public asset must not fail the whole dev sync) but it now logs a warning with the asset path and error, and the registration keeps its previously stored file for that path instead of losing it. - **`isAbsoluteUrl` location** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571524234)): moved to `twenty-shared/utils/url`. The server, and now also `twenty-sdk`'s `normalize-application-assets`, use the shared util. - **Soft delete vs file cleanup** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571589558)): per review, deleting a registration is now a hard delete. Stored assets (bytes + rows) are deleted with it, dependent rows are removed by their existing FK cascades, and installed applications keep working with their registration link nulled. No soft-delete/cron mechanism. - **Asset cap too generous** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571595745)): lowered to 10MB per review and documented in the publishing and public-assets docs pages. - **One missing image retriggers a full asset sync** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571646243)): `storeRegistrationAssets` now takes `skipAlreadyStoredPaths`; the catalog sync passes it when the package version is unchanged, so only assets missing a stored file are fetched instead of re-downloading everything. - **`existing.logo` already contains the new logo** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571667847)): correct, `updateFromManifest` runs first, so the previous "keep fileId when the path did not change" guard compared the new logo against itself. The fileId preservation is now keyed on the stored server file for the exact path (files are unique per `(applicationRegistrationId, path)`): a changed logo path no longer inherits the old file's id, and a transient download failure on an unchanged path still keeps the working file. This also removed the fileId-preservation bookkeeping from `storeRegistrationAssets`. ## Endpoint convergence - **Path-addressed public route for registration assets**: `GET /file/server/application-registration/:fileId` is replaced by `GET /files/application-registrations/:registrationId/*path`, mirroring the manifest's public-folder paths and leaving room for a future `:version` segment. Assets stay addressable by stable ids server-side; the fileId now only marks a path as stored. No URL is ever persisted (all are built at query time), and the old route never shipped in a release, so there is nothing to migrate. - **`Application.logoUrl` resolved server-side**: new `ResolveField` on the `Application` type builds the `/public-assets/...` display URL (or passes absolute URLs through). `useApplicationChipData` now reads it from `currentWorkspace.installedApplications`, and the frontend `buildApplicationLogoUrl` util is deleted, so clients no longer construct file URLs themselves. ## Validation - Unit: `file.controller.spec` (route renamed, traversal case added), `server-file-storage.service.spec` (`findServerFile`, `deleteByApplicationRegistrationId`), `application-registration-asset-url.service.spec` (new URL shape, url-encoding), new `isAbsoluteUrl` test; all application/file suites pass. - Live against a local server: new route serves tarball and rehosted npm assets with `public, max-age=3600` (nested paths included), 404s on missing files, unknown registrations, traversal attempts, and the removed old route; `findManyApplicationRegistrations` returns path-addressed URLs for stored assets, CDN fallback for npm, absolute passthrough; `installedApplications.logoUrl` resolves the public-assets URL and stays null for logo-less apps. Registration hard delete verified against the DB: file rows cascade, application rows keep a nulled registration link. - Typecheck + lint on twenty-server, twenty-front, twenty-shared, twenty-sdk; metadata codegen and client-sdk regenerated. |
||
|
|
f4ff234db8 |
feat: make record avatar/icon resolution data-driven via a configurable image identifier field (#22644)
## Summary Today the avatar/icon shown for a record is hardcoded per object — Company pulls a favicon from its domain link, Person uses `avatarUrl`, etc. This PR replaces that hardcoding with a generic, data-driven abstraction based on a configurable **image identifier field** on each object's metadata (mirroring the existing **label identifier** concept). An object's image identifier can point to: - a **`FILES`** field → the uploaded image is used directly (rounded avatar), or - a **`LINKS`** field → a favicon is derived from the primary URL via the Twenty icons service (squared avatar), gated by `ALLOW_REQUESTS_TO_TWENTY_ICONS`. This lets any object type (Opportunity, a custom "Listing", etc.) define its own avatar/icon without code changes, and makes the field configurable/overridable for standard objects. ## ❓ Open question: also allow `TEXT` → direct image URL? Right now the image identifier is restricted to `FILES` (uploaded file) and `LINKS` (favicon). We deliberately left out `TEXT` → **direct image URL** (e.g. an imported/synced photo URL stored in a text field). There's precedent for it — Person's avatar was originally a `TEXT` `avatarUrl`, and WorkspaceMember still is — and it's unambiguous (a `TEXT` field has no favicon-vs-image ambiguity, and selecting it as the image identifier is itself the declaration of intent). It's a small, clean extension: - add `TEXT` to the allowed image-identifier types, - add an explicit `TEXT → raw URL` case - `getAvatarType`: `TEXT → rounded`. Caveats: it relies on admin assertion that the text values are image URLs (no data-level guarantee), and external image URLs load third-party content in the browser (IP-leak/hotlinking, same as favicons — a proxy/cache would be the more robust long-term answer). ### ✅ Resolution Decision: **we will not support `TEXT` as an image identifier.** Image identifiers stay restricted to `FILES` and `LINKS`, and any other type fails closed (returns no avatar) on both the frontend and backend. Instead, the legacy items that still rely on a `TEXT` avatar — Person's deprecated `avatarUrl` and WorkspaceMember's `avatarUrl` — will be migrated to `FILE` fields in a follow-up PR. Until then, WorkspaceMember remains an exception (its `avatarUrl` still resolves through the existing CorePicture path), and legacy Person `avatarUrl` values that haven't been migrated will show initials placeholders. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22644?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. --> |
||
|
|
4e83a64f81 |
fix(front): read fresh metadata in SSE update path to avoid false unknown-field warnings (#22897)
## Problem Sentry `warning`: *"SSE update event for person carried fields unknown to this tab's metadata: lastInboundAt, pdlCertifications, pdlBirthYear, …"* ([TWENTY-FRONT issue](https://twenty-v7.sentry.io/issues/7610855477)). The listed fields are all custom fields created by the **People Data Labs app** (`packages/twenty-apps/public/people-data-labs`). The flow that triggers this: 1. The app installs a batch of `person` fields (emits metadata SSE events). 2. Its enrichment logic-function updates a person record with all of those fields (emits a record-update SSE event). Field creation already emits metadata SSE events, and the front applies them **synchronously** to the Jotai metadata store (`MetadataStoreSSEEffect` → `applyChanges` → `store.set`). So the store converges. The bug is that the SSE **record-update** handler doesn't read the converged store. `useTriggerOptimisticEffectFromSseUpdateEvents` reads `objectMetadataItems` from a React/Jotai closure captured at render time. The long-lived SSE subscription in `useTriggerEventStreamCreation` holds a metadata snapshot that lags the store, so even after the field-create events have been applied, the record path still sees the old field set. It then: - flags the new fields as "unknown" and logs to Sentry, and - **drops those field values** from the optimistic update (`getUnknownRecordInputFields` filters them out), so already-loaded views miss the enriched data until a refetch. Metadata events are dispatched before record events within each SSE message (`useTriggerEventStreamCreation` lines 126-128), and the store update is synchronous, so a fresh store read at processing time sees fields that converged in the same or any earlier message. ## Fix Read `objectMetadataItems` fresh from the Jotai store at event-processing time instead of from the render closure, and re-resolve the object metadata item from that fresh list. This eliminates the false-positive warnings and stops dropping legitimately-known field values. Follows the design from #22474: the metadata-event pipeline owns schema convergence; the record pipeline just reads the converged store (now actually reading the current store rather than a stale snapshot). A genuine race where the record update truly precedes the field-create event is still tolerated and still warns. ## Testing - `nx typecheck twenty-front` passes - `nx lint:diff-with-main twenty-front` passes <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22897?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. --> |
||
|
|
055e8b5335 |
Add tab param to open a record side panel page on a specific tab (#22905)
## Context
`CommandOpenSidePanelPage` (and `openSidePanelPage` in the front
component SDK) could open a record in the side panel, but always landed
on the default tab. This adds an optional `tab` param to the
`ViewRecord` page params so an app command can open a record directly on
a specific tab.
## What changed
- **twenty-sdk**: `OpenSidePanelPageParams` `ViewRecord` variant accepts
an optional `tab` (a page layout tab id). Since
`CommandOpenSidePanelPage` props are `OpenSidePanelPageParams`, the
component picks it up automatically.
- **twenty-front**:
- New `setRecordPageActiveTabId` util resolves the record page layout
for the object (custom layout from the store, or the default layout id)
and presets `activeTabIdComponentState` on the tab list instance
(`${pageLayoutId}-tab-list-${recordId}`), which is shared by the side
panel and the full record page.
- `useOpenRecordInSidePanel` accepts `tab` and presets the active tab
before navigating; it also applies when the record is already open in
the side panel (tab switch only).
- `useFrontComponentExecutionContext` forwards `tab` to the side panel
open, and presets the tab when falling back to full-page navigation
(mobile, or objects that can't open in the side panel).
- **Docs**: mention the optional `tab` id in the
`CommandOpenSidePanelPage` description.
Unknown tab ids are harmless: `PageLayoutTabListEffect` falls back to
the layout's default tab when the preset id doesn't exist in the layout.
Dashboards are skipped since their layout id comes from record data, not
object metadata.
## Tests
- `useOpenRecordInSidePanel`: new test asserting the active tab atom is
preset on the correct tab list instance id.
- `useFrontComponentExecutionContext`: new tests for tab passthrough to
the side panel and tab preset on full-page fallback.
- `npx nx typecheck twenty-front`, `typecheck twenty-sdk`,
`lint:diff-with-main twenty-front`, `lint twenty-sdk` all green.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VaWY1H9RZqgJkytqZakvZi)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22905?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. -->
|
||
|
|
75d9e0b93a |
fix(front): constrain 2FA sign-in screens and dedupe their shared shell (#22886)
## Problem On the card-less onboarding sign-in (`/welcome`), the **2FA verification** step renders with a full-viewport-wide submit button. The 2FA **verify** and **provision** forms hard-code `width: 100%` on their root `StyledForm`. That was harmless while `/welcome` rendered inside the `AuthModal` `medium` card, which bounded the width. Since #22398 removed v1 onboarding, `/welcome` renders card-less under `BlankLayout`, so nothing bounds those forms and they stretch to the full viewport. Other steps are **not** affected, which is why only 2FA looks wrong: - Sign-in form: root sets `width: ONBOARDING_CONTENT_BLOCK_WIDTH; max-width: 100%` -> capped at 440. - SSO selection / workspace-scope: base container (`min-width: 240`, no width) -> shrink-to-fit. - **2FA verify / provision: `width: 100%` -> full viewport.** ## Fix Cap the two 2FA forms at `ONBOARDING_CONTENT_BLOCK_WIDTH` (with `max-width: 100%`), so they sit in the same block as the sign-in page instead of forcing full-width. ## Refactor (same PR) The verify and provision components (both introduced together in #13141) duplicated their layout shell. Extracted the shared instruction-text and main-content blocks into `SignInUpTwoFactorAuthenticationStyles.ts`. The form container stays local to each component since the element differs (a `div` in provision, a `form` in verification). ## Verification - Reproduced the flex box-model at 1280px: `width:100%` root -> 1196px full-width button; `width: 440px` -> 440px centered button. - lint + format + typecheck pass on the changed files. |
||
|
|
fd7935f343 |
fix(front): allow dismissing [Credits limit reached] banner (#22843)
Dismiss is session-only, he banner reappears on reload, because the underlying "out of credits" condition is still true closes #22774 fix: <img width="1222" height="147" alt="image" src="https://github.com/user-attachments/assets/082e68c1-d121-4e31-b261-386de8231896" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22843?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. --> |
||
|
|
5426536004 |
Fix(Twenty-front): Junction field unable to display as table (#22844)
Fixes: #22783 The junction field can now be displayed as a table. <img width="1911" height="961" alt="image" src="https://github.com/user-attachments/assets/2aad15af-baca-4563-85eb-ef0826008a7d" /> |
||
|
|
94192a2164 |
Resolve application registration logo and gallery image urls at query time (#22827)
## Context
Application registration logo and gallery image urls were baked into the
stored manifest and display columns at write time, with each source flow
doing it differently: npm catalog sync baked CDN urls, local dev sync
baked `public-assets` urls, and tarball uploads left raw manifest paths
that never displayed in the UI. The entity also carried a `logoUrl`
getter computed field.
This moves url generation to query time, the same way the workspace logo
works.
## What changed
**Read side**
- `ApplicationRegistrationAssetUrlService` builds display urls when
queried: stored files are served by fileId, absolute urls pass through
untouched, and not-yet-rehosted npm assets fall back to the registry CDN
from `sourcePackage@latestAvailableVersion`.
- The `logoUrl` getter on `ApplicationRegistrationEntity` is replaced by
`logoUrl` and `galleryImages` `@ResolveField`s on the metadata resolver,
the admin panel resolver, and a new resolver for
`ApplicationRegistrationSummary` (used by
`Application.applicationRegistration`).
- The marketplace detail/card DTOs and the public OAuth authorize DTO
(`findApplicationRegistrationByClientId`) go through the same url
builder.
- New public route `GET /file/application-registration/:id` streams
registration server files (these are instance-global marketplace assets,
also shown on the public OAuth authorize page).
`ServerFileStorageService.readServerFileById` now returns the mime type
alongside the stream.
**Write side**
- New `logoFileId` column on `applicationRegistration` (2.21 fast
instance command, constraint names match TypeORM naming), complementing
the fileIds already stored in the `galleryImages` jsonb.
- `ApplicationRegistrationAssetService` copies the manifest logo and
gallery images into instance-global server file storage, so all three
sources behave the same:
- **TARBALL**: from the uploaded package (previously only gallery images
were stored, never the logo).
- **LOCAL**: dev sync reads the already-uploaded public assets from
workspace storage (the CLI uploads files before syncing).
- **NPM**: catalog sync downloads the assets from the registry CDN.
Downloads are skipped when the package version is unchanged and the
files are already stored; failed or pending downloads fall back to CDN
urls at query time.
- Write-time url rewriting is removed
(`ManifestAssetUrlResolverService`, `resolveManifestAssetUrls`);
manifests now keep raw asset paths. Existing rows with baked absolute
urls keep working through the absolute-url passthrough, so no backfill
is needed.
- `updateFromManifest` and `upsertFromCatalog` preserve stored gallery
fileIds for unchanged paths, so installs and the hourly catalog sync no
longer clobber them.
## How it was verified
Against a local Postgres/Redis with the server running:
- Fresh database init runs the new instance command; column and FK/UQ
constraint names match TypeORM's generated names, and the CI
pending-migration check produces no diff.
- `findManyApplicationRegistrations { logoUrl galleryImages }` returns
fileId-served urls for a TARBALL registration (absolute urls passed
through), and null/[] for a LOCAL registration without assets.
- Ran `marketplace:catalog-sync` against the real npm registry: 14
packages synced, logos and gallery images rehosted from unpkg with
fileIds set; a second run re-downloaded nothing (version-unchanged
skip); `findMarketplaceAppDetail` for `twenty-linear` returns
fileId-served urls for the logo and all four gallery images.
- `GET /file/application-registration/:id` serves stored files with the
right content type (png and svg verified), 404s on unknown ids, and the
token-guarded generic `/file/:folder/:id` route still returns 403
without a token.
- Unit tests for the url builder and the assets-stored check; server
unit test suites for the application module pass; typecheck and lint
clean.
|
||
|
|
8e022d3c49 |
Fix stale relation table in field widget when switching records in side panel (#22829)
The relation table rendered by a FIELD widget in TABLE display mode kept its jotai component states (loaded rows, virtualization maps, loading guards, query identifiers) in instances keyed only by widget id and view id. Since the side panel record pages share those instances across records, switching to another record kept rendering the previous record's related rows until an asynchronous catch-up reload landed, and any race or error in that catch-up left the previous record's data on screen permanently. Scope the record-table widget's context store instance and record index instance by target record id (and side panel surface), the same way FieldsWidget already scopes its field list instances. Each record now gets its own table state, so a record's rows can never appear under another record, and loads that land after a record switch write into their own instance instead of the visible one. loadRecordIndexStates and setRecordGroupsFromViewGroups accept an optional recordIndexId override so the widget view load effect can populate the record-scoped instance instead of deriving the shared one from object name and view id. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22829?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. --> |
||
|
|
6201d06141 |
Preload Stripe.js before the onboarding payment step (#22858)
## Context On the plan-required onboarding step, the card form was slow to appear because Stripe.js is loaded lazily (`@stripe/stripe-js/pure`): the script download only started once the payment page rendered, and the PaymentElement iframe could only boot after that. ## What this does - Adds `usePreloadStripeForPlanRequiredStep`, called once from `OnboardingStepLayout` (the shared layout for the authenticated onboarding step routes), so Stripe.js is already loaded by the time the user reaches the payment step. The hook only triggers when billing is enabled, the workspace has no subscription yet, and a publishable key is configured, so self-hosted instances still never contact Stripe. - Moves the memoized loader to `settings/billing/utils/getStripePromise.ts`, shared by `useStripePromise` and the preload hook. - Stops caching failed script loads: previously a rejected `loadStripe` promise stayed in the cache forever, which would have made a failed preload permanently break the payment form. Now a later call retries (stripe-js re-injects the script tag on retry). - Extracts the plan-required predicate into `onboarding/utils/getIsPlanRequired.ts`, now shared with `useSetNextOnboardingStatus`. The in-app add-credit-card modal is intentionally left untouched: it has no preceding step to preload from. ## Tests - `getStripePromise.test.ts`: dedup per publishable key, retry after a failed load. - `usePreloadStripeForPlanRequiredStep.test.ts`: preloads when billing is enabled and no subscription exists; skips when billing is disabled, a subscription exists, or the key is missing. --- _Generated by [Claude Code](https://claude.ai/code/session_01NtUN99tHPZ6bPWwpYKMbpE)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22858?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. --> |
||
|
|
7381038452 |
Paginate admin panel app registrations list (#22734)
## Context The `findAllApplicationRegistrations` query on the admin panel Apps page (`/settings/admin-panel#apps`) loaded every application registration at once, with search and filtering done client-side. ## Changes **Server** - `findAllApplicationRegistrations` now takes `limit` / `offset` / `searchTerm` / `isPreInstalledOnly` args and returns a `PaginatedApplicationRegistrations` object (`registrations`, `totalCount`, `hasMore`), following the same pattern as `getQueueJobs`. - `ApplicationRegistrationService.findAll` uses `findAndCount` with `take`/`skip`, and moves the search (name, source package, universal identifier via `ILIKE`) and the pre-installed filter into the SQL query, mirroring how `getInstalledWorkspacesGlobal` filters installed workspaces. **Frontend** - `SettingsAdminApps` passes the page, the debounced search term (300ms, like the installed workspaces table), and the pre-installed toggle as query variables instead of filtering client-side. - Adds a Previous / Next pagination footer (25 per page) matching the queue jobs table, shown only when there is more than one page. - The "unconfigured first" ordering is kept within each page (`isConfigured` is a dataloader-resolved field, so it can't be sorted in SQL). ## Notes - Regenerated `generated-admin/graphql.ts` follows in a subsequent commit. --- _Generated by [Claude Code](https://claude.ai/code/session_015erumgPozkbNA3zPeKrrFW)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22734?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: Weiko <corentin@twenty.com> |
||
|
|
b0dc637dbd |
Throw proper error on duplicate emailing domain (#22790)
Adding an emailing domain that already exists blew up with a raw QueryFailedError and the client just saw a generic "An error occurred". The unique index on domain is global, so the workspace-scoped existence check never caught rows owned by another workspace. Now the check is unscoped and throws an EmailingDomainException mapped to CONFLICT with a proper user-facing message, in both the createEmailingDomain mutation and the email group channel flow. Also dropped the hardcoded catch-all snackbar on the new channel page so server messages actually reach the user. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22790?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. --> |
||
|
|
7babc049f5 |
[Twenty-Front]: Record board column drag and drop functionality (#22323)
Closes #22321 - Used `@dnd-kit` library for core drag and drop logic. - Tried to keep as much similar to #21304 as possible. https://github.com/user-attachments/assets/bff100cf-d727-4281-a5fa-b010a373f189 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22323?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: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com> |
||
|
|
60f5964c64 |
Run front components in a sandboxed opaque-origin iframe (#22588)
Front components run untrusted third-party React in a Web Worker. That
worker previously shared the host origin, so it could reach
origin-scoped storage (the metadata-store IndexedDB, the
`twenty-sign-out` BroadcastChannel), cookies, and same-origin resources.
This runs the worker inside a `sandbox="allow-scripts"` (no
`allow-same-origin`) iframe, giving it an opaque origin where the
browser denies localStorage, cookies, IndexedDB, and BroadcastChannel
outright. The worker is kept inside the iframe (rather than a bare
iframe) so untrusted code always runs off the main thread; the
remote-dom render path is unchanged.
- **Transport:** host ↔ iframe ↔ worker over a re-transferred
`MessagePort` (`ThreadMessagePort`); a small bootstrap script is inlined
into the iframe via `srcdoc` (bundled at build time by a prebuild step)
and relays the port to the worker it spawns. Messages across the
boundary use a typed discriminated union with a single parse/guard.
- **Network:** under the opaque origin, direct fetches to the Twenty API
would be `Origin: null`, so the component source and SDK modules are
fetched through an allowlisted, credential-omitting `hostFetch` bridge
and blobbed inside the worker. The allowlist is single-sourced on the
host (http(s) origins only) and carried in the render context. The
bridge is mandatory (rendering fails closed if it is missing), refuses
redirects except for GET/HEAD to the known file-storage URLs, and caps
response body size.
- **SDK loading:** SDK client modules now load inside the worker through
the bridge, replacing the host-side SDK-blob state/effect/provider with
a pure `getSdkClientUrls` URL builder.
- **Isolation tests:** a unit test locks the sandbox attribute
(`allow-scripts`, never `allow-same-origin`); a browser test asserts the
worker actually gets an opaque origin with storage denied, probing
cookies by writing one rather than reading an empty jar.
Also adds a "List Companies" seed front component that queries workspace
data via the SDK client (exercising the bridge end-to-end),
single-sources the command-menu confirmation-modal result event name and
detail type in `twenty-shared` (previously a hand-synced duplicate), and
decomposes the renderer (bridge, sandbox, worker orchestration) into
small single-purpose utils with unit tests.
## How it works
```mermaid
sequenceDiagram
autonumber
participant Host as Host window (twenty-front · host origin)
participant Frame as Sandboxed iframe (allow-scripts · opaque origin)
participant Worker as Worker (untrusted component · opaque origin)
participant API as Twenty API (host origin)
rect rgb(238,242,248)
Note over Host,Worker: 1 — Boot handshake
Host->>Frame: create iframe sandbox="allow-scripts", srcdoc = inlined bootstrap script
Host->>Host: MessageChannel + ThreadMessagePort(port1)<br/>exports = host API + hostFetch
Frame-->>Host: READY
Host->>Frame: INIT + transfer port2
Frame->>Worker: spawn inlined Worker + re-transfer port2
Worker->>Worker: ThreadMessagePort(port)<br/>exports = render / updateContext
Note over Host,Worker: Port now entangles Host ↔ Worker directly
end
rect rgb(246,240,248)
Note over Host,Worker: 2 — Render
Host->>Worker: render(connection, { componentUrl, sdkClientUrls, hostFetchOrigins, token })
Worker->>Worker: override globalThis.fetch<br/>(Twenty origins → hostFetch)
end
rect rgb(248,244,238)
Note over Worker,API: 3 — Network via hostFetch bridge (opaque Origin:null cannot reach the API directly)
Worker->>Host: hostFetch(componentUrl, Bearer)
Host->>Host: origin allowlist + credentials:'omit'
Host->>API: fetch(componentUrl)
API-->>Host: source
Host-->>Worker: { status, headers, body }
Worker->>Host: hostFetch(sdkClientUrls.core / .metadata)
Host-->>Worker: SDK module sources
Worker->>Worker: blob each source in its own opaque origin → import() → run untrusted React
end
rect rgb(238,248,242)
Note over Worker,Host: 4 — Render mirror
Worker->>Host: remote-dom mutations (RemoteConnection)
Host->>Host: RemoteReceiver → RemoteRootRenderer → host DOM
end
Note over Worker: Opaque origin ⇒ browser denies localStorage,<br/>cookies, IndexedDB, BroadcastChannel
```
|
||
|
|
f667ba500c |
fix(front): clean stale morph relations from metadata store on object deletion (#22681)
## Problem After deleting a custom object (e.g. `meeting`), the app crashes with "Sorry, something went wrong" on pages that load records referencing that object through a morph relation. The console shows: ``` Target object metadata item not found for target (morph target meeting) ``` It reproduces on the machine that used the object before deletion but not on a fresh machine, which points at a stale client metadata store rather than a server issue. ## Root cause Every field carries its own server-provided `morphRelations` array; a morph relation field (note/task/timeline targets, etc.) lists every object it can point to, including the deleted one. When an object is deleted, `useDeleteOneObjectMetadataItem` and the SSE `delete` handler only remove the deleted **object** and its own fields from the metadata store. The sibling morph fields on other objects keep their now-dangling `morphRelations` entry pointing at the deleted object. Those stale entries were only meant to be cleaned up later by a collection-hash-triggered `network-only` refetch. When that reconciliation does not win, `generateDepthRecordGqlFieldsFromFields` can't resolve the deleted morph target in `objectMetadataItems` and throws, crashing the page. ## Fix Clean `morphRelations` entries referencing the deleted object from the field metadata store at deletion time, so the store stays self-consistent immediately instead of relying on an async refetch. Applied in both paths that handle object deletion: - `useDeleteOneObjectMetadataItem` (the client performing the deletion) - `MetadataStoreSSEEffect` delete handler (other tabs/clients receiving the event) The throw in `generateDepthRecordGqlFieldsFromFields` is intentionally left in place so any genuine future metadata inconsistency still surfaces rather than being silently swallowed. ## Test Added a unit test for the cleaning util covering: morph relations targeting the deleted object are removed, only changed fields are returned, non-morph fields are untouched, and nothing is returned when no relation targets the deleted object. |
||
|
|
b682162f31 |
fix(front): render +N button for right-edge-clipped chips in expandable list (#22748)
## Problem Fixes #22383. Overflowing relation & multi-select chip cells didn't show the "+N" overflow button, so hidden records/values were unreachable. There were several distinct causes behind this, addressed below. Before <img width="263" height="34" alt="Capture d’écran 2026-07-10 à 11 43 13" src="https://github.com/user-attachments/assets/1001d47b-0f54-4fbd-a5cb-83a5c32c35ac" /> After <img width="263" height="34" alt="Capture d’écran 2026-07-10 à 11 41 54" src="https://github.com/user-attachments/assets/d4f5cf2f-fda4-422e-b72d-2df3fc81a06f" /> ## Changes **1. Overflow detection missed right-edge-clipped chips** `isFirstOverflowingChildElement` used `childElement.offsetLeft > containerElement.clientWidth` (left edge past the container), which is false when a chip is only right-edge clipped. Switched to a right-edge check: `childElement.offsetLeft + childElement.offsetWidth > containerElement.clientWidth`. **2. Multi-select never used the overflow list when unfocused** `MultiSelectFieldDisplay` rendered a plain clipped `MultiSelectDisplay` when not focused, and only used `ExpandableList` on focus. That non-focused fallback painted over the focused "+N" and hid it. It now always renders through `ExpandableList` with `isChipCountDisplayed={isFocused}`, matching `RelationFromManyFieldDisplay`, so the count shows on focus only (not idle). **3. The hover portal was never actually focused** `FieldFocusContextProvider` silently ignored its `isFocused` prop (`({ children }: any)` + hard-coded `useState(false)`), so `RecordInlineCellAnchoredPortal`'s `<FieldFocusContextProvider isFocused={true}>` had no effect and the hovered cell's display always saw `isFocused=false`. That's why "+N" never appeared on hover for any multi-value field. The portal now uses the existing `FieldFocusStaticFocusedProvider`, fulfilling the intent — so relations, multi-select, emails and phones all surface their "+N" on hover. **4. "+N" was hard to read on multi-select** The "+N" chip has a transparent background (shared component), and the hover portal let the base layer's colored option chips bleed through it. Gave the hover portal content an opaque `background.primary` so nothing bleeds through; the "+N" component itself is untouched, so relations/emails/phones keep their existing look. ## Test plan - Hover a relation or multi-select field whose values overflow the cell: the "+N" button appears (and is readable), and clicking it lists the hidden records. When not hovered, no "+N" shows. - Verify fully-overflowing rows still show the correct "+N" count. |