a159a68e2ce0a3de9135a306d2a96e8833bcb6b2
4712 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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"
/>
|
||
|
|
617f571400 |
20215 convert application variable to a syncable entity (#20269)
## Summary - Converts applicationVariable from a bespoke sync path to a proper SyncableEntity, unifying it with the workspace migration pipeline used by all other manifest-managed entities (agent, skill, frontComponent, webhook, etc.) - Removes the upsertManyApplicationVariableEntities method and its direct-DB-mutation approach in favor of the standard validate → build → run action handler pipeline - Adds universalIdentifier, deletedAt columns and makes applicationId NOT NULL via an instance command migration ## Motivation Before this change, applicationVariable was the only manifest-managed entity that bypassed ApplicationManifestMigrationService.syncMetadataFromManifest(). It used a bespoke service method called directly from syncApplication(), creating two mental models, two validation styles, and two cache invalidation patterns. Now there's one unified pipeline for all manifest entities. ## What changed ### Entity refactor: - ApplicationVariableEntity now extends SyncableEntity (gains universalIdentifier, non-nullable applicationId with CASCADE, soft-delete via deletedAt) ### New flat entity layer (flat-application-variable/): - Type, maps type, editable properties constant, entity-to-flat converter, cache service, module ### New migration pipeline wiring: - Manifest converter (fromApplicationVariableManifestToUniversalFlatApplicationVariable) - Validator service (FlatApplicationVariableValidatorService) - Builder service (WorkspaceMigrationApplicationVariableActionsBuilderService) - Create/Update/Delete action handlers with secret encryption hooks - Registered in orchestrator, builder module, runner module, and all type registries ### Removed bespoke path: - Deleted upsertManyApplicationVariableEntities from ApplicationVariableEntityService - Removed its call from ApplicationSyncService.syncApplication() - Kept update() (operator-set value at runtime) and getDisplayValue() (runtime display) ### Database migration: - Instance command to add columns, backfill universalIdentifier, enforce NOT NULL constraints, and update indexes ## Test plan - npx nx typecheck twenty-server passes (0 errors) - Unit tests pass (application-variable.service.spec.ts, build-env-var.spec.ts) - Install an app with applicationVariables in its manifest → variables appear with correct universalIdentifier - Update app manifest (add/remove/modify a variable) → migration pipeline handles diff correctly - Operator-set value via update endpoint persists correctly with encryption - Uninstall app → variables cascade-deleted - app dev --once on example app syncs without errors |
||
|
|
bbd9720ab3 |
[Dashboards] [Warning] Remove gauge chart support and delete existing widgets (#20172)
## Summary Removes gauge chart from the chart-type picker and deletes existing gauge widgets via a workspace migration. The gauge was rendering a hardcoded `0.7 / "Progress"` stub regardless of configuration -- never wired to real data. The contract stays in place. We keep `WidgetConfigurationType.GAUGE_CHART`, the DTO, the GraphQL union member, and the gauge folder -- so stored gauge JSON still resolves through the schema. The render path falls through to `default: return null`, so any un-migrated gauge widget renders as an empty cell, not a crash. This PR just removes existing gauge widgets if there are any (via `upgrade:2-3:delete-gauge-widgets`). The deliberate cleanup -- deleting the type definitions, the gauge folder, the DTO -- comes in a follow-up PR after the migration has run. --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
a03c2647cf |
Fix unreliable SSE event stream updates during workflow form transitions (#20242)
Before - workflow run not up to date, needs refresh to see created company in some cases https://github.com/user-attachments/assets/28517e97-2404-4f75-8bce-cc33e3cbea20 After https://github.com/user-attachments/assets/60f930cb-1265-4c50-8ec5-aa4f978b1873 ## Summary - Split `SSEQuerySubscribeEffect`'s single debounced `updateQueryListeners` into separate `syncAdditions` (leading edge, 1s debounce) and `syncRemovals` (trailing edge, 200ms debounce) callbacks. This prevents query unregistrations during component mount/unmount transitions from creating gaps where events are missed, while keeping new registrations immediate. - Each sync path now updates `activeQueryListenersState` granularly (append-only for additions, filter-only for removals) instead of overwriting the entire state, eliminating a race condition where removals could mark unregistered queries as active. - Mount `WorkflowRunSSESubscribeEffect` inside `WorkflowEditActionFormFiller` so the workflow-run query subscription stays active during form steps. - Extract `buildSortedConnectionEdges` util that builds the resulting edge list of a cached record connection after new records are created. Position placeholders (`'first'` / `'last'`) bypass orderBy and are pinned to the front/back; sortable positions (numeric or undefined) are merged into existing edges and sorted by the connection's actual `orderBy`. This replaces the broken `length * position` insertion logic in `triggerCreateRecordsOptimisticEffect` that treated the sortable `position` field as a 0-1 ratio, causing new records from SSE to land at invisible indices in the cached list. Also fixes `totalCount` increment for batched creates, derives `pageInfo` cursors from the final array, and gracefully skips records whose `toReference` returns null. ## Test plan - [x] Run a workflow with a form step — verify the workflow status updates live after form submission (no stuck "running" state) - [x] Run the same workflow multiple times — verify company creation events appear live on the record index page for every run, not just the first - [x] Click the "+" button to create a record in first position — verify it appears immediately at the top - [x] Verify other SSE-backed live updates (record creation, deletion, updates) still work correctly --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
cbd2a017da |
Improve app gallery image sizing (no cropping) (#20287)
<img width="908" height="808" alt="Screenshot 2026-05-05 at 7 38 46 PM" src="https://github.com/user-attachments/assets/a6f0a9b7-f676-46a3-8642-48c4bd06c7f4" /> |
||
|
|
3d60e6dbfc |
Fix stale address coordinates after clearing autofill (#20264)
Closes #20082 |
||
|
|
633553f729 |
feat(sdk): add defineCommandMenuItem (#20256)
## Summary - Add `defineCommandMenuItem` and `definePageLayoutWidget` as standalone SDK defines, mirroring the existing `definePageLayoutTab` pattern. Both entities can still be declared nested inside their parent (`defineFrontComponent.command` / `definePageLayout.tabs[].widgets[]`). - Add `CommandMenuItem` and `PageLayoutWidget` to the `SyncableEntity` enum and the dev-mode UI labels. - Wire the SDK manifest-build to extract the two new defines into top-level `commandMenuItems` / `pageLayoutWidgets` arrays on the manifest, and the server aggregator to consume them through the existing flat-entity converters. - On the server, expose `Application.commandMenuItems` (relation + DTO + service hydration in `findOneApplication`). - On the front, list command menu items in the application content tab and add a dedicated detail page with a settings tab, mirroring how `frontComponents` are surfaced. - Add `twenty add` templates and Vitest unit tests for both new defines. - Document the standalone-vs-nested pattern in `packages/twenty-sdk/README.md`. ### Why Until now, command menu items could only be declared as the nested `command:` field on `defineFrontComponent` — there was no way to register a command menu item from a separate file or from another package. The `SyncableEntity` enum had 12 values, while the server already synced 18 (including `commandMenuItem` and `pageLayoutWidget`). The same gap existed for `pageLayoutWidget`, which had no top-level define despite being synced server-side. This PR closes both gaps and aligns the SDK surface with what the server actually accepts. The standalone defines coexist with the nested form — pick one per entity, never both with the same `universalIdentifier` (the manifest aggregator will throw on duplicates). The README now documents this. ## Test plan - [x] `npx nx typecheck twenty-sdk` / `twenty-server` / `twenty-front` - [x] `npx nx lint:diff-with-main twenty-front` / `twenty-server` - [x] `npx nx lint twenty-sdk` / `twenty-shared` - [x] New unit tests: `define-command-menu-item.spec.ts`, `define-page-layout-widget.spec.ts` - [x] Existing manifest extract config tests still pass - [ ] Codegen `npx nx run twenty-front:graphql:generate --configuration=metadata` should be re-run after merge — the generated `graphql.ts` was patched manually to include `commandMenuItems` on `Application` and the `FindOneApplication` document. - [ ] Smoke test: scaffold an app with `twenty add` for both new entity types, run `twenty dev`, confirm the dev UI shows them in the sync list and the settings page surfaces command menu items in the content tab. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
90a1a9274f |
fix: show pinned commands in side panel search results (#20265)
Discord issue: https://discord.com/channels/1130383047699738754/1498996539530412053 |
||
|
|
53fdac1417 |
feat(apps): split AI tool and workflow action triggers in LogicFunction manifest (#20208)
## Summary Replaces the bolted-on `isTool` + `toolInputSchema` fields on `LogicFunctionManifest` with two distinct, opt-in triggers that align with the existing `cron` / `databaseEvent` / `httpRoute` trigger pattern: - **`toolTriggerSettings`** — exposes the function as an AI tool (chat / MCP / function calling). Uses standard JSON Schema (the format LLMs natively understand). - **`workflowActionTriggerSettings`** — exposes the function as a step in the visual workflow builder. Uses Twenty's rich `InputSchema` so the builder can render proper `FieldMetadataType`-aware editors, variable pickers, labels, and an optional `outputSchema`. A function can opt into none, one, or both. Each surface gets the schema format appropriate for it. ### Why `isTool: true` previously exposed the function as both an AI tool AND a workflow node, with the same JSON Schema feeding both — but the workflow builder really wants Twenty's `InputSchema` (with `CURRENCY`, `RELATION`, `EMAILS`, etc.) and the AI surface really wants standard JSON Schema. Today the workflow builder hacks around this by treating JSON Schema as `InputSchema`, which silently breaks for any non-primitive field type. Splitting the triggers fixes that and lets each surface evolve independently. ### Migration - **Fast** instance command adds the two new nullable columns. - **Slow** instance command backfills `toolTriggerSettings` + `workflowActionTriggerSettings` from `isTool=true` rows (preserving today's both-surfaces behaviour) then drops the legacy columns. ### Stacked Stacked on top of #20181. Merge that first, then this. ## Test plan - [ ] CI green (oxlint, typecheck, jest, vitest) - [ ] Run `--include-slow` upgrade against a workspace with existing `isTool=true` logic functions; verify both new columns populated and old columns dropped - [ ] Verify AI chat sees migrated tool functions (Linear create-issue, Exa search) and can call them with the JSON Schema - [ ] Add an AI-tool function from the Settings UI (toggles `toolTriggerSettings`) and verify it shows up in chat - [ ] Add a workflow-action function from the Settings UI (toggles `workflowActionTriggerSettings`) and verify it appears in the workflow node picker - [ ] In the workflow builder, edit a `LOGIC_FUNCTION` step and verify input fields render (no more JSON-Schema-as-InputSchema hack) - [ ] Try defining a function with no triggers in the SDK and verify `defineLogicFunction` rejects it 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: martmull <martmull@hotmail.fr> |
||
|
|
36452ecc8b |
fix: show 'Not shared' for RLS-hidden morph relation records (#20272)
## Summary Follow-up to #20260. The `MorphRelationManyToOneFieldDisplay` component (used for polymorphic MANY_TO_ONE relations) was missing the FK-presence check that `RelationToOneFieldDisplay` already has. When RLS hides a related record (e.g., a Rocket with a policy filtering by name), the API response contains a populated FK (`polymorphicOwnerRocketId`) but a `null` relation object. The component was rendering an empty cell instead of the "Not shared" lock icon. **Fix:** - In `useMorphRelationToOneFieldDisplay`, read the record from the store and check if any morph relation FK field is populated while the relation value is null - In `MorphRelationManyToOneFieldDisplay`, render `<ForbiddenFieldDisplay />` when that condition is true | Scenario | FK in response | Relation object | Frontend display | |----------|---------------|-----------------|-----------------| | Live record | "abc" | `{ id: "abc", ... }` | Record chip | | Soft-deleted record | null | null | Empty cell | | RLS-hidden record | "abc" | null | "Not shared" | ## Test plan - Create a polymorphic MANY_TO_ONE relation (e.g., Pet → Rocket) - Add an RLS policy on the target object (e.g., Rocket name contains "Starship") - Verify the morph relation field shows "Not shared" (lock icon) for RLS-hidden records - Verify live records still display normally as record chips - Verify soft-deleted records still display as empty cells |
||
|
|
820f97f53d |
[Headless Front component] Support multiple selected record (#20268)
# Introduction Support multiple selected record ids for headless front components ### Changes **Added:** - `recordIds: string[]` field to `FrontComponentExecutionContext` - `useRecordIds()` hook to get all selected record IDs **Deprecated:** - `recordId` field - use `recordIds` instead - `useRecordId()` hook - use `useRecordIds()` instead Backward compatibility is preserved |
||
|
|
ff65b5001d | fix: show AI chat filter button only on hover in navigation drawer (#20274) | ||
|
|
e3be1f4971 |
Make ConnectionProvider a true SyncableEntity (#20232)
## Summary PR #20181 left `ConnectionProvider` in the `SyncableEntity` enum but bypassing the standard sync pipeline — manifest sync called the bespoke `ApplicationOAuthProviderService.upsertManyFromManifest()` instead of going through the workspace-migration orchestrator like every other SyncableEntity. Anything that assumed *"all SyncableEntity values flow through the same pipeline"* (dev UI sync tracking, verification tooling) was wrong about ConnectionProvider — that's the inconsistency this PR closes. This PR follows the `.cursor/skills/syncable-entity-*` guides religiously, all six steps. ## What changes **Step 1 — Types & Constants** (`@syncable-entity-types-and-constants`) - Add `connectionProvider` to `ALL_METADATA_NAME` (twenty-shared) - Make `ApplicationOAuthProviderEntity` extend `SyncableEntity` (drops the ad-hoc columns since the base class provides them, adds `deletedAt`, drops the old `(applicationId, universalIdentifier)` unique in favour of SyncableEntity's `(workspaceId, universalIdentifier)`) - `FlatConnectionProvider`, `FlatConnectionProviderMaps`, `FLAT_CONNECTION_PROVIDER_EDITABLE_PROPERTIES`, `UniversalFlatConnectionProvider`, six action types - Register in **all** the central registries: `AllFlatEntityTypesByMetadataName`, `ALL_METADATA_ENTITY_BY_METADATA_NAME`, `ALL_ENTITY_PROPERTIES_CONFIGURATION`, `ALL_MANY_TO_ONE_*`, `ALL_ONE_TO_MANY_*`, `ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION`, `ALL_METADATA_SERIALIZED_RELATION`, `ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION`, `WORKSPACE_CACHE_KEYS_V2` (`flatConnectionProviderMaps`), `METADATA_EVENTS_TO_EMIT` - `case 'connectionProvider':` in seven discriminated-union switches (`derive-metadata-events-*`, `optimistically-apply-*`, `enrich-create-*`) **Step 2 — Cache & Transform** (`@syncable-entity-cache-and-transform`) - `WorkspaceFlatConnectionProviderMapCacheService` (extends `WorkspaceCacheProvider`, decorated with `@WorkspaceCache`, soft-delete-aware) - `fromConnectionProviderEntityToFlatConnectionProvider` util - `fromConnectionProviderManifestToUniversalFlatConnectionProvider` util - `FlatConnectionProviderModule` wires the cache service - Wired the manifest converter into `compute-application-manifest-all-universal-flat-entity-maps` **Step 3 — Builder & Validation** (`@syncable-entity-builder-and-validation`) - `FlatConnectionProviderValidatorService` — never throws, returns error arrays; uses indexed `byUniversalIdentifier` for the (name, applicationUniversalIdentifier) uniqueness check (no `Object.values().find()` on the hot path) - `WorkspaceMigrationConnectionProviderActionsBuilderService` - Registered in both validators-module + builder-module - **Wired into the orchestrator** (the most-commonly-forgotten step per the rule) — constructor inject, destructure `flatConnectionProviderMaps`, `validateAndBuild`, append actions to the final migration **Step 4 — Runner & Actions** (`@syncable-entity-runner-and-actions`) - Three handlers (create / update / delete) using the canonical `WorkspaceMigrationRunnerActionHandler` mixin - Registered in `WorkspaceSchemaMigrationRunnerActionHandlersModule` **Step 5 — Integration** (`@syncable-entity-integration`) - Delete the `upsertManyFromManifest` bypass on `ApplicationOAuthProviderService` - Remove the bypass call from `ApplicationSyncService` — manifest sync now flows through the standard pipeline - Drop `ApplicationOAuthProviderModule` from `ApplicationManifestModule` (no longer needed) - Import `FlatConnectionProviderModule` from `ApplicationOAuthProviderModule` to keep the cache discoverable - 3 new exception codes: `INVALID_CONNECTION_PROVIDER_INPUT`, `CONNECTION_PROVIDER_NOT_FOUND`, `CONNECTION_PROVIDER_NAME_ALREADY_EXISTS` **Migration** - Generated via `database:migrate:generate` (instance command `1777896012579`): drops the old `(applicationId, universalIdentifier)` unique constraint, adds `deletedAt` column, adds the `(workspaceId, universalIdentifier)` unique index that `SyncableEntity` requires. - Verified clean — a second `migrate:generate` pass produces zero drift. **Step 6 — Tests** (`@syncable-entity-testing`) - 3 new specs for the manifest converter (defaults, optional fields, all-fields) - All 32 existing OAuth-provider tests still pass - ConnectionProvider has no end-user GraphQL CRUD (it's manifest-driven only), so the GraphQL integration suite that other SyncableEntities ship doesn't apply here **Codegen** - Regenerated GraphQL artifacts (twenty-front + twenty-client-sdk) against the live schema ## Why this matters Before: - `ConnectionProvider` claimed to be a `SyncableEntity` (in the enum) - But the entity didn't extend `SyncableEntity` - And the manifest sync bypassed the standard pipeline - → Verification tooling, dev UI sync tracking, anything iterating over `ALL_METADATA_NAME` got inconsistent behaviour After: - `ConnectionProvider` is a `SyncableEntity` end-to-end - Single sync path through the workspace-migration orchestrator (same as `agent`, `skill`, `frontComponent`, `webhook`, …) - One mental model ## Out of scope (deliberate) - **Renaming the table** from `applicationOAuthProvider` to `connectionProvider` — the `metadataName` is `connectionProvider` (what consumers see in code); the table name is internal. A rename would balloon this PR with mechanical churn unrelated to the sync-pipeline wiring. Worth doing as a follow-up. - **`applicationVariable` SyncableEntity conversion** — the other manifest-sync holdout. Tracked in #20215. ## Test plan - [ ] Migration up/down clean against fresh DB - [ ] Install an app whose manifest declares connection providers — providers appear in the workspace - [ ] Re-deploy the app with one provider added, one removed, one renamed → all reconciled correctly via the sync pipeline - [ ] Verify the dev-UI sync-tracking page shows ConnectionProvider entries the same way it shows agents/skills/etc - [ ] OAuth flow still works (existing connections, new connections, reconnect, list/get from SDK) — should be unchanged since the runtime code path didn't move 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7c4302d02a |
fix: show empty cell instead of 'Not shared' for soft-deleted related records (#20260)
## Summary Fixes #20076 (supersedes #20250) When a related record is soft-deleted, the frontend displays "Not shared" (lock icon) because it sees a populated FK but a null relation object. This is misleading -- the record was deleted, not permission-restricted. **Backend fix** (`process-nested-relations-v2.helper.ts`): - For MANY_TO_ONE relations, widen the relation query with `.withDeleted()` and include `deletedAt` in the select - In `assignRelationResults`, if the matched record has `deletedAt` set, nullify both the FK and the relation object in the API response - Records filtered by RLS are still not returned (even with `withDeleted()`), so they correctly continue to show "Not shared" - Strip `deletedAt` from relation results before returning to the client **Frontend fix** (`RelationFromManyFieldDisplay.tsx`): - For ONE_TO_MANY junction relations, return `null` instead of `<ForbiddenFieldDisplay />` when junction records exist but target records are unavailable ### Three cases now handled correctly: | Scenario | FK in response | Relation object | Frontend display | |---|---|---|---| | **Live record** | `"abc"` | `{ id: "abc", ... }` | Record chip | | **Soft-deleted record** | `null` | `null` | Empty cell | | **RLS-hidden record** | `"abc"` | `null` | "Not shared" | ## Test plan - [ ] Create a record with a MANY_TO_ONE relation (e.g., a person linked to a company) - [ ] Soft-delete the related record (the company) - [ ] Verify the relation field shows an empty cell, not "Not shared" - [ ] Restore the related record and verify the relation reappears - [ ] Verify that RLS-hidden relations still show "Not shared" Made with [Cursor](https://cursor.com) Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
df63dbff05 |
fix: Invalid configuration instead of related notes (#20251)
## Summary
The Notes widget (and other record-bound widgets like Tasks, Files,
Calendar, Emails) incorrectly displayed "Invalid Configuration" when
rendered on dashboard or standalone page contexts. The root cause was an
overly broad `ErrorBoundary` that caught all runtime errors uniformly
and displayed a misleading error message.
## Related issue
Fixes: #20118
## Problem Analysis
**Proximate Cause:**
- `NotesCard` calls `useTargetRecord()` which throws a generic `Error`
when `targetRecordIdentifier` is undefined
- `ErrorBoundary` in `WidgetCardShell.tsx` catches this error and
renders `PageLayoutWidgetInvalidConfigDisplay`
- This displays "Invalid Configuration" which is factually misleading
**Triggering Cause:**
- Commit
|
||
|
|
e6399b180e |
Fix/workspace member avatars 20193 (#20200)
Fixes #20193 **Bug Description:** Previously, workspace member avatars failed to render correctly in table views and relation chips (such as the Account Owner field). While the avatar picker dropdown correctly fetched fresh GraphQL data, table views and chips relied on the cached defaultAvatarUrl or avatarUrl fields, which were frequently resolving to empty strings or failing to parse external OAuth URLs correctly. **Root Cause:** - Empty String Defaults: Deleting an avatar or failing to retrieve one defaulted the database state to an empty string ("") instead of null, which caused frontend image components to break rather than render their fallback states. - Missing Permanent URLs: The WorkspaceMemberTranspiler was strictly expecting internal signed URLs. If an avatar was an external OAuth URL, it incorrectly returned an empty string, breaking SSO profile pictures. - Missing Fallbacks: New users lacked a proper Gravatar fallback assignment upon workspace creation. **Changes Made:** - user-workspace.service.ts: Updated the avatar computation logic during user creation to implement a reliable Gravatar fallback and correctly set missing avatars to null instead of empty strings. Updated the storage to use permanent file URLs. - file-url.service.ts: Implemented a getRawFileUrl method to support rendering permanent, non-expiring file URLs for avatars. - workspace-member-transpiler.service.ts: Refactored the URL transpilation logic to gracefully pass through external OAuth URLs (e.g., Google/Microsoft profile pictures) instead of stripping them. - WorkspaceMemberPictureUploader.tsx: Fixed the frontend removal logic so that deleting a profile picture sets the avatarUrl to null (consistent with the backend) rather than an empty string. **Testing:** - Verified that avatars correctly display in relation chips and table views. - Verified that external OAuth avatars load properly. - Verified that deleting an avatar correctly resets the UI to the fallback initials component. Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
97ec720d2c |
fix: show active advanced filter count badge in dropdown button (#20229)
## Summary Replaces the hardcoded `0` in `ViewBarFilterDropdownAdvancedFilterButton` with the actual count of active advanced filter rules, matching the behavior of `AdvancedFilterChip` in the view bar. ## What changed In `packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAdvancedFilterButton.tsx`: - Imported `useAtomComponentSelectorValue` and `rootLevelRecordFilterGroupComponentSelector` - Imported `useChildRecordFiltersAndRecordFilterGroups` - Replaced `const advancedFilterQuerySubFilterCount = 0; // TODO` with the real computed count via the same hook pattern used in `AdvancedFilterChip.tsx` The pill badge will now appear on the "Advanced filter" dropdown menu item showing the number of active advanced filter rules (e.g. "2" when two rules are active). ## References - Fixes #20207 --------- Co-authored-by: wadeKeith <wade@twenty.app> Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
95a34d8517 |
Return false instead of throwing when event stream does not exist (#20165)
## Summary - When an event stream expires (TTL), `addQueryToEventStream` and `removeQueryFromEventStream` now return `false` instead of throwing `EVENT_STREAM_DOES_NOT_EXIST` as an `InternalServerError` - Frontend checks the mutation return value and triggers the destroy/recreate cycle, same recovery behavior without the error path - Removes `EVENT_STREAM_DOES_NOT_EXIST` from exception code, exception filter, and frontend graceful error check since it's no longer thrown ## Test plan - [x] Verify that when an event stream TTL expires, the frontend silently recreates the stream without error noise in logs/Sentry - [x] Verify that `NOT_AUTHORIZED` errors still throw correctly on both mutations - [ ] Verify that the subscription `onEventSubscription` still works end-to-end with stream creation, query registration, and heartbeat TTL refresh Made with [Cursor](https://cursor.com) |
||
|
|
4852ac401a |
Add server upgrade status on admin panel (#20107)
## Summary Adds an admin upgrade-status panel that surfaces per-instance and per-workspace migration health, backed by a Redis-cached aggregate to keep the page snappy on large fleets. <img width="827" height="880" alt="Screenshot 2026-04-28 at 10 21 03" src="https://github.com/user-attachments/assets/8f88baa9-7268-4eff-bf6a-906a7f06ca91" /> <img width="804" height="892" alt="Screenshot 2026-04-28 at 10 21 11" src="https://github.com/user-attachments/assets/1e6decf8-766a-4d0e-96b1-03a9962bba3c" /> ## Computed metrics **Instance** (`InstanceUpgradeStatus`) - `inferredVersion` — version derived from the latest non-initial instance command name - `health` — `upToDate` | `behind` | `failed`, derived from the latest attempt vs. the last expected instance step in the upgrade sequence - `latestCommand` — `{ name, status, executedByVersion, errorMessage, createdAt }` from the most recent attempt **Per-workspace** (`WorkspaceUpgradeStatus`) - `workspaceId`, `displayName` - `inferredVersion`, `health`, `latestCommand` (same shape as instance), computed against the latest expected step in the sequence **Aggregate** (`AllWorkspacesUpgradeStatus`, only across `ACTIVE` / `SUSPENDED` workspaces) - `instanceUpgradeStatus` - `totalCount`, `upToDateCount`, `behindCount`, `failedCount` - `workspacesBehindIds[]`, `workspacesFailedIds[]` - `computedAt` ## Fetching strategy All reads go through `UpgradeStatusCacheService` (cache namespace: `EngineHealth`). - **Aggregate read** (`getAllWorkspacesStatus` → `getAllWorkspacesUpgradeStatus` query): reads summary + behind-ids + failed-ids in parallel; if any of the three keys is missing, full recompute (`recomputeAllWorkspaces`) is triggered, which also primes per-workspace entries. - **Per-workspace read** (`getWorkspacesStatus(ids)` → `getUpgradeStatus(ids)` query): `mget` on workspace keys; misses are recomputed individually (`recomputeWorkspace`), and aggregates are reconciled in place (count + id list deltas) without a full recompute. - **Recompute on demand**: `refreshUpgradeStatus` mutation calls `recomputeAllWorkspaces` to bypass cache and rewrite all keys. - **Auto-invalidation**: `InstanceCommandRunnerService` (fast + slow paths) and `WorkspaceCommandRunnerService` invalidate after every run via `safeInvalidateUpgradeStatusCache()` (`flushByPattern('upgrade-status:*')`). Failures in cache invalidation are swallowed and logged so they never break the migration runner. - **TTL**: `60 * 60 * 1000` ms (1 hour) on every key — protects against stale data even if a runner crashes before invalidating. ## Introduced cache keys All under the `EngineHealth` cache-storage namespace: | Key | Type | Purpose | | --- | --- | --- | | `upgrade-status:all-workspaces:summary` | `CachedAllWorkspacesStatusSummary` | Counts + instance status + `computedAt` | | `upgrade-status:all-workspaces:behind-ids` | `string[]` | Workspace ids in `behind` state | | `upgrade-status:all-workspaces:failed-ids` | `string[]` | Workspace ids in `failed` state | | `upgrade-status:workspace:<workspaceId>` | `CachedWorkspaceUpgradeStatus` | Per-workspace status (one key per workspace) | Full invalidation uses the pattern `upgrade-status:*`. ## Index added on `upgradeMigration` (already added on prod) Migration `2-2-instance-command-fast-1777308014234-addUpgradeMigrationWorkspaceIdIndex.ts`: ```sql CREATE INDEX "IDX_upgradeMigration_workspaceId_name_attempt" ON "core"."upgradeMigration" ("workspaceId", "name", "attempt") WHERE "workspaceId" IS NOT NULL; |
||
|
|
0bb345b75f |
Fix empty record page on system objects for non-English workspace members (#20235)
## Context Since #19890 (Translate standard page layouts), the server's `PageLayoutTab.title` resolver translates standard tab titles at query time. A workspace member in French viewing a `messageChannel` (or any system object with a standard page layout) receives `tab.title = "Accueil"` / `"Chronologie"` instead of `"Home"` / `"Timeline"`. The `SYSTEM_OBJECT_TABS` guard in `PageLayoutTabsRenderer` was comparing against an English-only literal allow-list, so every tab was dropped, `sortedTabs` became empty, and `<PageLayoutMainContent />` never mounted — the record page rendered blank (no fields, timeline, email thread, etc.). ## Fix Only run the allow-list filter when the resolved layout is the synthetic `DEFAULT_RECORD_PAGE_LAYOUT` (the client-side fallback for the few system objects with no server-side standard page layout config, e.g. `workspaceMember`, `attachment`, `message`). That layout ships hardcoded English tabs, so the English allow-list still works in every locale. System objects that do have a server-side standard page layout (`messageChannel`, `connectedAccount`, `workflowRun`, …) are no longer filtered at all, the server only ever persists Home/Timeline/Flow tabs for them, so no filter is needed. ## Before <img width="1262" height="722" alt="Screenshot 2026-05-04 at 15 15 54" src="https://github.com/user-attachments/assets/348c9e9d-0ae1-4046-8ead-470ed8263cb5" /> ## After <img width="1281" height="658" alt="Screenshot 2026-05-04 at 15 15 36" src="https://github.com/user-attachments/assets/7a9953b1-7320-4f1a-8c02-1688d3eda3ae" /> ## Note Next step should be to backfill those system objects with real record page layouts so we can remove this filter logic |
||
|
|
596ce32bd6 |
Fix front unit test on main (#20233)
Jest mocks runs before the const is defined |
||
|
|
fa8304d323 |
Fix record table dashboard save (#20202)
## Summary Fixes `METADATA_VALIDATION_FAILED` / “view field already exists” when saving dashboard **record table** widgets after the first persist (e.g. several table widgets on one dashboard). ## Problem Draft view columns used **client-generated** `viewField` ids. After save, the API stored **different** ids. The next upsert still sent the old draft ids as `viewFieldId`. The server only matched on that id, missed every row, and tried to **create** columns that already existed for the same `fieldMetadataId` + view. |
||
|
|
9e94045fa5 |
feat(apps): generic OAuth provider support for app SDK (#20181)
## Summary
App developers can now declare third-party OAuth integrations (GitHub,
Linear, Slack, etc.) in their manifest and the platform handles the full
authorize → callback → token-exchange → refresh → injection lifecycle.
The dev writes ~10 lines of config and reads tokens via
`useOAuth('linear')` inside any logic function.
```ts
// app/src/oauth-providers/linear.ts
export default defineOAuthProvider({
universalIdentifier: '...',
name: 'linear',
displayName: 'Linear',
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
connectionMode: 'per-user',
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
tokenRequestContentType: 'form-urlencoded',
});
// app/src/logic-functions/handlers/...
const { accessToken } = useOAuth('linear'); // throws OAuthNotConnectedError if missing
```
## Architecture
- **Storage**: extends the existing `connectedAccount` table — new
nullable `applicationOAuthProviderId` FK + new `app` value on the
`ConnectedAccountProvider` enum. Existing Google/Microsoft flows are
untouched.
- **OAuth flow**: a single `/apps/oauth/authorize` +
`/apps/oauth/callback` controller pair handles every app provider. State
travels in a JWT signed via the existing `JwtWrapperService` (new
`APP_OAUTH_STATE` token type).
- **Token exchange**: goes through
`SecureHttpClientService.createSsrfSafeFetch()` (so an installed app
can't point `tokenEndpoint` at internal hosts).
- **Refresh**: piggybacks on the existing
`ConnectedAccountRefreshTokensService` dispatch — Google/Microsoft
drivers untouched, new app driver lives engine-side under
`application-oauth-provider/refresh/`.
- **Injection**: the executor injects refreshed tokens as env vars
(`OAUTH_<NAME>_ACCESS_TOKEN`, `_HANDLE`, `_SCOPES`, `_CONNECTED`); the
SDK helpers `useOAuth` / `useOptionalOAuth` read them.
- **Frontend**: auto-rendered "OAuth Connections" section under each
app's settings tab (no custom front component needed). App-managed
connections are filtered out of `/settings/accounts` so the
email/calendar page stays focused.
- **Disconnect**: best-effort revoke against the manifest's
`revokeEndpoint` before deleting the row.
## Reference app
`packages/twenty-apps/internal/twenty-linear/` exercises the full
pipeline:
- `defineOAuthProvider` for Linear
- `POST /linear/create-issue` and `GET /linear/teams` HTTP-route logic
functions
- Vitest tests for the handlers
## Tests
- 14 server-side Jest tests: token-exchange util (form-urlencoded vs
JSON, PKCE, error paths), flow service (authorize URL shape, state
binding, ConnectedAccount upsert on first/reconnect, per-workspace mode,
invalid state)
- 8 app-level Vitest tests: handler error paths, GraphQL request shape,
Linear error propagation
- All 4 packages clean: `npx nx lint:diff-with-main` and `npx tsc
--noEmit`
## Test plan
- [ ] Apply migration on a dev DB: `npx nx run
twenty-server:database:migrate:prod`
- [ ] Regenerate frontend types: `npx nx run
twenty-front:graphql:generate --configuration=metadata`
- [ ] Create a Linear OAuth app at
https://linear.app/settings/api/applications/new with redirect URI
`<SERVER_URL>/apps/oauth/callback`
- [ ] Deploy + install `twenty-linear` on a workspace, paste the Linear
client id/secret into the app's variables
- [ ] Click "Connect Linear" in the app's settings tab → complete OAuth
→ verify `connectedAccount` row created with `provider = 'app'`
- [ ] Trigger `POST /linear/create-issue` with a valid teamId → verify
issue lands in Linear
- [ ] Disconnect → verify the row is deleted and (if Linear's revoke
endpoint is configured in the manifest) the revoke call fires
- [ ] Verify `/settings/accounts` does NOT show the Linear connection —
it appears only under the Linear app's settings tab
## Out of scope (deliberately)
- **Cron + per-user providers**: a cron-triggered function with a
per-user OAuth provider currently returns `CONNECTED=false` (no user
context). The follow-up design is `useOAuthForUser(name,
userWorkspaceId)` paired with a `POST /apps/oauth/connection-token`
endpoint, deferred to keep this PR focused.
- **Token encryption at rest**: tokens stored as plain `varchar`
matching the existing Google/Microsoft pattern. Worth a separate
cross-cutting PR.
- **Manifest endpoint pinning**: a malicious app upgrade could change
`tokenEndpoint` silently. Same trust model as logic-function source code
(which already runs arbitrary server-side); worth tightening across the
whole upgrade pipeline rather than just OAuth.
- **CLI helpers** (`twenty oauth show-callback-url`, `twenty oauth
connect`): manual setup for v1.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
|
||
|
|
e1828b6f41 |
[AI] Add thread actions, filters, and archive support (#20068)
## PR Description ### Summary - Add AI chat thread actions: rename, archive (soft-delete via `deletedAt`), and hard-delete with confirmation. - Add chat thread filtering by status (active/archived/all), group-by mode, and last activity. - Rework drawer/side-panel thread lists to share thread sections, item menus, archive icons, and empty-state behavior. - Extend server chat thread model/API with `deletedAt`, mutations, broadcasts, and archive-aware stream guards. ### Decisions - Two-stage lifecycle: Archive sets `deletedAt` (soft); Delete is a separate action on archived threads that hard-deletes the row. Aligns with Twenty's soft-delete convention (Felix's suggestion). - `lastMessageAt` is derived from `MAX(agentMessage.createdAt)` on read, not stored. List query does inline aggregation for sort; `@ResolveField` covers single-thread / mutation paths so the schema contract is honest everywhere. Matches `timeline-messaging.service.ts` precedent and the existing `totalInputCredits` / `totalOutputCredits` `@ResolveField` pattern in the same resolver. - Replaced auto-CRUD `chatThreads` (cursor-paginated Connection) with a custom `[AgentChatThreadDTO!]` resolver. Frontend metadata-store treats threads as a flat collection and filters/sorts client-side, so cursor pagination was performative. - Sending in an archived chat unarchives it optimistically on the client and authoritatively on the server. - Grouping and last-activity filtering use `lastMessageAt ?? updatedAt` so archive/rename don't bump threads in the list. - Kept metadata-store core API unchanged; AI chat uses the same local cast pattern already used by other metadata-store partial updates. https://github.com/user-attachments/assets/1b179b7b-1a2a-4a7a-aa0a-c88f6f051a87 |
||
|
|
b44fb1ad23 |
fix(security): reject ?token= URL query parameter for authentication (#20154)
## Summary
Removes the `?token=` URL query-parameter fallback from JWT
authentication. Every authenticated route (`/graphql`, `/metadata`,
REST, etc.) used to accept a full workspace JWT in the URL alongside the
`Authorization` header. The fallback was intended for the REST API
Playground only, but it was wired into the global Passport JWT extractor
and applied to every route.
URL-borne tokens leak into:
- Server access logs (nginx / Apache / CDN / proxy / load balancer)
- Log aggregators (Datadog, CloudWatch, Loki, Sumo, …)
- Browser history (and synced across devices)
- `Referer` headers when navigating to external pages
- Browser extensions with `tabs`/`webNavigation` permissions
A leaked log line was equivalent to a leaked workspace credential for
the lifetime of the token.
## What changed
- **`jwt-wrapper.service.ts`** — `extractJwtFromRequest()` is now
header-only (`ExtractJwt.fromAuthHeaderAsBearerToken()`). No URL
fallback anywhere in the system.
- **`open-api.service.ts` / `base-schema.utils.ts`** — Dropped the
`token?: string` plumbing that propagated the URL token into the schema
description. The "Authentication" section gains a "Never put your token
in a URL" warning. The "Usage with LLMs" section is rewritten to point
at the **Twenty MCP server** (header-authenticated, exposes typed tools
— the right tool for AI agents) instead of telling users to paste
tokenized OpenAPI URLs into Cursor/ChatGPT.
- **`RestPlayground.tsx`** — Now fetches the OpenAPI schema with
`Authorization: Bearer ${playgroundApiKey}` and passes the JSON document
to Scalar via `spec.content` instead of constructing a URL with
`?token=`. Aborts in-flight fetches on unmount/key change.
- **New integration test** — Asserts `?token=` is rejected on `/rest/*`,
`/graphql`, `/metadata`, and that `/rest/open-api/core?token=` returns
the unauthenticated base schema (no workspace object paths).
## Why not keep `?token=` scoped to the OpenAPI endpoint only
The first instinct was to narrow the fallback to just
`/rest/open-api/*`, since that endpoint is what the Scalar playground
component fetches. But the same log-leakage attack still applies to that
endpoint — the workspace JWT would still sit in access logs, just from
one URL pattern instead of all of them. The cleaner long-term fix is to
remove the URL pattern entirely and let the playground fetch with a
header (Scalar supports `spec.content` natively). For LLM agent use, the
MCP server is a strictly better path — typed tools, OAuth or
header-based API key auth, no tokens in URLs anywhere.
## Not affected
File downloads at `file-url.service.ts` also use `?token=` URLs but with
separate, short-lived `FILE`-typed tokens validated by
`file-by-id.guard.ts` directly (not via `extractJwtFromRequest`). That
mechanism is scoped per-file with limited TTL and is acceptable.
## Action required for users
Anyone who previously pasted `?token=` URLs into LLM tools, scripts,
bookmarks, or shared configs should rotate their workspace API keys.
Those tokens are likely captured in server logs / chat histories
somewhere.
## Test plan
- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx nx typecheck twenty-front` — clean
- [x] `npx nx lint:diff-with-main twenty-server` — clean
- [x] `npx nx lint:diff-with-main twenty-front` — clean
- [x] OpenAPI utils unit tests + snapshots — 11/11 pass
- [ ] Run the new integration test against a live server: `nx run
twenty-server:test:integration:with-db-reset` and verify
`url-token-auth-rejection.integration-spec.ts` passes
- [ ] Manually open Settings → Playground → REST, confirm the schema
loads (now via Bearer header instead of `?token=` URL)
- [ ] Manually verify `POST /metadata?token=<jwt>` (no Authorization
header) returns Forbidden, and the same request with the token in the
header returns the user
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
|
||
|
|
bddd23fd9c |
Fix application icons (#20142)
fixes application chip (icon Name) in all setting tables ## After <img width="1200" height="896" alt="image" src="https://github.com/user-attachments/assets/bd377f47-1d52-4142-b904-f2ce90c1db78" /> <img width="1200" height="917" alt="image" src="https://github.com/user-attachments/assets/f49cc742-f11e-47e3-86ed-34beffe493c7" /> <img width="1234" height="878" alt="image" src="https://github.com/user-attachments/assets/2ab459de-5f9d-4d39-9490-eec4ed9ee432" /> <img width="1239" height="845" alt="image" src="https://github.com/user-attachments/assets/3c1bf258-285a-47b9-a60d-05ba1564334d" /> <img width="1183" height="907" alt="image" src="https://github.com/user-attachments/assets/715b2470-2d88-48e3-88ac-d3daf3451717" /> <img width="1300" height="912" alt="image" src="https://github.com/user-attachments/assets/d7c829fa-bf1d-4f19-82de-a8bf29e22bfa" /> |
||
|
|
11628d19a3 |
add recurring calendar events for google cal (#19748)
Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
4649736d49 |
[Command Menu] Fix record-selection command filtering in edit mode (#20034)
https://github.com/user-attachments/assets/fe1461c7-0d5c-4c6f-8c2e-2cf569e7de90 ## What Fix `RECORD_SELECTION` items leaking into the command menu when nothing is selected, and unify how the menu renders in normal vs edit mode. ## The bug `RECORD_SELECTION`-availability items were showing up even when `numberOfSelectedRecords === 0`. New util `doesCommandMenuItemMatchSelectionState` gates them, applied consistently in the runtime provider and the editor. ## The refactor `PinnedCommandMenuItemButtonsEditMode` was a 140-line near-duplicate of `PinnedCommandMenuItemButtons` with its own (drifting) filter logic. Killed it. Edit mode now flows through the same `CommandMenuContextProvider` with a new `isInPreviewMode` flag — one filter chain, one rendering path. ## Behavior in edit mode **Header (pinned buttons in page header):** - Runs the full filter chain — object metadata, page type, selection state, page layout, *and the conditional availability expression* - Buttons render at full styling but are inert via `pointer-events: none` + `cursor: not-allowed` - Preview now reflects exactly what users will see on the live page (not a grayed-out approximation) **Side panel editor:** - New `useEditableCommandMenuItems` hook - Same filters as runtime *minus* the conditional availability expression and `FALLBACK` items — so it surfaces everything that's actually configurable for this page context - Still gates on selection state — if no records selected, `RECORD_SELECTION` items are hidden from the editor too. Open to feedback if we'd rather always show them so users can pin them ahead of time. ## Misc - `usePinnedCommandMenuItemsInlineLayout` — visible count now waits until every item is measured before committing. Fixes a flash of wrong counts on mount/resize - Renamed `useCommandMenuContextApi` → `useCurrentCommandMenuContextApi` — name now conveys it reads from the *current* scoped context store - Copy: "Records selected" → "Record(s) selected" |