5effee7754b9b00d3558197a4c19cbee4b2d7e72
6612 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5effee7754 |
Fix grouping a view that can no longer be changed or removed (#23619)
Fixes #23529 https://github.com/user-attachments/assets/2dbcf5ac-9b2e-4331-b7e8-703c8c5384b5 Grouping People by Company was a one-way door: once the view was grouped, the grouping could neither be changed nor removed. Two independent bugs on the same path caused it, and both had to be fixed. ## 1. The Group by entry was disabled, so the picker was unreachable `ObjectOptionsDropdownRecordGroupsContent` disabled the `Group by` entry whenever the object had a single groupable field. People exposes exactly one (Company), so the entry was always disabled there. That entry is the only way back to the field picker once a view is grouped: `ObjectOptionsDropdownCustomView` sends `Group` to the picker while the view is ungrouped, and to the group management screen once it is grouped. With the entry disabled, the picker, and with it the `None` option, became unreachable. A table view can always drop its grouping through `None`, so the entry now stays enabled there and is only disabled for layouts that require a grouping. ## 2. The view groups created by the server were never synced back The server deletes and recreates the view groups whenever `mainGroupByFieldMetadataId` changes (`handleFlatViewUpdateSideEffect`), and returns them in the `updateView` payload. `usePerformViewAPIUpdate` only wrote the view itself back to the metadata store, so the `viewGroups` entity kept the pre-change rows. The view create path already syncs them; the update path did not. On top of that, `useHandleRecordGroupField` overwrote the groups returned by the mutation with client-generated ones whose ids matched no persisted row, and `resetRecordGroupField` bailed out on `viewGroups.length === 0`. Since a relation grouping legitimately starts with no groups, clicking `None` was a no-op even when it could be reached. - sync the view groups returned by `updateView` into the metadata store - use those groups instead of regenerating them client-side - reset the grouping based on `mainGroupByFieldMetadataId`, and reload the record index states so the table regroups and ungroups without a refresh ## 3. Drive-by: No Value missing from the widget draft preview `buildDraftViewGroupsForFieldMetadataItem` mirrors `computeFlatViewGroupsOnViewCreate` so the page layout widget preview matches what gets persisted, but it returned early for relation fields and skipped the empty group. The server keeps creating it for nullable fields, relations included, so the group appeared out of nowhere once the widget was saved. It now skips only the option groups and keeps the empty group. ## Not changed Grouping by a relation shows no groups until you add them through `New group`. That is intended, since a relation can have an unbounded number of groups, and nothing here changes it. |
||
|
|
61c72942ac |
feat(workflow): dispatch automated triggers from core behind a flag (#23775)
## Context
Part of the workflow → core migration. Before we can stop writing
workspace `trigger`/`steps`, automated-trigger dispatch must read from
core. Dispatch currently reads the workspace `workflowAutomatedTrigger`
table (populated from the workspace trigger), so it would go blank once
those writes stop. This flips the dispatch reads behind a flag,
mirroring the version-content read switch.
## What this does
New flag `IS_WORKFLOW_DISPATCH_FROM_CORE_ENABLED` (per-workspace,
default off). At each dispatch read site, flag-on reads the core-derived
trigger map and flag-off keeps the current workspace query.
- **DB-event listener** (`workflow-database-event-trigger.listener.ts`):
extracted `getDatabaseEventListeners(workspaceId, eventName)`. Flag-on
filters the core map (`getOrRecompute → byWorkflowId`, `type ===
DATABASE_EVENT && settings.eventName === name`); flag-off keeps the repo
`find`. The evaluation type is broadened to the structural `{
workflowId, settings }` that both the entity and the map entry satisfy;
the enqueue loop and `shouldTriggerJob` are unchanged.
- **CRON job** (`workflow-cron-trigger-cron.job.ts`): extracted
`getWorkspaceCronTriggers(workspaceId)`. Flag-on filters the core map
for `type === CRON` → `{ workflowId, pattern }`; flag-off keeps the raw
SQL. The redis cron cache, dedup and dispatch loop are unchanged; only
the rebuild source swaps.
## Why it's safe
- The core map is keyed by the workspace `workflowId`, and both sites
enqueue `workflowId` only. Nothing consumes the map's core
`workflowVersionId`, so `workflow-trigger.job.ts` still re-derives the
version from workspace `lastPublishedVersionId` (no id translation).
- Flag defaults off, per-workspace rollout. The drift cron's
`checkAutomatedTriggerSync` already compares the core map against the
workspace table, so it's the soak signal for flipping the flag.
- The CRON source is only re-read on a cron-cache rebuild (cache miss),
so a flag flip takes effect on the next rebuild: bounded by the cache
TTL, or immediately on activation/deactivation, which invalidates the
cache. Both sources emit identical `{ workflowId, pattern }` for a
synced workspace, so the switch is a no-op in output.
## Prerequisite
- The orphan-ACTIVE core-version cleanup (#23739) must land first: the
core map is built from core ACTIVE versions, so a phantom orphan would
become a live phantom trigger the moment this flag flips.
## Verification
- Server unit specs cover both sites with the flag off (existing
behavior) and on (reads the core map).
- Live-verified on a dev instance: DB-event and CRON dispatch both fire
from the core map with the flag on, and from the workspace entity with
it off.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23775?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
b05d2406ec |
fix(twenty-front): stop field widget layout dropdown from crashing th… (#23784)
…e record page RecordTableWidgetViewDraftInitEffect read the page layout edit mode and the page layout instance id from context, but the widget settings side panel renders outside the page layout tree. Opening the Layout picker on a relation field widget displayed as a table threw "PageLayoutEditModeContext Context not found" and took down the whole record page. Both values are now passed in by the caller. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23784?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
ded3f1efb3 |
Add messages support to runAgent for multi-turn bot conversations (#23395)
- Extend `runAgent` so callers can pass either a one-shot prompt or a multi-turn messages array (user / assistant text), matching AI SDK’s XOR shape — for Slack/Discord/Teams bots that need thread history. - Enforce exactly one of prompt | messages in AgentRunService; map messages 1:1 to AI SDK ModelMessages in AgentAsyncExecutorService - Update shared types, GraphQL/SDK inputs, docs (skills-and-agents), and regenerate metadata clients; existing prompt-only callers stay unchanged <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23395?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
7c9ec6a770 |
Continue the workspace setup chat in the side panel when navigating away (#23744)
https://github.com/user-attachments/assets/579a8d41-901e-41c0-85a4-f23e6bc3da8d The /workspace-setup full-page chat shows the nav drawer, so users can navigate away mid-conversation and lose sight of the chat. Leaving the page by any means (drawer link, browser back) now opens the same conversation in the Ask AI side panel, with the full-page chat visually shrinking into the panel via the panel's existing width transition. The page marks a handoff atom while mounted; the side panel consumes it in a mount layout effect (pre-paint, so no flash frame), opens the Ask AI page, and enters at full width before shrinking. The Close button still exits without reopening the panel, the Collapse button keeps its behavior and gains the same animation, and prefers-reduced-motion skips it. Mobile is unchanged since the full-screen panel would cover the destination page. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23744?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
8bfa9c4adb |
Proxy API routes through the vite dev server to keep local dev same-origin (#23779)
Replaces #23774 (closed), rebased on latest main. ## Problem Since the cookie-session migration (#23642), the front sends every request with `credentials: 'include'` and the server only reflects `Access-Control-Allow-Origin` for the exact origins in the credentialed allowlist (`SERVER_URL`, `FRONTEND_URL`, `AUTH_COOKIE_ALLOWED_ORIGINS`). Any other origin gets the `*` wildcard, which browsers reject for credentialed requests. Local dev is split-origin by default (front on `localhost:3001`, API on `localhost:3000`), and with `IS_MULTIWORKSPACE_ENABLED` every workspace subdomain (`apple.localhost:3001`, ...) is yet another origin. Each locally created workspace would need a manual `AUTH_COOKIE_ALLOWED_ORIGINS` entry. ## Solution Make local dev same-origin instead of widening the CORS policy: the vite dev server now proxies all top-level API route prefixes to the backend, and the front calls its own origin. - `vite.config.ts` adds a `server.proxy` covering the backend's top-level prefixes (`/graphql`, `/metadata`, `/admin-panel`, `/auth`, `/rest`, `/file`, `/client-config`, ...), defined in `src/config/apiProxyPrefixes.ts`. Keys are anchored regexes (`^/auth($|[/?])`) so SPA routes sharing a prefix (`/authorize`, `/settings`) are not swallowed. The target defaults to `http://localhost:3000` and follows `REACT_APP_SERVER_BASE_URL`. `changeOrigin` stays off so the backend sees the browser's Host: same-origin checks (CSRF, cookie issuance) and workspace resolution by subdomain work unchanged through the proxy. - `config/index.ts` collapses to `window._env_?.REACT_APP_SERVER_BASE_URL || window.location.origin`. Every supported production path injects `window._env_` (docker entrypoint fails hard without `REACT_APP_SERVER_BASE_URL`; a server-served front gets it from `generateFrontConfig()`), and in dev the current origin is correct on `localhost:3001` and every `*.localhost:3001` workspace subdomain thanks to the proxy. The removed `http://<hostname>:3000` fallback only served an un-injected production bundle browsed on localhost, a setup whose credentialed auth the cookie-session migration had already broken. The credentialed allowlist itself is unchanged and stays strict; since dev traffic is same-origin, the per-subdomain cookie-allowlist problem disappears without loosening any production CORS/CSRF policy. ## Tests - `src/config/__tests__/apiProxyPrefixes.test.ts` guards the proxy boundary in both directions: representative backend path shapes (including `/metadata?query=...` and `/auth/...`) must match, every SPA route from the `AppPath` enum and vite's own dev paths must not — so a future route collision fails unit tests instead of breaking dev. - Verified against running dev servers: API paths proxy to the backend from both `localhost:3001` and `apple.localhost:3001`, while SPA routes `/settings` and `/authorize` still serve the vite app; a same-origin POST from `apple.localhost:3001` goes through with no CORS involvement. - `lint:diff-with-main` and `typecheck` pass for twenty-front. --------- Co-authored-by: Félix Malfait <felix@twenty.com> |
||
|
|
8a856a4bce |
Standardize dragged element feedback (#23772)
## Summary - make dragged table rows use a consistent background across sticky cells - apply one shared opacity treatment to table rows, Kanban cards, and other dnd-kit feedback ## Before/After <img width="1400" height="1980" alt="drag-feedback-before-after" src="https://github.com/user-attachments/assets/ee1fe81f-a762-4343-8db6-c51cf63dbac8" /> |
||
|
|
29aa6e85d6 |
fix(front): only show Discard Draft when workflow has a published version (#23756)
## Problem Draft workflows expose a **Discard Draft** action, but it fails when the draft is the *only* version of the workflow. The backend refuses the delete with `The initial version of a workflow can not be deleted` (guard in `validateWorkflowVersionForDeleteOne`), yet the action was still shown. The display condition and the delete-guard disagreed: - Display condition: `every(selectedRecords, "versions.length")` -> truthy when there is **at least one** version. - Delete guard: forbids deletion unless **another** non-deleted version exists. So a workflow whose only version is a draft showed the button, and clicking it hit a `FORBIDDEN` error. ## Fix Show the action only when the workflow has a published version to fall back to, which mirrors the backend guard: ``` every(selectedRecords, "lastPublishedVersionId") and everyEquals(selectedRecords, "currentVersion.status", "DRAFT") and noneDefined(selectedRecords, "deletedAt") ``` `lastPublishedVersionId` is a plain scalar already on the workflow. `every` (truthiness) is used rather than `everyDefined` because the field is an empty string for never-published workflows, and `everyDefined` would treat `""` as present. The command-menu evaluator reads records straight from the store, and the index/table view only fetches visible columns, so the enrichment provider now also backfills `lastPublishedVersionId` (already fetched by `useWorkflowsWithCurrentVersions`) to keep the condition reliable outside the record show page. ## Existing workspaces The standard-application full sync only runs at workspace creation, so editing the constant alone would fix new workspaces but leave existing ones showing the broken button. A `2.27.0` workspace upgrade command re-syncs the `discardDraftWorkflow` availability expression for existing workspaces, updating it only when it still equals the legacy `versions.length` value (so custom expressions are left untouched). Mirrors the existing 2-23 command-menu-item sync pattern. ## Notes - The `>`-style "more than one version" comparison is not expressible in the current `conditionalAvailabilityExpression` grammar (comparison operators only reach top-level scalars like `numberOfSelectedRecords`, not per-record paths). Gating on `lastPublishedVersionId` achieves the same intent without adding a parser helper. ## Test - Fresh workflow (single draft) -> Discard Draft hidden. - Publish, then edit to create a new draft -> Discard Draft shown and works. - Unit test on the sync-operations builder: updates the legacy expression, no-ops when already synced / custom / missing. |
||
|
|
be051c8724 |
Keep the token pair as a fallback after switching to cookie auth (#23755)
`CookieSessionBootEffect` cleared the token pair the moment it switched a client onto cookie auth. That leaves the client with a single credential, and a server that still has `AUTH_COOKIE_SESSIONS_ENABLED=false` ignores the session cookie entirely — `extractSessionTokenFromRequest` early-returns when the flag is off. A cookie-only client is therefore unauthenticated against such a server, `handleTokenRenewal` finds no refresh token, and `onUnauthenticatedError` signs the user out. That is not a hypothetical state. It is every request routed to a not-yet-rolled pod while the flag is being enabled, and every request after the flag is rolled back. Requests are load-balanced per request, so a migrated client hits an old pod almost immediately and gets signed out; signing back in can migrate it again and repeat for the length of the rollout. It also means rollback was not free, contrary to how it was described: flipping the flag back to `false` signed out everyone who had already migrated, because the pair they were supposed to fall back to had been deleted. ## Approach Keep the token pair as a dormant fallback, and stop *sending* it while cookie auth is active. Both halves are needed. Retaining it without suppressing the header would be worse than the bug: `validateTokenByRequest` checks the Bearer token first and only falls back to the session cookie when there is none, so a client that keeps sending Bearer would never exercise the cookie at all, and `CookieSessionCsrfMiddleware` bypasses on any Bearer-carrying request. Cookie sessions would silently become a no-op. So: - `switchToCookieAuth` no longer nulls the token pair - the auth link omits `authorization` while cookie auth is active, leaving the cookie as the credential in use - on an unauthenticated error while cookie auth is active, the client deactivates cookie auth once per operation and falls through to the existing renewal path, which replays with a fresh Bearer The fallback deliberately goes through renewal rather than replaying immediately: access tokens live 10 minutes, so the retained one has usually expired while the client was authenticating by cookie, and an immediate replay would just fail again. `isCookieAuthActive` is read and written through `localStorage` from the link because the links run per request and must agree with the atom synchronously — a React state update lands a render too late to affect the request being built. ## Follow-up This trades the immediate removal of the token pair from `localStorage` for rollout safety, so the XSS-exfiltration surface that cookie sessions close stays open a while longer. Once cookie sessions are stable across every environment, the retained pair should be dropped — reverting to a clear on `switchToCookieAuth` is a one-line change. ## Test Three cases added to `apollo.factory.test.ts`: no Bearer header while cookie auth is active; an unauthenticated response falls back and replays with the token pair rather than calling `onUnauthenticatedError`; and the fallback is attempted only once before going through renewal. The existing `CookieSessionBootEffect` assertion that the pair is cleared is inverted to assert it is retained. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23755?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
72e2301697 |
fix(settings): unbreak the AI tools table and validate page-level graphql documents (#23731)
## Problem
Opening Settings > AI (tools tab) fails with a misleading error:
```
{"errors":[{"message":"App version mismatch.","extensions":{"code":"APP_VERSION_MISMATCH"}}]}
```
The real error is a GraphQL validation failure. Both queries on that
page select a `logo` field that no longer exists:
- `FindManyApplicationsForToolTable` selects `Application.logo`
- `FindManyMarketplaceAppsForToolTable` selects `MarketplaceApp.logo`
#23411 renamed `MarketplaceApp.logo` to `logoUrl` and stopped exposing
`logo` on `Application`, but missed these two queries (added in #21121)
and the types/component behind them. So the schema exposes `logoUrl`
while the AI settings page still asks for `logo`.
The reason the error says "App version mismatch" is that
`useGraphQLErrorHandlerHook.onValidate` does not return validation
errors as-is: when a document fails validation and the request's
`x-app-version` is semver-lower than the server's `APP_VERSION`, it
throws `APP_VERSION_MISMATCH` instead. On an instance where the frontend
build trails the backend, every genuine query bug on that instance
surfaces as this message, and refreshing never helps because the query
is wrong in the code.
## Why nothing caught it
Two gaps lined up:
1. **The documents were invisible to codegen.** `codegen-metadata.cjs`
lists documents as an explicit allow-list of
`./src/modules/*/graphql/**` entries; nothing under `./src/pages/**` was
ever in it. Codegen validates every matched document against the live
schema and CI fails on drift, so the rename would have been caught had
these two queries been in the matched set.
2. **The response types were hand-written.**
`SettingsAgentToolApplication` / `SettingsAgentToolMarketplaceApp` were
free-standing object types declaring `logo?: string | null`, passed as
the `useQuery` generic. Nothing tied them to the schema, so they kept
compiling after the field was gone.
## Changes
Fix:
- Select `logoUrl` instead of `logo` in
`findManyApplicationsForToolTable` and
`findManyMarketplaceAppsForToolTable`.
Prevention:
- Add `./src/pages/**/graphql/**/*.{ts,tsx}` to `codegen-metadata.cjs`,
so page-level documents are validated by the CI codegen check and get
generated operation types.
- Add three more previously-unvalidated metadata modules to the same
list: `metadata-store`, `sse-db-event`, `geo-map`.
- Derive `SettingsAgentToolApplication` /
`SettingsAgentToolMarketplaceApp` from the generated operation types
instead of hand-writing them.
- Type `SettingsToolIcon`'s `ApplicationInfo` / `MarketplaceAppInfo` as
`Pick` of those, so a field disappearing from the schema is a compile
error rather than a silently-optional property.
- Regenerate `generated-metadata/graphql.ts` (additive: operation types
+ typed document nodes for the five newly covered documents).
App logos in the tools table also render again, which they hadn't since
#23411.
## Verification
Swept every file containing a `gql` document in twenty-front (484)
against the document globs of all three codegen configs. 65 are
uncovered, most legitimately so (runtime-generated record queries,
mocks, tests, stories). The static documents no config validated were
the two fixed here (broken), `metadata-store` / `sse-db-event` /
`geo-map` (valid, now covered), and `information-banner` /
`settings/legal` (valid, core schema — left alone since `codegen.cjs`
targets a schema that can't be validated offline; worth a follow-up).
Validated the fixed documents against the checked-in metadata SDL, and
reproduced `generated-metadata/graphql.ts` with the pinned codegen
toolchain to confirm the regenerated file matches what CI produces. CI's
own codegen check (`server-validation`) then confirmed it against a live
server, alongside front typecheck, lint, jest and builds.
## Follow-up, not in this PR
The error masking in `use-graphql-error-handler.hook.ts` is worth
revisiting:
- It discards the original validation errors, so a real query bug is
unreportable on any deployment where the frontend trails the backend.
Attaching the underlying errors (or at least logging them) would have
made this a one-minute diagnosis.
- The `x-schema-version` branch above it is dead: no client in the repo
sends that header.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FuH2fAvLct7NvTUEnAWPSV)_
|
||
|
|
d8cb7cfb55 |
i18n - translations (#23748)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23748?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
4b3614b413 |
fix(front): show relation value chip (Me / record names) in advanced filters (#23718)
## Problem
In advanced filters, a relation filter on a workspace-member field (e.g.
Assignee "is Me") displayed its raw JSON value
`{"isCurrentWorkspaceMemberSelected":true,...}` instead of a readable
chip.
Regular (non-advanced) filters handle this correctly:
`EditableRelationFilterChip` computes the label at runtime via
`useComputeRecordRelationFilterLabelValue`, rendering "Me", the selected
record names, or "N members".
The advanced filter value input instead relied on the deprecated stored
`displayValue` through `getRecordFilterDisplayValue`, which has no
`RELATION` branch and falls back to the raw value. When a saved view
filter carries no `displayValue` (it defaults to the raw stringified
value in `mapViewFiltersToFilters`), the raw JSON leaked into the UI.
## Fix
- Extract the relation value-label computation into a shared hook
`useComputeRecordRelationFilterDisplayValue` (parses the relation value,
resolves "Me" + record names).
- `useComputeRecordRelationFilterLabelValue` now consumes it (regular
chips unchanged).
- The advanced filter clickable select renders a dedicated
`AdvancedFilterRelationValueInputClickableSelect` for `RELATION`
filters, computing the label at runtime just like regular filters.
## Proof
Both filter surfaces render the relation value as **Me**, not the raw
`{"isCurrentWorkspaceMemberSelected":...}` JSON. The advanced-filter
shot loads a **saved view in a fresh session** — the exact bug
condition, where the view filter carries no stored `displayValue`.
**Regular filter chip**
<img width="1280" height="760" alt="image"
src="https://github.com/user-attachments/assets/8a867f51-a538-46f2-ba21-a16bb70d85a5"
/>
**Advanced filter**
<img width="1280" height="760" alt="image"
src="https://github.com/user-attachments/assets/b09b9d70-5692-4bac-8cec-3cb006961042"
/>
## Test
Verified manually on a local instance: created a saved view with an
advanced filter `Account Owner Is Me`, then reloaded it in a fresh
session — the condition where the view filter carries no stored
`displayValue`. The value renders as "Me" instead of the raw JSON.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23718?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
5bafaa0994 |
Hide onboarding credits when billing is disabled (#23717)
Onboarding advertises free credits (the header pill and the green "Earn +N free credits" tags) even when `IS_BILLING_ENABLED` is false, promising a reward that can never be granted: `creditWorkspaceBalance` already no-ops when billing is off. The server now omits the `onboarding` credit-rewards block from the client config when billing is disabled, which hides every reward tag on its own since they all render behind a defined-config guard. The header pill gets an explicit gate. Also stops treating onboarding invites as reward-eligible when billing is off, so they are minted as plain invitation tokens and the 10-invite `ONBOARDING_INVITE_TEAM_MAX_INVITES` cap no longer applies to self-hosted instances. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23717?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
6f6da14cc8 |
Stop showing raw chunk load errors (#23571)
On iOS Safari, a failed chunk load showed a snackbar containing the raw browser string `Importing a module script failed.` `PromiseRejectionEffect` snackbars the raw `error.message` of any unhandled rejection, so a floating dynamic import leaked browser internals to the user. It now skips the snackbar for stale chunk errors, which are still captured by Sentry. This only changes what the user sees, not the underlying fetch failure. |
||
|
|
4e3747f3a9 |
fix(twenty-front): expand HTML preview viewport in DocumentViewer (#23668) (#23676)
## Description Fixes #23668. When previewing `.html` or `.htm` files in the file preview modal, `@cyntler/react-doc-viewer` mounts an `iframe` inside `#html-renderer`. Previously, `StyledDocumentViewerContainer` applied `height: 100%; width: 100%;` to `#react-doc-viewer`, `#proxy-renderer`, and `#msdoc-renderer`, but omitted `#html-renderer` and its inner `iframe`. As a result, the preview iframe defaulted to inline iframe bounds instead of expanding to fill the modal container. This PR adds `#html-renderer`, `#html-renderer iframe`, and `iframe` selectors to `StyledDocumentViewerContainer`, ensuring HTML previews expand fully within the modal viewport. ## Testing - Verified StyledDocumentViewerContainer CSS rules target `#html-renderer` and `iframe` elements. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23676?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
116c04d8b2 |
Record and enforce per-user OAuth application authorizations (#23678)
Sits on `main` now that #23642 has merged. 18 files changed. ## Why Application tokens are stateless JWTs. When a user completes an OAuth `authorization_code` exchange, the server issues an access/refresh pair carrying `userId` as a claim and stores nothing. So today: - there is no record that a person ever authorized an app, hence nothing to list on a settings screen - there is no way for that person to take an app's access away. The only revocation that exists is uninstalling the app, which is workspace-wide and admin-only - `/oauth/revoke` accepted a refresh token, logged it and did nothing, because there was no state to change `client_credentials` is unaffected: no user is involved and it returns an access token with no refresh token. ## What **`core."applicationAuthorization"`**, one row per (user, application), unique on that pair so re-authorizing updates in place. Written at the `authorization_code` exchange, before the token pair is issued, so a refresh token is never handed out without the grant that makes it redeemable. A dedicated table rather than a new `AppTokenType`: this is a grant keyed on identity, not a token keyed on a secret, and `appToken` is already overloaded. FKs to user, workspace, application and userWorkspace all cascade, which covers hard deletes. Membership removal soft-deletes the `userWorkspace` row, so that cascade does not fire and the grant outlives the membership. The refresh path therefore rechecks membership on every renewal rather than trusting the row's existence. **Enforcement.** `refresh_token` checks the row when the token carries a user, and returns `invalid_grant` if it is revoked. Revoking does not kill live access tokens, so access ends within one access-token window (`APPLICATION_ACCESS_TOKEN_EXPIRES_IN`, 30 minutes) rather than instantly. The alternative is a DB read on every API request, which is not worth it for a 30 minute tail; the UI should say so. **RFC 7009 revocation now revokes.** Revoking a refresh token revokes the authorization behind it. It also now checks the token was issued to the client asking, which it never did before. That check did not matter while revocation was a no-op; it does now. **Introspection** reports a refresh token inactive once its authorization is revoked. Access tokens keep reporting active until they expire, because they genuinely still work. **API:** `currentUserApplicationAuthorizations` and `revokeApplicationAuthorization`, both behind `UserAuthGuard`. The mutation scopes by `userId` inside the `UPDATE` rather than read-then-write, so one user cannot revoke another's authorization by guessing an id. ## Backwards compatibility Refresh tokens already in the wild have no row. Rejecting them would sign every live integration out on deploy, so the first refresh backfills the grant that was always implied. A revoked authorization keeps its row, so this never resurrects access someone turned off, and the backfill is insert-only so it cannot overwrite a real consent. If the user has since left the workspace, the refresh fails instead. Those tokens carry no scope claim and no record of when consent was given, so `scopes` and `lastAuthorizedAt` are nullable and left null on a backfilled row. Null means "the original consent is not on record" rather than a guess assembled from what the application declares today; a real re-authorization fills both in. Revoking such a token lays the row down before marking it, so the revocation sticks instead of being undone by the next refresh. ## Not in this PR The settings UI, following how #23643 shipped the sessions API and #23645 the devices screen. Introspection still reports a refresh token active once the membership is gone. That matches access tokens, which genuinely keep working in that case, so closing it belongs with the wider question of validating membership on every application-token request. ## Testing - 29 unit tests across the authorization service and the three OAuth grant paths - 9 integration tests on `/oauth/token`, `/oauth/revoke` and the GraphQL API: scopes as granted are recorded, revoking blocks the next refresh, re-authorizing reinstates, a pre-record token backfills without inventing a consent, a revoked pre-record token stays revoked, the authorization is listed to the user who granted it, revoking from that list stops the refresh token being redeemed, a repeated revocation reports no-op, and another user can neither see nor revoke it - the cross-user isolation and revoke-from-list tests are mutation-checked: dropping the `userId` scoping from `revokeAuthorizationById` fails only the isolation test, and disabling the `revokedAt` check in `oauth.service.ts` fails the revoke-from-list test plus two pre-existing ones - full `twenty-server` suite green - instance command applied against a fresh `database:reset`, table/index/FK shape verified against `information_schema` Closes part of https://github.com/twentyhq/core-team-issues/issues/2747 --------- Co-authored-by: prastoin <45004772+prastoin@users.noreply.github.com> |
||
|
|
713fae189d |
i18n - translations (#23720)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23720?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
267ecb12db | Migrate web auth from localStorage token pairs to httpOnly cookie sessions (#23642) | ||
|
|
d359496b8b |
Keep chart palette colors stable when series order changes (#23638)
https://discord.com/channels/1130383047699738754/1522812783140540538 Chart palette colors were assigned by array position, so changing the sort order (or any reordering of the data) reshuffled every series color on line, bar and pie charts. Grouping by a select field was unaffected since options carry their own colors — this only hit groupings without intrinsic colors (relations, text fields, etc). Colors are now assigned by the alphabetical rank of the series key, so a key keeps its color no matter what order the data arrives in. Side effect: existing palette-colored charts get a one-time color reassignment. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23638?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
b7724bbbee |
i18n - translations (#23713)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
2389b4f807 |
Add captcha and throttling to the password reset link (#23372)
The public `emailPasswordResetLink` mutation was the only email-taking auth mutation without `CaptchaGuard`, so bots could drive reset email spam against arbitrary addresses. - Adds `CaptchaGuard` and a `captchaToken` argument (no-op when no captcha provider is configured). The frontend sends it like sign-in does, and `/settings/profile` joins the captcha-protected paths so the Change Password button keeps working - Throttles reset emails per address, 3 per 15 minutes, and surfaces a rate limit error once the bucket is empty - Acknowledges the request as soon as the throttle passes and generates the link off the request path, so the response time no longer depends on whether the address is registered - Returns a generic success instead of distinguishing found from not-found, with matching frontend copy - Rotates the reset token in a single transaction, so a failed write can no longer revoke a still valid link This does not close user enumeration on its own: `checkUserExists` exposes `exists` on the same unauthenticated surface, and sign-in returns distinguishable errors. Tracked in #23711. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23372?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
22d83c75e6 |
fix(twenty-front): Scrolling and Dragging conflict on mobile devices (#23677)
Fixes: #23675 https://github.com/user-attachments/assets/1f0d4f0f-0dd6-4731-8359-6da13a5e11b3 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23677?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
e81fdbcc7a |
feat(workflow): variable pickers for Search Records limit, offset and date filters (#23696)
## Summary <img width="491" height="390" alt="Capture d’écran 2026-08-03 à 11 30 40" src="https://github.com/user-attachments/assets/6db59b8a-3e8e-41b8-80b3-c736e56b7f7f" /> Adds workflow variable pickers to the **Search Records** action for fields that previously only accepted static values: - **Limit** and **Offset** number inputs now expose the `WorkflowVariablePicker`, so they can be bound to a variable from a previous step. The stored value can be a standalone variable string; the backend coerces the resolved value back to a number. - **Date filters** using the `Is before` (`IS_BEFORE`) and `Is after or equal` (`IS_AFTER`) operands now expose the variable picker in the advanced filter side panel (previously disabled for all date filters). The backend already resolves these inputs via `resolveInput`; the only backend change is a small numeric coercion of the resolved limit/offset. ## Changes - `WorkflowEditActionFindRecords.tsx` — pass `WorkflowVariablePicker` to the Limit/Offset inputs; make `onChange` and form state variable-aware (`number | string`). - `AdvancedFilterSidePanelValueFormInput.tsx` — enable the date `VariablePicker` only for `IS_BEFORE` / `IS_AFTER`. - `useGetRecordFilterDisplayValue.ts` — return the raw variable for a standalone `{{variable}}` value so date filters don't crash `Temporal.*.from`. - `find-records-action-settings-schema.ts` — allow a string (variable) for `limit` / `offset`. - `find-records.workflow-action.ts` — coerce resolved `limit` / `offset` to numbers before querying. ## Testing Built a workflow locally (Manual trigger → Code step returning `{ limit: 2, offset: 1, sinceDate }` → Search Records) with all three fields bound to those variables. The run completed successfully; the Search Records step returned exactly 2 records (limit applied) filtered by `createdAt >= sinceDate`, confirming the backend resolves each variable. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23696?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
e2d71d2635 |
i18n - translations (#23698)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23698?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
536b91c1dd |
Update record page layout card (#23654)
## Summary - Redesign the data-model Layout card to match the record-page customization design. - Add dedicated light and dark cover assets and preserve the existing customization workflow. - Reuse a dedicated discovery-hero footer across the workspace and record layout cards. - Match the Figma cover, footer, icon, typography, and spacing metrics. ## Before/After <img width="1588" height="720" alt="before-after" src="https://github.com/user-attachments/assets/28be577c-9f1b-40f1-99e2-66eeafbac75b" /> |
||
|
|
503e40d51b |
i18n - translations (#23659)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
f663cd3c68 |
Move open-record-in to object metadata and member preference (#23614)
Replaces the per-view "Open in" setting with a two-level model, following up on #23422 / #23424 and superseding the closed #23446 and #23457: - `objectMetadata.openRecordIn`: `SIDE_PANEL` | `RECORD_PAGE` | `USER_CHOICE` (default `USER_CHOICE`) - `workspaceMember.openRecordIn`: `SIDE_PANEL` | `RECORD_PAGE` (default `SIDE_PANEL`), editable in Settings > Experience The rule: records open where the member prefers, unless the object pins them, and never in a panel there is no room for (mobile always resolves to the record page). ## Why Having the setting on views, objects and members at once was heavy, and view-level resolution was fragile: a chip rendered outside a view (notes, front components, kanban cards pointing at another object) had no view to read from, which is the class of bug behind #23422. Resolution is now context-free: it needs only the object, the current member and the viewport, so chips behave identically everywhere by construction. ## Changes **Object level** - New `openRecordIn` enum column on `objectMetadata`, editable through `updateOneObject` and surfaced in Settings > Data model > Object > Layout ("Open records in": Member preference / Side Panel / Record Page) - Standard definitions pin `workflow`, `workflowVersion`, `dashboard` and `messageCampaign` to the record page (matching the previously hardcoded list) and `calendarEvent` to the side panel (it has no curated record page); everything else, including `workflowRun`, follows the member preference - Apps can set it in `defineObject()` via the object manifest **Member level** - New `openRecordIn` standard field on `workspaceMember`, persisted through the existing settings path (same as `colorScheme`) and exposed in Settings > Experience **View level (deprecated)** - `view.openRecordIn` is no longer read or written by the frontend; the "Open in" entry is gone from the view options dropdown - The column, DTO field and inputs are kept for one release for API compatibility: the output field carries a `deprecationReason`, the inputs keep accepting the value with a `Deprecated:` description (NestJS silently drops input fields that have a `deprecationReason`, which would have been a breaking change) **Upgrade (2.27)** - Fast instance command adds the `objectMetadata.openRecordIn` column defaulting to `USER_CHOICE` - Workspace command adds the `workspaceMember.openRecordIn` field - Workspace command seeds the object column from the standard definitions (any non-`USER_CHOICE` value), then lifts deliberate per-view record page choices onto objects the definitions don't pin **Debt removed** - `canOpenObjectInSidePanel` hardcoded object list and its test - `ObjectOptionsDropdownLayoutOpenInContent` and the `layoutOpenIn` dropdown wiring - `DefaultViewOpenRecordIn` - Context-store/view-based resolution in `useResolveOpenRecordIn` (now reads object metadata + member + viewport) - Front components no longer guess from the current view: an explicit side-panel call honours a pinned object and the viewport, nothing else ## Verification - Ran the three upgrade commands against a live database: column created, the pinned standard objects seeded per workspace (record page pins plus calendarEvent to side panel), member field backfilled to `SIDE_PANEL`; seed rerun is a no-op - Seed command verified on a simulated pre-upgrade workspace (index view set to record page on company): pins the standard objects plus company, idempotent on rerun - Both packages typecheck and lint clean; affected unit suites and the application sync, view creation and metadata cache integration specs pass --------- Co-authored-by: Thomas des Francs <tdesfrancs@gmail.com> |
||
|
|
706d72e53e |
fix(front): reload record board groups when view groups change (#23637)
## Problem Fixes #23462 On a Kanban (record board) view grouped by a SELECT field, adding a new option to that field creates the column, but dragging a record into the new column silently fails (no move, no error) until a hard page reload. ## Root cause `RecordIndexLoadBaseOnContextStoreEffect` builds its load key from the view id and the calendar-week flag only: ``` `${contextStoreCurrentViewId}-${isCalendarWeekViewEnabled}` ``` The effect early-returns when `loadedViewKey === currentViewLoadKey`. When a new `ViewGroup` is created from the added SELECT option, the view id does not change, so the key is unchanged and `loadRecordIndexStates` never re-runs. The record group state (`recordGroupIdsComponentState` / `recordGroupDefinitionFamilyState`) stays stale, so the drop handler cannot resolve the new group's field value and the move no-ops. A hard reload fixes it because the view then loads with the new group present from the start. ## Fix Include a signature of `view.viewGroups` (ordered `id:position:isVisible`) in the load key so the effect re-runs `loadRecordIndexStates` whenever the view's groups change, not only when the view id changes. The calendar-week flag is kept in the key. ## Testing Verified end to end on a local instance against an Opportunities "By Stage" Kanban, with the record's stage change confirmed in the database: - **Before the fix:** add a new Stage option in-session, then drag a record into the new column. Dragging into an existing column persists the move; dragging into the newly created column does nothing (record's stage unchanged in DB). - **After the fix:** same flow, dragging a record into the newly created column moves it and persists the new stage in DB, with no reload. `nx typecheck twenty-front` and `oxlint` pass. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23637?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
d02332834b |
Fix record title cell reopening on every page refresh (#23551)
## What `location.state.isNewRecord` is set when navigating to a freshly created record so the title cell opens for naming. But router state lives in **browser history state, which survives page refreshes** — so every refresh of that record's page re-opens the title cell with an empty draft and a blinking cursor. Strip the flag after its one intended consumption in `PageChangeEffect` (react-router keeps user state under `history.state.usr`). ## Repro (on current main, any view set to open records in record page — or on mobile) 1. Create a record from a table; you land on its record page with the title focused (intended). 2. Name it, click away, then refresh the page. 3. The title cell re-opens, empty, focused — on every refresh, forever. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23551?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com> |
||
|
|
17ddff8470 |
i18n - translations (#23620)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
3ed11054a0 |
Keep leading + when filtering phones by calling code (#23546)
Fixes #23528 Filtering a PHONES field with `CONTAINS` / `DOES_NOT_CONTAIN` stripped every non-digit character from the filter value, so `+33` became `33` and the generated `ilike`/`like` predicates could not distinguish an international calling code from any number containing those digits. `turnRecordFilterIntoGqlOperationFilter` now preserves a leading `+` while still removing other formatting characters (spaces, dashes, parentheses). `+33 6 12` becomes `+33612`; values without a `+` are unchanged. Added a regression test in `computeViewRecordGqlOperationFilter.test.ts` for a `+`-prefixed value. Lint and typecheck pass on `twenty-shared` and `twenty-front`; the filter test suites pass in both packages. --------- Co-authored-by: Thomas Trompette <tom@twenty.com> |
||
|
|
8f9f2f390e |
fix(ai-chat): show streaming activity during and between steps (#23581)
https://github.com/user-attachments/assets/e72e313c-66f8-40af-bf48-9225422ffa78 ## Problem During a streaming turn with tool calls, the chat goes completely static in two places: - **Between two steps**: once a tool's output arrives, its row flips to past tense and nothing animates until the model's next chunk arrives (a full LLM round trip, often several seconds). This window is defined by the absence of parts, so no part-driven component can fill it — and the pre-turn "…" indicator can't either, since it's cleared on the turn's first chunk and never comes back. - **During tool execution**: the active tool row in `ThinkingStepsDisplay` is a static icon + label; the only animated element there is the orbit loader on an actively-streaming reasoning part. Users can't tell whether the AI chat is still thinking or blocked. ## Fix - **Pending thinking row between steps.** The renderer flags the trailing thinking-steps group of a streaming, error-free message (`showPendingThinkingRow`), and `ThinkingStepsDisplay` appends the thinking row (orbit loader + "Thinking") inside its rows container when none of its own steps is active (`isThinking`, which it already computes). The row occupies the exact slot where the next real step row materializes, so the handoff happens in place with no layout shift. - **One shared row component.** `AiChatThinkingRow` renders the orbit loader + "Thinking" and is used both for an actively-streaming reasoning step and for the pending row. - **Shimmer on executing tools.** Active tool rows wrap their label ("Searching the web for…") in the existing `ShimmeringText` while awaiting output, with the text as a direct child of the background-clip element so the effect applies reliably. - **Activity derived from the tool lifecycle state.** `isThinkingStepPartActive` now checks `input-streaming` / `input-available` instead of output presence, so a tool completing with a legitimate `null` output is no longer classified as still running. Why the trailing-group check is sufficient: anything in progress outside the group — streaming answer text, a running code execution card, a pending question — is itself a later render item, so the group isn't last and never gets flagged. No message-wide part scanning needed. ## Notes - The row renders only while `agentChatIsStreaming`, which the existing keepalive watchdog force-clears (with a visible connection-lost error) after ~5s of subscription silence — it cannot spin forever on a dead stream. - It never shows while waiting on the user: `ask_questions` renders as its own item after the group, and the server ends the stream on that tool anyway (`stopWhen`). - Consciously not covered, for simplicity: a pause right after a mid-turn text part or right after the routing row. ## Tests - Renderer: trailing group flagged as pending while streaming; not flagged when answer text follows or when not streaming - `ThinkingStepsDisplay`: pending row appended after completed steps, suppressed while a tool step runs, loading label shown on a running tool - `isThinkingStepPartActive`: lifecycle-state cases, including a completed tool with `null` output Lint, format, and `typecheck twenty-front` are clean. |
||
|
|
a9084604b4 |
Use the fast model for the onboarding setup chat (#23586)
The workspace setup chat ran on the smart model. The hidden kickoff turn enqueued its job without a `modelId`, and the frontend sends none unless the user picks one, so every turn fell through to `modelId ?? workspace.smartModel` in `chat-execution.service.ts`. Two halves, since the kickoff is server-initiated and the frontend never sends it: - `startHiddenKickoffStream` takes a `modelId` and the setup chat passes `workspace.fastModel`. - `useAgentChatModelId` requests `workspace.fastModel` on the setup page, so user turns follow. Everywhere else it still sends nothing and the server fallback is unchanged. `workspace.fastModel` defaults to the `default-fast-model` sentinel, so the model still resolves through the registry and stays admin-overridable. An explicit pick from the model picker still wins. |
||
|
|
53a18d7528 |
feat(workflow): route all version content readers through the flag-aware sources (#23583)
## What Follow-up to #23499. Migrates every remaining reader of `record.trigger`/`record.steps` so all version content flows through the flag-aware sources, then removes `trigger`/`steps` from the record field sets. The record CRUD path no longer carries version content anywhere in the app. Reading is still entirely behind `IS_WORKFLOW_VERSION_IN_CORE_ENABLED`: this PR changes who asks, never where the answer comes from. Flag off remains record reads (via the content hook's record branch), flag on the core query. ## Per reader | reader | now reads | | --- | --- | | `WorkflowDiagramCanvasEditable` (connect, drag-stop) | flow atom | | `useDeleteStep` | flow atom | | `WorkflowEditActionIfElseBody` (branch cleanup) | flow atom | | `SidePanelWorkflowCreateStepContent` (parent-step lookup) | flow atom | | `SidePanelWorkflowStepInfo` | flow atom (explicit instance id), falling back to `useWorkflowVersionContent` when the visualizer is not mounted | | `TestWorkflowSingleRecordCommand` | `useWorkflowVersionContent`; `ready` gates on content being loaded | | headless enrichment hook (imperative) | core content query when the flag is on, record otherwise | | `WorkflowRunVisualizerEffect` (step output schemas) | the run snapshot (`state.flow`), which is what a run should show anyway | ## Field-set slimming `useWorkflowVersion` and `useWorkflowWithCurrentVersion` stop fetching `trigger`/`steps` (identity fields only). Three call sites lost their only reason to call `useWorkflowWithCurrentVersion` and were dropped entirely. Verified by grep that no `currentVersion.trigger/steps` reads remain; the only remaining `.trigger`/`.steps` accesses are argument-taking utils whose callers now pass flow/content-sourced objects. ## Verification - `nx typecheck twenty-front` green - Front tests: 1058 green (the enrichment test gained mocks for the apollo client and flag its hook now uses) - Full-tree `oxfmt` + `oxlint --type-aware` green (3 remaining warnings are pre-existing in unrelated record-field files) - Live click-through pending, flag off and on <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23583?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
0d63906a58 |
Fix calendar field picker state handling (#23595)
## Context Changing a calendar date field already updated the record-index calendar state immediately, so the calendar moved to the new field before the `updateView` mutation completed. The options dropdown still derived its selected checkmark and field labels from `currentView`, which remains unchanged while persistence is pending. On slower environments this left the previous field name visible even though the calendar was already using the new field. Locally the same mismatch existed, but was only visible briefly because the mutation completed faster. ## What changed - Read the active start and end date field IDs from the record-index calendar component state in the calendar options dropdown. - Use that state for the main options label, the two-field submenu, and both field-picker selections. - Use the optimistic end-field state when filtering compatible start fields and deciding whether an incompatible end field must be cleared. - Keep the existing view mutation and calendar-state writes unchanged. ## Why The calendar and its configuration UI now share the same source of truth while persistence is pending. A field selection updates the calendar, checkmark, and contextual labels together instead of temporarily mixing optimistic calendar state with stale persisted view metadata. ## Safety and expected impact This is frontend state synchronization only. It does not change the metadata schema, API payloads, or persistence flow. Existing date and datetime compatibility rules remain in place. Users should see the selected field name update immediately, including when the metadata mutation is slow. ## Limitations This does not change mutation error handling or add rollback behavior. The calendar atoms were already updated optimistically before this change, this PR only makes the configuration UI reflect those same values. ## Validation - Reproduced the stale selection on qacoco and locally. - Verified locally that the checkmark moves immediately after selecting another date field, before the mutation closes the dropdown. - `npx nx typecheck twenty-front` - Focused type-aware oxlint on the four changed files, 0 warnings and 0 errors. - `npx oxfmt --check` on the four changed files. - `git diff --check` |
||
|
|
b4102946f4 |
i18n - translations (#23598)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
a747e62970 |
fix(workflow): use as draft with an existing draft and cross-object record page filters (#23524)
Fixes two workflow issues.
### "Use as draft" fails when a draft already exists
`UseAsDraftWorkflowVersionSingleRecordCommand` rendered its own
`OverrideWorkflowDraftConfirmationModal`. Headless commands are
unmounted by `HeadlessEngineCommandWrapperEffect` as soon as `execute`
resolves, so the modal was removed from the tree right after `openModal`
was called and the user saw nothing happen. This regressed when the
command was converted from a rendered `<Command onClick>` to a
self-unmounting effect.
The command now uses `HeadlessConfirmationModalEngineCommandEffect`, the
existing mechanism for headless commands that need a confirmation: it
opens the app-wide `CommandMenuConfirmationModalManager` and keeps the
command mounted until the modal emits its result.
`OverrideWorkflowDraftConfirmationModal`, its modal id and its config
state are deleted.
To keep the "Go to Draft" shortcut, the shared confirmation modal config
gains an optional `linkButton` rendered as a secondary link button that
emits a `cancel` result on click.
The command also resolved the workflow id through `useWorkflowVersion`,
which is `undefined` while the query is in flight, so it threw and
surfaced an error snackbar through the command error boundary. It now
reads the workflow id off the selected record like the sibling workflow
version commands, and waits for the workflow to load before deciding
whether a confirmation is needed.
### `workflow object doesn't have any "workflowId" field` when opening a
workflow from a version
`useRecordShowPagePagination` builds prev/next queries from the parent
view stored in `contextStoreRecordShowParentViewComponentState`.
Navigating from a workflow version record page to its workflow through
the relation chip keeps the workflow version parent view, whose relation
filter compiles to `workflowId: { in: [...] }` and is then sent against
the `workflow` object, which the API rejects.
`useQueryVariablesFromParentView` now ignores the parent view when
`parentViewObjectNameSingular` does not match the current object, which
covers every navigation path between record pages of different objects.
### Test
Added `useQueryVariablesFromParentView.test.tsx` covering both the
matching and mismatching parent view object.
Manually checked on a local instance: override with an existing draft,
"Go to Draft", cancel then re-trigger, and the no-existing-draft path,
plus navigating from a filtered workflow versions view to a workflow.
---------
Co-authored-by: Tom <tom@twenty.com>
|
||
|
|
f3de8ce631 |
i18n - translations (#23587)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
5848c9bd30 |
Display object, field and view links as chips in the AI chat (#23573)
<img width="3840" height="1876" alt="CleanShot 2026-07-30 at 15 37 46@2x" src="https://github.com/user-attachments/assets/9fe178b9-c2fa-4b05-9c9d-0cdc80270b67" /> https://github.com/user-attachments/assets/34c4e486-c462-4300-ae98-da99f614f069 The AI chat already renders record chips from a `[[record:...]]` marker the model writes in its prose, but naming an object, field or view produced plain text. This adds three sibling markers so those render as chips too, as in the [Figma design](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=104416-116261). - `[[object:<nameSingular>:<label>[[/object]]` links to the record index page. It is name-keyed rather than id-keyed so an object the assistant only *proposes* to create still renders as a chip, just without a link. - `[[field:<id>:<label>[[/field]]` links to the field's settings page, gated on the `DATA_MODEL` permission. - `[[view:<id>:<label>[[/view]]` links to the object index page for that view. Field and view ids must come from a tool, so an unresolvable one falls back to plain text rather than a chip that goes nowhere. The record-only parser becomes one scan over all four kinds. Alternative order is load-bearing: `[[view:<uuid>:` is shaped exactly like the legacy prefix-less record marker, so metadata kinds are tried first and only records keep the legacy `]]` terminator. Server side is prompt-only. The metadata and view tools return bare objects rather than `ToolOutput`, so there is nowhere to hang a structured reference array without wrapping every factory, and the names and ids the markers need are already in those results verbatim. Also fixes a pre-existing issue in `LazyMarkdownRenderer`: its `components` map was rebuilt on every render, and react-markdown uses each entry as the JSX element type, so every node remounted on every streamed chunk. Harmless before, expensive once the model is told to chip every metadata name it writes. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23573?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
65155fe50c |
feat(apps): add enqueueJob to run a logic function on the workers (#23527)
Closes twentyhq/core-team-issues#2742 A logic function run is capped by its own `timeoutSeconds` (900s max), so anything that can't finish in one run — a full re-sync, a per-record fan-out, a rate-limited third-party API — had no way to continue. This adds a way to hand that work to the workers. ## What it looks like for an app author ```ts import { enqueueJob } from 'twenty-sdk/logic-function'; await enqueueJob({ logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33', payload: { cursor: nextCursor }, retryLimit: 3, priority: 2, delayMs: 60_000, }); ``` The target runs in its own process with its own timeout budget. The classic shape is a function that enqueues *itself* with the next cursor until there is nothing left. ## Changes **twenty-shared** — `EnqueueJobInput` / `EnqueueJobOptions` / `EnqueueJobResult` in `application`. **twenty-server** — new `application-job` module under `core-modules/application`, following the `application-key-value` pattern: - `enqueueJob` mutation on the metadata API, `@AuthApplication`-scoped - the lookup is scoped to `applicationId` + `workspaceId` — that's the authorization boundary, an app can only enqueue its own logic functions, anything else is `LOGIC_FUNCTION_NOT_FOUND` - pushes a `LogicFunctionTriggerJob` onto the existing `logicFunctionQueue`, so the enqueued run goes through the same executor (and the same execution throttling) as every other trigger - the queued run inherits the caller's `userId`/`userWorkspaceId`, so its app access token carries the same permissions as the function that queued it **Job options** are range-checked via `ResolverValidationPipe`, since the values come from application code and an unbounded delay or retry count would let an app pin work in the shared queue: | Option | Default | Range | |--------|---------|-------| | `retryLimit` | `0` | `0`–`10` | | `priority` | queue default | `1`–`10` (lower first) | | `delayMs` | `0` | `0`–7 days | `retryLimit` defaults to `0` rather than inheriting the server-route path's `3`: retries re-run the whole handler, so opting in should be the author's explicit choice. **twenty-sdk** — `enqueueJob` in `twenty-sdk/logic-function`, same shape as `runAgent`/`kv`. **Docs** — new "Background Jobs" page under Extend → Apps → Logic, plus nav and overview entries. **Generated** — regenerated `twenty-front/src/generated-metadata` and `twenty-client-sdk/src/metadata/generated` for the new mutation. ## Tests - `application-job.service.spec.ts` — 5 unit tests: job options mapping, defaults, acting-user propagation, application-scoped lookup, not-found - `enqueue-job.integration-spec.ts` — 5 integration tests: rejects a non-`APPLICATION_ACCESS` token, enqueues a function the app owns, rejects a function owned by another application, rejects an unknown identifier, rejects out-of-range options All green locally, along with `typecheck` for `twenty-server`/`twenty-sdk` and oxlint/oxfmt on the touched files. ## Notes for review - The target is addressed by `universalIdentifier`, matching `runAgent({ agentUniversalIdentifier })` and `ServerRouteDispatchResult.targetLogicFunctionUniversalIdentifier`. Addressing by `name` would be friendlier, but logic function names aren't validated for uniqueness within an app — happy to add it as a convenience if you'd rather. - `enqueueJob` returns as soon as the job is accepted; it can't return the target's result, since the queue driver's `add` returns void. Documented, with a pointer to the KV store for handing results back. --- _Generated by [Claude Code](https://claude.ai/code/session_01QrYvGonS3HMdeuMAVjs5hR)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23527?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
6447b7f935 |
feat(workflow): make the flow atom authoritative for version content, read core behind a flag (#23499)
## What
The frontend half of the workflow-version read switch, plus the small
server query it consumes. Two ideas:
1. **One hook owns where content comes from.**
`useWorkflowVersionContent(workflowVersionId)` returns `{ trigger, steps
}` from the workspace record when `IS_WORKFLOW_VERSION_IN_CORE_ENABLED`
is off (default), and from the new `workflowVersionContent` core query
when on. Switching the source later (core-only, after the column drop)
is a change inside this one hook.
2. **`flowComponentState` becomes authoritative for the builder.** The
canvas, diagram and step output schemas derive from the jotai atom; the
atom is seeded once per version through the hook above; mutations keep
it up to date.
## Why the seeding change is required
Today `WorkflowDiagramEffect` re-seeds the atom from the Apollo record
on **every** `currentVersion` identity change. That has two
consequences:
- Three of the five step/edge hooks (`delete step`, `create edge`,
`delete edge`) never write the atom themselves; they only write the
record and the re-seed papers over it.
- The model breaks the moment content comes from a source mutations do
not write (i.e. core): the stale fetch would be re-applied over every
optimistic edit, and your just-added step would vanish from the canvas.
So the atom is now seeded **once per version**, and
`useUpdateWorkflowVersionCache` applies the mutation's
`stepsDiff`/`triggerDiff` to the atom directly. All five step/edge hooks
get that through their existing call, which closes the three-hook gap in
one move. The step-update, trigger and tidy-up hooks write the atom too.
The record-cache writes are all kept while `trigger`/`steps` still live
on the record (dropped later with the columns).
## The dead wire, now the refresh path
`shouldWorkflowRefetchRequestFamilyState` was set by
`WorkflowSSESubscribeEffect` (reconnect, other-tab create) and
**consumed by nothing**. It is now the external-refresh path: when set,
the builder refetches content and reseeds. Known trade-off: while
connected, another tab's edits no longer live-patch the canvas through
record cache updates (they arrive on reconnect, version switch or
reload). Given concurrent editing of one draft has no conflict handling
anyway, that seemed acceptable; easy to extend the SSE effect to set the
flag on update events if we want live propagation back.
## Untouched by design
- **Run visualizer**: feeds the same atom from the immutable
`workflowRun.state.flow` snapshot; that duality (version content or run
snapshot) is exactly why the atom stays separate from the record store.
- **Version visualizer** (read-only): reseeds on content change, safe
because nothing writes its instance optimistically.
- Peripheral readers of `currentVersion.trigger/steps` (test-workflow
command, headless command enrichment, if-else body, etc.) still read the
record. Correct while dual-writing continues; they move to the content
hook before workspace content writes stop (tracked in the migration
plan).
## Verification
- `nx typecheck` green on both packages; `oxfmt` + `oxlint --type-aware`
green on all 16 changed files
- Front unit tests: 134 suites / 993 tests green (the two hook tests
gained the visualizer instance context their hooks now require)
- New server integration test for `workflowVersionContent`
- **Live click-through pending**: step create/delete/duplicate, edge
create/delete, trigger edit, tidy-up, draft create/discard, activation,
version viewer, run viewer, with the flag off and on. The failure mode
this PR guards against (an edit vanishing from the canvas) does not show
up in typecheck or unit tests.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23499?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
5977185c34 |
i18n - translations (#23572)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23572?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
a3beea893d |
Revert the external link confirmation popup for front components (#23567)
Reverts #23270 and #23404. Links in front components navigate natively again, with no confirmation popup and no per-app trusted-origins state in localStorage. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23567?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
38ad13655c |
Auto-start the workspace setup chat with a data model proposal (#23437)
https://github.com/user-attachments/assets/d447372b-c1b4-4c95-bac9-a8f8efa7d4a1 When the workspace creator lands on `/workspace-setup` after onboarding, the AI chat now starts on its own: an invisible first message, built server-side from the company enrichment collected in #23199, asks the assistant to propose a data model tailored to the business. The proposal streams in; the user never sees the prompt. - New `startWorkspaceSetupChat` mutation: creator only, gated on `IS_ONBOARDING_AI_CHAT_ENABLED`, available models and credits. Idempotent per user and workspace via a `keyValuePair` pointing at the thread, so a reload or a second tab joins the same conversation instead of starting a new one. - The thread holds exactly one hidden `USER` message combining the company context and the setup instructions, which keeps the one-hidden-message-per-thread index from #23199 satisfied. It goes through a dedicated streaming path that never queues, so the prompt cannot resurface as a visible message. - The assistant only proposes. It creates nothing until the user approves, then builds the model with the `metadata-building` skill. Objects and fields get English names with labels in the user's language, and the conversation continues in that language. - With no enrichment (consumer email domain, or the integration disabled) the kickoff still runs, and the assistant asks one short question about the business before proposing. - `findLatestSentUserMessage` no longer filters out hidden messages, so a failed kickoff turn stays retryable, and the no-message chat error surface now offers retry for stream errors. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23437?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
079e9b8e56 |
feat(dashboard): extend number format option to bar, line and pie charts (#23505)
https://discord.com/channels/1130383047699738754/1509604545381142649 Extends the Format option (Short/Full) added for the Number widget in #21521 to bar, line and pie charts. Format controls the numbers printed on the chart face: data labels and the pie center metric. Axis ticks stay abbreviated and tooltips always show the full value. Defaults to Short, so existing charts render unchanged. Server: nullable `numberFormat` on the bar/line/pie configuration DTOs, exposed in the dashboard AI tool schema. No migration, configuration is jsonb. Deferred: - The Format row has no visible effect while data labels are off, since tooltips are always full. - Number widget format defaults differ by field type (CURRENCY defaults to Short, NUMBER to Full). Pre-existing, untouched here. https://github.com/user-attachments/assets/0778f08a-6681-4e7a-8716-fb3026d1e01f <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23505?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
0b335d15b3 |
Refresh billing state after ending trial period (#23534)
Fixes #23530 After adding a credit card in the billing prompt, the credits section and subscription details stayed stale until a full page refresh. The `endSubscriptionTrialPeriod` mutation only returned `status` and `hasPaymentMethod`, and the frontend hook only patched the subscription status into the workspace state. The credits query was never refetched, so granted credits kept showing trial values, and `currentPeriodEnd` (renewal date) and `billingCustomer.hasPaymentMethod` stayed outdated. The backend already syncs everything to the database synchronously before the mutation returns, so fresh data was available, just never fetched. Changes: - `BillingEndTrialPeriodDTO` now includes nullable `currentBillingSubscription` and `billingSubscriptions`, returned by the resolver on success, mirroring the other billing update mutations (`switchSubscriptionInterval`, etc.) - `useEndSubscriptionTrialPeriod` applies the full billing update via `useApplyCurrentWorkspaceBillingUpdate` (falling back to the previous status-only patch), marks the billing customer as having a payment method, and refetches `GetResourceCreditUsage` so the credits section updates for any active observer This covers all entry points that end the trial: the billing page card modal, the trial banner, the AI chat banner, and the return from the Stripe portal. --- _Generated by [Claude Code](https://claude.ai/code/session_01W1J7cW2MhaqXGfdjvHeFoX)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23534?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
33fb57d128 |
i18n - translations (#23525)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
030ee2c7cc |
Move settings tabs below page headers (#23519)
## Summary - Position the Admin Panel tabs directly below the settings header. - Position the Communication tabs directly below the settings header. - Reuse the shared settings tab bar while preserving permissions, disabled states, and hash navigation. - Keep the responsive tab overflow menu available on narrow settings cards. ## After <img width="3456" height="2008" alt="Admin Panel tabs positioned below the settings header" src="https://github.com/user-attachments/assets/c8ff965c-d4b2-4700-85e1-0763f9f0852d" /> |
||
|
|
ada7eb1d88 |
Restore the Figma halftone shapes and calm the welcome animation (#23495)
https://github.com/user-attachments/assets/0c06d190-977e-4ad6-8a02-251f341ee310 The welcome overlay settled into circles instead of the halftone from Figma: the densify step that took the dot set from 681 to 2897 emitted points, so every dash had zero length. All 681 dashes in the Figma export (`public/images/onboarding/welcome-halftone.svg`) share a constant length / stroke width ratio of 1.452, so the shape is restored by deriving the length from the stroke width, with no data regeneration. Dashes now stretch into shape while they are still flying in, rather than popping once they have landed. The rest of the pass makes the animation quieter. The shine sweep is gone, along with the highlight colour that only fed it. Particles approach from much closer on a gentler ease, the idle drift is roughly halved, and the exit is a soft outward drift instead of a burst that threw everything off screen. The white pill behind the title and the person chip's surface are removed too, so the title reads directly against the halftone. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23495?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
684259fd5e |
Make onboarding cards contrast with the page background (#23516)
Onboarding card surfaces used `background.secondary`, the same token as the onboarding page background, so they blended in (visible on the workspace selection step). Switched them to `background.primary`, matching the other onboarding cards (plan card, install apps, trust badges). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23516?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |