mirror of
https://github.com/twentyhq/twenty.git
synced 2026-09-16 07:56:04 -04:00
c-async-csv-export
15547
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f1cdb7fd50 | Merge branch 'c-async-csv-export-server' into c-async-csv-export | ||
|
|
7490f0778e | Gate export menu progress and simplify button labels | ||
|
|
f0492f94cf | Share CSV export cleanup job options | ||
|
|
acd028bae0 | Merge branch 'c-async-csv-export-server' into c-async-csv-export | ||
|
|
bf28af8cb9 |
Merge branch 'c-csv-export-shared' into c-async-csv-export-server
# Conflicts: # packages/twenty-client-sdk/src/metadata/generated/types.ts |
||
|
|
79f00bbd06 | Merge commit 'ef6afd5a20' into c-csv-export-shared | ||
|
|
7d86bfb3ee | Apply CSV export frontend review conventions | ||
|
|
d947492045 | Merge branch 'c-async-csv-export-server' into c-async-csv-export | ||
|
|
5f0faa94b7 | Cover CSV export column mapping edge cases | ||
|
|
b6da586e31 |
Merge branch 'c-async-csv-export-server' into c-async-csv-export
# Conflicts: # packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemRenderer.tsx |
||
|
|
e0ac181852 | Merge branch 'c-csv-export-shared' into c-async-csv-export-server | ||
|
|
75175ff555 | Merge remote-tracking branch 'origin/main' into c-csv-export-shared | ||
|
|
ef6afd5a20 |
Let runAgent messages carry file attachments (#25835)
Follow-up to #25695, and the platform half of https://github.com/twentyhq/core-team-issues/issues/2895. Not Slack specific: it affects every app that calls `runAgent`. ## Why The in-app chat already gets files to a model. `buildFilePartsFromAttachments` validates ids against `FileFolder.AgentChat`, `loadMessagesFromDB` swaps in a signed URL, and `convertToModelMessages` emits a file part. An app cannot reach that path for one reason: `RunAgentMessage` is `{role, content: string}`, and `agent-async-executor.service.ts` mapped it straight through. So an app could tell an agent a screenshot exists and nothing more. Apps can already write the bytes. `createFileUpload` / `completeFileUpload` are `@MetadataResolver()` mutations, which is where the SDK already sends everything, and `permissions.service.ts` has an application-token branch that resolves the app's default role, so an app declaring `UPLOAD_FILE` can upload into `agent-chat` today. Only the message contract was missing. ## What - `RunAgentMessage` gains an optional `attachments` list of `{fileId, filename?}`, with a matching GraphQL input. - `RunAgentAttachmentService` resolves each fileId against uploaded files in the caller's workspace under `agent-chat`, signs each distinct file once, and emits `{type: 'file', data, mediaType, filename}` parts alongside the text. The shape mirrors what `convertToModelMessages` produces for chat, including for images. - The executor builds its messages through that service. No SDK change was needed: `runAgent` forwards the input object and the mutation does not enumerate input fields. ## Behaviour worth reviewing - **A message with no attachments still reaches the model as a bare string**, not a one-element parts array. This path runs for every app and workflow agent, and widening it must not perturb prompt caching or model behaviour for callers who never asked for files. There is a test pinning this. - **An unresolvable fileId throws** rather than quietly answering without the file. Chat filters silently, but for an API an app calls, a dropped attachment that nobody reports is much harder to debug, and a caller that wants to degrade can catch and retry without it. - **Attachments on an assistant message throw.** They cannot be represented in an assistant model message, so accepting and dropping them would be the same trap. - Capped at 10 attachments per message and 255 characters of filename, enforced in the resolution path. Resolution stays workspace-scoped and folder-scoped, and only `UPLOADED` files resolve, so a fileId referenced before `completeFileUpload` fails here rather than producing a URL that 404s inside the provider. ## A pre-existing gap this surfaced `AgentRunResolver` installs no `ResolverValidationPipe`, so **nothing** on `RunAgentInput` is validated today, including the existing `@ArrayMaxSize(100)` on `messages` and `@IsNotEmpty` on `prompt`. That is why the new limits are enforced in `RunAgentAttachmentService` rather than by decorators alone. Adding the pipe here would switch on every dormant decorator at once, and one of them breaks a shipped caller: `build-slack-conversation-messages.ts` replays thread history, and an assistant turn whose text strips to nothing with no files attached reaches `runAgent` with `content: ''`. That passes today and would start failing `@IsNotEmpty`. Since the Slack app ships separately from the server, an old app build against a new server would break. Worth fixing, with its callers first, but not as a side effect of this PR. ## Testing - 9 unit tests on the resolver service; full `ai-agent-execution` and `ai` module suites pass. - `tsgo --noEmit` clean on twenty-server, twenty-shared and twenty-sdk; oxlint and oxfmt clean. - Metadata GraphQL artifacts regenerated against a locally running server. The schema diff is the new input type; the churn in the client SDK `types.ts` is index renumbering from inserting it. Not verified end to end: that an image actually lands in front of a model. That needs a live run against a real provider. The message shape matches what chat already sends successfully, but that is inference rather than observation. ## Next The Slack consumer is a separate app-package change: declare `UPLOAD_FILE`, add the `files:read` bot scope (which forces existing installations to reauthorise), download from `url_private` server-side, upload, and pass the fileIds here. The names-only path from #25695 stays as the fallback. |
||
|
|
6d2e8c3591 |
Let every application admin manage a workspace-shared connection (#25773)
Follow-up to #25708, taking the first option from the product note in that review: creator ownership is the wrong UI boundary for a workspace-scoped connection. On the connection detail page, the Reconnect, Share with workspace and Disconnect actions only rendered for the member who originally created the connection. That gating was presentation only, and everyone who can open the page holds the Applications permission. It meant an admin could not repair or remove a connection the whole workspace depends on without chasing whoever happened to create it. Disconnect was already authorized server-side for this: `deleteConnectedAccount` goes through `verifyAdministrableByCaller` (`verifyOwnership` when this PR opened), which accepts any member who can administrate a workspace-shared connection. Reconnect was not. `startAuthorizationFlow` only checked that the reconnect target lived in the caller's workspace for the requested provider, so `/auth/apps/authorize` (a `NoPermissionGuard` endpoint) let any member pass `reconnectingConnectedAccountId` for *another member's private* connection and have the callback overwrite its tokens, handle and visibility. This PR closes that before removing the front-end gate that was hiding it. ## Changes - `SettingsApplicationConnectionDetail`: the actions render for every viewer. "Share with workspace" still only appears on a personal connection, and the query only returns personal connections owned by the caller, so nothing changes for those. - `startAuthorizationFlow` scopes the reconnect lookup with `buildConnectedAccountUsableByCallerWhere`, the same predicate the list query already uses, so the SQL enforces it and the rule has one definition. - `isOwnedByCurrentUser` on `ApplicationConnectedAccountDTO` is marked deprecated and the front stops selecting it, since nothing consumed it once the front stopped gating on ownership. The field and its type stay in the schema: removing them fails `api-breaking-changes`, and the repo retires API surface with `deprecationReason` rather than deletion. - `generateTransientToken` moved out of `connect-messaging-account.util.ts` into the common integration-test layer. - Detail page tests: the owner and non-owner cases collapse into one that checks a failed workspace-shared connection offers Reconnect and Disconnect, with exact call-count assertions. Integration spec asserts the returned ids only. ### Managing a shared connection now needs the Applications permission Widening the UI boundary to "any admin" made the missing server-side boundary worth closing in the same PR. `/auth/apps/authorize` sits behind `NoPermissionGuard`, so a member without the Applications permission could create a workspace-shared connection or, by passing `visibility=user` with a shared connection's id, have the callback convert it into their own private one. The settings page never offered either, but the API did. #25777 has since landed the same boundary on `deleteConnectedAccount`, so what remains here is the authorize flow. - New `AppConnectionAccessService` states the rule once: a workspace-shared connection is workspace infrastructure and managing it takes `PermissionFlagType.APPLICATIONS`; a private one stays under its owner's control. Gating on the row rather than the endpoint is what keeps personal email and calendar accounts, which are always `visibility: 'user'`, manageable without the settings permission. - `startAuthorizationFlow` checks it, covering both creating a shared connection and reconnecting one. The check belongs at authorize rather than callback because the signed state carries the visibility decision for its ten-minute lifetime. - It lives in `core-modules/application/connection-provider/` because `ConnectedAccountMetadataModule` already imports `ConnectionProviderModule`; putting it on the metadata side would need the reverse edge. ### The administration flag follows the provider #25777 landed the same boundary on `deleteConnectedAccount` while this was open, choosing `PermissionFlagType.WORKSPACE` because a shared connected account there is a group mailbox, and deleting one destroys its whole message history. That is right for a group mailbox and wrong for an app connection, which is administered from Settings > Applications. With one flag serving both rows, this PR's front-end change would have shown Disconnect to an Applications admin on an `APPLICATIONS`-gated page and had the server refuse it. `getConnectedAccountAdministrationPermissionFlag` states the mapping once, keyed on the provider, and both `isAdministrableByCaller` and `AppConnectionAccessService` read it, so the two paths cannot drift apart again. Group mailboxes keep the `WORKSPACE` requirement #25777 gave them. ### Reconnect re-attributes the connection Reconnect rewrote tokens, handle and visibility but left `userWorkspaceId` at the original creator. Until this PR only the owner could reconnect, so the two always matched. Now an admin can repair a shared connection, and `ConnectedAccountOwnershipTransferService` archives and revokes by `userWorkspaceId`: leaving it stale means the creator off-boarding revokes credentials the admin authorized under their own identity, breaking a connection the workspace still depends on. The reconnect update now sets `userWorkspaceId` to whoever completed the flow. The reconnect guard already restricts the target to workspace-shared or the caller's own, so on a private connection this is a no-op. Off-boarding taking a workspace-shared connection down with its owner is pre-existing and left alone here: `transferOwnership` archives and revokes by `userWorkspaceId` with no visibility filter, so it already happens when the creator leaves. Discussed on [this thread](https://github.com/twentyhq/twenty/pull/25773#discussion_r4004484226); the fix belongs in the off-boarding flow and needs a product call on whether a departed member's grant should outlive their deprovisioning. ## Schema One line, non-breaking: `isOwnedByCurrentUser` gains `@deprecated`. An earlier revision of this PR deleted the field and `ApplicationConnectedAccountDTO` with it, which `api-breaking-changes` correctly rejected. ## Test - `app-connection-management-guards.integration-spec.ts` (renamed from `app-oauth-authorize-reconnect.integration-spec.ts`), 6 tests, all passing: Jane is refused on Tim's private connection and passes the guard on Tim's workspace-shared one; Jony, who holds no Applications permission, is refused when reconnecting a shared connection, when requesting `visibility=workspace`, and when promoting his own private connection to workspace visibility, and is allowed reconnecting his own private connection. Each refusal case passes on the branch and fails without the corresponding change. The delete cases moved to #25777's own spec. - `get-connected-account-administration-permission-flag.util.spec.ts` pins the provider-to-flag mapping. - The `userWorkspaceId` re-attribution has no automated test: nothing in the repo drives the app OAuth callback, since the token exchange calls the real provider through `secureHttpClientService.createSsrfSafeFetch()`, and the only seam would be a util extracted for a one-line spread. - The connected-account, connection-provider and message-channel integration suites pass (17 suites, 89 tests) against a freshly seeded database, including the `connected-account-resolver` spec #25777 added. `successful-save-imap-smtp-caldav-account` fails locally for want of a Dovecot container, identically with the branch stashed. - `npx jest src/pages/settings/applications --config=packages/twenty-front/jest.config.mjs` passes (12 tests). - `graphql:generate --configuration=metadata` and `twenty-client-sdk:generate-metadata-client` run against the server built from this branch produce exactly the committed generated diff. - `tsgo --noEmit` clean on twenty-front and twenty-server. oxlint and oxfmt clean on the changed files. |
||
|
|
ad2180654b | Merge branch 'c-async-csv-export-server' into c-async-csv-export | ||
|
|
99386fdd24 | Refactor CSV export connection cleanup and browser coverage | ||
|
|
563eb2886f | Fix CSV export connection and download lifecycle | ||
|
|
3033965952 |
i18n - translations (#25990)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
de26eb10ab |
fix(record-form): render rich text fields with the BlockNote editor (#25920)
## The bug
Creating a record through the creation form with a rich text field
containing a bullet list — typically pasted — saves a value the record
page cannot read. The field renders `Invalid Configuration` instead of
the content.
Console: `Error creating document from blocks passed as
'initialContent'` → `Cannot read properties of undefined (reading
'isInGroup')`, thrown in `RichTextFieldEditor` and caught by the
page-layout widget error boundary.
## Why
The two editors for the same field disagree on the format.
The record page uses a BlockNote editor and writes BlockNote blocks. The
creation form used a TipTap editor and passed its output through
`convertTipTapDocumentToBlockNote`, which does not convert: it parses
the TipTap document, strips the outer `doc` wrapper and returns
`document.content` unchanged.
So the column ends up holding TipTap nodes while every reader treats it
as BlockNote:
```
seeded note (renders fine) [{"id":"block-1","type":"paragraph","props":{...
created via the form (broken) [{"type":"bulletList","content":[{"type":"listItem"...
```
Plain text survives because `paragraph` exists in both schemas, which is
why this went unnoticed. A bullet list does not: TipTap nests
`bulletList > listItem > paragraph`, BlockNote uses flat
`bulletListItem` blocks.
## The change
`FormFieldInput` now routes rich text to a BlockNote editor when the
caller supplies no `VariablePicker`, and keeps the TipTap editor when it
does.
That split matters because the component is shared. Workflow step
editors pass a picker and need variables: their value lives in step
settings, is read back by the same TipTap editor, and the existing
stories pin an `onChange` payload containing `variableTag` nodes.
Changing their format would break that round trip. The creation form and
Update Multiple Records pass no picker; their value goes into a record
column, so they get BlockNote and write what the record page reads.
The picker is a capability switch rather than a guess about storage:
only the TipTap editor can host variable tags, so any surface offering
variables must use it. A RICH_TEXT field is not filterable
(`FILTERABLE_FIELD_TYPES` excludes it), so the advanced-filter and
role-permission panels — where the picker comes from a context that does
not always supply one — cannot reach this branch.
`FormRecordRichTextFieldInput` is the BlockNote input. It reuses
`parseInitialBlocknote` for parsing and a new
`filterBlocksSupportedByBlockSchema` — modelled on the dashboard
widget's `filterSupportedBlocks` — to drop blocks the schema does not
know, per block and recursing into children, with the allow-list derived
from `BLOCK_SCHEMA` rather than hand-maintained. A stored TipTap value
therefore mounts as an empty editor instead of throwing.
It also pushes onto the focus stack on focus and pops on blur, matching
the record page editor, so global single-key hotkeys do not fire while
typing.
## Scope
Only the rich text branch changes. Every other field type in
`FormFieldInput` is untouched, as is the record page editor and the
TipTap path.
Update Multiple Records switches too, since it also writes a record
column and passes no picker. Same bug, same fix, not separately tested
here.
## Deliberately not fixed
**Workflow Create / Update / Upsert Record steps still write TipTap into
record columns.** They pass a `VariablePicker`, so they keep the TipTap
editor, and `resolveRichTextFieldsInRecord` on the server substitutes
variables inside the string and stores it verbatim. Pasting a bullet
list there reproduces the same crash. Fixing it needs a real TipTap →
BlockNote conversion applied after variable resolution, which is a
larger change; this PR is scoped to the reported path.
Records already created through the form keep their TipTap value and
will keep erroring until repaired. No backfill here.
## Failing loudly instead of silently
Three ways the new editor could lose or swallow something quietly, all
closed:
- **A stored value it cannot fully read.** The block filter drops
unknown types, so a legacy TipTap value would mount as an empty editor
and the first keystroke would persist the blank over it. A recursive
block count before and after filtering detects any drop — a document
mixing supported and unsupported blocks included, where survivors would
otherwise mask the loss — and the editor goes readonly saying so, rather
than letting content be overwritten by accident.
- **The `markdown` fallback.** The TipTap input read `blocknote ??
markdown`; the BlockNote one now does too, instead of only `blocknote`.
- **File and image blocks.** The slash menu offers File, Image, Video
and Audio, and `FileBlock` calls `editor.uploadFile?.()` — with no
handler supplied that silently did nothing. The record-page editor
attaches uploads to an existing record, which a creation form has no id
for, so the handler now says that instead and returns an empty string —
the no-op path `FileBlock` already handles, since it has no try/catch
and a rejection would go unhandled.
## Testing
- `twenty-front` typecheck clean; oxlint on the touched modules and
oxfmt across the package clean.
- 62 specs pass across `blocknote-editor` and
`record-field/ui/form-types`, including new unit specs for
`filterBlocksSupportedBySchema` (unknown blocks, missing types,
unsupported children, whole dropped subtrees, a stored TipTap document)
and `countBlocksDeep`.
- The `onChange` story assertion was checking
`stringContaining('"type":"paragraph"')`, which TipTap output also
satisfies, so it could not tell the two formats apart; it now parses the
payload and asserts the BlockNote shape (`props` on the block, `styles`
on the text).
- Verified end to end on a branch instance: a bullet list built in the
creation form is stored as `bulletListItem` blocks with
`props`/`styles`/`children`, and the record page renders it instead of
`Invalid Configuration`, with no `isInGroup` error in the console.
- Stories cover a seeded bullet list rendering, the label, readonly, and
that `onChange` emits BlockNote-shaped blocks.
- The bug was reproduced before the fix: pasting a nested bullet list
into the form's Body and saving produced `Invalid Configuration`, with
the column holding raw TipTap JSON.
- Not done: a browser pass on the fix. Both local dev slots are held by
other work, so the editor's height and styling inside the side panel are
unverified.
|
||
|
|
98e6c04602 | Merge branch 'c-async-csv-export-server' into c-async-csv-export | ||
|
|
5097f92ecd | Add asynchronous CSV export pipeline behind a feature flag | ||
|
|
9bde7a563e | Gate asynchronous CSV exports behind a workspace feature flag | ||
|
|
9d3fd32616 | Share CSV formatting and composite field labels | ||
|
|
78443d6561 |
i18n - translations (#25985)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/25985?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> |
||
|
|
9e6fbd2458 | Import shared CSV utilities directly in callers | ||
|
|
7ebccecea3 | Simplify CSV export Redis state and connection leases | ||
|
|
4f3f65c27e |
feat(server): validate workflow versions through exceptions, malformed at write and non-activable on activate or validate (#24964)
Second of 3 stacked PRs (stacked on #24963, merged). Fixes prod Sentry `TWENTY-SERVER-H9K` / `TWENTY-SERVER-HCF`: `resolveRichTextFieldsInRecord` crashes at run time on RICH_TEXT record fields persisted as bare strings by the AI workflow builder. Both issues are still firing (14K and 11K events over the last 30 days, last seen today). ## TL;DR Workflow version validation is now exceptions only. There is one issue set, computed by one set of check utils, two explicit code sets over it in `twenty-shared`, and two exceptions: | Exception code | Thrown by | When | Effect | |---|---|---|---| | `MALFORMED_WORKFLOW_VERSION` | the write chokepoint, `writeWorkflowVersionAndMirror` | on every version-content write | the transaction rolls back; nothing malformed is ever stored | | `NON_ACTIVABLE_WORKFLOW_VERSION` | `activateWorkflowVersion`, the new `validateWorkflowVersion` mutation, the `validate_workflow` tool | when asked to activate, or to check without activating | the version stays a draft; the error lists every blocking issue | Both are `WorkflowVersionValidationException`, carry `issues` (code, message, stepId), map to `BAD_USER_INPUT` with a Lingui userFriendlyMessage for the builder (the per-issue list is joined into the error message; it is not on the GraphQL extensions yet), and come back as `{ success: false, error, issues }` from the AI tools. There is no advisory report anymore: the former `validate_workflow` report service and the incomplete-codes set are deleted, and the tool now runs the exact activation check. **Malformed** means present-but-invalid data that crashes the executor: a bare string where a rich text object belongs, a step targeting an object that does not exist, a broken graph. It is refused at write time so incremental drafting keeps working (an empty draft, a skeleton step, a half-configured record step all save) while nothing that can crash a run is ever durable. **Non-activable** means a well-formed draft a run could not proceed through: no trigger or trigger type, no steps, a trigger that leads nowhere, an if/else with fewer than two branches, an iterator without a loop body, an unfinished PICK_RECORD config, an AI agent step without an agent, a variable reference that is malformed or points at an unknown or downstream step. Those are reported only when someone asks, through activation or the validate endpoint, and as a whole list rather than the first failing assertion. ## How it fits together Three flows, one issue set. Every check util returns `WorkflowValidationIssue` values; `twenty-shared` holds two explicit code sets over them, `MALFORMED_WORKFLOW_VALIDATION_ISSUE_CODES` and `NON_ACTIVABLE_WORKFLOW_VALIDATION_ISSUE_CODES`; and each flow throws one exception on the subset that matters to it. ### 1. Updating a workflow version ```mermaid flowchart TB subgraph ENTRY["Entrypoints"] direction LR UI["Builder UI"] GQL["GraphQL dedicated mutations<br/>createWorkflowVersionStep · updateWorkflowVersionStep · deleteWorkflowVersionStep<br/>updateWorkflowVersionTrigger · edges · positions · createDraftFromWorkflowVersion"] MCP["AI · MCP tools<br/>create_complete_workflow · update_workflow_version_step"] REST["Generic ORM mutations<br/>(REST and generic GraphQL)"] end UI --> GQL GQL --> SERVICES["workflow-builder write services"] MCP -- update_workflow_version_step --> SERVICES MCP -- create_complete_workflow --> APPLY SERVICES --> APPLY REST -- "createWorkflow → post-hook seeds the v1 draft" --> APPLY REST -- "any workflowVersion create / updateMany,<br/>or updateOne touching steps · trigger · status · …" --> FORBID["pre-query hook → FORBIDDEN"] subgraph TX["writeWorkflowVersionAndMirror — one DB transaction"] direction TB APPLY["apply write, uncommitted"] --> REFETCH["re-fetch merged row"] REFETCH --> CHECK{"assertWorkflowVersionIsNotMalformedOrThrow<br/>MALFORMED codes only"} CHECK -- "malformed" --> RB["rollback + throw<br/>MALFORMED_WORKFLOW_VERSION"] CHECK -- "well-formed" --> MIR["mirror to core · upsertToCore"] MIR --> CMT["commit → DRAFT version saved"] end RB --> ERR["BAD_USER_INPUT, issues joined in the message · GraphQL<br/>success:false + structured issues · AI / MCP"] ``` An empty draft, a skeleton step, a half-configured record step all commit. Only content that would crash the executor is refused. ### 2. Validating a workflow version (new) ```mermaid flowchart TB subgraph ENTRY["Entrypoints"] direction LR GQL["GraphQL mutation<br/>validateWorkflowVersion(workflowVersionId)"] MCP["AI · MCP tool<br/>validate_workflow"] end GQL --> LOAD["load the version<br/>getWorkflowVersionOrFail"] MCP --> LOAD LOAD --> CHECK{"assertWorkflowVersionIsActivableOrThrow<br/>MALFORMED ∪ NON_ACTIVABLE codes"} CHECK -- "issues remain" --> NA["throw NON_ACTIVABLE_WORKFLOW_VERSION<br/>with the full issue list · draft unchanged"] CHECK -- "none" --> OK["validateWorkflowVersion: true<br/>validate_workflow: success: true · draft unchanged"] NA --> ERR["BAD_USER_INPUT, issues joined in the message · GraphQL<br/>success:false + structured issues · AI / MCP"] ``` Same check as activation, same error, no side effect. This is what the AI builder calls once before activating. ### 3. Activating a workflow version ```mermaid flowchart TB subgraph ENTRY["Entrypoints"] direction LR UI["Builder UI · Activate"] GQL["GraphQL mutation<br/>activateWorkflowVersion(workflowVersionId)"] MCP["AI · MCP tool<br/>activate_workflow_version"] end UI --> GQL GQL --> LOAD["load the version and its workflow"] MCP --> LOAD LOAD --> CHECK{"assertWorkflowVersionIsActivableOrThrow<br/>MALFORMED ∪ NON_ACTIVABLE codes"} CHECK -- "issues remain" --> NA["throw NON_ACTIVABLE_WORKFLOW_VERSION<br/>with the full issue list · nothing changed"] CHECK -- "none" --> SYNC["pre-existing sync assertions<br/>assertVersionCanBeActivated · PICK_RECORD load-balance config"] SYNC --> BUILD["build code steps · switch logic functions to prebuilt"] BUILD --> ACT["performActivationSteps<br/>archive previous · status ACTIVE · mirror to core<br/>enable automated trigger (cron / webhook / database event) · command menu item"] ACT --> ACTIVE["ACTIVE version"] NA --> ERR["BAD_USER_INPUT, issues joined in the message · GraphQL<br/>success:false + structured issues · AI / MCP"] ``` The activability check runs before any side effect, so a refused activation changes nothing. ## Where each exception is thrown **Malformed, at the chokepoint.** Inside the transaction, after the persisted row is re-fetched and before the core mirror and commit, `assertWorkflowVersionIsNotMalformedOrThrow` (`workflow-validation/utils/assert-workflow-version-is-not-malformed-or-throw.util.ts`) runs the structure and record-step checks, keeps the codes in `MALFORMED_WORKFLOW_VALIDATION_ISSUE_CODES`, and throws if any remain. It is a pure util fed with the flat metadata maps, so the core-sync module only needs the metadata read module from #24963 and stays out of the `WorkflowCommon → CoreSync` cycle. It covers all surfaces at once: GraphQL dedicated mutations, the AI/MCP tools, and the generic-path draft creation. (The generic ORM path cannot write step or trigger content at all: the pre-query hooks forbid every `workflowVersion` create and updateMany, and an updateOne that touches steps, trigger, status, position, workflowId or coreWorkflowVersionId.) **Non-activable, on request.** `WorkflowVersionValidationWorkspaceService.assertWorkflowVersionIsActivableOrThrow` (`workflow-validation/workflow-version-validation.workspace-service.ts`) runs the structure, AI-agent and record-step checks and throws with every issue whose code is in `MALFORMED_WORKFLOW_VALIDATION_ISSUE_CODES` or `NON_ACTIVABLE_WORKFLOW_VALIDATION_ISSUE_CODES` (malformed is included because legacy versions written before the write gate can still hold some). Its module depends on the metadata read module only. Three callers, one piece of code: - `activateWorkflowVersion` calls it before its existing sync assertions and before any side effect, so a refused activation changes nothing. - The new `validateWorkflowVersion(workflowVersionId)` mutation on `WorkflowVersionResolver` calls it and returns `true`; a non-activable version fails with the same error activation would give, and the version stays a draft. - The `validate_workflow` AI tool calls it and returns `{ success: true }`, or `{ success: false, error, issues }` with the exception's issues. Rejections surface as `BAD_USER_INPUT`, not a 500, through `WorkflowVersionValidationGraphqlApiExceptionFilter` on the builder and trigger resolvers, so the builder shows a clean message and Sentry is not spammed with expected rejections. One scoping note on "nothing malformed is ever stored": that holds for workflow **version** content. `create_complete_workflow` inserts the parent `workflow` row before it opens the gated transaction, so a malformed payload leaves an empty workflow with no version behind. That is the pre-existing behaviour of that tool and is harmless, but it is not covered by the rollback. ## What counts as malformed (rejected at write) `MALFORMED_WORKFLOW_VALIDATION_ISSUE_CODES` in `twenty-shared`: - `INVALID_STEP_PARAMS`, `INVALID_TRIGGER_PARAMS` — the step or trigger fails its Zod schema - `INVALID_RICH_TEXT_FIELD` — a RICH_TEXT value is not `{ markdown, blocknote? }` (the crash this fixes) - `OBJECT_NOT_FOUND` — a record step targets an object that does not exist in the workspace - `DUPLICATE_STEP_ID`, `DANGLING_REFERENCE` — broken graph structure `NON_ACTIVABLE_WORKFLOW_VALIDATION_ISSUE_CODES` in `twenty-shared`, refused at activation and by the validate endpoint: - `MISSING_TRIGGER`, `MISSING_TRIGGER_TYPE`, `NO_STEPS`, `TRIGGER_HAS_NO_NEXT_STEP` - `IF_ELSE_INSUFFICIENT_BRANCHES`, `ITERATOR_MISSING_LOOP_BODY` - `INCOMPLETE_PICK_RECORD_CONFIG`, `AI_AGENT_MISSING_AGENT` - `VARIABLE_INVALID_PATH`, `VARIABLE_UNKNOWN_STEP`, `VARIABLE_NOT_UPSTREAM` Activation also refuses the malformed set, since versions written before the write gate can still hold malformed content. The codes in neither set are computed by the shared structure validator but block nothing: `UNREACHABLE_STEP`, `IF_ELSE_BRANCH_HAS_NO_NEXT_STEP` (an empty branch is legal), `VARIABLE_MISSING_OUTPUT_SCHEMA` and `VARIABLE_PATH_NOT_FOUND` (both depend on inferred output schemas, so they can be false positives), and the warning `AI_AGENT_MISSING_OUTPUT_VARIABLE`. A record step whose object is not picked yet is malformed, not non-activable, since a missing or unknown `objectName` is `OBJECT_NOT_FOUND`; in practice this never bites the builder because `createWorkflowVersionStep` seeds every CREATE_RECORD skeleton with a default object. Activation used to stop at the first failing sync assertion and only checked trigger presence, at least one step, form steps and the PICK_RECORD config. It now refuses on the whole blocking set, with every issue listed. That is a behaviour change: a draft that activated before with, say, a variable pointing at a step that no longer exists will now be refused until fixed. Output-schema hints are deliberately not in the set: a CODE step or a webhook trigger without a declared output schema activates as before. ## Notable changes - `WorkflowVersionValidationException` with codes `MALFORMED_WORKFLOW_VERSION` and `NON_ACTIVABLE_WORKFLOW_VERSION`, each with a Lingui userFriendlyMessage, carrying the `issues` list. `WorkflowQueryValidationException` is back to main's shape (FORBIDDEN only). - `assertWorkflowVersionIsNotMalformedOrThrow`, the pure util used at the chokepoint. - `WorkflowVersionValidationWorkspaceService.assertWorkflowVersionIsActivableOrThrow`, replacing the advisory report service (`WorkflowValidationWorkspaceService.validateWorkflowVersion / validateWorkflowDefinition` on main). Its module imports the metadata read module only: the output-schema enrichment the report did, and the schema-hint checks that depended on it (missing output schema on CODE, HTTP and webhook steps, logic-function schema mismatch, steps without variable references, and the iterator items-is-an-array check that resolved against the referenced step's stored schema), are removed along with their codes `CODE_STEP_MISSING_OUTPUT_SCHEMA`, `STEP_HAS_NO_VARIABLE_REFERENCE`, `LOGIC_FUNCTION_OUTPUT_SCHEMA_MISMATCH` and `ITERATOR_ITEMS_NOT_ARRAY`. The iterator action already fails a run cleanly on non-array items. They were guidance for the AI builder, not conditions for a run. - `NON_ACTIVABLE_WORKFLOW_VALIDATION_ISSUE_CODES` in `twenty-shared`, next to the malformed set. - The front's generated GraphQL types pick up the new mutation. - New `validateWorkflowVersion` mutation. The `validate_workflow` AI tool keeps its name, since the standard skill and the setup prompt reference it, but now runs the activation check and fails with issues instead of returning a report. Its description, the standard workflow-building skill text and the setup prompt are updated accordingly, and the stale `validate: false` instruction is gone. - Semantic record-step checks get their own codes so `INVALID_STEP_PARAMS` means structural-schema-only: `OBJECT_NOT_FOUND` (malformed) and `INCOMPLETE_PICK_RECORD_CONFIG` (non-activable). The record-step metadata checks are shared through `getWorkflowRecordStepMetadataIssues`. - The pre-existing generic-mutation guard is renamed `WorkflowVersionQueryValidationWorkspaceService`, after the `WorkflowQueryValidationException` it throws, so it cannot be confused with the content validation service. - The `create_complete_workflow` and `update_workflow_version_step` AI tools no longer run a validation after writing; `update_workflow_version_step` therefore loses its `validate` boolean input, and the orphaned `summarizeValidation` helper is deleted. `create_complete_workflow`'s contract now tells the model RICH_TEXT must be `{ markdown }`. - `findRichTextFieldNames` is extracted from the executor's `resolveRichTextFieldsInRecord` and shared instead of duplicated. ## Verification Integration tests: - **Malformed at write** (`workflow-version-malformed-validation.integration-spec.ts`), via `updateWorkflowVersionStep`: a bare-string rich text value and an unknown target object are rejected (`BAD_USER_INPUT` / `MALFORMED_WORKFLOW_VERSION`), a valid `{ markdown }` object is accepted, and an incomplete record step still saves. The skeleton `createWorkflowVersionStep` succeeding is exercised in the setup of the same spec. Writing `steps` through the generic `updateWorkflowVersion` mutation is `FORBIDDEN` by the pre-query hook. - **Non-activable on request** (`workflow-version-activation-validation.integration-spec.ts`): `validateWorkflowVersion` on an empty draft fails with `BAD_USER_INPUT` / `NON_ACTIVABLE_WORKFLOW_VERSION` and names the missing trigger; `activateWorkflowVersion` fails with the same subCode; after a trigger and a skeleton step, `validateWorkflowVersion` returns `true` and the version is still a draft. - **AI / MCP** (`workflow-ai-tool-malformed-validation.integration-spec.ts`), `create_complete_workflow` through the `/mcp` `execute_tool` dispatch: a bare-string rich text returns `{ success: false, error: "…rich text…" }` with `isError: true` instead of crashing, while a valid `{ markdown }` object and an incomplete record step both create the workflow. - Unit: record-step metadata util, rich-text issue utils, tool spec. Typecheck and lint pass. Verified by hand on 2026-09-14 against the branch running locally (server, front, migrated dev database): - Dedicated GraphQL mutations: empty draft, manual trigger and skeleton CREATE_RECORD step all save; a bare-string `bodyV2` is rejected with `MALFORMED_WORKFLOW_VERSION` and the re-read version still holds the previous `objectRecord` (rollback confirmed); an unknown object is rejected; `{ markdown }` and an empty `objectRecord` are accepted; generic `updateWorkflowVersion(steps)` is `FORBIDDEN`; deleting the step re-links the graph. - `validateWorkflowVersion` on an empty draft fails with `NON_ACTIVABLE_WORKFLOW_VERSION`, message "The workflow has no trigger.; The workflow has no steps.", userFriendlyMessage "This workflow version is not ready to be activated."; `activateWorkflowVersion` fails identically; the `validate_workflow` tool returns `success: false` with `issues` `[MISSING_TRIGGER, NO_STEPS]`; after a trigger and a step, `validateWorkflowVersion` returns `true`, the version is still a draft, and `activateWorkflowVersion` then succeeds. - `/mcp` `create_complete_workflow`: bare string → `success: false` + `isError`; `{ markdown }` and an incomplete record step → created. - Builder UI: add a Create Record step, switch it to Notes, type in the Body rich text field (the front sends `{ blocknote, markdown: null }`, which is well-formed), reopen the step and see the body re-hydrated, delete the step. No console errors, no error toast, no server errors. ## Rollout Data backfill for the existing affected versions is the next PR (#24965, re-homed to the 2.41 upgrade directory). Until it runs, a legacy version that already holds a bare-string rich text value is write-locked by the malformed check: any content edit to it is rejected until the backfill normalizes it. Those versions already crash at execution time today, so this turns an opaque runtime crash into a clear write-time rejection, but the two PRs should ship in the same release to keep that window short. **2026-09-14:** rebased on main after the September workflow core migration work. The chokepoint and all its callers are unchanged; the new core-API workflow creation path (`createInitialDraftVersionForWorkflow`) also goes through it, and the remaining direct `workflowVersion` writes only touch `status`. |
||
|
|
bc038ed106 |
i18n - website translations (#25981)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/25981?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> |
||
|
|
62ce737552 |
Use Twenty UI components in presentation stories (#25976)
Use Twenty UI components for supporting buttons, inputs, links, counters, and typography in presentation stories so examples follow the design system. Remove the replaced custom styles and preserve accessible button names. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/25976?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. --> |
||
|
|
49a2b78aa8 | Address CSV export review and verify the full export lifecycle | ||
|
|
613d3f6c5e |
Pin catalog sync to a registration's own source package (#25965)
Reworks how the marketplace catalog sync writes into application registrations. - `upsertFromCatalog` now requires a manifest, splits update from create into private methods, and returns the upserted registration so the sync no longer re-reads the row for asset storage. A skipped entry returns null. - The catalog identifier is validated as a UUID and lowercased before any comparison, and the sync only attaches to registrations sourced from npm whose `sourcePackage` matches the registry package. Other rows are skipped with a warning. - `latestAvailableVersion` only moves forward: the catalog update goes through the same semver guard as the install flow, and an entry with no version never erases a known one. `updateFromManifest` reports whether the version rose, so the publish metric and auto-upgrade fan-out fire only on a real bump. - Removes the six-hourly application version-check cron and its compare-and-swap. The hourly catalog sync already fetches every registry package, refreshes manifest and assets, and enqueues auto-upgrades, so the second writer only left the listing's manifest stale and could observe the same transition twice. - The catalog sync integration spec moves onto reusable utils (registry package stub, catalog registration insert, lookup by universal identifier) and covers source package pinning, non-npm registrations, identifier normalization and downgrade refusal. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/25965?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. --> |
||
|
|
f25a066541 |
i18n - translations (#25979)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/25979?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> |
||
|
|
3bb53150a9 |
revert(email-tool): restore the default sender resolution (#25972)
Reverts #25908. That PR made an API key caller resolve its default sender through the workspace-visibility filter, so `send_email` and `draft_email` from MCP need a mailbox shared with the workspace. Connected account visibility is not editable or writable for email accounts today, so there is no way to actually share one. The filter therefore has no path to a working state: it only removes the two email tools for MCP users. Reverting until we have a product vision for connected account visibility. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/25972?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. --> |
||
|
|
0f8eaea79c | Tie CSV export progress and automatic download to the current page | ||
|
|
89084cbc92 |
i18n - translations (#25973)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
aca4b9ddbd |
Add copy button on hover to Emails/Phones/Links in the record side panel (#25940)
## What Adds the same secondary hover button (Copy, or Open link when the field click action is set to Copy) that the table view already shows to the record side panel (and other inline-cell surfaces), for Emails, Phones and Links fields. Fixes #25936 ## Why In the table view, hovering an Emails/Phones/Links cell shows a Copy button next to the Edit pencil. The side panel showed only the pencil, so copying an email meant opening edit mode and using the item menu, which puts Copy one click away from an irreversible Delete. Read-only users could not reach Copy at all. ## Screenshots Hovering an Emails/Phones/Links value. Before shows only the Edit pencil; after shows Copy + Edit. <img width="1440" height="900" alt="image" src="https://github.com/user-attachments/assets/5a38bac2-b88c-442d-9a7d-0935e47f93bd" /> <img width="399" height="632" alt="image" src="https://github.com/user-attachments/assets/5dd086e3-6b64-4840-91cf-b99dd6714ca7" /> <img width="184" height="204" alt="image" src="https://github.com/user-attachments/assets/8c7a053f-858a-4e75-bfe6-c9d6760d26e3" /> ## How - Moved `useGetSecondaryRecordTableCellButton` from `record-table/record-table-cell/hooks/` to a shared location `record-field/ui/hooks/useGetSecondaryFieldButton.ts`, since it now serves both the table cell and the inline cell. Behavior (field type handling and the `clickAction` setting) is unchanged; it now also returns an `ariaLabel` per action. - `RecordInlineCellDisplayMode` calls the hook and renders the secondary button(s) next to the pencil. Because Copy does not mutate data, the secondary button also shows on read-only fields (where the pencil is hidden). - Threaded the `ariaLabel` through `RecordInlineCellButton` and `RecordTableCellButtons` so both the table and side-panel buttons are labeled. Since the logic lives in `RecordInlineCellDisplayMode`, the button also appears on the board/calendar card hovered cells, matching the table. ## Test - Added `RecordInlineCellDisplayMode.stories.tsx` covering: hovered (Copy + Edit shown), not hovered (no buttons), read-only (Copy only), and clicking Copy on Emails/Phones/Links asserts the exact clipboard value with the field staying in display mode. - `nx lint:diff-with-main twenty-front`, package typecheck, and the new Storybook interaction tests all pass. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/25940?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. --> |
||
|
|
73b957e764 |
Organize Twenty UI into primitive families (#25948)
Move all existing Twenty UI component families into `primitives/<family>`, including components that still use their older APIs. Reserve `components` for future app building blocks. Replace the old family entry points with `twenty-ui/primitives/<family>` and update workspace imports, renderer examples, documentation, and generated exports. |
||
|
|
a482b18565 |
Record sharing: gate events through one access policy entry point (#25964)
Cleanup pass on the record sharing code merged in #25421, #25425, #25426, #25428, #25650, #25439 and #25925, following the review of #25925: no functional change, the event side of the gate loses its duplicated plumbing and a few dead pieces go. Net: 28 files, +399 / -1068. ## What changes - The four event consumers (subscription publisher, webhook job, logic function trigger job, workflow trigger listener) each rebuilt the record share gate of a batch: a flag check, the share rows read and indexed, the gate kind switch, the admission test, and the parents resolution wired through two callbacks. `RecordAccessPolicyService.buildEventRecordShareGate(batch)` now builds that gate once per batch and `resolveAdmittedRecordIds(subject)` answers the record ids the subject may receive. The consumers build their subject and filter; the flag, the share rows (read at most once per batch, as before) and the parents stay inside the service. - Gone with it: `RecordShareGate`, `DENY_ALL_RECORD_SHARE_GATE`, `buildRecordShareGate`, `isRecordAdmittedByRecordShareGate`, `indexRecordSharesByRecordId` and their specs. The share row admission rule survives as one pure util, `resolveRecordIdsSharedWithPrincipals`, used by the service and by the twin integration spec that checks it against the SQL gate. The service's own nested evaluation (captured child rows) now runs through the same gate switch as the batch level instead of a second copy. - `resolveRecordIdsReadableThroughParents` is no longer public: nothing outside the service needs to reach the parents resolution on its own, and the integration specs that did now go through the batch gate, which is what production calls. - Dead code: `RecordShareService.findByRecord` (tests used it, they call `findByRecordIds`), the `recordIdExpression` override of `buildRecordShareCondition` that only its spec passed. - Repository: `createPermissionBypassingQueryBuilder` and `getRecordShareTableExpression` duplicated `buildBypassingEventSelectQueryBuilder` and `getTableExpression`; the column parents a record inherits through are filtered with a type guard instead of being copied into link objects. ## What does not change - The SQL row access policy, the inheritance rules, the deletion capture, the shareWith path and the recordShare standard object are untouched. The webhook, workflow, logic function and subscription outcomes are the same for every subject; the publisher and workflow listener specs now run the real `RecordAccessPolicyService` over a mocked share service, so the private object, system object and unrelated stream cases still assert the outcome rather than a mocked callback. - `deleteBySourceId` stays: it has no production caller yet, but it is the row API a sharing rule needs and the integration specs clean up through it. ## Verified Typecheck, lint, the 44 unit suites around the touched code, and the 25 integration suites of `object-records-permissions` and `hooks`. |
||
|
|
bd74b1e121 |
i18n - translations (#25971)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
faf4ce0d3a |
Split the admin application registration update input (#25963)
The admin panel mutation and the tenant mutation for updating an application registration shared a single input type. This gives each its own. - Adds `AdminUpdateApplicationRegistrationInput`, which carries the listing flags (`isVetted`, `isListed`, and friends) and is the only input accepted by `updateAdminApplicationRegistration`. - `UpdateApplicationRegistrationInput` is trimmed to the fields a workspace edits on its own registration. - `ApplicationRegistrationService` splits the global update from the tenant update accordingly. - Regenerates the front and client SDK GraphQL types and updates the admin panel mutation document. - Adds an integration spec for the registration instance flags covering both update paths, plus reusable request utils for the two mutations. The catalog sync spec is updated to the new admin input name. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/25963?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. --> |
||
|
|
64f5165b8a |
Scope application file uploads to the registration owner (#25962)
Resolves application file upload targets from the registration the workspace owns instead of from the raw upload input, so the development service and the file upload service share one lookup. - `ApplicationRegistrationService` gains a helper that loads a registration by identifier for a given workspace, used by both the direct upload path and the development flow. - The upload target computation moves out of `ApplicationDevelopmentService` into `ApplicationFileUploadService`. - Integration test helpers for creating and completing uploads are extracted into reusable utils under `test/integration/metadata/suites/application/utils`, with query factories split from the request wrappers. - Adds an integration spec covering upload creation and completion against registrations the workspace does not own. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/25962?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. --> |
||
|
|
466b76cfdc |
i18n - website translations (#25967)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
4e02581851 | Move record CSV exports to background jobs | ||
|
|
bfabaa16a3 |
[2/4] Desktop Recorder: add the installable Twenty integration (#25633)
Add the installable Desktop Recorder integration for locally recorded calls. - Store recordings, process Recall uploads, and generate summaries. - Share Recall transport, retry policy, and signature verification with Call Recorder. - Retry transient transcript requests, finish failed transcripts after media recovery, and stagger recovery jobs across workspaces. - Deduplicate summary jobs with existing `jobId`; cache results using existing KV operations. - Resolve participants and their CRM avatars. - Provide the macOS download link in app settings. **Scope:** `twenty-apps/public/companion` and `twenty-apps/shared/recall`. Call Recorder imports the shared transport; its bot lifecycle stays separate. No core or billing changes. **Local validation:** 298 integration tests, 80 Call Recorder Recall tests, integration typecheck, changed-file lint, and both webhook bundles passed. **Before release:** verify live installation/settings, configure the download URL and credentials. Ambiguous charging failures are not retried automatically. **Merge order:** #25632 → #25633 → #25616 → #25900. |
||
|
|
d7e0abfffe |
Complete Organization naming in settings and activation (#25961)
## Summary Rename the self-hosted Admin Panel tab to Organization and update settings links to `/settings/admin-panel#organization`. Keep redirects from the old `#enterprise` fragment and `/settings/enterprise` route. Align security, logs, record permissions, and legacy page headings with Organization wording. Move the website activation page and checkout success URL to `/organization/activate`, with permanent redirects from the old English and localized URLs. Update English instructions and edit the documentation screenshot to show the Organization tab. Keep the updated image at the legacy image path until Crowdin updates localized documentation. Use Organization wording in website license API errors and related diagnostics. Error codes, response fields, license configuration, and backend API identifiers remain unchanged. Follow-up to #25941. ## Before/After - Enterprise settings tab and feature labels → Organization. - `/settings/admin-panel#enterprise` → `/settings/admin-panel#organization`. - `/enterprise/activate` → `/organization/activate`, preserving existing links. |
||
|
|
4019e10d1e |
[1/4] Desktop Recorder: support existing UI controls (#25632)
Let the desktop app reuse existing Twenty UI controls. - `AnimatedPlaceholder`: allow a custom asset base path. - `IconButton`: forward refs and native props for menu triggers. - `Button`: import Loader and Pill directly. **Scope:** four existing UI files. Desktop-specific components live in #25616. **Latest validation:** desktop typecheck, 115 desktop main/shared tests, and changed-file lint passed. **Merge order:** #25632 → #25633 → #25616 → #25900. <details> <summary>Screenshot — existing AnimatedPlaceholder in the desktop app</summary> Local renderer preview with sample data; the surrounding desktop UI is in #25616.  </details> |
||
|
|
85e02f6a4c |
Record sharing: inherited records follow the parent's complete access policy, in queries and events (#25925)
Follow-up to #25439 and #25914 (both merged), now based on `main`. Two review findings on the inheritance gate: a parent only asked for a non-null key or a share row, never for the reader's permission on the parent object nor their row-level restrictions, so a member blocked from reading a person by their role still read the person's notes and attachments through it; and the event path (subscriptions, webhooks, workflows, logic functions) kept treating every INHERITED record as open, a second interpretation of the policy next to the SQL one. Inert until a parent object is PRIVATE and `IS_RECORD_SHARING_ENABLED` is on. ## What changes - One row access policy builder, `buildRowAccessPolicy`, composes for an alias the role's permission on the object, its row-level predicate and the record share gate (OPEN, PRIVATE share rows, INHERITED parents, SYSTEM and APPLICATION denials). Direct queries go through it for their root and joined aliases, and so do the column parents and child rows of an INHERITED object: a gated parent is correlated through the parent row under that policy, `(fk IS NOT NULL AND EXISTS (SELECT 1 FROM parent p WHERE p.id = fk AND <policy>))`, a denied parent grants nothing. A parent the reader may not query grants nothing on a child. - The builder takes a subject, the pieces of an identity the policy depends on: object permissions, share principals, owning application, row-level filter. The repository derives it from its auth context; the object permission predicate moved to a util the query-level validation shares, so the API keeps reporting a denied root object with the error it documents. - `RecordAccessPolicyService` evaluates that policy for a subject holding no auth context, in SQL on a system-context repository. For an INHERITED record's events it decides which of the records the event snapshot points at, and which live child rows point back at it, the subject may read; the snapshot stands in for the record so a destroyed one is still decided on. The record's own share rows stay evaluated in memory as before. - A deleted record's links are captured with its deletion: a note's targets are soft-deleted with it and cascade away when it is destroyed, so the repository reads the child rows an INHERITED record inherits through before a soft delete or a destroy, and the `deleted` and `destroyed` events carry them as `inheritedReadabilityChildRecords`, a server-side transit property stripped before any client payload. The service evaluates those captured rows as snapshots under the subject's policy (object permission, row-level filter, the child's own share rows or parents), so the readers who could see the record through its links still receive its deletion. Subscriptions and logic functions strip the capture before handing the event over; webhooks and workflows only ever took `before` and `after`. - The query gate applies the same rule to a trashed record: a child link counts when it is live, or when it was trashed at or after the record itself. A note soft-deleted with its targets stays visible in the trash, and restorable, for the readers who saw it live; a target detached before the deletion still grants nothing. - A soft delete through the ORM now touches live rows only, unless the builder opted into `withDeleted`, matching the rows its own snapshot select already returned. The target cascades restamped targets detached earlier with the current time, which the rule above would have read as a link trashed with the note. - The note and task target hooks, which trash and restore the targets with their note or task, now run with permission checks bypassed. They cascade a mutation the actor was already permitted on the parent, and the repository they built without a role config carried no object permission at all, which the inherited gate reads as denied on every parent: with the flag on, they trashed nothing. - Subjects per consumer: a subscriber's object permissions, principals and row-level filter; the standard application's default role for workflows; the application's default role for logic functions; everyone alone for webhooks, which carry no identity. `resolveRecordShareGateKind` returns `inherited` for an INHERITED object and the gate carries the record ids readable through parents next to the share rows; `isRecordSharedWithPrincipals` became `isRecordAdmittedByRecordShareGate`. - The subscription publisher checks that a stream has a query on the event's object before building its gate, so a batch no longer costs a parent lookup per stream subscribed to other objects. ## Things to know - A gated parent now costs one correlated EXISTS on the parent table per candidate row instead of a share-row lookup keyed by the foreign key, since the parent's predicate needs the parent row. - An INHERITED object's events cost, per subject and per batch, one gated query per parent object and per child object of that batch, and a soft delete or destroy of such a record costs one read of its child rows per child object at write time. - The timeline entries exposing a private linked record's title (#25454) remain a prerequisite before activation, as does a backfill of share rows for the notes and tasks that exist unattached before the flag turns on (they hold no parent and no share row, so the gate would hide them from everyone). ## Tests - Unit: `buildRowAccessPolicy` composition (open, denied by object permission, role predicate alone, share rows, parents under their own predicate, a parent the subject may not read dropped, owning application), the condition builder on policy-shaped parents including the trashed-with-the-record rule, the gate builder on INHERITED, the gate kind; the mutation builder keeping a soft delete off trashed rows unless `withDeleted`; the event formatter carrying the captured child rows on the deleted and destroyed events of their record only; the publisher skipping the parent lookup for a stream on another object and stripping the capture from what it broadcasts; the logic function payload without the capture. - Integration, in the children spec: a member whose role cannot read people no longer sees the private person's notes, targets and attachments through it even with a share row; the event gate resolves, for the member's subject, exactly the notes the member's query returns; a note on the private person soft-deleted by the admin (who reaches it through its company target) has its targets trashed with it, is unreadable through its live links and readable through the captured ones, and the admin restores it through the target trashed with it; a note deleted after one of its targets was detached stays out of the member's trash. In the note target hooks spec: a target detached before its note keeps its own deletion time when the note is deleted. |
||
|
|
064ac45abe |
i18n - website translations (#25960)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
9c26006dbf |
feat(workflow): read the workflow show page shell through core (#25901)
## Problem The workflow show page's *content* reads core behind the flag (#25669), but its shell still went through the workspace record API for everyone: the generic `useRecordShowPageResource` ran `findOneWorkflow` to feed the record store, the page title and the layout renderer. The first attempt branched on `nameSingular === workflow` inside `useRecordShowPageResource`. That does not scale: `useRecordShowPageResource` is workspace-record machinery, and every core-owned object would add another branch to it. The switch belongs higher. ## What this PR does **A registry of core-owned show pages, resolved at the page level.** `RecordShowPage` looks the route's `objectNameSingular` up in `CORE_OBJECT_SHOW_PAGES` and, on a hit, renders that page instead of mounting any workspace record machinery. On a miss it renders `WorkspaceRecordShowPageContent`, unchanged. Adding the next core object is one registry entry plus one page component; nothing in `object-record` changes again. The registry is a `Map`, not an object literal — a route segment is raw user input, and an object literal would resolve `constructor` / `toString` and render a prototype member as a page component. **`RecordShowPageShell`** is the old `RecordShowPageContent`, extracted verbatim and made object-agnostic: it takes `objectNameSingular`, `objectRecordId`, `record`, `loading`, `error`. Both the workspace path and any core page render it, so the two get the same header, title, layout renderer and deleted banner. **`WorkflowCoreShowPage`** is the first entry. It reads `coreWorkflow`, maps the DTO onto the record-store shape through a pure util, and renders the shell. `useRecordShowPageResource` keeps a generic `skip?: boolean` (no workflow knowledge) so the core page can hold the workspace read dormant behind the fallback below. **Server:** `CoreWorkflowDTO` gains `createdAt` (the header renders it), and the mirror now carries the workspace record's creation time onto the core row instead of letting the column default. Without that, a core row minted by a restore — or by the first mirror write for a workflow that predates the mirror — is stamped `now()`, and a workflow created months ago reads "Created 2 minutes ago". The mirror is self-healing: the next write to any workflow corrects a row that already drifted. ## Fallback, not a flag No flag on this path: the core mutation surface is considered ready, `IS_WORKFLOW_CORE_INDEX_PAGE_ENABLED` stays scoped to the index and the routes, and the read falls back to the workspace record whenever core has no row. That covers both a not-yet-mirrored workflow and a soft-deleted one (core keeps no soft-deleted rows), so the deleted-record banner and the restore path work until teardown. **Deletions that happen elsewhere.** Skipping the workspace read also removed the only live query SSE could re-broadcast through, so a page left open did not learn about a delete from another tab and never showed the banner. The page now listens for delete and restore operations on the workspace workflow and refetches the core query, which flips `isCoreRecordAbsent` and un-skips the workspace read. Known gap: the flow diagram goes blank in the deleted state and stays blank after Restore until a reload, because `useWorkflowWithCurrentVersion` reads its own `useFindOneRecord` without `withSoftDeleted` and nothing re-runs it — tracked for a follow-up. A core query *error* is not a missing row. An outage surfaces as an error on the shell rather than silently falling through to the workspace read, which would have made an outage look like success. ## Deliberate limits - **The core record is narrower than a workspace record**, and it replaces the stored record wholesale — `createdBy`, `position` and the versions/runs/attachments relation sections have no data behind them. The workflow layout ships only the visualizer widget, so nothing reads them today; growing the DTO is the fix if a fields or relations widget is ever added to workflows. Coming from the workflows table, the same replacement drops fields the table had already loaded; the table refetches on return. - **SSE stays as is.** Workflow writes still land on the workspace record first (writers invert at a later stage), so the existing workspace-event subscription remains live and correct. SSE on core events cannot exist until core writers emit events. - **Prev/next pagination untouched.** `useRecordShowPagePagination` still reads workspace records; the workspace read path stays functional until teardown. - **`SeeActiveVersionWorkflowSingleRecordCommand` untouched.** The command is slated for removal, so its flag-on dead end is not worth a core rewrite. ## Tests `findCoreObjectShowPage` (hit, miss, undefined, prototype members), `isCoreRecordAbsent` (settled-and-absent, loading, errored, row present) and the `buildWorkflowShowPageRecordFromCoreWorkflow` mapping util — all pure, no mocks. --------- Co-authored-by: Thomas Trompette <tom@twenty.com> |
||
|
|
0caaf77995 |
i18n - translations (#25958)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/25958?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> |