1a9f786e428fa5b4a98983d9a3adea5ed8335b95
4739 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1a9f786e42 |
refactor(filters): pass fieldMetadataItems array to dispatcher (#20737)
## Summary Alternative to #20717. Same goal (clean up the filter dispatcher API after #20670) but smaller and follows the codebase's "pass data, not behavior" style. The dispatcher takes a `fieldMetadataItems: FieldShared[]` array directly instead of a `findFieldMetadataItemById: (id) => FieldShared | undefined` callback. The util builds the id lookup internally — once per call, used for both source-field and relation-target-field lookups. No new types, no separate hydration step. ## What changes **`twenty-shared`** - `computeRecordGqlOperationFilter` / `turnRecordFilterIntoRecordGqlOperationFilter` / `turnRecordFilterGroupsIntoGqlOperationFilter`: replace `findFieldMetadataItemById` param with `fieldMetadataItems` / `fieldMetadataItemById` (internal Map). - Remove the exported `FindFieldMetadataItemById` type. - `turnAnyFieldFilterIntoRecordGqlFilter`: rename its internal `fieldById` Map for consistency. - Tests updated to pass arrays. **Frontend (15 call sites)** - Switch from `fieldMetadataItemByIdMapSelector` to `flattenedFieldMetadataItemsSelector`. - Pass `fieldMetadataItems: flattenedFieldMetadataItems` to the dispatcher. - `useFindManyRecordsSelectedInContextStore` keeps the Map selector because it still does a per-filter lookup for the soft-delete check. **Server (5 call sites)** - Pass `Object.values(flatFieldMetadataMaps.byUniversalIdentifier).filter(isDefined)`. ## Why this over #20717 #20717 moves resolution into a separate hydration step + introduces a `HydratedRecordFilter` type. The bug that #20717 originally surfaced was Sentry catching 4 critical runtime errors during review (`fieldMetadataItemByIdMap` declared but not passed). The added type and the explicit hydration boundary are extra surface area for not much benefit — the existing API was a callback wrapping a Map at every call site, and the natural simplification is to just pass the Map (or its array) directly. Net diff: **196 insertions, 203 deletions** (~7 lines net removed). 32 files. ## Test plan - [x] Shared filter unit tests pass (461 tests) - [x] Frontend filter/context-store tests pass (13 tests) - [x] Frontend typecheck passes - [x] Server typecheck passes - [x] Lint passes (frontend + server) - [ ] Integration tests on #20670 still pass — workflow find-records + chart-data with relation-traversal filter still work end-to-end through the new array param |
||
|
|
d5ff9eb515 |
Create twenty app improvements (#20688)
create-twenty-app updates: - remove --example option - sync --once when scaffolding an applicaiton - rename --api-url option to --workspace-url - create a standalone page when scaffolding an app <img width="1494" height="765" alt="image" src="https://github.com/user-attachments/assets/0e35ed0c-b0aa-466c-9f56-7939294fd2cf" /> --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> |
||
|
|
57f13c9b92 |
[CONNECTED_ACCOUNT_BREAKING_CHANGE] Encrypt ConnectedAccount connectionParameters (#20673)
# Introduction Prevent any cross user `connectedAccount` `connectionParamaters` leak Also encrypt in db all `connectionParameters` password Never return any password through `DTO` anymore The settings now allow update mutation without providing the password in edition mode Verified all `connectionParameters.password` interaction ## Integration tests - Added more coverage for both failing and successful paths - Introduced a new env var that allow bypass the provider connection test ## Legacy connected Account decryption support Stop allowing non encrypted decryption on `accessToken` and `refreshToken`, only allow legacy decryption on refactored `connectionParameters` ## Upsert ownership Completely got rid of the connected workspace schema context which is legacy Also now a user can only upsert a connected account for him only.. ## New UI <img width="1770" height="1852" alt="image" src="https://github.com/user-attachments/assets/55c1dc89-42ff-4084-95e2-cc5f9e23753b" /> If in edition the password is by default disabled It needs to be selected as being edited to be enabled ## Next - Refactor tool permissions flag not to include connected accounts - Remove the legacy connected standard object - Refactor and improve connected account resolver auth |
||
|
|
72c0c36db5 |
fix(twenty-front): prevent connected account row overflow on long status label (#20713)
Reproducible on German language Before <img width="638" height="262" alt="SCR-20260519-ofhi" src="https://github.com/user-attachments/assets/633ddd9a-203d-472a-bf29-379d6f088e80" /> After <img width="806" height="434" alt="SCR-20260519-oesa" src="https://github.com/user-attachments/assets/ca37c06b-e439-4156-8447-0e75a5f3fe9c" /> /closes #20594 |
||
|
|
3281d37bdf |
Fix(twenty-front): BlockNote slash command shows empty state when no match (#20689)
Fixes: #20625 Original PR: #20626 New changes: - Now the text says "Close menu" instead of "No command found". - "Close menu" is interactive like other commands ( With keyboard and with mouse ). - It closes automatically when user type 3 extra character past the point of no result. https://github.com/user-attachments/assets/9c1315b4-5a63-424d-8da8-dc1283535725 --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
291ce5ccdb |
fix(filters): make filter dispatcher own relation-target resolution (#20670)
## Summary Two relation-traversal bugs surfaced post-merge of #20533, both rooted in the same architectural smell: the GraphQL filter dispatcher took a flat `fields: FieldShared[]` array and silently dropped any filter whose `relationTargetFieldMetadataId` wasn't in that array. Callers had to remember to pre-augment the list with relation targets — and 16+ call sites did not all know this. This PR fixes both bugs and removes the smell. ### Bug 1 — Save as new view loses the relation target `useCreateViewFromCurrentView` built the create-filter input without `relationTargetFieldMetadataId`. The saved view's filter persisted without the traversal — on reload the chip showed "Company contains 'air'" instead of "Company → Name contains 'air'". Discarded at save time, not at read time. Fix: include `relationTargetFieldMetadataId` in the create input. (Commit 1.) ### Bug 2 — Workflow Search Records drops one-hop traversals `FindRecordsWorkflowAction` built its fields list from `flatObjectMetadata.fieldIds` only (source object's fields). The shared dispatcher then couldn't resolve the relation target field on the related object and silently dropped the filter — a configured "People where Company → Name Contains 'Airbnb'" came through as `{ and: [] }`. This was the same shape as bugs already fixed in 5 other call sites (chart filters, view filters, record table, etc.). The pattern was: caller forgets to augment fields → dispatcher silently drops the filter. Fix (commit 2): change the dispatcher to take a `findFieldMetadataItemById: (id) => FieldShared | undefined` resolver callback. Both source-field and relation-target-field lookups go through the same resolver, so callers no longer need to know about the augmentation requirement. Frontend callers pass a workspace-wide resolver built from `flattenedFieldMetadataItemsSelector`; server callers wrap `findFlatEntityByIdInFlatEntityMaps` on `flatFieldMetadataMaps`. In both cases relation-target lookups just work, because the resolver can see fields on related objects. ## Why this matters Before: "if you call the dispatcher, pre-augment your fields list with relation targets, or filters get silently dropped." An invariant only enforceable by code review, broken often enough to ship two user-visible bugs in one week. After: the dispatcher resolves field ids itself. There's no list to forget to augment. The failure mode (filter silently dropped) becomes structurally impossible at the dispatcher boundary. Net diff: 240 insertions, 319 deletions. Removed `augmentFieldsWithRelationTargets` (frontend) and the workflow whack-a-mole code (server). ## Test plan - [ ] Save view: create an advanced filter using a one-hop relation traversal, click "Save as new view", reload, confirm the chip still reads "Source → Target operator value" - [ ] Workflow: configure a Search Records action with a relation-traversal filter, run the workflow, confirm the filter is actually applied - [ ] Dashboard chart: configure a chart with a relation-traversal filter, confirm the chart data respects it - [ ] Record table, group-by, calendar, total count, footer aggregates: all continue to work with both plain and relation-traversal filters |
||
|
|
e6b7f31fea |
fix(front): prevent standalone page layout crash from useTargetRecord (#20698)
## Context
Reported in production on `engineering.twenty.com` — standalone page
layouts (e.g. "Release overview") crash with the React error boundary
fallback ("Sorry, something went wrong"). The console shows:
```
Error: useTargetRecord must be used within a record page context (targetRecordIdentifier is required)
```
The minified stack trace points at `SidePanelToggleButto…`, but that's
just the bundle chunk name — the actual call site is
`PageLayoutTabsRenderer`.
## Root cause
#19296 added an unconditional `useTargetRecord()` call inside
`PageLayoutTabsRenderer` so it could read the target object's metadata
and hide tabs whose widgets reference deactivated relations:
```ts
const targetRecord = useTargetRecord();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: targetRecord.targetObjectNameSingular,
});
```
But `PageLayoutTabsRenderer` runs on **both** record pages and
standalone pages. On standalone pages, `StandalonePageLayoutPage`
intentionally sets `targetRecordIdentifier: undefined` in
`LayoutRenderingProvider`, which makes `useTargetRecord()` throw — and
the follow-up `useObjectMetadataItem()` would also throw on miss.
|
||
|
|
89579f5225 |
fix(ai-chat) - upload files (#20681)
closes https://github.com/twentyhq/twenty/issues/20437 bonus : persist file filename for UI display |
||
|
|
7c252ff233 |
Fix 19026 deactivated relation unassignable (#19296)
PR to fix the bug #19026 This PR will ensure that if an object has some relation deactivated, the relation will not be visible in the side panel tab and will not be assignable in the deactivated relation. ## Notes deactivated in People <img width="1068" height="1106" alt="image" src="https://github.com/user-attachments/assets/e8c2dbf3-5391-4dbc-8e40-79fcc44e8158" /> ## Notes not visible in the side panel of people <img width="2390" height="892" alt="image" src="https://github.com/user-attachments/assets/308a78aa-2c6d-4d3d-b67c-b49795839aae" /> --------- Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr> |
||
|
|
4ba9c0ca0b |
[Navigation Drawer] Multiple fixes in settings and app drawer (#20634)
closes - https://discord.com/channels/1130383047699738754/1487720717192527942 https://github.com/user-attachments/assets/6db2df8b-be01-4b5f-a958-575d87b41559 ~~waiting on @Bonapara 's feedback!~~ |
||
|
|
db0547f503 |
[1/3] Rename permissionFlag to rolePermissionFlag + add permissionFlag catalog/backfill (#20481)
Split of #20377. ## Summary This PR separates available permission flags from per-role permission flag grants. Previously, `core.permissionFlag` stored the role assignment directly: `roleId + flag`. This PR renames that legacy grant table to `core.rolePermissionFlag`, then recreates `core.permissionFlag` as the catalog of available permission flags. ## What changed - Rename the existing `core.permissionFlag` grant table to `core.rolePermissionFlag`. - Add the new syncable `core.permissionFlag` catalog entity with key, label, description, icon, permission type, relevance flags, and custom/standard metadata. - Add stable `SystemPermissionFlag` universal identifiers for the built-in `PermissionFlagType` values. - Seed the standard permission flags for every workspace under the Twenty standard application. - Backfill existing role grants: - create missing catalog rows for existing grant keys, - add `rolePermissionFlag.permissionFlagId`, - migrate grants from the old string `flag` column to the new catalog FK, - replace the old `(flag, roleId)` uniqueness with `(permissionFlagId, roleId)`. - Rewire role permission flag caches, permission checks, role DTO mapping, and `upsertPermissionFlags` to resolve through the catalog. - Keep the existing public role permission API shape: product/app surfaces still talk about `permissionFlags` and return `{ id, roleId, flag }`. - Update metadata flat-entity machinery, migration builders, validators, action handlers, snapshots, generated schemas, docs, and app fixtures for the new `permissionFlag` / `rolePermissionFlag` split. ## Behavior after this PR - Existing permission flag grants keep working. - Existing GraphQL role permission flows keep the same public naming. - Standard permission flags are represented as catalog rows. - Permission checks now compare grants through catalog universal identifiers instead of the legacy `flag` column. - Workspace deletion cleanup now verifies both `permissionFlag` and `rolePermissionFlag`. ## What is not in this PR - Public GraphQL CRUD for custom permission flags. - App manifest support for declaring new custom permission flags. - Frontend UI for creating or assigning custom permission flags beyond the existing role permission flow. --------- Co-authored-by: Weiko <corentin@twenty.com> |
||
|
|
45ac3e8218 |
fix(front): align currency icon vertically with amount text (#20646)
## Summary - Replaced inline `<span>` wrapping the currency icon with a Linaria styled component using `display: flex` and `align-items: center` - The icon was misaligned with the amount text in table views and settings because the inline span didn't vertically center the SVG icon ## Changes - `packages/twenty-front/src/modules/ui/field/display/components/CurrencyDisplay.tsx` Fixes #20640 |
||
|
|
140dceebd1 |
fix(front): use theme-aware color for side panel title (#20645)
## Summary
- Added `color: ${themeCssVariables.font.color.primary}` to
`StyledPageInfoTitleContainer` in `SidePanelPageInfoLayout.tsx`
- The "Update records" title had no explicit color, so it didn't adapt
to dark mode and was nearly invisible against the dark background
- Now correctly uses the theme-aware primary font color
## Changes
-
`packages/twenty-front/src/modules/side-panel/components/SidePanelPageInfoLayout.tsx`
Fixes #20627
|
||
|
|
6b49a14b9f |
feat(auth): set 50-character maximum length on passwords (#20655)
## Summary - Cap password length at 50 characters in the shared regex used by sign-up, password reset, and password change (both `twenty-front` and `twenty-server`). - Update the user-facing validation message on sign-up and password reset to mention both the 8 min and 50 max bounds. - Extend the `PASSWORD_REGEX` unit test to cover the new upper bound. The cap also prevents unbounded inputs from reaching bcrypt, which silently truncates passwords above 72 bytes and can mask user-visible bugs. ## Test plan - [x] `npx jest src/modules/auth/utils/__tests__/passwordRegex.test.ts` passes (8-char min and 50-char max). - [ ] Sign up with a 51-character password — form rejects with "Password must be between 8 and 50 characters". - [ ] Sign up with an 8–50 character password — succeeds. - [ ] Password reset rejects a 51-character password with the same message. - [ ] Existing users with longer passwords (if any pre-exist) can still sign in (the regex only gates write paths: sign-up, change, reset). |
||
|
|
c938fbf4d6 |
feat(twenty-front): relation traversal in filter dropdown (stacked) (#20533)
**Stacked on #20527** https://github.com/user-attachments/assets/48995655-401a-4c35-8094-e88da8408bdd ## Summary Surfaces the one-hop relation traversal added in #20527 through the existing **composite sub-field dropdown pattern**. Clicking a MANY_TO_ONE relation field in the "+ Filter" picker now opens the same second-level dropdown that composite fields (FULL_NAME, ADDRESS, CURRENCY, etc.) already use — populated with the target object's filterable fields. Picking one (e.g. `Company → Name`) builds a filter that serializes to the nested GraphQL filter the backend now accepts: `{ company: { name: { ilike: "%X%" } } }`. No new components. The whole feature reuses `AdvancedFilterSubFieldSelectMenu` + the existing `subFieldNameUsedInDropdownComponentState` + the existing `MenuItem hasSubMenu` indicator. Only the conditions that gate the sub-menu (and the sub-menu's content for relations) were broadened. ## What landed | File | Change | |---|---| | `ObjectFilterDropdownFilterSelectMenuItem` | Sub-menu chevron now shows on MANY_TO_ONE relations (`isManyToOneRelationField` util). | | `AdvancedFilterFieldSelectMenu` | Relation clicks open the sub-menu alongside composite clicks. | | `AdvancedFilterSubFieldSelectMenu` | New branch: when the sub-menu type is `'RELATION'`, render the target object's filterable fields via `useFilterableFieldMetadataItems(targetObjectMetadataId)`. Composite logic untouched. | | `objectFilterDropdownSubMenuFieldType` state | Widened to accept a `'RELATION'` sentinel. Role-permissions sub-field menu narrows it back out (it doesn't traverse relations). | | `useSelectFieldUsedInAdvancedFilterDropdown` | New optional `targetFieldMetadataItem` arg. When present, the stored RecordFilter's `type` is the target field's type so the operand picker and value input render the target's operands (`'TEXT'` operators when filtering `company.name`, etc.). | | `turnRecordFilterIntoGqlOperationFilter` (shared) | When the filter targets a `RELATION` field with a `subFieldName`, synthesize a field-metadata for the target, recurse to build the inner filter, then wrap it under the relation field's name → `{ relationName: { targetFieldName: { ...operator } } }`. | `RecordFilter.subFieldName` stays narrowly typed as `CompositeFieldSubFieldName` so the wide downstream consumers (`shouldShowFilterTextInput`, composite handlers in the serializer, etc.) don't change. The relation target field's name is stored through a narrowly-scoped cast at the dropdown's storage point — the serializer checks `filter.type === 'RELATION'` before interpreting it as a target field name, so the cast can't be mis-read by composite-only code paths. ## Test plan - [ ] Open a table view on People, click "+ Filter", click "Company" → sub-menu opens with Company's filterable fields - [ ] Pick "Name" → operand picker shows TEXT operators (Contains, Equals, …) - [ ] Type "Airbnb" → filter applies, table shows people whose company name contains "Airbnb" - [ ] Verify network tab: the GraphQL filter variable is `{ company: { name: { ilike: "%Airbnb%" } } }` - [ ] Same flow with a composite target field (e.g. `Company → annualRecurringRevenue → amountMicros`) — should work end-to-end (backend supports composite-within-relation; #20527 has an integration test covering this) - [ ] Composite fields (FULL_NAME, ADDRESS) still open their normal sub-menu and filter correctly — no regression - [ ] Role-permissions field-select sub-field menu is unaffected (it bails out early on the RELATION sentinel) ## Out of scope - ONE_TO_MANY traversal (no backend support yet) - Aggregates (`people.count > 5`) - Persisting relation-traversal filters into a saved view (ViewFilter has no `relationPath` column yet; that's a separate slice) - REST API DSL changes - AI Tools 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
75b9b2fe5d |
feat(admin-panel): signing keys management tab with usage tracking (#20586)
## Summary - Adds a new admin-only **Security** tab to the Admin Panel (alongside General/Apps/AI/Config/Health) containing a **Signing Keys** section. The tab is intentionally introduced now so the upcoming **Encryption rotation** work can land as a sibling section. - Lists every JWT signing key with key id, `createdAt`, `revokedAt`, current/active/revoked status, and a **7-day verification count** read from Redis. A trailing row aggregates **legacy HS256** verifications so it is clear when the deprecated path is still in use. - Lets an admin **revoke** a public key. Revoking the current key drops `isCurrent`, sets `revokedAt`, nulls the encrypted `privateKey` and clears the in-process cached current key; the existing lazy path in `JwtKeyManagerService.getCurrentSigningKey()` then mints a fresh current key on the next sign. ## Backend - `SigningKeyVerifyCounterService` — bucketed Redis counter under the existing `EngineMetrics` namespace. 1-day UTC-aligned buckets, 8-day TTL refreshed on every increment, batched read via `mget`. Failures are swallowed and logged at `warn` so a Redis hiccup cannot break auth. - `JwtWrapperService.verifyJwtToken` records verifies **after success** for both ES256 (`kid` as identifier) and HS256 (the literal `legacy` identifier). - `JwtKeyManagerService.listSigningKeys()` and `revokeSigningKey(id)`: list ordered by `isCurrent DESC, createdAt DESC`; revoke is idempotent, validates the UUID, invalidates the public-key cache, and resets the cached current-key promise. - `AdminPanelResolver.getSigningKeys` (query) and `revokeSigningKey` (mutation) are both decorated with `@UseGuards(AdminPanelGuard)` so they are admin-only, like the 35 existing admin-only methods on this resolver. `privateKey` is never returned over GraphQL. ## Frontend - New `SECURITY` tab id wired into `SettingsAdminContent` and `SettingsAdminTabContent` (gated by `canAccessFullAdminPanel`). - `SettingsAdminSecurity` / `SettingsAdminSigningKeysTable` strictly reuse existing admin-panel components: `Section`, `H2Title`, `Table`/`TableRow`/`TableCell`/`TableHeader` from `@/ui/layout/table`, `Tag`/`Button` from `twenty-ui`, and `ConfirmationModal` mirroring the queue retry/delete modals. Only one minimal styled helper for the monospaced UUID rendering. - `useRevokeSigningKey` uses `useApolloAdminClient`, refetches `GetSigningKeys`, shows success/error snackbars (same pattern as `useRetryJobs`/`useDeleteJobs`). <img width="1293" height="881" alt="image" src="https://github.com/user-attachments/assets/7cf98664-950b-4451-af85-27781a8e9a9c" /> |
||
|
|
663ef332ad |
feat(auth): resume workspace selection on /welcome with valid tokenPair cookie (#20575)
## Summary After a user completes a multi-workspace social-SSO sign-in, [auth.service.ts:988-1011](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts#L988-L1011) issues a **workspace-agnostic** access + refresh token pair and lands them on `app.twenty.com/welcome?tokenPair=…`. [SignInUpGlobalScopeFormEffect.tsx](packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect.tsx) reads the URL param, writes the cookie, pushes them to `SignInUpStep.WorkspaceSelection`. The problem: if the user revisits `app.twenty.com/welcome` later (e.g. ChatGPT pings `/authorize` and the global page-change effect redirects them to `/welcome` with `returnToPath=/authorize?…`), the existing branch is a no-op — the URL param is gone. The user sees the regular email/SSO form and has to re-authenticate, even though the workspace-agnostic cookie is still valid. This PR adds a second branch in the same `useEffect` that handles the "valid cookie, no URL param" case: ```ts if (signInUpStep !== SignInUpStep.Init) return; if (!hasAccessTokenPair) return; loadCurrentUser(); setSignInUpStep(SignInUpStep.WorkspaceSelection); ``` Single `useEffect`, no `useRef`, no async then/catch. The synchronous `setSignInUpStep(WorkspaceSelection)` is the gate — once the step transitions, subsequent effect runs early-return. Mirrors the existing URL-param branch's pattern exactly. If the cookie is stale, `loadCurrentUser` triggers Apollo's renewal middleware. Renewal of a workspace-agnostic refresh token is supported end-to-end (verified in audit, see below) — if it succeeds the user sees their workspaces; if both tokens are expired, `onUnauthenticatedError` clears the cookie and the next render lands them on the regular sign-in form. Same fallback as if the cookie had never been there. ## Behavior matrix | State on /welcome mount | Before | After | |---|---|---| | No tokenPair anywhere | Show sign-in form | Show sign-in form | | tokenPair in URL (just bounced from SSO) | Set tokens → WorkspaceSelection | (unchanged) Set tokens → WorkspaceSelection | | tokenPair in cookie, access valid | Show sign-in form ❌ | **→ WorkspaceSelection ✓** | | tokenPair in cookie, access expired, refresh valid | Show sign-in form (Apollo eventually 401s on a query) | Renewal succeeds silently → WorkspaceSelection ✓ | | tokenPair in cookie, both expired | Show sign-in form | `onUnauthenticatedError` clears cookie → fall back to sign-in form | ## Workspace-agnostic renewal: confirmed working end-to-end Audit summary: - **Refresh token carries the type**: [refresh-token.service.ts:104](packages/twenty-server/src/engine/core-modules/auth/token/services/refresh-token.service.ts) preserves `targetedTokenType` in the JWT payload and returns it from `verifyRefreshToken`. - **Renewal branches on type** ([renew-token.service.ts:70-87](packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.ts)): ```ts const accessToken = isDefined(authProvider) && targetedTokenType === JwtTokenTypeEnum.WORKSPACE_AGNOSTIC && !isDefined(workspaceId) ? await this.workspaceAgnosticTokenService.generateWorkspaceAgnosticToken({...}) : await this.accessTokenService.generateAccessToken({...}); ``` Renewed refresh token preserves `targetedTokenType` (line 93). - **Resolver is workspace-agnostic**: `@UseGuards(PublicEndpointGuard, NoPermissionGuard)` on `renewToken` ([auth.resolver.ts:796-804](packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts)) — no `@AuthWorkspace()` requirement, callable from `app.twenty.com`. - **Frontend middleware is type-agnostic**: [apollo.factory.ts:180-209](packages/twenty-front/src/modules/apollo/services/apollo.factory.ts) just passes the refresh token blob. Net: no backend change needed. The full workspace-agnostic lifecycle (issue → cookie → renew → re-issue) already works. ## Test plan - [x] `npx oxlint` + `prettier --check` — clean. - [x] `npx nx typecheck twenty-front` — clean. - [ ] Manual: complete one full SSO flow ending on a workspace subdomain. Visit `https://app.twenty.com/welcome` directly — expect the workspace picker, not the sign-in form. - [ ] Manual: same but with tokenPair cookie cleared — expect the regular sign-in form (no regression). - [ ] Manual: sign-out from a workspace, then visit `app.twenty.com/welcome` — expect the regular form (sign-out clears the cookie via full page reload). - [ ] Manual: stale/expired tokenPair cookie — Apollo renewal kicks in transparently; if renewal fails, regular form (no infinite loop, no crash). - [ ] Manual: pair with #20572 — visit `app.twenty.com/authorize?…` with a stale workspace-agnostic cookie. Expected chain: `/authorize` renders → `PageChangeEffect` redirects to `/welcome?returnToPath=/authorize?…` → this effect lands the user on WorkspaceSelection → picking a workspace bounces to `<workspace>/authorize?…` where consent renders. ## Out of scope - Fixing `lastAuthenticatedWorkspaceDomain` for custom-domain users (separate cookie-scoping issue, tracked separately). |
||
|
|
dbc033b29b |
fix(auth): exclude /authorize from MinimalMetadataGater loading gate (#20572)
## Summary Fixes the blank `/authorize` page reported when a user reopens the OAuth consent screen on `app.twenty.com` after a prior multi-workspace SSO sign-in. ### Reproduction 1. ChatGPT (or any MCP client) opens `https://app.twenty.com/authorize?client_id=…` while signed out. 2. User picks "Continue with Google", lands in workspace selection, picks a workspace, authorizes the app. Works. 3. Some time later, ChatGPT re-opens `https://app.twenty.com/authorize?client_id=…`. 4. **Observed:** fully blank page, no console errors. Deleting the `tokenPair` cookie unblocks it. ### Root cause The multi-workspace social-SSO branch ([auth.service.ts:988-1011](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts#L988-L1011)) lands the user on `app.twenty.com/welcome?tokenPair=…` with a workspace-agnostic token. `SignInUpGlobalScopeFormEffect` writes that into the host-scoped `tokenPair` cookie on `app.twenty.com`, and nothing clears it after the user proceeds to a workspace subdomain. On the next visit to `app.twenty.com/authorize?…`, `MinimalMetadataGater` sees `hasAccessTokenPair === true` and renders `<UserOrMetadataLoader />` instead of `<Authorize />`. The loader never goes away because: - `IsMinimalMetadataReadyEffect` waits for `metadataStore.status === 'up-to-date'`. - `MinimalMetadataLoadEffect` only loads metadata when `hasAccessTokenPair && isActiveWorkspace`, and the default domain has no workspace context. Skeleton loader stays forever → user perceives "blank page". Some users get rescued by `WorkspaceProviderEffect` auto-redirecting them to their last-authenticated workspace subdomain, but that cookie is set with `domain: .twenty.com` and silently fails to persist for users on custom domains — so the bug is most visible there. ### Fix `/authorize` only issues `findApplicationRegistrationByClientId`, which is a `PublicEndpointGuard` query. It doesn't need workspace metadata. Add it to the gater's excluded-paths list alongside the existing pre-auth pages (`SignInUp`, `Verify`, `Invite`, …) so the page renders immediately regardless of token state. This is a one-line, minimum-blast-radius fix. Two related cleanups are separate concerns and not addressed here: - Clearing the workspace-agnostic `tokenPair` cookie on `app.twenty.com` after workspace selection. - Fixing `lastAuthenticatedWorkspaceDomain` propagation for custom-domain users. ## Test plan - [x] `npx oxlint` + `prettier --check` on the touched file — clean. - [x] `npx nx typecheck twenty-front` — clean. - [ ] Manual: with a stale `tokenPair` cookie set on `app.twenty.com`, open `https://app.twenty.com/authorize?client_id=<valid>&…` — consent screen renders, no blank. - [ ] Manual: signed-out → open the same URL — still redirects to `/welcome` and back through the flow. - [ ] Manual: signed-in on a workspace subdomain → open `https://app.twenty.com/authorize?…` — `WorkspaceProviderEffect` auto-redirect still kicks in (unchanged behavior). |
||
|
|
04eb913551 |
chore(page-layout): remove IS_RECORD_PAGE_LAYOUT_* feature flags (#20556)
## Summary
- Both \`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED\` and
\`IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED\` are force-enabled on
every existing workspace by the 1.23.0 upgrade command
\`BackfillRecordPageLayoutsCommand\` and seeded enabled for new
workspaces via \`DEFAULT_FEATURE_FLAGS\` +
\`seed-feature-flags.util.ts\`. They are no longer load-bearing.
- Unwrap all \`if (flag) { … }\` conditionals to their enabled branch on
both server and front.
- Delete legacy fallback files that only the disabled branch reached:
\`PageLayoutRelationWidgetsSyncEffect\`,
\`usePageLayoutWithRelationWidgets\`,
\`reInjectDynamicRelationWidgetsFromDraft\`,
\`injectRelationWidgetsIntoLayout\`, \`isDynamicRelationWidget\` (and
their tests).
- Strip the two \`enableFeatureFlags\` calls from the 1.23 upgrade
command — the page-layout backfill data logic itself is kept intact
since old workspaces upgrading from < 1.23 still need it.
- No DB cleanup migration: stale \`featureFlag\` rows are left in place,
matching the precedent set by #20531 and #20460.
Net diff: 37 files, +106 / -1727.
## Test plan
- [x] \`npx nx typecheck twenty-shared twenty-server twenty-front\` —
all pass
- [x] \`npx nx lint:diff-with-main twenty-server twenty-front\` — all
pass
- [x] \`cd packages/twenty-front && npx jest page-layout\` — 1240 tests,
all pass
- [x] \`cd packages/twenty-server && npx jest
workspace-entity-manager.spec\` — pass
- [ ] Manual smoke: open a record page, verify tabs render and \"Edit
Layout\" command-menu action is available
- [ ] Manual smoke: Settings → Data model → object → Layout tab is
visible (and hidden for remote / Dashboard objects)
- [ ] Manual smoke: edit a tab title, save, reload — confirm persistence
|
||
|
|
49b9660420 |
fix(auth): preserve returnToPath across Google/Microsoft SSO redirects (#20537)
## Summary Fixes the consent-modal-not-reopening half of [#20535](https://github.com/twentyhq/twenty/issues/20535): when a signed-out user opens an OAuth `/authorize?...` URL (e.g. ChatGPT connecting to `api.twenty.com/mcp`) and signs in with **Google or Microsoft**, the original `/authorize` request was lost and the consent screen never reopened. ### Root cause `PageChangeEffect` already saves the deep link as `returnToPath` (Jotai atom) before navigating to `/welcome`. That atom is in-memory: it survives SPA navigation, and the cross-subdomain workspace hop is handled by `useBuildSearchParamsFromUrlSyncedStates` round-tripping the value through the URL. But the social-SSO path leaves `app.twenty.com` entirely — `app.twenty.com/welcome` → `api.twenty.com/auth/google` → Google → `api.twenty.com/auth/google/redirect` → frontend — so the atom is wiped. None of the existing code paths plumbed `returnToPath` through that hop: - `useAuth.buildRedirectUrl` packed `workspaceInviteHash`/`action`/etc. but not `returnToPath`. - `SocialSSOState` / the Google + Microsoft strategies didn't carry it through the OAuth `state` blob. - `signInUpWithSocialSSO` + `computeRedirectURI` didn't re-emit it on the redirect back to the frontend. The email path worked because all transitions stay on the default frontend domain, so the atom survives until `SignInUpGlobalScopeForm` bakes it into the workspace URL. ### What changed Plumb `returnToPath` through the SSO state the same way `workspaceInviteHash` and `action` already flow: - **Frontend** (`useAuth.buildRedirectUrl`): read `returnToPath` from the Jotai store and append it to `/auth/google` / `/auth/microsoft` when set and structurally valid. - **Server types** (`SocialSSOState`, `GoogleRequest['user']`, `MicrosoftRequest['user']`): add optional `returnToPath`. - **Strategies** (`google.auth.strategy.ts`, `microsoft.auth.strategy.ts`): include `returnToPath: req.query.returnToPath` in the JSON `state` and read it back in `validate`. - **auth.service.ts** (`signInUpWithSocialSSO`, `computeRedirectURI`): forward `returnToPath` on both branches — the multi-workspace redirect to `AppPath.SignInUp?tokenPair=...` and the single-workspace redirect to `<workspace>/verify?loginToken=...`. Validated via a new `isValidReturnToPath` helper so a tampered query value can't become an open-redirect vector. After the round-trip, `useInitializeQueryParamState` rehydrates the atom from the URL and `usePageChangeEffectNavigateLocation` resolves it as the post-auth destination — same mechanism the email path already relied on. Out of scope: the OAuth `resource` parameter handling tracked in [#20296](https://github.com/twentyhq/twenty/issues/20296) is independent and not addressed here. ## Test plan - [x] `npx jest src/engine/core-modules/auth` (twenty-server) — 27 suites / 183 tests pass, including new `is-valid-return-to-path.util.spec.ts`. - [x] `npx jest src/modules/auth` (twenty-front) — 13 suites / 52 tests pass, including two new cases in `useAuth.test.tsx` covering the happy path and the protocol-relative open-redirect guard. - [x] `npx nx typecheck twenty-server` / `twenty-front` — clean. - [x] `npx oxlint` + `prettier --check` on touched files — clean. - [ ] Manual: signed-out user opens `https://app.twenty.com/authorize?client_id=...` → Continue with Google → completes Google → selects workspace → consent screen renders. - [ ] Manual: same flow, single workspace — lands on consent screen directly after Verify. - [ ] Manual: email path still works (regression). - [ ] Manual: tamper `returnToPath=//evil.com` on the `/auth/google` URL → server validation rejects, user lands at default home, not at `evil.com`. E2E note: existing `return-to-path.spec.ts` already covers deep links with query params through the email path. A mock OAuth provider would be needed to cover the SSO path end-to-end; unit coverage stands in for now. |
||
|
|
e16977f97b |
[breaking: deploy server before front] feat(view-sort): pick sort sub-field inline on the chip (#20445)
## Summary Lets users choose which sub-field of a composite column to sort by — directly from the sort chip — by clicking the sub-field label and picking from a dropdown. Persists per view via a new nullable \`subFieldName\` column on \`ViewSort\`. Replaces #20438, which proposed a field-settings (admin) configuration for the same problem. The chip-level approach is more discoverable (the option lives where the user is looking) and per-view, so different views on the same object can sort by different sub-fields. ### What changes for users - **FullName columns**: previously sorted by \`firstName\` and \`lastName\` together as a stable dual-key sort. Now the user can pick which sub-field is primary (the other is the tie-breaker). Default remains \`firstName\` primary, \`lastName\` tie-breaker. - **Address columns**: previously not sortable at all (not in \`SORTABLE_FIELD_METADATA_TYPES\`). Now sortable, with a chip dropdown listing each enabled sub-field. Default is \`addressCity\` if enabled, else the first enabled sub-field. Disabling a sub-field at the field-metadata level (existing setting) removes it from the dropdown. - **Other composite types** (Currency, Phones, Emails, Links, Actor) and scalar fields keep their existing single-key sort behavior. ### UX ``` ┌─────────────────────────┐ ┌─────────────────────────┐ │ ↑ Name · Last name ✕ │ │ ↑ Address · City ✕ │ └────────┬────────────────┘ └────────┬────────────────┘ ▼ (click sub-field) ▼ ┌────────────┐ ┌────────────┐ │ First name │ │ Address 1 │ │ Last name ✓│ │ Address 2 │ └────────────┘ │ City ✓│ │ State │ │ Postcode │ │ Country │ └────────────┘ ``` The chip body still toggles direction on click — the \`Dropdown\`'s internal wrapper calls \`stopPropagation\` so the sub-field click doesn't bubble to the chip's onClick. ## What changed **Backend:** - \`ViewSortEntity\` — new nullable \`subFieldName: varchar\` column - \`ViewSortDTO\`, \`CreateViewSortInput\`, \`UpdateViewSortInputUpdates\` — new \`@Field(() => String, { nullable: true })\` - \`FLAT_VIEW_SORT_EDITABLE_PROPERTIES\` — \`'subFieldName'\` added so the property flows through the update merge path - \`ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME.viewSort\` — new \`subFieldName\` entry with \`toCompare: true\` so cache diffs notice it - \`fromCreateViewSortInputToFlatViewSortToCreate\` — threads \`subFieldName\` through - Instance command migration (\`add-sub-field-name-to-view-sort\`) — single \`ALTER TABLE core.viewSort ADD subFieldName varchar\` / \`DROP\` **Frontend:** - \`RecordSort\` and \`ViewSort\` types — \`subFieldName?: string | null\` - \`VIEW_SORT_FRAGMENT\` — adds \`subFieldName\` so the field round-trips - \`mapRecordSortToViewSort\` + \`areViewSortsEqual\` — carry the new field through, include it in the diff so the usual \`useSaveRecordSortsToViewSorts\` create/update flow fires when it changes - \`useSaveRecordSortsToViewSorts\` — passes \`subFieldName\` in both \`CreateViewSortInput\` and \`UpdateViewSortInputUpdates\` - \`getOrderByForFieldMetadataType(field, direction, subFieldName?)\` — new optional third arg. \`turnSortsIntoOrderBy\` threads \`sort.subFieldName\` into it. - \`Address\` added to \`SORTABLE_FIELD_METADATA_TYPES\` - New helpers: \`getEnabledAddressSubFields\` (filters by the field's \`subFields\` setting, falls back to the 6 default visible address sub-fields), \`getDefaultSortSubFieldForAddress\`, \`getDefaultSortSubFieldForFullName\` - New shared types/constants: \`AllowedFullNameSubField\`, \`ALLOWED_FULL_NAME_SUBFIELDS\`, \`DEFAULT_VISIBLE_ADDRESS_SUBFIELDS\` - \`SortOrFilterChip\` — new \`labelSubField?: ReactNode\` slot; renders as \` · {sub-field}\` with subdued weight after the main label - \`EditableSortChip\` — builds options from field metadata (\`ALLOWED_FULL_NAME_SUBFIELDS\` for FullName, \`getEnabledAddressSubFields\` for Address), uses i18n-wrapped labels, persists picks via \`upsertRecordSort\` ## Test plan - [x] \`npx nx typecheck\` passes for twenty-shared, twenty-front, twenty-server - [x] \`oxlint --type-aware\` on all 19 frontend + 9 server changed files: 0 errors - [x] \`prettier --check\`: clean - [x] 16 unit tests pass — \`getOrderByForFieldMetadataType\` covers the new \`subFieldName\` override branch for FULL_NAME and ADDRESS; \`getDefaultSortSubFieldForAddress\` covers the city/first-enabled fallback path; \`getDefaultSortSubFieldForFullName\` exercises its constant - [ ] Manual: sort a People view by Full Name → click the chip's sub-field label → switch between First name and Last name → reload page → choice is preserved - [ ] Manual: sort a Company view by Address → confirm dropdown lists only enabled sub-fields → disable Address \`addressCity\` in field settings → confirm dropdown options update and runtime falls back to the first enabled sub-field 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
fcd2d586ee |
chore(billing) - remove feature flag (#20531)
- remove feature flag - remove old enforce cap usage logic |
||
|
|
695a374efd |
fix - nav drawer expansion (#20545)
[PR](https://github.com/twentyhq/twenty/pull/20505), I merge, does not fix the whole issue Closes https://github.com/twentyhq/twenty/issues/20502 |
||
|
|
dea1f89904 |
Inject none secret env variables into front components (#20511)
## Summary - Inject non-secret application variables (`isSecret: false`) into front component `process.env` via the existing Web Worker `setWorkerEnv` mechanism - Filter secret variables server-side in the resolver so they never reach the browser - Set application variables before system variables (`TWENTY_API_URL`, `TWENTY_APP_ACCESS_TOKEN`) to prevent override - Wire up environment variable keys in the logic function code editor for TypeScript autocomplete ## Test plan - [x] Unit tests for `buildNonSecretEnvVar` (6 passing) - [x] Typecheck passes for `twenty-front` and `twenty-server` - [x] Install an app with both `isSecret: false` and `isSecret: true` variables, open a front component, verify only non-secret vars appear in `process.env` - [x] Open a logic function editor, verify autocomplete suggests declared variable keys |
||
|
|
aecfe699f4 |
feat(ai-chat) - Stop ai thinking if credits exhausted (#20526)
Billing is now decremented per-step, not per-turn. The onStepFinish callback in chat-execution.service.ts calls a new decrementAndCheckAvailableCredits method on each model step, so Redis is debited incrementally as the agent runs rather than all at once at the end. Credit exhaustion stops the stream mid-run. When a step depletes the remaining credits, a hasNoMoreAvailableCredits flag is set and passed into the stopWhen predicate of streamText, causing the agent to halt before starting the next step. A new credits-exhausted event is introduced. After the stream drains and the response is persisted, if credits ran out the job publishes a dedicated credits-exhausted event to the frontend instead of the normal message-persisted event. The frontend handles this new event. useAgentChatSubscription has a new credits-exhausted case that sets a BILLING_CREDITS_EXHAUSTED-coded error on the atom, closes the writer, and stops the streaming state — triggering the existing AiChatCreditsExhaustedMessage UI. |
||
|
|
068fc9363d |
fix(navigation): settings drawer should never appear collapsed (#20505)
## Summary Fixes #20502 /claim #20502 The navigation drawer's collapsed state is persisted to \`localStorage\` via \`isNavigationDrawerExpandedState\`. When a user collapses the drawer in the main app and then opens settings via a **direct URL, refresh, or new tab**, the settings layout renders in collapsed mode — no \`useOpenSettingsMenu\` call is made in those paths to force expansion. \`StyledAnimatedContainer\` (which controls the outer drawer width) used raw \`isNavigationDrawerExpanded\` with no settings-route override. Inner components (\`NavigationDrawerItemsCollapsableContainer\`) already guard with \`isExpanded = isNavigationDrawerExpanded || isSettingsPage\` — the outer container simply needed the same treatment. **Fix:** derive \`isExpanded = isSettingsDrawer || isNavigationDrawerExpanded\` using the already-in-scope \`useIsSettingsDrawer()\` result and pass it to both \`StyledAnimatedContainer\` and \`StyledContainer\`. 1 derived variable, 2 prop changes, no new hooks or state. **Changed files:** - \`packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx\` ## Test plan Verified via code review and CI. The fix is structurally identical to the existing \`isSettingsPage\` guard already used in \`NavigationDrawerItemsCollapsableContainer\` — the outer container simply lacked the same treatment. No new hooks, no side effects, no state mutations. Manual UI verification (collapse → navigate to settings → confirm expanded) was not performed against a running instance. If the maintainers want to verify, the logic path is the same as the inner component guard that already ships in production. > **Note (2026-05-12):** PR #20508 was submitted after this PR with a \`useEffect\`-based approach. That approach has already received a review comment from a team member noting that \`useEffect\` should be a last resort per the project's own React guidelines. This fix uses no effects or imperative state — only a derived variable. --- > [!NOTE] > **AI-Assisted Contribution** > This patch was generated by [Mesopredator](https://github.com/GusFromSpace), an autonomous code intelligence system. > Static analysis located the bug, the fix was written and verified with \`tsc --noEmit\` + \`node\` test suite, and reviewed by a human before submission. > Please treat this as a community contribution and request changes if needed. |
||
|
|
bbc55193f5 |
Fix phone unique constraints (#20261)
## Summary Closes #20195 Fix phone field unique constraints so phone numbers are considered unique by both `primaryPhoneNumber` and `primaryPhoneCallingCode`. - Include `primaryPhoneCallingCode` in the shared phone composite unique constraint metadata - Align the frontend settings composite field config with the backend metadata - Return all included unique composite subfields when building create-many conflict fields - Match composite unique conflict fields as a group during create-many upserts ## Root Cause Phone composite metadata only marked `primaryPhoneNumber` as part of the unique constraint. That made different international phone numbers with the same national number conflict, for example `+1 123456789` and `+32 123456789`. ## Test Plan - `yarn workspace twenty-shared build` - `jest --runTestsByPath <index action handler and create-many utility specs>` - `prettier --check <touched files>` - `oxlint --type-aware <touched files>` - `nx run twenty-shared:typecheck` - `nx run twenty-server:typecheck` - `nx run twenty-front:typecheck` --------- Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
c4e897a7b5 |
Improve linear app (#20453)
- Add front component form to create linear issue <img width="1512" height="831" alt="image" src="https://github.com/user-attachments/assets/ffbb223f-30a8-4c64-ac6d-002c29b604c1" /> <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/a5ed2464-35a9-4a60-804c-5f15eb0043b4" /> - improve marketplace Linear app page <img width="1302" height="834" alt="image" src="https://github.com/user-attachments/assets/cdec7ec2-953d-4a49-a797-5369834b03c1" /> - update admin settings to display non secret values <img width="861" height="473" alt="image" src="https://github.com/user-attachments/assets/41dadf02-aa5d-4eb6-befe-0ad8ad4049b2" /> |
||
|
|
d53f3e4ccc |
fix(kanban): give title full width when card is not hovered (#20455)
## Summary On kanban cards, the title was being truncated even when the checkbox wasn't displayed. The checkbox is hidden via `opacity: 0` on the card's non-hovered state, which keeps it in flex flow and still reserves its ~24px of width — so the title's flex item was shrinking unnecessarily. This change collapses the checkbox container's `max-width` to `0` (with `overflow: hidden`) while it's hidden, and expands it back to the checkbox's natural size (`spacing[6]` = 24px) on hover or when selected. The existing `transition: all ease-in-out 160ms` animates the title expanding into the freed space. ### Before Title truncates with ellipsis even though the checkbox slot is empty: <img width="350" alt="before" src="https://i.imgur.com/placeholder-before.png" /> ### After Title uses the full row width when not hovered; the checkbox slides in on hover (or when the card is selected) and the title reflows. ### Tooltip The full title is already exposed on hover when truncated — `RecordChip` → `Chip` already wraps the label in `OverflowingTextWithTooltip`, which detects overflow (`scrollWidth > clientWidth`) and renders an `AppTooltip` with the full text. No additional wiring needed. ## Test plan - [ ] On a kanban board, verify a long record title now uses the full card width when the card is not hovered (no ellipsis if the title fits). - [ ] Hover the card: the checkbox slides in smoothly (animated), and the title reflows (may now truncate if it doesn't fit). - [ ] Hover the (now-truncated) title: tooltip with the full title appears. - [ ] Select the card via the checkbox: checkbox stays visible (and title stays in its hover-state width) without hovering. - [ ] Compact view (eye icon) still renders correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1adb59f7f8 |
Support optional labels on logic-function input schema fields (#20471)
Adds an optional label field to logic-function input schema properties (InputSchemaProperty and InputJsonSchema). When set, the workflow builder renders the label in place of the raw property key for both leaf inputs and nested sections; when unset, it falls back to the key. jsonSchemaToInputSchema propagates the label so app authors can declare it in their JSON schema. Payload paths, the variable picker, and saved workflow inputs continue to use the property key — labels are display-only. |
||
|
|
93df64b9b0 |
Show logic function label instead of technical name in workflow UI (#20470)
### Before <img width="1304" height="812" alt="Screenshot 2026-05-12 at 7 02 32 AM" src="https://github.com/user-attachments/assets/94ca4b1d-69c0-4059-8c45-dd8eae8e2a29" /> ### After <img width="1296" height="782" alt="Screenshot 2026-05-12 at 6 53 26 AM" src="https://github.com/user-attachments/assets/47f2e291-73df-4471-9174-bd5aca23e228" /> |
||
|
|
b03f044d0f |
feat(messaging): add workspace toggle to sync internal emails (#20457)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
c227b0d06a |
Fix(UI): Side panel having two scrollbars (#20456)
Fixes: #20417 Screenshot: <img width="411" height="945" alt="image" src="https://github.com/user-attachments/assets/925b3ac9-49f2-4fcc-920e-3b9dc34ac466" /> |
||
|
|
e7032d0638 |
fix: prevent admin panel workspace upgrade error from overflowing the table (#20394)
## Summary In the admin panel workspace detail page, the **Upgrade Status > Last error** row was rendering the raw `errorMessage` string directly. Long messages (typically full stack traces) overflowed the table cell and overlapped neighbouring rows, breaking the layout. The `Last command` row in the same table already uses `OverflowingTextWithTooltip` (the helper used elsewhere in settings tables) to clamp long values to a single line and reveal the full text in a tooltip on hover. This PR applies the same treatment to the `Last error` row, with `isTooltipMultiline` so newlines in the stack trace are preserved when the tooltip opens. ## Test plan - [ ] Open Admin Panel > a workspace with a failed upgrade and verify the `Last error` row stays on a single line with an ellipsis - [ ] Hover the row and verify the full multi-line error is shown in the tooltip - [ ] Verify other rows (Last command, Last updated, etc.) and the workspace info section are unaffected |
||
|
|
ed75fc8a25 |
Use workflow inputSchema to render boolean, number, and enum fields in code/logic function steps (#20439)
<img width="415" height="772" alt="Screenshot 2026-05-11 at 4 44 08 PM" src="https://github.com/user-attachments/assets/32dbdd3c-e60b-4c43-90bc-18be05f22dcf" /> <img width="414" height="371" alt="Screenshot 2026-05-11 at 4 48 24 PM" src="https://github.com/user-attachments/assets/83be062c-7ed3-4953-98bb-e4290865040b" /> |
||
|
|
5c1fe45760 |
fix: update broken AI documentation link (#20401)
## Summary Updated the broken AI documentation link in `AiChatApiKeyNotConfiguredMessage.tsx`. ## Changes * Replaced outdated self-hosting AI docs URL * Updated link to a valid self-hosting documentation page Fixes #20071 --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
34b927ff23 |
feat(public-domain): bind public domains to apps + reorganize settings (#20360)
## Summary - **Public domains can now be bound to a specific app.** When a request hits an app-bound public domain, route resolution restricts logic-function matching to that app's HTTP-routed functions only — isolating each app's routes to its own domain instead of letting routes from other apps in the workspace match nondeterministically. - **Settings sidebar reorganized.** Removed the standalone Domains page. Workspace Domain → General. Approved Domains + Invitations → Members "Access" tab. Emailing Domains + Public Domains → Apps "Developer" tab. Roles → Members "Roles" tab. ## Why The use case: someone building a partner portal app or a lead-collection app declares private objects (leads, partners…) plus a few public HTTP routes. Each app needs its own domain (`partners.acme.com`, `leads.acme.com`) without those domains exposing every other app's routes in the same workspace. Today's PublicDomainEntity is workspace-scoped only, so all HTTP-routed logic functions in a workspace compete for any public domain — first match wins nondeterministically. ## Backend - Added nullable `applicationId` FK to `PublicDomainEntity` (cascade-deleted with the app); indexed for the route-trigger lookup. - New fast instance command `2-4-instance-command-fast-1798000003000-add-application-id-to-public-domain` adds the column, index, and FK constraint. - `createPublicDomain(domain, applicationId)` accepts an optional app binding; new `updatePublicDomain(domain, applicationId)` mutation rebinds/unbinds an existing domain. Both validate the application belongs to the workspace. - `WorkspaceDomainsService.resolveWorkspaceAndPublicDomain(origin)` returns both the workspace and the matched public domain in one query — replacing the old back-to-back lookups in the route-trigger hot path. `getWorkspaceByOriginOrDefaultWorkspace` is preserved as a thin wrapper. - `RouteTriggerService` filters `logicFunction` by `applicationId` when the matched public domain is app-scoped; falls back to workspace-wide when unbound. - Three sequential validation queries in `createPublicDomain` now run in parallel via `Promise.all`. ## Frontend | Old location | New location | |---|---| | Settings sidebar → Domains (standalone page) | Removed | | Domains page → Workspace Domain | General page | | Domains page → Approved Domains | Members → Access tab | | Domains page → Emailing Domains | Apps → Developer tab | | Domains page → Public Domains | Apps → Developer tab | | Settings sidebar → Roles (standalone) | Members → Roles tab | | `pages/settings/roles/` | `pages/settings/members/roles/` | - The Public Domain detail page has an Application picker that uses `Select`'s native `emptyOption` + `null` value pattern (matches `SettingsDataModelObjectIdentifiersForm`). - Members page tabs use the existing `TabListFromUrlOptionalEffect` mechanism (rendered automatically by `TabList`) for hash-based tab activation. - `/settings/members/roles` redirects to `/settings/members#roles` so role sub-pages' `navigate(SettingsPath.Roles)` lands on the Members page with the Roles tab pre-selected. - All affected breadcrumbs updated to nest under their new parents. - `SettingsPath.Roles` and friends now nest under `members/`; `Subdomain` and `CustomDomain` under `general/`; `PublicDomain` and `EmailingDomain` under `applications/`. ## Test plan - [x] `nx typecheck twenty-front` passes - [x] `nx typecheck twenty-server` passes - [x] `oxlint --type-aware` clean on all touched files - [x] `prettier --check` clean on all touched files - [x] Migration applied locally; `publicDomain.applicationId` (uuid, nullable) confirmed in DB - [x] GraphQL schema exposes `PublicDomain.applicationId`, `createPublicDomain.applicationId`, `updatePublicDomain` mutation - [x] **End-to-end route resolution scenarios verified locally:** - Domain bound to App A, function in App A → route matches ✅ - Domain bound to App B, function in App A → route does NOT match (HTTP 404 `TRIGGER_NOT_FOUND`) ✅ - Domain unbound (`applicationId = NULL`) → route matches workspace-wide ✅ - Unknown path on bound domain → returns 404 cleanly ✅ - [x] UI sanity (browser-tested at `apple.localhost:3001`): - General page shows Workspace Domain card - Members page shows Team / Access / Roles tabs - Access tab combines Invite by link + by email + Approved Domains - Roles tab embeds the role list - `/settings/members/roles` direct URL → redirects + Roles tab pre-selected - Apps Developer tab shows Emailing Domains + Public Domains sections - Public Domain detail page has Application picker dropdown listing workspace apps - Sidebar nav: "Domains" and "Roles" no longer present (now folded into General/Members) ## Notes for reviewers - Creating a public domain via the UI still requires Cloudflare credentials in the dev `.env` (`CLOUDFLARE_API_KEY`, `CLOUDFLARE_PUBLIC_DOMAIN_ZONE_ID`, `PUBLIC_DOMAIN_URL`). The DNS step is unchanged from main. - The `applicationId` column is nullable, so existing public-domain rows continue to work workspace-wide — no data backfill required. - `SettingsRolesContainer` was deleted (no longer referenced after `SettingsRoles` index page was removed). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
524e5d8cf7 |
fix: scroll AI chat to bottom on side panel reopen (#20413)
## Summary - Fix: when reopening the AI chat side panel, users landed at the top of the conversation and had to scroll down to find the latest messages - Root cause: the side panel fully unmounts on close ([SidePanelForDesktop.tsx](packages/twenty-front/src/modules/side-panel/components/SidePanelForDesktop.tsx) clears `shouldShowContent` after the close transition), so on reopen the scroll wrapper is recreated with `scrollTop = 0`. The existing initial-scroll-to-bottom only fires on thread change, but the displayed-thread atom outlives the unmount, so no thread change is detected on a remount and the scroll-to-bottom never runs - Fix: add a tiny `AgentChatScrollToBottomOnMountLayoutEffect` rendered inside the message list that calls `scrollAiChatToBottom()` directly in `useLayoutEffect`. Because the parent returns `null` when there are no messages, the mount only fires when there is content to scroll past ## Why direct scroll, not the existing flag `agentChatIsInitialScrollPendingOnThreadChangeState` is paired with a `MutationObserver` settle that only clears the flag once the subtree is quiet for 150 ms. During a live stream the message subtree mutates on every token, the settle resets indefinitely and `visibility: hidden` never lifts. The thread-change handler avoids this because it is gated on `!agentChatIsStreaming` ([AgentChatStreamSubscriptionEffect.tsx:78-79](packages/twenty-front/src/modules/ai/components/AgentChatStreamSubscriptionEffect.tsx)), but a mount can happen at any time, including mid-stream. Scrolling the DOM directly in `useLayoutEffect` runs synchronously between commit and paint, so the user sees the bottom on the first paint with no flash and no settle dependency. ## Tradeoff A user who scrolled up to read history and then closes/reopens the panel will land back at the bottom instead of where they were. Standard chat UX (Slack, ChatGPT, iMessage); preserving per-thread scroll position would need a new atom and is left out of scope. ## Test plan - [ ] Open AI chat with messages, close the side panel, reopen → lands at the bottom (latest messages visible) - [ ] Reopen the side panel **mid-stream** → lands at the bottom and continues to follow new tokens (chat is not hidden) - [ ] Switch between threads → existing thread-change scroll still works (no double-scroll, no regression) - [ ] Open AI chat with no messages → no flash, no errors - [ ] Rapidly close/reopen the panel a few times → each reopen lands at the bottom --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
4da8878697 |
feat: add email forwarding message channel (#19535)
## Summary - Add email forwarding as a new message channel type, allowing users to forward emails from addresses like `support@mycompany.com` into Twenty - Inbound emails arrive via S3 (SES → S3 bucket), are polled by a cron job, parsed, routed to the correct workspace/channel, and persisted as messages - Dedicated settings page at `/settings/accounts/new-email-forwarding` where users provide their source email handle and receive a unique forwarding address - Forwarding channels bypass the IMAP/mailbox sync state machine — they skip cron-driven sync, relaunch, and message-list-fetch lifecycle stages - Forwarding address section shown at the top of the Emails settings page so users can find/copy their addresses after initial setup - Tab names for forwarding channels display the user-provided handle (e.g. `support@mycompany.com`) instead of the internal routing address - Shared utilities extracted from IMAP driver: `extractThreadId`, `extractParticipants`, `extractAddresses` to avoid code duplication - Uses the existing S3 bucket (STORAGE_S3_*) with `inbound-email/` prefix — no separate bucket needed - Feature gated behind `isEmailForwardingEnabled` client config (requires `INBOUND_EMAIL_DOMAIN` + S3 storage) ## New backend modules - `InboundEmailS3ClientProvider` — lazy-initialized S3 client using existing storage config - `InboundEmailStorageService` — S3 operations (get, move to processed/unmatched/failed) - `InboundEmailParserService` — RFC 822 parsing via `postal-mime`, builds `MessageWithParticipants` - `InboundEmailImportService` — orchestrates download → parse → route → persist → archive - `MessagingInboundEmailPollCronJob` — polls S3 `incoming/` prefix, enqueues import jobs - `CreateEmailForwardingChannelInput` DTO — accepts user-provided `handle` ## New frontend components - `SettingsAccountsNewEmailForwardingChannel` — dedicated page with handle input form + forwarding address result - `SettingsAccountsEmailForwardingSection` — forwarding address list on the Emails settings page - `useConnectedAccountHandleMap` — shared hook for account ID → handle lookup - `useCreateEmailForwardingChannel` — mutation hook accepting handle parameter ## Test plan - [x] 17 unit tests for inbound email import service (all outcomes: imported, unmatched, loop_dropped, unconfigured, parse_failed, persist_failed) - [x] 16 tests for `computeSyncStatus` including EMAIL_FORWARDING cases - [x] 11 tests for `extractEnvelopeRecipient` utility - [x] TypeScript typechecks pass for both twenty-server and twenty-front - [x] Lint passes for both packages - [ ] Manual: create forwarding channel, verify forwarding address generated - [ ] Manual: send email to forwarding address, verify it appears in Twenty https://claude.ai/code/session_01KpyF6p4cUEnuaT4h8DP5Pm --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com> Co-authored-by: neo773 <neo773@protonmail.com> |
||
|
|
7999cd3dde |
fix: Use settings table rows and detail page for app connections (#20257)
## Summary Closes #20220 - Replace app connection-provider `SettingsListCard` rows with settings table rows that link to a per-connection detail page. - Add a connection detail page with inline display-name editing, provider and handle metadata, visibility, scopes, timestamps, reconnect, and confirmed disconnect. - Add a scoped connected-account rename mutation and persist visibility when reconnecting an existing app OAuth account. ## Tests - `./node_modules/.bin/jest packages/twenty-front/src/pages/settings/applications/__tests__/SettingsApplicationConnectionDetail.test.tsx --config packages/twenty-front/jest.config.mjs --runInBand` - `./node_modules/.bin/jest packages/twenty-front/src/pages/settings/applications/tabs/__tests__/SettingsApplicationConnectionsSection.test.tsx --config packages/twenty-front/jest.config.mjs --runInBand` - `./node_modules/.bin/jest packages/twenty-shared/src/utils/navigation/__tests__/getSettingsPath.test.ts --config packages/twenty-shared/jest.config.mjs --runInBand` - `./node_modules/.bin/jest packages/twenty-server/src/engine/metadata-modules/connected-account/resolvers/__tests__/connected-account.resolver.spec.ts --config packages/twenty-server/jest.config.mjs --runInBand` - `./node_modules/.bin/jest packages/twenty-server/src/engine/core-modules/application/application-oauth-provider/__tests__/application-oauth-provider-flow.service.spec.ts --config packages/twenty-server/jest.config.mjs --runInBand` - `./node_modules/.bin/oxlint ...` - `./node_modules/.bin/prettier --check ...` - `./node_modules/.bin/tsgo -p packages/twenty-shared/tsconfig.json` ## Notes Full frontend and server typechecks are currently blocked by unrelated existing workspace issues: - frontend implicit `any` errors in `useFrontComponentExecutionContext.ts` - server missing workspace/dependency modules such as `twenty-emails`, `twenty-client-sdk/generate`, and `@ai-sdk/azure` |
||
|
|
9fc5be1c4c |
Billing - Migrate from Stripe metering (#20298)
**Overall strategy** **1. Introduce “Billing V2” behind a workspace flag** Gate the new model with FeatureFlagKey.IS_BILLING_V2_ENABLED so existing workspaces stay on the old behavior until they’re migrated or explicitly on V2. **2. Replace workflow metered SKUs with a resource-credit product** Conceptually, billable “workflow execution” usage is not the primary subscription line item anymore. Add a RESOURCE_CREDIT product (and keep WORKFLOW_NODE_EXECUTION as deprecated for the transition). Usage and limits are expressed through credit buckets (e.g. price metadata like credit_amount), so one product can represent pooled credits instead of a narrow workflow-only meter. **3. Migrate subscriptions in two layers** Schema/catalog: persist extra price metadata (instance upgrade) so the server knows credit amounts and can match Stripe prices to the new model. Per workspace: the registered workspace command upgrade:2-2:migrate-to-billing-v2 finds subscriptions that still have WORKFLOW_NODE_EXECUTION, swaps those items to the right RESOURCE_CREDIT prices (using existing Stripe schedule + BillingSubscriptionUpdateService stack), then treats the workspace as V2 (flag). Workspaces without that legacy item or without a subscription are skipped. **4. Unify subscription lifecycle + usage on the server** **5. Refresh the product surface in Settings** Test : - [x] Subscribe v1 + Update subscribe + Migrate - [x] Subscribe v2 + Update subscribe |
||
|
|
2b4fa9d8cf |
fix: workspace member "me" filters now work in dashboard widgets (#20266)
## Summary Fixes #20225 Dashboard graph widgets only passed `timeZone` in `filterValueDependencies` when computing the GraphQL operation filter. As a result, `isCurrentWorkspaceMemberSelected` ("me") filters were silently ignored — the current workspace member ID was `undefined`. Regular view filters already use `useFilterValueDependencies` which provides both `timeZone` and `currentWorkspaceMemberId`. This PR replaces the manual `{ timeZone: userTimezone }` object in `useGraphWidgetQueryCommon` with `useFilterValueDependencies()`, giving dashboard widgets full feature parity with view filters. **Changed file:** `packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon.ts` --------- Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
e49673df79 |
Fix plan-required modal issue (#20346)
Commit
|
||
|
|
027331c893 |
chore(front): move mocked-metadata helpers under src/testing (#20341)
## Summary Follow-up to #20308. After that PR, production code no longer loads mocked metadata when the user is unauthenticated. The remaining mocked-metadata helpers (`useLoadMockedMetadata`, `preloadMockedMetadata`) are now only consumed by Storybook decorators, so move them next to the other testing-only utilities to make their scope explicit and prevent accidental re-introduction of a runtime dependency on mocks. - `src/modules/metadata-store/hooks/useLoadMockedMetadata.ts` → `src/testing/hooks/useLoadMockedMetadata.ts` - `src/modules/metadata-store/utils/preloadMockedMetadata.ts` → `src/testing/utils/preloadMockedMetadata.ts` The three Storybook decorator imports (`MockedMetadataLoadEffect`, `ObjectMetadataItemsDecorator`, `WorkflowStepDecorator`) are updated to the new `~/testing/...` paths. No runtime behavior change. ## Test plan - [x] `npx oxlint --type-aware -c .oxlintrc.json` on touched files — clean - [x] `npx prettier --check` on touched files — clean - [ ] CI: storybook, unit tests, e2e tests |
||
|
|
948ed964bb |
Add isConfigured to application registration in App admin panel (#20326)
## After Added "Configured" column in admin panel apps tab <img width="1171" height="611" alt="image" src="https://github.com/user-attachments/assets/e6850b39-5789-4ad2-b3df-192a28cb03e2" /> Add a banner to ask to configure the application <img width="1218" height="483" alt="image" src="https://github.com/user-attachments/assets/1491363c-3479-41ca-97b7-c7dc9e07ff16" /> |
||
|
|
be4b466234 |
Remove broken total count from workflow version (#20324)
## Problem
Opening a workflow run with a form step in the side panel, closing the
form and reopening it crashes the app: `<SidePanelWorkflowStepInfo>`
blows up on `workflow.versions.find` because `versions` is `null` in the
Apollo cache.
## Root cause
`useWorkflowVersion` was selecting:
```ts
workflow: { id, name, statuses, versions: { totalCount: true } }
```
Twenty's GraphQL field generator doesn't support connection-level
scalars — { totalCount: true } is interpreted as fields on the inner
WorkflowVersion node, gets filtered out, and the query collapses to:
versions { edges { node { __typename } } }
The server returns versions: null for that empty-node selection.
Why now
The selection has always been wrong, but two recent changes made it
consistently surface:
Apollo Client v4 upgrade (#18584): stricter normalized writes, null
always wins.
#20242: WorkflowRunSSESubscribeEffect in the form filler keeps SSE
flowing, which re-fires useWorkflowVersion more often, making the bad
query consistently the last writer.
|
||
|
|
10876138d2 |
refactor: stop reading joinColumnName from relation field settings (#20304)
## Summary `joinColumnName` on relation field settings is always derivable from the field name (and the target object name for morph relations). This PR stops reading it from settings anywhere in production code; the stored value is no longer used. The settings field is **not** removed from data yet — a follow-up can drop it once we are confident nothing depends on the stored value. ## Helpers The helpers are split by layer because frontend and backend hold morph relations differently: the frontend has a base name plus a `morphRelations[]` array, the backend has one row per target with the name already morph-resolved. | Helper | Layer | When to use | |---|---|---| | `computeRelationGqlFieldJoinColumnName` | Shared / frontend (`gqlField`) | Non-morph relation on the frontend. | | `computeMorphRelationGqlFieldName` | Shared / frontend (`gqlField`) | Need the per-target morph gqlField name (e.g. `targetCompany`). | | `computeMorphRelationGqlFieldJoinColumnName` | Shared / frontend (`gqlField`) | Per-target morph join column on the frontend. Prefer over the non-morph helper for any morph field — it forces the per-target inputs. | | `computeMorphOrRelationFieldJoinColumnName` | Backend (`FlatFieldMetadata.name`) | Any backend read or write — the flat name is already morph-resolved, so one helper covers both cases. | | `computeMorphRelationFlatFieldName` | Backend (`FlatFieldMetadata.name`) | **Mutation paths only** (create / update / object rename). Reads consume the stored `field.name` and never call this. | ## Test plan - [x] Typecheck and lint (front, server, shared) - [x] Existing unit tests pass - [ ] CI green |
||
|
|
9ac503e3af |
fix(front): defer default home redirect when object metadata is not loaded (#20330)
## Summary Fixes the merge-queue E2E failures introduced after #20308. After login, users were being silently redirected to `/settings/profile` instead of their workspace home, which broke every dependent E2E test that re-uses the post-login URL (`workflow-creation.spec.ts`, `authentication/signup_invite_email.spec.ts`, etc.). ## Root cause `useDefaultHomePagePath` falls back to `/settings/profile` when `readableNonSystemObjectMetadataItems` is empty. That list is empty in two cases: 1. The user genuinely has no readable objects → `/settings/profile` is the intended fallback. 2. Object metadata simply hasn't been loaded yet (transient post-login window). Before #20308 the frontend always loaded mocked metadata for authenticated users, so case (2) never happened. After #20308 mocked metadata is gone, and during the post-verify window (`handleLoadWorkspaceAfterAuthentication` finishes, `setIsAppEffectRedirectEnabled(true)` re-enables redirects, `PageChangeEffect` fires) the metadata store is still empty. The hook then returns `/settings/profile`. Because that path is not in `ONBOARDING_PATHS` / `ONGOING_USER_CREATION_PATHS`, `usePageChangeEffectNavigateLocation` doesn't fire a corrective redirect once metadata finally loads — the user is stranded. `login.setup.ts` captures `process.env.LINK = page.url()` after verify, so subsequent tests `goto(LINK)` end up in Settings looking for app navigation that isn't there → click timeouts. ## Fix Distinguish the two empty cases by reading `metadataStoreState('objectMetadataItems').status`. If it isn't `'up-to-date'` we defer to `AppPath.Index` instead of `/settings/profile`. The memo recomputes when the status flips, and the user is then routed to their actual home page. A regression test is added in `useDefaultHomePagePath.test.ts` for the not-loaded-yet case. ## Test plan - [x] Unit: `npx jest src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts` (5/5 pass, including new regression case) - [ ] CI: Playwright E2E (`workflow-creation.spec.ts`, `authentication/signup_invite_email.spec.ts`) pass on this branch - [ ] Manual: log in to a fresh local instance and confirm landing page is the workspace home, not `/settings/profile` |
||
|
|
ee6c0ef904 |
Replace sign-in mocked metadata with hardcoded BackgroundMock (#20308)
## Summary When the user is logged out, we render the auth modal on top of a sample table to make the empty page feel alive. So far this was achieved by **loading a full set of mocked object / field / view / navigation-menu metadata into the runtime metadata store** and then mounting the real `RecordTable` and `AppNavigationDrawer` behind the modal. This had a few downsides: - Significant bundle weight pulled in for unauthenticated users (mocked GraphQL fixtures + the real `RecordTable` virtualization stack). - Plenty of code paths that had to know about the "showAuthModal" case (`useRecordIndexTableQuery`, `useTriggerInitialRecordTableDataLoad`, `MainContextStoreProvider`, `IsMinimalMetadataReadyEffect`...). - Any change to metadata-store internals or to the record-table runtime risked breaking the logged-out background. This PR replaces the entire flow with a small, self-contained `BackgroundMock` component tree that **does not consume any metadata** and **does not load any mocked metadata at runtime**. ### What changed - New module under `sign-in-background-mock`: - `BackgroundMockPage` + `BackgroundMockViewBar` + `BackgroundMockTable` + `BackgroundMockTableRow` render a hardcoded "Companies" table that visually mirrors the real one. - `BackgroundMockNavigationDrawer` renders a hardcoded sidebar with People / Companies / Opportunities / Tasks / Notes (with their standard colors). - Hardcoded constants in `BackgroundMockCompanies.ts`, `BackgroundMockColumns.ts`, `BackgroundMockNavigationItems.ts`. - `MinimalMetadataLoadEffect` no longer calls `loadMockedMetadataAtomic` for unauthenticated users — it just doesn't load anything. - `IsMinimalMetadataReadyEffect` now reports ready immediately when there is no access token pair, so the skeleton loader doesn't hang waiting for metadata that will never come. - `MainContextStoreProvider`, `useRecordIndexTableQuery`, and `useTriggerInitialRecordTableDataLoad` drop their `showAuthModal` branches — the real `RecordTable` is no longer mounted behind the modal. - `DefaultLayout` and `NotFound` now lazily load `BackgroundMockPage` / `BackgroundMockNavigationDrawer` instead of the deleted `SignInBackgroundMockPage` / `SignInAppNavigationDrawerMock`. - Removed: `SignInBackgroundMockPage`, `SignInBackgroundMockContainer`, `SignInBackgroundMockContainerEffect`, `SignInAppNavigationDrawerMock`, `SignInBackgroundMockColumnDefinitions`, `SignInBackgroundMockCompanies`, `SignInBackgroundMockViewFields`. `useLoadMockedMetadata` and `preloadMockedMetadata` are kept on purpose: Storybook decorators (`ObjectMetadataItemsDecorator`, `WorkflowStepDecorator`) still rely on the mocked metadata fixtures, but **production** unauthenticated runtime no longer touches them. ### Visual parity Side-by-side at 1440×900 on `/sign-in`: **Before** (loads mocked metadata + real RecordTable):  **After** (purely hardcoded BackgroundMock):  ## Test plan - [ ] `npx nx typecheck twenty-front` ✅ (passes locally) - [ ] `npx nx lint:diff-with-main twenty-front` ✅ (oxlint + prettier clean) - [ ] `npx jest useRecordIndexTableQuery` ✅ - [ ] Manually verify `/sign-in` renders the table + nav drawer behind the modal - [ ] Manually verify `/not-found` still renders the background - [ ] Verify CI: storybook, unit tests, e2e tests |
||
|
|
0608bae9ae |
fix(front): resolve labelIdentifier per target for morph relation depth=1 (#20305)
## Summary
On the show page, morph relations were showing "Untitled" entries for
targets whose `labelIdentifier` is not `name` (for example
`Note.title`). The GraphQL response only contained `id` for those
records.
`generateDepthRecordGqlFieldsFromFields` was hardcoding the morph
depth=1 sub-selection to `{ id, name }` for every target instead of
resolving each target's `labelIdentifier` (and `imageIdentifier`) from
`objectMetadataItems`, the way the non-morph relation branch already
does. The morph branch was also ignoring
`shouldOnlyLoadRelationIdentifiers`.
<img width="1300" height="860" alt="image"
src="https://github.com/user-attachments/assets/ebdb5287-0b4c-4a96-95a2-33b19b31446e"
/>
|