cbcfba0de21bd9bf7f3544009a61d455f433b8b5
13869 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cbcfba0de2 |
feat(workflow): dedicated updateWorkflowVersionTrigger mutation + close version CRUD holes (#23207)
Prerequisite for the workflow-core soft-ref migration: every
`workflowVersion` content write must go through a dedicated,
draft-guarded server mutation so it can later be wrapped in a
transactional core mirror. This closes the generic-CRUD holes that let
writes bypass that path.
## Part A - dedicated `updateWorkflowVersionTrigger` mutation
The builder saved a version's trigger through generic
`updateOneWorkflowVersion` - the only content write not going through a
dedicated mutation. Added:
- Server: `updateWorkflowVersionTrigger(input: { workflowVersionId,
trigger })` resolver +
`WorkflowVersionStepWorkspaceService.updateWorkflowVersionTrigger`,
draft-guarded via `getValidatedDraftWorkflowVersion` then
`updateWorkflowVersionStepsAndTrigger` (reuses existing write logic).
- Front: `useUpdateWorkflowVersionTrigger` now calls the dedicated
mutation instead of `useUpdateOneRecord`.
## Part B - restrict generic `updateOneWorkflowVersion`
`validateWorkflowVersionForUpdateOne` previously allowed writing
`trigger`, `position`, `workflowId` (re-parenting),
`coreWorkflowVersionId`, and let `steps: null` slip through on a draft.
It now rejects any update that sets `steps`, `trigger`, `status`,
`workflowId`, or `coreWorkflowVersionId`, or that clears the `name`,
while still allowing a plain rename. (A name-only allowlist was tried
first but blocked legitimate renames - at the pre-hook the generic
update payload is not single-key - so it was replaced by this denylist,
verified live.)
## Part C - close the destroy/restore hole
`workflowVersion` had no `destroyOne/destroyMany/restoreOne/restoreMany`
query hooks, so a caller with object permission could hard-destroy any
version (including active) or resurrect one with no validation. Added
pre-hooks that forbid all four via the API ("Method not allowed"),
matching the existing forbidden generic mutations (`createOne`,
`deleteMany`, ...). Rationale: there is no legitimate API use for
standalone version destroy/restore - retention purging happens through
the trash-cleanup cron (internal, not hook-gated) and restore happens
through the workflow-restore cascade or create-draft-from-version.
## Tests
Integration specs that set a trigger through the generic mutation were
migrated to the new `updateWorkflowVersionTrigger` mutation (new
`update-workflow-version-trigger.util.ts`). Unit test for the front hook
updated.
## Verification
- `twenty-server` + `twenty-front` typecheck: clean.
- oxlint + oxfmt on all changed files: clean.
- `graphql.ts` regenerated for the new mutation; its types match the
server DTOs exactly. Local `graphql:generate` introspects a running
server, so it only succeeds against a server built from this branch - CI
regenerates against the PR server and verifies.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23207?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: prastoin <paul@twenty.com>
|
||
|
|
25b0b2601f |
Replace admin app rollout buttons with upgrade-application CLI command (#23212)
## What Removes the two rollout buttons from the admin application detail page and replaces the upgrade flow with a CLI command that can be run directly from a server or worker pod. Also restructures application stop into its own module with a kill switch CLI command, and surfaces stopped apps in workspace settings. ### Removed - "Install on all workspaces" button (General tab, `SettingsAdminApplicationRegistrationGeneralToggles`), its confirmation modal and tooltip - "Upgrade existing installations" button (`SettingsApplicationRegistrationGeneralStats`), its confirmation modal and batch size input - `backfillApplicationInstallation` and `upgradeRegistrationApplications` admin GraphQL mutations and their frontend documents / generated types - `BackfillApplicationInstallationJob` (its only trigger was the removed mutation); `UpgradeApplicationsJob` is kept since the auto-upgrade flow still enqueues it Per review, the "install on all workspaces" flow is dropped without a CLI replacement for now; a dedicated command will be added when needed. ### application:upgrade command Located in `application-upgrade/commands`, registered in `ApplicationUpgradeModule`: ``` yarn command:prod application:upgrade \ --application-registration-universal-identifier <universalIdentifier> \ [--batch-size 5] \ [--workspace-id <id> --workspace-id <id2>] \ [--workspace-count-limit 10] \ [--dry-run] [--yes] ``` - `--workspace-id` (repeatable) restricts the upgrade to specific workspaces; `--workspace-count-limit` caps how many installations are upgraded (max 50, for canary rollouts) - `--batch-size` and `--workspace-count-limit` are validated as positive integers, max 50 - `--dry-run` reports how many (and which) workspaces would be upgraded, without upgrading - Without `--dry-run`, a confirmation prompt shows the app, target version and impacted workspaces; the run then executes exactly the confirmed set; `--yes` skips the prompt for non-interactive usage The upgrade plan is computed by a new `ApplicationUpgradeService.findApplicationsToUpgrade`, and batches run through a new `upgradeApplications` method — both reused by `upgradeAllApplications`, so the auto-upgrade job path is unchanged. ### Application kill switch (per review) Global mechanism only — a per-workspace stop had no demonstrated operational need and added a Redis key format, execution branching, CLI options and tests; an isolated workspace issue can be handled directly in the DB or Redis with the same effort. - `ApplicationStopService` moved to a dedicated `application-stop/` folder with its own `ApplicationStopModule` (imported and re-exported by `ApplicationModule`) - `stop` / `remove` methods that enable or clear the Redis-backed global kill switch; the logic function executor checks it before executing - `application:kill-switch` command with a positional action, confirmation prompt (shows the installation count) and `--yes` bypass: ``` # Enable the kill switch (stop is the default action) yarn command:prod application:kill-switch stop -u <universalIdentifier> [-y] yarn command:prod application:kill-switch -u <universalIdentifier> # Remove the kill switch yarn command:prod application:kill-switch remove -u <universalIdentifier> [-y] ``` ### Stopped apps surfaced in workspace settings (per review) - Dedicated `isApplicationStopped(applicationUniversalIdentifier)` query backed by the kill switch, fetched with `network-only` policy solely by the application detail page — listing applications triggers no extra Redis reads - Application detail page shows a danger banner when the app is stopped: "We are currently encountering issues with this app, its behavior may be degraded while we work on a fix." ## Test - `npx nx typecheck twenty-server` / `npx nx typecheck twenty-front` pass - `npx nx lint:diff-with-main` passes for both packages - `application-stop.service.spec.ts` covers stop, remove, caching and fail-open behavior - Verified end to end locally: ran the kill switch command on a seeded workspace and confirmed the banner renders on the app detail page (screenshot shared separately) |
||
|
|
940d150775 |
chore(self-hosting): upgrade to latest twenty CLI tooling (#23279)
## What Upgrades the internal `self-hosting` app to the latest Twenty CLI tooling, bringing it in line with the other actively-maintained apps in the monorepo. - `twenty-sdk`: `2.19.0-alpha.1` → `2.23.0-alpha.2` (dependency + devDependency) - `twenty-client-sdk`: `2.19.0-alpha.1` → `2.23.0-alpha.2` (dependency + devDependency) - `engines.twenty`: `>=2.19.0` → `>=2.23.0` - Regenerated `yarn.lock` to match. The `twenty` CLI ships inside `twenty-sdk`, so this pulls the app onto the same CLI version every other recently-updated app (call-recorder, people-data-labs, twenty-partners, real-estate, last-contact, postcard) already uses. ## Verification - `yarn typecheck` passes - `yarn lint` passes (0 warnings, 0 errors) - `yarn test:unit` passes (3/3) Integration tests (`yarn test`) require a running Twenty server and were not run in this environment. ## Notes Scope is limited to the CLI/SDK tooling. Framework deps (React 18) were left untouched since they are not tied to the CLI version and vary across apps. --- _Generated by [Claude Code](https://claude.ai/code/session_01HUPkxerLhethaP9Lw6qDyd)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23279?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. --> |
||
|
|
86d0e15a6a |
i18n - translations (#23281)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
d1c6b8ee72 |
Show relation record labels instead of UUIDs in dashboard charts (#23163)
https://github.com/user-attachments/assets/d012a013-2c90-49a1-a27e-b8e4b684a84f Charts grouped by a relation without a sub-field rendered raw FK UUIDs on axis ticks, legends and tooltips. The server now batch-resolves the grouped record ids to their label identifier through a permission-scoped query and formats every bucket with the record's display name. Unresolvable records (deleted or not readable) render as Unknown and their ids are stripped from the response payload. Same-named records get an ordinal suffix so their buckets don't merge. Covers bar, line and pie, plain and morph relations. ```mermaid flowchart TD A["Dashboard widget load"] --> B["Chart data service<br/>(bar / line / pie)"] B --> C["executeGroupByQuery:<br/>group by relation FK id,<br/>ORDER BY target label identifier,<br/>scoped to source object permissions"] C --> D["filterOutEmptyChartBuckets"] D --> E{"Bare relation axis?<br/>(no sub-field)"} subgraph RL["ChartRelationLabelService.resolveRelationLabels"] direction TB G1["Collect distinct record ids<br/>per target object"] --> G2["Batch SELECT label identifier columns,<br/>scoped to TARGET object permissions"] G2 --> G3["buildRawLabelByRecordId:<br/>display name per record"] G3 --> G4["buildUniqueRelationLabels:<br/>suffix duplicates, Unknown for unresolved"] end E -- No --> H["formatDimensionValue per bucket"] E -- Yes --> G1 G4 --> H H --> I["Strip unresolved ids from<br/>formattedToRawLookup"] I --> J["Chart DTO to frontend"] ``` The chart settings sub-field dropdown gains a Record option to group by the related record itself, and now only offers sub-fields the backend accepts (system fields like a workspace member's updatedBy were selectable but rejected at query time). Chart-data errors are now logged server-side. Also fixes two latent bugs on this path: sorting a bare-relation chart by field threw `Cannot orderBy unknown field: agentId`, and the pie chart truncated slices before sorting. The AI dashboard tool guidance and the seeded dashboards no longer force the sub-field workaround. The group-by query orders buckets by the related record's label identifier at the database level (the engine now accepts ordering by a target field when grouping by its id), so with more than 100 distinct related records the surviving buckets match the label order. |
||
|
|
b036d67ec9 |
Configure async ClickHouse inserts for pageview events (#23274)
## Context Pageview tracking goes through the `trackAnalytics` mutation on the metadata API and is persisted through the unified event pipeline before the mutation resolves. ClickHouse inserts already use: ```text async_insert = 1 wait_for_async_insert = 1 ``` `async_insert` lets ClickHouse buffer and batch small inserts, but `wait_for_async_insert = 1` still keeps the API request open until that buffer is flushed successfully. For sparse pageview inserts, the buffer timeout can therefore account for most of the request duration and contribute to metadata API tail latency. ## What this changes - Adds a named `ClickHouseService.insert` option for overriding `async_insert_busy_timeout_max_ms`. - Caps the pageview buffer wait at 100 ms. - Keeps `wait_for_async_insert = 1`. - Leaves workspace, object, usage, application-log, and other event inserts on the existing default timeout. ## Why this approach This removes the avoidable buffer wait from the pageview request path without changing the delivery guarantees of the event pipeline. In particular, this does **not** use `wait_for_async_insert = 0` or fire-and-forget writes. The API still receives an acknowledgement only after ClickHouse flushes the pageview successfully, and insert/schema errors still propagate through the existing handling. The 100 ms value caps only the batching wait. It does not impose a 100 ms deadline on the complete ClickHouse request. ## Expected impact - Lower ClickHouse span duration for pageview tracking. - Lower tail latency for metadata API requests that emit pageviews. - No behavior or durability change for other event types. The trade-off is that pageviews may be flushed in smaller batches. The setting remains scoped to the pageview table so higher-value event streams keep their current batching behavior. ## Testing - Added coverage for the optional ClickHouse busy-timeout setting. - Added coverage verifying that only pageview inserts receive the 100 ms override. - Existing insert failure/retry behavior remains covered. - `twenty-server` typecheck passes. - Focused test result: 23 tests passed. ## Post-deploy verification - Compare pageview ClickHouse span p95/p99 before and after deployment. - Compare metadata API p95/p99. - Check ClickHouse asynchronous-insert failures. - Watch ClickHouse part creation and merge pressure for unexpected growth. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23274?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. --> |
||
|
|
6a8457d8c5 |
fix(ci): diff upgrade mutation guard against the merged base, not stale PR base (#23278)
The `server-previous-version-upgrade-mutation-guard` check falsely flags upgrade commands that landed on `main` as being added/modified by unrelated PRs (e.g. [run on #23207](https://github.com/twentyhq/twenty/actions/runs/30097349274/job/89494554178) flagged six `2-23`/`2-24` files the PR never touched). ## Cause The guard action diffs `git diff "$BASE_SHA" HEAD` where the PR caller passes `base_sha: github.event.pull_request.base.sha`. On a `pull_request` event `HEAD` is the `refs/pull/N/merge` ref, whose first parent is the *current* tip of `main` it was merged with. But `pull_request.base.sha` is pinned to the base at the last branch sync and lags behind. Any upgrade command merged into `main` after that point exists in `HEAD` but not in the stale base, so the two-dot diff attributes it to the PR. Verified against the real merge ref for #23207: diffing against `base.sha` reproduces the six false offenders from the failing run; diffing against the merge ref first parent is clean. ## Fix Derive the base from the merge ref first parent (`HEAD^1`, the actual merged `main` tip), falling back to `base.sha` only when `HEAD` is not a merge commit (non-mergeable PR). The merge-queue caller in `ci-merge-queue.yaml` is unaffected: it passes `merge_group.base_sha`, which already matches its checkout. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23278?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. --> |
||
|
|
dee653dfa6 |
fix: move message list member backfill command to 2.25 (#23271)
## What
Moves the `backfill-message-list-members-junction-target` workspace
upgrade command from `2.24.0` to `2.25.0`.
Introduced in #23176 (commit
|
||
|
|
ba5cb6ba15 |
fix(server): repair missing keyValuePair.applicationId on 2.23 upgrades (#23272)
Fixes #23254 ## Problem Upgrading a self-hosted instance from `2.23.x` to `2.24.0` leaves `core.keyValuePair` without the `applicationId` column. Database-backed config loading then fails on startup and on every refresh (~every 15s) with: ``` column KeyValuePairEntity.applicationId does not exist ``` The frontend shows "Unable to reach the backend". ## Root cause `AddApplicationIdToKeyValuePairFastInstanceCommand` was added in #23089 (after `2.23.x` shipped) but registered under the already-released `2.23.0` segment: ```ts @RegisteredInstanceCommand('2.23.0', 1784659343818) ``` The upgrade cursor is **positional and forward-only**: - `resolveStartCursor` resumes at `lastAttemptedIndex + 1`. A fully-upgraded `2.23.x` instance has its cursor at the last `2.23` workspace command, which sits *after* this newly-inserted fast command in the sequence. So the runner steps right over it and the DDL never runs. - The upgrade-aware metadata layer decides "applied" the same way (`stepIndex < currentCursor` in `upgrade-aware-entity-metadata.adapter.ts`). Since the step index is below the cursor, the column is considered applied and is **not** hidden from TypeORM SELECTs, so every query references a column that was never created. Fresh `2.24.0` installs replay the whole sequence, so only `2.23.x -> 2.24.0` upgrades are affected. The instance log `1 fast instance ... for 2.24.0` confirms the command landed in the `2.23.0` bundle rather than `2.24.0`. ## Fix - Add `RepairKeyValuePairApplicationIdFastInstanceCommand` under the current version (`2.24.0`) with a fresh timestamp, so it sorts last in the sequence and runs for every existing instance regardless of cursor position. Its DDL mirrors the original command and is fully idempotent (`ADD COLUMN IF NOT EXISTS`, `DROP INDEX IF EXISTS` + recreate, `ADD VALUE IF NOT EXISTS`), so it is a no-op on healthy instances. `down()` is intentionally empty: the column lifecycle is owned by the `2.23.0` introduction command. - Repoint the entity's `@WasIntroducedInUpgrade` to the new command so the column stays hidden from queries until the repair has actually run, eliminating the error window during the migration itself. ## Notes - `2.24.0` (`TWENTY_CURRENT_VERSION`) is the correct target: the upgrade sequence only covers previous + current versions, so a command under `2.25.0` (a next version) would not run. If a version bump lands before this merges, the command should be moved to the new current version. - Follow-up worth considering: nothing currently prevents registering a command under a version in `TWENTY_PREVIOUS_VERSIONS`. A startup validation rejecting that would have caught this at PR time. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23272?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. --> |
||
|
|
abe4d7491c |
Cap nested relation query concurrency (#23252)
## Context Common API queries load selected relations after fetching the root records. Relation loading is batched: one query pipeline loads a relation for all parent records, so this is not an N+1 problem. However, every sibling relation currently starts concurrently through `Promise.all`. Nested relations repeat the same behavior recursively. A wide selection can therefore submit many independent relation query pipelines at once. Existing query complexity and record limits restrict what can be requested, but they do not limit how much database work starts concurrently. ## What this changes This PR adds a request-local FIFO concurrency limiter for nested relation loading. - At most four `findRelations` pipelines execute concurrently. - One limiter is created for the outer relation-loading call. - The same limiter is shared by every recursive level. - Queued work starts as permits become available. - Permits are released in `finally`, including when a query fails. Conceptually: ```text Before: all sibling relations -> database concurrently nested siblings -> more database work concurrently After: all sibling relations -> FIFO queue -> at most 4 database pipelines nested siblings -> same FIFO queue and same limit ``` Note: Also addressing https://github.com/twentyhq/twenty/pull/23251#discussion_r3644510597 |
||
|
|
9390c28cb6 |
ci: report ci-shared-status-check on merge_group (#23276)
Follow-up to #23275. `ci-shared` was the one required check left off that batch, so `ci-shared-status-check` still sits at "Expected - Waiting for status to be reported" and blocks the merge queue. Same fix as the other workflows: add a `merge_group` trigger and gate `changed-files-check` with `if: github.event_name != 'merge_group'`. On a queued candidate, `changed-files-check` skips, `shared-test` (gated on `any_changed`) cascades to skipped, and the `always()`-gated `ci-shared-status-check` job reports success in seconds. The full suite still runs on `pull_request`. --- _Generated by [Claude Code](https://claude.ai/code/session_015mZozbvyvha1S6wsEVLBnF)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23276?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. --> |
||
|
|
29747f5d6b |
ci: report required status checks on merge_group so the queue isn't blocked (#23275)
Follow-up to #23216, which added the `merge_group`-triggered `upgrade-mutation-guard`. This makes the merge queue actually usable. ## Why The merge queue waits for every **required status check** to report a conclusion on the `merge_group` candidate commit, and there is no queue-only subset: it uses the branch's required status checks. Our required `ci-*-status-check` contexts only trigger on `pull_request`, so on a queued PR they sit at "Expected - Waiting for status to be reported" and block the queue until the status-check timeout (60 min), which then counts them as failed. Only `upgrade-mutation-guard` (from #23216) triggers on `merge_group`, so today it is the only check that reports in the queue. ## What Add a `merge_group` trigger to each of the seven required-check workflows and short-circuit the expensive work so the check reports success in seconds, while the full suite keeps running on `pull_request` to gate PRs. `upgrade-mutation-guard` stays the only check the queue genuinely validates against `main`. Mechanism: on `merge_group` the root jobs skip, everything downstream cascades to `skipped`, and the `always()`-gated `*-status-check` job runs, sees no failing needs, and succeeds. Kept as the same job in the same workflow so the required-check context is byte-identical to the PR-level one (a separate pass-through workflow could register a different context and not satisfy branch protection). Per workflow: - **ci-front**: trigger only. It already cascades - `changed-files-check` is `pull_request`-only and `front-sb-build` gates on `push || any_changed`, so nothing runs on `merge_group`. - **ci-server**: trigger + `if: github.event_name != 'merge_group'` on the three ungated root jobs (`changed-files-check`, `upgrade-changed-files-check`, `server-previous-version-upgrade-mutation-guard`). The guard would otherwise fail on `merge_group` since `pull_request.base.sha` is empty there; the queue-side guard in `ci-merge-queue.yaml` already covers that case. - **ci-sdk / ci-website / ci-test-docker-compose**: trigger + the same guard on `changed-files-check`. - **ci-twenty-apps**: trigger + the guard on `discover` (its `ci`/`integration` jobs gate on `discover` output, so they cascade off). ## Settings note This complements the branch-protection changes for the queue (enable the queue, max group size 1, per #23216). If the branch has **other** required checks beyond these seven whose workflows are `pull_request`-only, they need the same `merge_group` treatment or the queue will wait on them too. --- _Generated by [Claude Code](https://claude.ai/code/session_015mZozbvyvha1S6wsEVLBnF)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23275?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. --> |
||
|
|
bb4e427196 |
ci: run upgrade mutation guard in the merge queue against main (#23216)
Follow-up to #23215 (merged). Rebased on `main`. ## Why #23215 fixes an instance of a class of bug: an upgrade command whose version is chosen at `generate:instance-command` time from `TWENTY_CURRENT_VERSION`, then left behind when `main` bumps the version before the PR merges (base-drift). The command ships one minor early and instances already on the newer version skip it forever. The existing `server-previous-version-upgrade-mutation-guard` in `ci-server.yaml` runs on `pull_request`, so it validates against the PR's base. When the base is stale (main moved after the branch was cut), the guard reads the branch's own `TWENTY_CURRENT_VERSION` and the check passes even though the command is now a version behind main. That is exactly how the original bug slipped through. ## What The version-directory and append-only-timestamp validation is extracted into a shared composite action, `.github/actions/upgrade-mutation-guard`, diffed against a caller-supplied `base_sha`. It is called from two places: - **`ci-server.yaml`** (PR-level guard, `base = pull_request.base.sha`) for fast feedback. The job keeps its existing name/check. This replaces ~290 lines of inline shell. - **`ci-merge-queue.yaml`** (new, `merge_group`-triggered, `base = merge_group.base_sha`). GitHub builds each merge-queue candidate on top of the current tip of `main`, so the checks read `TWENTY_CURRENT_VERSION` and the existing per-directory timestamps from main's real state at merge time. Because the candidate is rebased onto main, base-drift is caught by construction: the same validation simply runs where the base is guaranteed current. No origin/main comparison hack; the logic now lives in one place. ## Bypass semantics The guard has two independent checks, and they are treated differently on purpose: - **Version-directory check** keeps its `ci:allow-previous-version-upgrade-mutation` bypass, a deliberate, reviewed escape hatch for legitimately touching a previous-version directory. The PR-level guard reads the label directly; the merge-queue guard resolves it from the queued PR (the `merge_group` event carries no labels) and passes it to the composite action, which skips only the version-directory step. - **Timestamp / append-only check has no bypass.** The old `ci:allow-upgrade-command-timestamp-exception` label is removed. A fake or out-of-order timestamp rewinds the upgrade cursor and re-hides already-applied columns, so there is no "allowed" version of it: the timestamp just has to be configured correctly (real epoch millis, strictly greater than every existing command in the same version directory). If a blocking existing max is itself a fabricated future timestamp, re-slot that command to its real merge epoch rather than reaching for a bypass. Preventing previous-version mutation is the guard's primary purpose. In the merge queue the guard job always runs and skips only the version-directory step when the bypass label is set, so it reports a real success/failure (the required check never resolves to a skipped state, and a label-lookup failure fails closed) and the timestamp check always runs. ## Requires a settings change (not in this diff) Enabling the merge queue and marking the check required are branch-protection settings, not file changes. After merge, an admin needs to: 1. Enable the merge queue for `main` in branch protection. 2. Add `CI - Merge Queue / upgrade-mutation-guard` to the merge queue's required checks. ## Notes - Composite action, not a `workflow_call` reusable workflow, deliberately: converting the `ci-server.yaml` job to a reusable-workflow call would rename its status check to `server-previous-version-upgrade-mutation-guard / ` and break that required-check mapping in branch protection. A composite action dedups the logic while keeping both callers' check names intact. --------- Co-authored-by: Paul Rastoin <paul.rastoin@gmail.com> |
||
|
|
c0c5ba33e8 |
i18n - translations (#23269)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23269?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> |
||
|
|
b61134ea5b |
i18n - translations (#23268)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
923035bf48 |
Extract front component host wrapper into hooks (#23262)
Refactors `createHtmlHostWrapper` into composable hooks (`useHtmlHostElementProps`, `useComposedElementRef`, `useCaretPreservingElementRef`) as groundwork for the geometry mirror. Behavior-focused, no feature change: - Caret preservation moves to a stable ref + `useLayoutEffect` re-assertion (covered by the caret suites). Highest regression surface in the series, isolated here for focused review. - The remote `ref` prop is now swallowed via `INTERNAL_PROPS` instead of leaking onto host elements. First of three PRs splitting the geometry mirror work. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23262?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. --> |
||
|
|
3ee8fc0973 |
Add front component skeleton loader (#23261)
Front components (dashboard widget, side panel, settings preview) showed blank space during their entire load. They now show a shimmering full-area skeleton continuously, from the lazy chunk load through metadata fetch, token/SDK wait, and worker boot, until the real UI mounts. The skeleton is threaded down as an optional `loadingFallback` prop so the shared `twenty-front-component-renderer` package stays dependency-free (react-loading-skeleton stays in twenty-front). The command-menu headless component opts out by not passing a fallback, so it stays blank as before. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23261?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. --> |
||
|
|
3a8f086d15 |
Converge drag and drop on shared dnd-kit primitives, remove @hello-pangea/dnd (#23211)
Follow-ups recorded in #23023, done in one pass. ## Shared primitives - Folded `PageLayoutWidgetSortableItem` and `PageLayoutWidgetDropLine` into the shared `DragDropItemSortableCell` / new `DragDropItemDropLine` (new `data`, `dropLine`, `highlightWhileDragging`, `hasTransition` props). - Added generic `DragDropProviderDragStartEvent` (and DragMove/DragOver/DragEnd/DropTarget) helpers and deleted the 7 copied `Parameters<...>` extractions across the dnd hooks. - Replaced the `useMovePageLayoutWidgetUp/Down` implementations (~140 lines) with `moveWidgetWithinTabInDraft`. - Migrated the remaining page-layout test suites onto `pageLayoutDraftFixtures`. ## Tab reordering off Pangea - Tabs are sortable cells on the same provider as widget drags, segregated by dnd type, so widget drops on tab buttons keep working while tabs reorder. - Reordering is ID based (`reorderTabInDraft`: insert before the hovered tab), which keeps the pinned first tab in place without index arithmetic. - Preserved overflow behaviors: the dropdown stays open while a tab drag is in flight, dropping a tab on the "+N More" button appends it and opens the dropdown, and both the visible strip and the overflow list have end drop zones. ## Fields configuration editors off Pangea - Group reorder, field reorder and cross-group field moves now run on the shared cells (same drop line and end-zone patterns). ## DraggableList off Pangea - `DraggableList` / `DraggableItem` keep their consumer-facing API — the ~9 consumers now type their handlers with a local `DraggableListDropResult` instead of pangea's `DropResult` — but run on the shared sortable cells; each list's uuid group doubles as its dnd type so nested lists stay isolated from page-level providers. - Items register their index in a list-scoped registry so the end drop zone can resolve the append index at drop time (with insert-before semantics an item could otherwise never reach the last position). - Deleted three dead files that only existed for pangea plumbing (the side panel navigation placeholder, `getCssCompatibleDraggableProps`, the orphaned `recordGroupPendingDragEndReorderState`). ## Record table row drag off Pangea - Rows register through `useSortable` directly on the row element — no wrapper div, so row CSS, sticky cells and virtualization stay untouched — with the grip cell wired as the drag handle via the shared sortable handle ref context. - Both table modes (virtualized flat list and record groups) share a `DragOverlay` clone that replaces pangea's virtual-mode `renderClone`, and end drop zones per record group (and after the virtualized list) allow dropping after the last row or into an empty group. - The drop handlers keep their pangea-shaped result object, retyped as a local `RecordDragDropResult`, so the position computation logic is untouched. ## Pangea removed `@hello-pangea/dnd` is gone from `package.json` and the lockfile, along with its orphaned transitive entries (`css-box-model`, `raf-schd`, `react-redux`, `redux`). Nothing in the repo imports it anymore. ## Dashboards: cross-tab widget drag for grids react-grid-layout drags never enter dnd-kit, so the bridge hit-tests the pointer against the tab buttons' `data-page-layout-tab-drop-target-id` rects during grid drags, highlights the hovered tab through state, and on drop moves the widget to the destination grid below its existing content (`moveWidgetToGridTabInDraft`, `buildTabWidgetLayouts`). The grid's own post-drag layout commit is suppressed once so it does not overwrite the cross-tab move. ## Fixes found while testing - With `feedback: 'clone'`, the drag source is its own initial drop target and its placeholder is a DOM clone taken at drag start, so the drop line rendered into the source got baked into the placeholder and stuck there for the whole drag. The line is now hidden on the source cell, leaving a single indicator at the actual target. - Reorderable tabs collapsed to text height and sat top-aligned next to "+ New Tab" because the sortable cell wrapper defaults to `display: block; height: auto`, breaking the tab height chain — the tab list now uses the cell's `fill` mode so tabs stretch to the strip height again. ## Testing Playwright against the dev app: - Record page: widget reorder up and down in the pinned column (single blue drop line at the target), drag to another tab via its tab button (highlight + move), drag back into content at a specific position, chained cross-tab moves, tab reorder with vertical drop line, new tab creation. - Overflow (narrow viewport): drop a tab on "+N More" (appends last, dropdown opens), reorder inside the dropdown (stays open), drag a tab from the dropdown back to the visible strip. - Dashboard: grid drag within a tab, cross-tab drag onto a tab button (hover highlight, widget lands below destination content, remaining widgets keep their positions), save and reload persistence in both directions. - Fields editor: field reorder, group reorder, field move across groups, plus the Move Up / Move Down widget actions. Since the pangea-removal commits: - Typecheck, oxlint and oxfmt green over the full front source; unit suites green including the migrated `useStartRecordDrag` test (jest needed a scoped transform exemption for `@preact/signals-core` once dnd-kit reached the side-panel suites). - Storybook visual regression unchanged across ~700 stories — expected, since the migrated surfaces render identical DOM at rest (drop lines and drag overlays only exist mid-drag). - The tab strip fix reverses the exact regression mechanism: the sortable cell wrapper defaulted to `display: block; height: auto`, collapsing the tab height chain next to the full-height "+ New Tab" button; `fill` restores the stretch. --- _Generated by [Claude Code](https://claude.ai/code/session_01XKRCzzu8oGyocXZtFp7VEG)_ <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23211?utm_source=github" rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a> |
||
|
|
d53ef11fe2 |
fix: bump linkify-it to 5.0.2 (Dependabot) (#23236)
Bumps **linkify-it -> 5.0.2** (sole descriptor `^5.0.1`, caret already permits it; recursive `yarn up`, lockfile-only, no resolution). Clears [1799](https://github.com/twentyhq/twenty/security/dependabot/1799) (GHSA-v245-v573-v5vm, high). `yarn install --immutable` passes. 5.0.2 published 2026-07-01, clears the age gate. |
||
|
|
bb22b216db |
Fix front components rendering a blank panel in Firefox (#23213)
Fixes #22973 In Firefox, front components rendered a blank panel with only `DataCloneError: Exception object could not be cloned` in the console. Accessing `caches` in the opaque-origin sandbox worker throws a Gecko `Exception` (worker-side CacheStorage code up to v2.22, or any component code touching it since), and `@quilted/threads` posts thrown values raw over the MessagePort. Firefox cannot structured-clone these exceptions, so the error report itself failed and the render promise never settled. Thread errors are now flattened to clonable payloads and rehydrated on the other side, so the real error surfaces in the error box instead of silently hanging the panel. Also makes the CacheStorage guards exception-safe (v2.23 already moved that code host-side, which removed the main trigger). Verified end to end in stock Firefox 149: before, a component touching `caches` hangs silently; after, render rejects with the full `NS_ERROR_FAILURE` diagnostic. Chromium behavior unchanged. ```mermaid sequenceDiagram participant Host as Host (React) participant Worker as Sandbox worker (null origin) participant Threads as @quilted/threads rect rgb(250, 235, 235) note over Host,Threads: Before — Firefox hangs Host->>Worker: render(component) Worker->>Worker: throws Gecko Exception<br/>(typeof caches) Worker->>Threads: postMessage(rawException) Threads--xHost: DataCloneError:<br/>Exception could not be cloned note over Host: CALL_RESULT never arrives<br/>render() promise never settles → blank panel end rect rgb(232, 245, 233) note over Host,Threads: After — error surfaces Host->>Worker: render(component) Worker->>Worker: throws Gecko Exception Worker->>Threads: serialize → { name, message, stack } Threads->>Host: postMessage(clonable payload) Host->>Host: rehydrate → Error, reject render() note over Host: error box shows NS_ERROR_FAILURE end ``` <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23213?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. --> |
||
|
|
ae2c8978d1 |
i18n - docs translations (#23258)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
e5c9fcf058 |
Add PostgreSQL connection pool pressure metrics (#23251)
## Summary - Add pool gauges for total, idle, waiting, and maximum connections - Record PostgreSQL connection acquisition duration and failures - Instrument core, workspace primary, and optional replica data sources - Add unit tests covering gauges, acquisition timing, failures, and deduplication <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23251?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. --> |
||
|
|
d0863dd1f7 |
i18n - translations (#23253)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
1fdb5605f1 |
feat: kanban, calendar and grouped-table layouts for relation field widgets (#23112)
## Context A relation field widget on a record page can already embed a record-scoped view rendered as a **table** (`FieldDisplayMode.TABLE`) — e.g. a Company's Opportunities. This brings **kanban, calendar and grouped-table** to that same embedded view, so the board/calendar stays scoped to *this* record's related records (not a standalone all-records widget — that was the earlier #23003 approach, closed). Builds directly on the merged dashboard widget layouts (#22963), reusing its renderer, draft/save pipeline, and settings dropdowns. ## Approach — extend the existing "Table" display mode The relation field widget already stores a `viewId` and renders it through the layout-agnostic `RecordTableWidgetRendererContent` (which branches on the embedded view's `type`), scoped to the current record via `RecordFilterValueDependenciesContext`. So rendering + persistence already work for any widget view type — only the authoring UI and one server gate were missing. **No new `FieldDisplayMode`, no data migration.** ## Server - `view-widget-upsert.service.ts`: a field widget in table display mode (`isFieldTableWidget`) could already persist viewFields/filters/sorts through this path, but was **blocked from updating view settings** (`type` / group-by / calendar), pinning its embedded view to a table. The widget-type guard earlier in the method already rejects every widget kind other than record-table and field-table, so the now-redundant record-table-only guard on the view-settings branch is dropped. The allowed-widget-view-types check and the downstream group-by / calendar-field validations still apply equally. ## Frontend - **One merged Layout picker.** The field widget's Layout dropdown lists **Field / Card / Table / Kanban / Calendar** in a single flat list — you pick Kanban directly, instead of "Display as: Table" first and a separate embedded-view layout second. Picking a view layout selects the `TABLE` display mode under the hood, seeds the record-scoped embedded view on first use (with a default group-by / date field), and applies the layout in the same click. Kanban/Calendar are disabled with a hint ("Needs a Select field" / "Needs a Date field") when the relation target can't support them — same gating as the dashboard picker. The row's icon and description reflect the effective selection (e.g. Kanban), and the dropdown mounts the draft-init effect so switching straight from Field/Card to Kanban works before the table renderer has ever mounted. - **Contextual rows** (Group by / Date field / Calendar view / Hide empty groups) extracted from the dashboard panel into a reusable `WidgetViewLayoutSettingsRows` (source object passed in — fixed to the relation target; no Source / Limit rows) and surfaced under the picker while a view layout is active. Its standalone layout row is hidden here (`isLayoutRowHidden`) since layout lives in the merged picker. - Reuses the dashboard draft snapshot + `upsertViewWidget` save pipeline and the group-by/calendar dropdown components unchanged. ## Scope - **One-to-many relations only** (matches the existing `getFieldWidgetAvailableDisplayModes` gate; junction / many-to-many stay table-only — a pre-existing inconsistency left untouched here). - Field-widget **calendars inherit the dashboard's behavior** (month read-only by default; day/week + drag-to-reschedule only behind `IS_CALENDAR_WEEK_VIEW_ENABLED`), since it's literally the same renderer. ## Tests - Server integration (`upsert-view-widget-view-settings.integration-spec.ts`): a FIELD + TABLE widget can switch its embedded view to `KANBAN_WIDGET` (with group-by) and `CALENDAR_WIDGET` (with date field), and the kanban group-by validation still applies through the newly-opened path. - Front unit: `getWidgetViewLayoutSettingsItemIds` (keyboard-nav row ids per layout/flag/group state). ## Follow-ups (intentionally not in this PR) - Migrate the dashboard settings panel onto the shared `WidgetViewLayoutSettingsRows` (kept out to avoid churning the just-merged #22963 file; behavior-preserving refactor). https://claude.ai/code/session_01E5N87kwwZWhDtEQaP72cMf |
||
|
|
a3a6a55051 |
i18n - docs translations (#23250)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
fdb8865933 |
test: cover uninstall logic function hook execution (#23249)
Follow-up to #23227 ([review comment](https://github.com/twentyhq/twenty/pull/23227#pullrequestreview-4771629391)): adds integration coverage for the `defineUninstallLogicFunction` hook execution on uninstall. ## What New integration suite `successful-uninstall-application-logic-function-hook.integration-spec.ts` that drives the real `syncApplication` / `uninstallApplication` GraphQL flow and asserts on the executor wiring: - When the synced manifest declares an `uninstallLogicFunction`, uninstalling the application resolves the hook and calls `LogicFunctionExecutorService.execute` exactly once, before deletion, with the `{ version }` payload. - When the manifest declares no uninstall hook, uninstalling the application does not call the executor. The executor is spied via the running app container (`getAppProviderByClassName`) and stubbed to a success result, so the test verifies the server-side resolution/trigger path deterministically without depending on the local function runtime. ## Test plan - `npx jest --config ./jest-integration.config.ts successful-uninstall-application-logic-function-hook` passes (2/2). - Typecheck green for twenty-server; oxlint and oxfmt clean on the new file. --- _Generated by [Claude Code](https://claude.ai/code/session_016zJPggkVEw1V7SqnPSiUQx)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23249?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: martmull <martin@twenty.com> |
||
|
|
f793f3c5a9 |
Fix application logos resolving to null on install and sync (#23245)
## Problem Application icons are no longer resolved: `logo` is `null` on `FindManyApplications` / `FindOneApplication`, so app chips and the settings applications table fall back to initials avatars. ## Root cause Manifests produced by the current SDK carry the logo path in `manifest.application.logo`; `logoUrl` is deprecated and stripped by `normalizeApplicationAssets` (external URLs are dropped, relative ones are moved to `logo`). Three server call sites still read only the deprecated `logoUrl`: - `application-sync.service.ts` (`syncApplication`): wrote `logo: manifest.application.logoUrl ?? null` on every install/upgrade/dev sync, overwriting `application.logo` with `null` - `application-sync.service.ts` (`buildVirtualDryRunFlatApplication`): same read on the dry-run path - `application-install.service.ts` (`ensureApplicationExists`): same read on the create path Since both `Application.logo` and the `Application.logoUrl` resolve field derive from that column, icons went null everywhere. Separately, OAuth-only apps (e.g. "Twenty CLI" from dynamic client registration) never get a logo at all: `OAuthRegisterInput` accepts `logo_uri` but the registration controller dropped it, so `applicationRegistration.logoUrl` also resolves to null for those. ## Fix - Read `manifest.application.logo ?? manifest.application.logoUrl ?? null` at all three sites, matching the fallback already used by `importLogoFile` and `fromManifestApplicationToDisplayFields` - Persist `logo_uri` into `applicationRegistration.logo` on OAuth dynamic client registration; `buildLogoUrl` passes absolute URLs through, so the consent screen and app chips can resolve it Existing rows that were already nulled will self-heal on the next app upgrade/sync, since the sync path rewrites `logo` from the manifest. --- _Generated by [Claude Code](https://claude.ai/code/session_01Us7BE5yBguYTACg5H3Y85z)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23245?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. --> |
||
|
|
f7683b7e47 |
i18n - translations (#23247)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
09e20eee5f |
i18n - translations (#23246)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23246?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> |
||
|
|
abc82d66b7 |
Consolidate per-queue worker tuning in one explicit config file (#23229)
## Context
Follow-up to the worker configuration analysis and to the worker pool
split rolled out in twentyhq/twenty-infra#805/#806. Worker tuning was
previously spread across two partial constants (`QUEUE_WORKER_OPTIONS`,
`MESSAGE_QUEUE_PRIORITY`), and most queues silently relied on implicit
BullMQ defaults.
## What this PR does
Introduces a single dedicated file to pilot worker behavior per queue:
`src/engine/core-modules/message-queue/message-queue-worker-config.constant.ts`
`MESSAGE_QUEUE_WORKER_CONFIG` declares, for **every** queue, an
explicit:
- `priority` (applied when enqueuing, lower runs first)
- `concurrency`
- `lockDuration`
- `maxStalledCount`
- `boundedShutdownDrain`
Explicitness is enforced at compile time: the record is typed
`Record<MessageQueue, { priority: number; workerOptions:
Required<MessageQueueWorkerOptions> }>`, so adding a queue without
declaring its full configuration is a type error, and no field can be
omitted.
Wiring changes:
- `message-queue.explorer.ts` passes
`MESSAGE_QUEUE_WORKER_CONFIG[queueName].workerOptions` when creating
workers
- `bullmq.driver.ts` reads the enqueue priority from the same record
- `message-queue-worker-options.constant.ts`,
`message-queue-priority.constant.ts` and
`ai-stream-lock-duration.constant.ts` are removed (the AI stream lock
duration is inlined into the one config entry that used it)
## Behavior
No behavior change — the previously implicit BullMQ defaults
(concurrency 1, lockDuration 30s, maxStalledCount 1) are now spelled out
per queue, and the existing overrides (`ai-stream-queue`: concurrency 20
/ 10 min lock / no stall retry / bounded shutdown drain;
`logic-function-queue`: concurrency 10) and all priorities are carried
over unchanged.
## Validation
- `npx nx typecheck twenty-server` ✅
- `oxlint --type-aware` + `oxfmt --check` on changed files ✅
- `npx jest "message-queue"` (7 tests) ✅
Companion infra PR: twentyhq/twenty-infra#808 moves the worker pool
topology (replicas, resources, queue filters) into a dedicated
`workers.yaml` per environment.
Session: https://claude.ai/code/session_01TL6Te48Lkys5NxyG9j2Nz6
|
||
|
|
91f0b77fdb |
Only suggest vetted apps in onboarding install step (#23222)
The onboarding "Install your first apps" step now only suggests marketplace apps where `isVetted` is true. |
||
|
|
fb52635d2a | Add defineUninstallLogicFunction hook for applications (#23227) | ||
|
|
cada1ef6d7 |
feat(website): live community stats, drop hard-coded fallback (#23047)
## Problem The menu's GitHub star and Discord member counts almost always render the hard-coded snapshot (49.6K / 6.6K, frozen June 2026), not live numbers. The render-time fetches run unauthenticated from Cloudflare Workers, whose egress IPs are shared across tenants; GitHub's unauthenticated quota is 60 req/hr per IP, so the call is effectively always rate-limited. Live prod today shows the frozen 49.6K GitHub count next to a live Discord count, confirming only GitHub is affected. ## Change - GitHub fetch sends `Authorization: Bearer $GITHUB_STATS_TOKEN` when the env var is set, moving it onto its own 5,000 req/hr quota. Discord keeps the public invite endpoint, which works fine from Cloudflare's IPs. - The hard-coded fallback is deleted rather than refreshed. `CommunityStats` fields are now `number | null`, resolved live -> last-good -> null, and the menu renders an icon-only chip when a count is genuinely unavailable. A fake number can never ship. - Last-good values persist in the worker's existing OpenNext R2 bucket (`NEXT_INC_CACHE_R2_BUCKET`, key `community-stats/latest.json` outside the `incremental-cache/` prefix), so a third-party outage shows the numbers from the previous refresh. No new infrastructure. - Revalidation tightens from 1h to 15min (~4 GitHub calls/hour, shared cache entry across pages). - `MenuSocial`/`MenuDrawer` import `formatCompactCount` from its module directly: the community barrel now re-exports server-only code, and a client value-import would pull `getCloudflareContext` into the client bundle. ## Rollout - twentyhq/twenty-infra#795 passes the built-in Actions token at build time so prerendered pages ship with a real star count from the first request after deploy. - One manual step: set the `GITHUB_STATS_TOKEN` secret (fine-grained PAT, public read-only, no permissions) on the twenty-website-dev and twenty-website-prod workers. Until it exists, behavior degrades to today's minus the fake numbers. ## Tests 5 unit tests cover the resolution ladder: live wins, cache fills a failed fetch, null on cold-cache failure, both-fail serves cache without overwriting, nothing written when nothing succeeded. Verified against the dev server: menu renders live 53.3K / 6.9K. |
||
|
|
d1b556f4a8 |
i18n - docs translations (#23244)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23244?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> |
||
|
|
7f1e3d3541 |
fix(admin): prevent fallback to 'latest' string when dockerhub tags c… (#22885)
Fixes #22849 ### What changed? When the `AdminPanelVersionService` hits a DockerHub API error (or filters out all valid tags), it was previously hardcoded to return `'latest'`. This caused the frontend to display `Latest version: latest`. I updated the GraphQL DTO to make `latestVersion` nullable, and modified the service fallback and unit tests to return and expect `null` instead. The frontend (`SettingsAdminVersionDisplay`) already has logic to handle a falsy version and gracefully display `No latest version found`, so this backend fix entirely resolves the UX issue without touching the frontend. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22885?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. --> |
||
|
|
0a53cc745d |
fix: bump brace-expansion 1.x/2.x lines (Dependabot) (#23240)
Bumps **brace-expansion 1.x -> 1.1.16** and **2.x -> 2.1.2** (caret consumer ranges permit both; recursive `yarn up`, lockfile-only, no resolution). Clears [1766](https://github.com/twentyhq/twenty/security/dependabot/1766) and [1767](https://github.com/twentyhq/twenty/security/dependabot/1767) for GHSA-3jxr-9vmj-r5cp (high, ReDoS). **Deliberately not covered:** the 5.x line stays at 5.0.6 because `nx@22.7.5` pins it exact (minimatch's `^5.0.x` consumers do lift to 5.0.7, but the nx copy remains), so alert [1765](https://github.com/twentyhq/twenty/security/dependabot/1765) stays open. Same nx-exact-pin situation as axios; will be handled in the resolutions batch. `yarn install --immutable` passes. 1.1.16 and 2.1.2 published 2026-07-08, clear the age gate. |
||
|
|
d6abb26cdb |
fix: bump body-parser to 1.20.6 / 2.3.0 (Dependabot) (#23239)
Bumps **body-parser** on both lines: `~1.20.3`/`~1.20.5` -> **1.20.6** and `^2.2.1` -> **2.3.0** (all within declared ranges; recursive `yarn up`, lockfile-only, no resolution). Clears [1779](https://github.com/twentyhq/twenty/security/dependabot/1779) and [1780](https://github.com/twentyhq/twenty/security/dependabot/1780) (GHSA-v422-hmwv-36x6, low). `yarn install --immutable` passes. 1.20.6 published 2026-07-09, 2.3.0 published 2026-06-15, both clear the age gate. |
||
|
|
78e0c11b34 |
fix: bump svgo to 3.3.4 (Dependabot) (#23238)
Bumps **svgo -> 3.3.4** (sole descriptor `^3.0.2`, caret already permits it; recursive `yarn up`, lockfile-only, no resolution). Clears [1802](https://github.com/twentyhq/twenty/security/dependabot/1802) (GHSA-2p49-hgcm-8545, high). `yarn install --immutable` passes. 3.3.4 published 2026-07-11, clears the age gate. |
||
|
|
35e6f58d70 |
fix: bump dompurify to 3.4.12 (Dependabot) (#23237)
Bumps **dompurify -> 3.4.12** (sole descriptor `^3.4.11`, caret already permits it; recursive `yarn up`, lockfile-only, no resolution). Clears [1801](https://github.com/twentyhq/twenty/security/dependabot/1801) (GHSA-c2j3-45gr-mqc4, low). `yarn install --immutable` passes. 3.4.12 published 2026-07-11, clears the age gate. |
||
|
|
b2a61adee7 |
fix: bump immutable to 5.1.8 (Dependabot) (#23235)
Bumps **immutable -> 5.1.8** (sole descriptor `^5.1.5`, caret already permits it; recursive `yarn up`, lockfile-only, no resolution). Clears [1796](https://github.com/twentyhq/twenty/security/dependabot/1796) (GHSA-v56q-mh7h-f735, high) and [1797](https://github.com/twentyhq/twenty/security/dependabot/1797) (GHSA-xvcm-6775-5m9r, high). `yarn install --immutable` passes. 5.1.8 published 2026-06-25, clears the age gate. |
||
|
|
5ee50b16db |
fix: bump fast-uri to 3.1.4 (Dependabot) (#23234)
Bumps **fast-uri -> 3.1.4** (sole descriptor `^3.0.1`, caret already permits it; recursive `yarn up`, lockfile-only, no resolution). Clears [1798](https://github.com/twentyhq/twenty/security/dependabot/1798) (GHSA-4c8g-83qw-93j6, high) and [1805](https://github.com/twentyhq/twenty/security/dependabot/1805) (GHSA-v2hh-gcrm-f6hx, high). `yarn install --immutable` passes. 3.1.4 published 2026-07-19, clears the 3-day age gate. |
||
|
|
148dc6dfaa |
Let server route resolvers answer the caller synchronously (#23233)
## Problem
A server route resolver can only return a dispatch target (`{
workspaceId, targetLogicFunctionUniversalIdentifier, payload }`), and
`ServerRouteTriggerService` always acks `202 {queued:true}`. The target
function runs off the queue, after the response has been sent, so its
return value can never reach the caller.
That makes it impossible to integrate a provider whose webhook URL has
to be proven with a handshake on the same response. Slack's Events API
is the case that surfaced it: `url_verification` sends `{ type,
challenge }` and will not accept the Request URL unless the challenge
comes back on that POST.
## Change
A resolver may now return a `Response` (the existing
`LogicFunctionHttpResponse`) instead of a dispatch target. The route
sends it as-is via `buildRouteTriggerResponse` and enqueues nothing.
- Reuses the marker and builder that HTTP route triggers already use, so
there is no new response shape.
- Dispatch results behave exactly as before; the resolver error path is
unchanged, just hoisted out of `parseResolverResult` so it runs before
the branch.
- SDK: `ServerRouteResolverResult` becomes `ServerRouteDispatchResult |
LogicFunctionHttpResponse`.
Additive: a resolver that returns a dispatch target sees no behavior
change. Previously, returning this shape threw
`RESOLVER_INVALID_RESULT`.
## Testing
`server-route-trigger.service.spec.ts` gains a case asserting the
resolver's response is sent verbatim and nothing is enqueued. 15/15
pass.
## Context
Split out of #22984 (Slack conversational assistant), which needs this
to complete the Slack Events URL verification. That PR depends on this
one merging first.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23233?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. -->
|
||
|
|
5bc97f8591 |
chore: sync AI model catalog from models.dev (#23242)
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev). This PR updates pricing, context windows, and model availability based on the latest data. New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically. Deprecated models are detected based on cost-efficiency within the same model family. **Please review before merging** — verify no critical models were incorrectly deprecated. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23242?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: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
6b99bcea7f |
feat(partners): require Twenty experience on apply, drop Cal success (#23223)
## Summary - Add a dedicated **Experience** step to the partner apply wizard (milestones, ≥200-char narrative, proof URL) before commercials - Stop collecting `applicationNotes`; rename Expertise chrome away from “experience” - Replace the post-submit Cal.com embed with a review-and-reach-out thank-you so unqualified inbound no longer books intros automatically **Companion PR (app):** #23224 — Partner schema, submit persistence, triage views, Tally CSV mapper (`twenty-partners` v1.4.0). Land the app PR with or before this one. ## Test plan - [ ] Open apply modal: wizard order is identity → profile → expertise → experience → commercials - [ ] Experience step blocks continue without ≥1 milestone, narrative ≥200 chars, and a valid https URL - [ ] Submit creates/updates Partner with the three experience fields (with #23224 deployed) - [ ] Success screen has no Cal embed / book-later CTA - [ ] `npx jest --config=jest.config.mjs partner-application` passes locally (71 tests) --------- Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com> |
||
|
|
34c2e11dcb |
i18n - translations (#23230)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23230?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> |
||
|
|
2c79093b74 |
feat: agent roleUniversalIdentifier for manifest-driven role assignment (#23206)
## Summary - Adds optional `roleUniversalIdentifier` on `AgentManifest` / `defineAgent` so apps can declaratively assign a role to an agent (same config shape as `defaultRoleUniversalIdentifier`). - Wires `agentUniversalIdentifier` as a sync many-to-one FK on `roleTarget`, and emits a deterministic `roleTarget` from the agent during app sync (create / update / delete). - Enables app agents (e.g. Slack assistant) to get a role on install without postInstall hooks or manual admin assignment. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23206?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. --> |
||
|
|
8b7ac02464 |
i18n - translations (#23226)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
18fb0946e6 |
v1.4.0 — Raise partner bar: Twenty experience fields + triage (#23224)
## Summary **Version:** `twenty-partners` **v1.4.0** (minor — new Partner fields + apply contract) - Add Partner fields `twentyExperience`, `twentyExperienceNotes`, `twentyExperienceProofLink` and persist them from `submit-partner-application` (≥200-char narrative at API boundary) - Surface Twenty experience on applications / validated / per-stage triage views and the Partner record side panel (drop empty Introduction from that panel) - Add pure Tally CSV match/map helpers (ops import script stays outside the repo) for backfilling existing partners by `partnerId` **Companion PR (website):** #23223 — Experience step on apply + thank-you without Cal. ## Test plan - [ ] `yarn twenty apply -r <remote>` on a workspace — Partner gains the three experience fields - [ ] Website apply (with #23223) persists milestones / notes / proof link on create and email-linked update - [ ] Applications + Validated views show experience columns; record side panel lists experience fields - [ ] `yarn lint` clean; `yarn test:unit` covers schema + map-tally helpers - [ ] After Tally campaign: dry-run then apply CSV import via local ops script under `~/twenty/docs/superpowers-specs/raise-bar-import/` |
||
|
|
59eead238d |
message list member backfill (#23176)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23176?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. --> |
||
|
|
97d2b52a71 |
fix(front): duplicate junction chip after Add New in relation picker (#23185)
Clicking Add New in a junction relation picker rendered the newly created target twice until a page reload. The handler appended the created junction to the source record's store field after awaiting the mutation, but useCreateOneRecord's post-optimistic effect had already attached it, so the same junction id ended up in the field array twice (single row in DB). Removes the redundant manual append in both to-many picker flows. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23185?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. --> |