1cd983a33073ef9c588ef8042be063aa06adda21
237 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9e94045fa5 |
feat(apps): generic OAuth provider support for app SDK (#20181)
## Summary
App developers can now declare third-party OAuth integrations (GitHub,
Linear, Slack, etc.) in their manifest and the platform handles the full
authorize → callback → token-exchange → refresh → injection lifecycle.
The dev writes ~10 lines of config and reads tokens via
`useOAuth('linear')` inside any logic function.
```ts
// app/src/oauth-providers/linear.ts
export default defineOAuthProvider({
universalIdentifier: '...',
name: 'linear',
displayName: 'Linear',
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
connectionMode: 'per-user',
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
tokenRequestContentType: 'form-urlencoded',
});
// app/src/logic-functions/handlers/...
const { accessToken } = useOAuth('linear'); // throws OAuthNotConnectedError if missing
```
## Architecture
- **Storage**: extends the existing `connectedAccount` table — new
nullable `applicationOAuthProviderId` FK + new `app` value on the
`ConnectedAccountProvider` enum. Existing Google/Microsoft flows are
untouched.
- **OAuth flow**: a single `/apps/oauth/authorize` +
`/apps/oauth/callback` controller pair handles every app provider. State
travels in a JWT signed via the existing `JwtWrapperService` (new
`APP_OAUTH_STATE` token type).
- **Token exchange**: goes through
`SecureHttpClientService.createSsrfSafeFetch()` (so an installed app
can't point `tokenEndpoint` at internal hosts).
- **Refresh**: piggybacks on the existing
`ConnectedAccountRefreshTokensService` dispatch — Google/Microsoft
drivers untouched, new app driver lives engine-side under
`application-oauth-provider/refresh/`.
- **Injection**: the executor injects refreshed tokens as env vars
(`OAUTH_<NAME>_ACCESS_TOKEN`, `_HANDLE`, `_SCOPES`, `_CONNECTED`); the
SDK helpers `useOAuth` / `useOptionalOAuth` read them.
- **Frontend**: auto-rendered "OAuth Connections" section under each
app's settings tab (no custom front component needed). App-managed
connections are filtered out of `/settings/accounts` so the
email/calendar page stays focused.
- **Disconnect**: best-effort revoke against the manifest's
`revokeEndpoint` before deleting the row.
## Reference app
`packages/twenty-apps/internal/twenty-linear/` exercises the full
pipeline:
- `defineOAuthProvider` for Linear
- `POST /linear/create-issue` and `GET /linear/teams` HTTP-route logic
functions
- Vitest tests for the handlers
## Tests
- 14 server-side Jest tests: token-exchange util (form-urlencoded vs
JSON, PKCE, error paths), flow service (authorize URL shape, state
binding, ConnectedAccount upsert on first/reconnect, per-workspace mode,
invalid state)
- 8 app-level Vitest tests: handler error paths, GraphQL request shape,
Linear error propagation
- All 4 packages clean: `npx nx lint:diff-with-main` and `npx tsc
--noEmit`
## Test plan
- [ ] Apply migration on a dev DB: `npx nx run
twenty-server:database:migrate:prod`
- [ ] Regenerate frontend types: `npx nx run
twenty-front:graphql:generate --configuration=metadata`
- [ ] Create a Linear OAuth app at
https://linear.app/settings/api/applications/new with redirect URI
`<SERVER_URL>/apps/oauth/callback`
- [ ] Deploy + install `twenty-linear` on a workspace, paste the Linear
client id/secret into the app's variables
- [ ] Click "Connect Linear" in the app's settings tab → complete OAuth
→ verify `connectedAccount` row created with `provider = 'app'`
- [ ] Trigger `POST /linear/create-issue` with a valid teamId → verify
issue lands in Linear
- [ ] Disconnect → verify the row is deleted and (if Linear's revoke
endpoint is configured in the manifest) the revoke call fires
- [ ] Verify `/settings/accounts` does NOT show the Linear connection —
it appears only under the Linear app's settings tab
## Out of scope (deliberately)
- **Cron + per-user providers**: a cron-triggered function with a
per-user OAuth provider currently returns `CONNECTED=false` (no user
context). The follow-up design is `useOAuthForUser(name,
userWorkspaceId)` paired with a `POST /apps/oauth/connection-token`
endpoint, deferred to keep this PR focused.
- **Token encryption at rest**: tokens stored as plain `varchar`
matching the existing Google/Microsoft pattern. Worth a separate
cross-cutting PR.
- **Manifest endpoint pinning**: a malicious app upgrade could change
`tokenEndpoint` silently. Same trust model as logic-function source code
(which already runs arbitrary server-side); worth tightening across the
whole upgrade pipeline rather than just OAuth.
- **CLI helpers** (`twenty oauth show-callback-url`, `twenty oauth
connect`): manual setup for v1.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
|
||
|
|
c8e405cb4e |
Add twenty sdk server upgrade command (#20158)
## The command pulls the image, compares it against the one the container was created from, and only recreates the container if the image actually changed. Your data volumes are preserved — only the container is replaced. |
||
|
|
bddd23fd9c |
Fix application icons (#20142)
fixes application chip (icon Name) in all setting tables ## After <img width="1200" height="896" alt="image" src="https://github.com/user-attachments/assets/bd377f47-1d52-4142-b904-f2ce90c1db78" /> <img width="1200" height="917" alt="image" src="https://github.com/user-attachments/assets/f49cc742-f11e-47e3-86ed-34beffe493c7" /> <img width="1234" height="878" alt="image" src="https://github.com/user-attachments/assets/2ab459de-5f9d-4d39-9490-eec4ed9ee432" /> <img width="1239" height="845" alt="image" src="https://github.com/user-attachments/assets/3c1bf258-285a-47b9-a60d-05ba1564334d" /> <img width="1183" height="907" alt="image" src="https://github.com/user-attachments/assets/715b2470-2d88-48e3-88ac-d3daf3451717" /> <img width="1300" height="912" alt="image" src="https://github.com/user-attachments/assets/d7c829fa-bf1d-4f19-82de-a8bf29e22bfa" /> |
||
|
|
d271ba06f1 |
chore: use navigation menu item type into the generated navigation menu layout (#20099)
This is a small PR to fix an issue that came up when I created an object with the default navigation menu item. <img width="811" height="461" alt="image" src="https://github.com/user-attachments/assets/70c7f55d-3d41-48ed-8cf9-5f649ea41f49" /> |
||
|
|
e3a668a054 |
chore: export the enum view calendar layout to avoid typing errors on apps (#20090)
This is a small PR to fix an issue that came up when I created an app with a custom object. I think the enum `ViewCalendarLayout` was not caught as part of the exports. <img width="1624" height="1114" alt="image" src="https://github.com/user-attachments/assets/7bca3d0d-0e68-48ef-ac5f-6a3375733499" /> |
||
|
|
0bb3660844 |
chore(twenty-sdk): shrink logic-function bundles via stubbing (#20033)
## Summary
Logic-function bundles produced by the SDK CLI were ~1.2 MB each (source
maps ~3.1 MB) because esbuild was inlining `twenty-sdk/define` and its
transitive dependencies (zod + locales, twenty-shared, etc.). Those
`define*` factories are pure build-time metadata used only by the
manifest extractor — the Lambda runtime only ever invokes
`default.config.handler`, so the factories are dead weight at runtime.
This PR shrinks the bundles to ~9.5 KB each (~99% reduction) without
changing runtime behaviour.
## What changes
- **Stub `twenty-sdk/define` at user-app build time.** New esbuild
plugin
(`packages/twenty-sdk/src/cli/utilities/build/common/plugins/stub-twenty-sdk-define.plugin.ts`)
intercepts every import of `twenty-sdk/define` during user-app builds
and replaces it with a tiny virtual module:
- Factory functions (`defineLogicFunction`,
`definePostInstallLogicFunction`, …) become `(config) => ({ success:
true, config, errors: [] })`.
- Enums and helpers become `Proxy`-based no-ops.
- Wired into both the one-shot build (`build-application.ts`) and the
watcher (`esbuild-watcher.ts`), for logic functions and front
components.
- **New runtime barrel `twenty-sdk/logic-function`.** Re-exports only
the types logic-function authors need (`InstallPayload`, `RoutePayload`,
`CronPayload`, `DatabaseEventPayload`, `LogicFunctionConfig`,
`InputJsonSchema`, …). Compiled `.mjs` is 36 bytes. Wired into Vite,
Rollup `.d.ts` bundling, `package.json#exports`, and `typesVersions`.
- **Lint enforcement.** Added an oxlint `no-restricted-imports` rule
that forbids `twenty-shared` / `twenty-shared/*` imports from
`**/*.logic-function.ts` and `**/logic-functions/**/*.ts`, with a help
message pointing at the new barrel. Applied to the `create-twenty-app`
template and to `github-connector`, `hello-world`, `postcard`.
- **Migrated existing sources.** All logic-function files across
`community/{github-connector, apollo-enrich}`, `examples/{hello-world,
postcard}`, and `internal/{twenty-for-twenty, self-hosting, exa}` now
import types from `twenty-sdk/logic-function` instead of
`twenty-sdk/define` or `twenty-shared/*`. Renamed leftover
`InstallLogicFunctionPayload` references to `InstallPayload`.
## Why this is safe
- `define*` exports from `twenty-sdk/define` are metadata factories
whose call expressions are statically inspected by the manifest
extractor (`manifest-extract-config.ts`). They're never evaluated at
runtime — the Lambda executor only walks `default.config.handler`
(`logic-function-drivers/constants/executor/index.mjs`).
- The stub keeps the same call shape (`{ success, config, errors }`), so
any logic-function module that re-exports
`defineX(config).config.handler` still resolves to the user's handler at
runtime.
- Front-component bundles are unaffected by the stub because the
pre-existing JSX transform plugin
(`jsx-transform-to-remote-dom-worker-format-plugin.ts`) unwraps
`defineFrontComponent(...)` earlier in the pipeline. That's intentional
— front-component bloat is React/Preact, not in scope here.
## Measurements (github-connector)
| Asset | Before | After |
|---|---|---|
| `*.logic-function.mjs` | ~1.2 MB | ~9.5 KB |
| `*.logic-function.mjs.map` | ~3.1 MB | ~22 KB |
|
||
|
|
2ccc293f99 |
Gate export/import command menu items by permission flag (#19991)
## Summary - Hides the `exportRecords`, `exportView`, and `importRecords` command menu actions from users whose role does not hold the matching `EXPORT_CSV` / `IMPORT_CSV` permission flag. - Exposes the current user's role permission flags to `conditionalAvailabilityExpression` by adding `permissionFlags: Record<string, boolean>` to `CommandMenuContextApi`, mirroring how `featureFlags` is already accessible. - Adds a `2.1.0` workspace upgrade command that rewrites the three existing rows on every active/suspended workspace. ## Before <img width="1294" height="287" alt="Screenshot 2026-04-22 at 19 37 40" src="https://github.com/user-attachments/assets/11ca8635-14d7-40a0-9ca0-76329c54e3c6" /> ## After <img width="1283" height="285" alt="Screenshot 2026-04-22 at 19 32 25" src="https://github.com/user-attachments/assets/5e49fa8a-4541-42ee-96da-4c1de7d00aae" /> |
||
|
|
097432d3a2 |
[Command Menu] Refactor layout customization conditional availability [Warning] (#19974)
closes https://discord.com/channels/1130383047699738754/1494312529286004837 |
||
|
|
4f938aa097 |
feat(app): infrastructure for pre-installed apps (#19973)
**PR 1 of 2.** Follow-up PR ships the Exa app, sets it as a default
pre-installed app, and removes the current `WebSearchTool` /
`WebSearchService` / `ExaDriver`. This PR adds the plumbing; no
user-visible change yet.
## Summary
- Server admins can declare a list of npm app packages to auto-install
on every new workspace and backfill onto existing workspaces via CLI.
- Server-level secrets (like Exa's API key) live on the
`ApplicationRegistration` (one row per server, encrypted) and are
injected into logic function execution env at runtime. No more
per-workspace storage of global secrets.
- A generic `POST /app/billing/charge` endpoint lets app logic functions
emit workspace usage events for metered features. Exa uses it in PR 2;
future apps (call recorder, etc.) reuse it.
- `LogicFunctionToolProvider` tool name prefix changes `logic_function_`
→ `app_`. Shorter, accurate (they come from installed apps).
## What's in this PR
**Logic function executor — server-level variables**
- `LogicFunctionExecutorService.getExecutionEnvVariables` now resolves
env vars in the order: hardcoded defaults →
`ApplicationRegistrationVariable[]` (server-level) →
`ApplicationVariable[]` (workspace-level override). The manifest
`serverVariables` schema has existed; this closes the loop.
**Config**
- `PRE_INSTALLED_APPS` — comma-separated list of npm packages. Default:
empty.
**\`PreInstalledAppsService\`** (new module)
- \`onApplicationBootstrap()\` — fetches each package's manifest from
the app registry CDN, upserts an \`ApplicationRegistration\`, and seeds
declared \`serverVariables\` from matching env vars (e.g.
\`EXA_API_KEY\` env → encrypted registration variable).
- \`installOnWorkspace(workspaceId)\` — installs all pre-installed apps
on a single workspace. Tolerates per-app failures.
**Auto-install on new workspace activation**
- \`WorkspaceService.prefillCreatedWorkspaceRecords\` invokes
\`installOnWorkspace\` after prefilling standard records. Non-blocking
on failure.
**Backfill CLI command**
- \`install-pre-installed-apps\` — iterates active and suspended
workspaces, installs pre-installed apps that aren't yet installed.
Idempotent. Run after changing \`PRE_INSTALLED_APPS\`.
**App billing endpoint**
- \`POST /app/billing/charge\`. Authenticated via \`APPLICATION_ACCESS\`
token (already injected into logic function execution env as
\`DEFAULT_APP_ACCESS_TOKEN\`). Body: \`{ creditsUsedMicro, quantity,
unit, operationType, resourceContext? }\`. Emits \`USAGE_RECORDED\` with
\`applicationId\` as \`resourceId\`. Generic — reusable by any app.
**Tool name prefix**
- \`LogicFunctionToolProvider.buildLogicFunctionToolName\` now produces
\`app_<name>\` instead of \`logic_function_<name>\`. Only affects tools
sourced from logic functions; other tool providers unchanged.
## Stats
- 16 files, +501 / −2
- 7 new files (1 command, 1 service × 2, 1 controller, 1 DTO, 2 modules)
- Typecheck: 7 pre-existing errors, zero new
- Prettier clean
## Behavior deltas
- **\`PRE_INSTALLED_APPS\` default = empty**: existing servers see no
change on merge.
- **\`ApplicationRegistrationVariable\` is now read by the executor**:
apps that were using manifest \`serverVariables\` but expecting them to
be ignored by the executor will now see them injected. No apps ship with
\`isTool: true\` logic functions today, so this is latent — first
consumer is Exa in PR 2.
- **Tool prefix**: currently no logic-function tools are named
\`logic_function_*\` in any production flow. The prefix change affects
only future tools emitted by \`LogicFunctionToolProvider\`.
## Risks
- **CDN unavailability at startup**: if the app registry CDN is down,
\`ensureRegistrationsExist\` logs warnings but doesn't block server
start. Installation on new workspaces during this window will find no
registrations and log a non-blocking error. Backfill command can retry
after CDN recovers.
- **Cold-start overhead**: \`ensureRegistrationsExist\` is called once
per process on bootstrap. Current configurable default is empty, so zero
overhead. When an admin sets \`PRE_INSTALLED_APPS\`, they accept one
HTTP call per package at boot.
- **Server-level variables flow**:
\`ApplicationRegistrationVariable.encryptedValue\` is shared by all
workspaces of a server. Appropriate for a single-tenant Exa key. Not
appropriate for per-tenant keys — those go in workspace-level
\`ApplicationVariable\` and override.
## Test plan
- [ ] \`npx nx typecheck twenty-server\` passes (verified: 7
pre-existing unrelated errors, zero new)
- [ ] Set \`PRE_INSTALLED_APPS=@twenty-apps/hello-world\` (or any real
npm-published app), \`HELLO_WORLD_API_KEY=xxx\`, restart server:
\`ApplicationRegistration\` row is upserted,
\`ApplicationRegistrationVariable\` for HELLO_WORLD_API_KEY is populated
(encrypted).
- [ ] Create a new workspace: the app is auto-installed,
\`ApplicationEntity\` row created, \`LogicFunctionEntity\` rows created.
- [ ] Existing workspace: run \`yarn nx run twenty-server:command
install-pre-installed-apps\`: apps install across all workspaces,
idempotent on re-run.
- [ ] Trigger a logic function that reads
\`process.env.HELLO_WORLD_API_KEY\`: value resolves from the
server-level \`ApplicationRegistrationVariable\`.
- [ ] Log a charge from the handler: \`POST /app/billing/charge\` with
\`Authorization: Bearer \$DEFAULT_APP_ACCESS_TOKEN\` body
\`{creditsUsedMicro: 1000, quantity: 1, unit: "INVOCATION",
operationType: "WEB_SEARCH"}\` → returns \`{success: true}\`,
\`USAGE_RECORDED\` event emitted with correct
\`resourceId=applicationId\`.
- [ ] Tool name generated by \`LogicFunctionToolProvider\` starts with
\`app_\`.
## What's NOT in this PR (PR 2 scope)
- The Exa app itself (\`packages/twenty-apps/...\` directory)
- Removing \`WebSearchTool\`, \`WebSearchService\`, \`ExaDriver\`,
\`web-search\` module
- Removing \`WEB_SEARCH_DRIVER\` config var
- Removing the current \`exa_web_search\` entry in
\`ActionToolProvider\`
- Chat preload list updated to \`app_exa_web_search\`
- Frontend \`getToolDisplayMessage\` branch for \`app_exa_web_search\`
- Setting \`PRE_INSTALLED_APPS\` default to include \`@twenty-apps/exa\`
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a445f4a6fa |
feat(sdk): add definePageLayoutTab for extending existing page layouts (#20004)
## Summary
Introduces `definePageLayoutTab` so apps can attach a single tab (with
optional widgets) to an **existing** `pageLayout` referenced by
`pageLayoutUniversalIdentifier`. The parent layout can be standard, from
the same app, or from another app — mirroring how `defineField`
references an object via `objectUniversalIdentifier`.
This complements `definePageLayout`: use `definePageLayout` when you own
the entire layout, use `definePageLayoutTab` when you only want to add
to one.
```ts
import { definePageLayoutTab, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
export default definePageLayoutTab({
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
pageLayoutUniversalIdentifier: 'STANDARD-OR-OTHER-APP-PAGE-LAYOUT-UUID',
title: 'Hello World',
position: 1000,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [/* ... */],
});
```
## Changes
- **twenty-shared**: new top-level `pageLayoutTabs:
PageLayoutTabManifest[]` on `Manifest`, optional
`pageLayoutUniversalIdentifier` on `PageLayoutTabManifest`, new
`SyncableEntity.PageLayoutTab`.
- **twenty-sdk**:
- new `definePageLayoutTab` + `PageLayoutTabConfig` exports;
- manifest extraction wiring (`TargetFunction.DefinePageLayoutTab`,
`ManifestEntityKey.PageLayoutTabs`);
- dev-mode label/state for the new entity;
- CLI scaffold (`getPageLayoutTabBaseFile`) + unit tests for `npx
twenty-cli add`.
- **twenty-server**: convert top-level `pageLayoutTabs` (and their
widgets) into universal flat entities in
`computeApplicationManifestAllUniversalFlatEntityMaps`. Cross-app FK
validation on `pageLayoutUniversalIdentifier` is already handled by the
existing `FlatPageLayoutTab` validator.
- **docs**: new `definePageLayoutTab` accordion in `apps/layout.mdx`
with usage example and guidance vs `definePageLayout`.
- **CI / rich-app fixture**: `extra-tab.page-layout-tab.ts` exercises
the new flow with a front-component widget; `expected-manifest.ts` and
`manifest.tests.ts` updated.
|
||
|
|
876214bc1d |
scaffold record page layout + fields view when adding an object (#19977)
## Summary
Extends `yarn twenty add` → **Object** so it scaffolds a complete record
page out
of the box:
- A **record-page-fields view** (`<name>-record-page-fields.ts`,
FIELDS_WIDGET)
pre-populated with the `name` field plus the auto-generated default
fields (`createdAt`,
`updatedAt`, `createdBy`, `updatedBy`) — the default-field entries are
emitted as
`generateDefaultFieldUniversalIdentifier({ objectUniversalIdentifier,
fieldName: '...' })`
calls rather than pre-computed UUIDs, so the generated file
double-serves as
documentation for the public util.
- A **record page layout** (`<name>-record-page-layout.ts`) with a Home
tab whose Fields
widget points at the new view (via `viewUniversalIdentifier`), plus a
Timeline tab.
- The companion prompt now covers all three artefacts (was view + nav
menu item).
Fix: Server-side, renames `viewId` → `viewUniversalIdentifier` on the
universal-flat FIELDS
widget configuration so it is consistent with other universal-flat
references. The DB-side
DTO keeps `viewId` (now typed as `SerializedRelation`), and the
conversion utils map
between the two.
<img width="337" height="349" alt="Screenshot 2026-04-22 at 15 40 22"
src="https://github.com/user-attachments/assets/59e36540-1761-46b0-808d-648c68604268"
/>
|
||
|
|
b010599000 |
fix(server): preserve kanban/calendar fields in view manifest sync (#19946)
## Summary The `fromViewManifestToUniversalFlatView` converter hardcoded five view fields to `null` instead of reading them from the manifest: - `mainGroupByFieldMetadataUniversalIdentifier` - `kanbanAggregateOperation` - `kanbanAggregateOperationFieldMetadataUniversalIdentifier` - `calendarLayout` - `calendarFieldMetadataUniversalIdentifier` As a result **any** Kanban view in an app manifest is rejected by `validateFlatViewCreation` with `"Kanban view must have a main group by field"`, and any Calendar view would trip the `view.entity.ts` check constraint requiring `calendarLayout` + `calendarFieldMetadataId` to be non-null. Discovered while trying to install [`twenty-crm-meeting-baas`](https://github.com/Meeting-BaaS/twenty-crm-meeting-baas) which ships a Kanban view. ## Changes - **Server converter**: read all five fields from the manifest (with `?? null` fallback). - **`ViewManifest` type** (`twenty-shared`): add the five fields so SDK users can set them type-safely. - **Move `ViewCalendarLayout`** from `twenty-server` to `twenty-shared` so the manifest type can reference it. Seven import sites updated; the front-end imports via generated GraphQL types and is unaffected. - **Unit tests**: extend `from-view-manifest-to-universal-flat-view.util.spec.ts` with preservation + null-default cases for both Kanban and Calendar (5 tests total). - **Regression coverage**: add a Kanban view (`post-cards-by-status.view.ts`) to the `rich-app` fixture grouped by the existing `status` SELECT field. The existing `applications-install-delete-reinstall` e2e test now exercises the Kanban path end-to-end — a future regression here would fail CI. Note: `expected-manifest.ts` and the `views.length` assertion in `manifest.tests.ts` were updated to reflect the new fixture view. ## Test plan - [x] `nx test twenty-server -- from-view-manifest-to-universal-flat-view` → 5/5 pass - [x] `nx typecheck twenty-shared` / `twenty-sdk` / `twenty-server` → no new errors (one pre-existing unrelated error in `admin-panel.module-factory.ts`) - [x] `nx lint twenty-shared` / `twenty-sdk` → clean - [x] Manual install of the Meeting BaaS app on a dev workspace succeeds with the Kanban view after this fix - [ ] CI: SDK e2e `applications-install-delete-reinstall` passes against the new fixture view - [ ] CI: integration test `calendar-field-deactivation-deletes-views` still passes after the enum move 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
c2cf3eac50 |
feat(sdk): confirm authentication method on remote add (#19947)
## Summary `yarn twenty remote add` only prints `✓ Default remote set to X.` after authenticating. When using the OAuth path, the browser flow happens silently — there's no line that says _"you authenticated"_ — so users (including me this morning while installing a Twenty app) are left wondering whether auth actually completed and which method was used. This PR adds explicit confirmation of the auth step: **New remote via OAuth** ``` ✓ Remote "myremote" added (https://app.twenty.com) via OAuth. ✓ Default remote set to "myremote". ``` **New remote via API key** ``` ✓ Remote "myremote" added (https://app.twenty.com) via API key. ✓ Default remote set to "myremote". ``` **Re-authenticating an existing remote** ``` ✓ Re-authenticated "myremote" via OAuth. ✓ Default remote set to "myremote". ``` ## Implementation - `authenticate()` now returns the method actually used (`'OAuth' | 'API key'`) instead of `void`. This correctly surfaces OAuth → API-key fallback: if OAuth fails and the user drops into the API-key prompt, the success line reflects that. - New-remote and re-auth paths print distinct messages so the user can tell which path they took. - No new API calls — method name comes from which branch of `authenticate()` succeeded. ## Test plan - [x] `nx typecheck twenty-sdk` — clean - [x] `nx lint twenty-sdk` — clean - [ ] Manual smoke test: `yarn twenty remote add --as test --api-url ...` via OAuth, API key, and re-auth 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
76582dc04a |
Export generateDefaultFieldUniversalIdentifier from SDK (#19937)
## Context
- Moved generateDefaultFieldUniversalIdentifier from the internal
cli/utilities/build/manifest/utils/ folder to sdk/define/objects/ and
re-exported it from
the public twenty-sdk/define entry point, so app authors can compute the
deterministic UID
of a default field (id, name, createdAt, …) from their own code.
- Narrowed the signature from { objectConfig: ObjectConfig; fieldName }
to {
objectUniversalIdentifier: string; fieldName: string } — the only thing
the helper needs,
and what app code has on hand.
- Updated the two internal callers and the spec to the new import path
and signature.
```typescript
import {
defineView,
generateDefaultFieldUniversalIdentifier,
} from "twenty-sdk/define";
export default defineView({
universalIdentifier: "70f10d44-144a-4da8-8c6f-3ec2422138c0",
name: "all-thing",
objectUniversalIdentifier: "c782b61c-70fd-4c88-9cd6-4e61ab8d7591",
icon: "IconList",
position: 0,
fields: [
{
universalIdentifier: "75a90bc4-d901-4df4-85e0-af29db5e0104",
fieldMetadataUniversalIdentifier: generateDefaultFieldUniversalIdentifier(
{
objectUniversalIdentifier: "c782b61c-70fd-4c88-9cd6-4e61ab8d7591",
fieldName: "createdAt",
},
),
position: 0,
isVisible: true,
size: 200,
},
{
universalIdentifier: "75a90bc5-d901-4df4-85e0-af29db5e0105",
fieldMetadataUniversalIdentifier: generateDefaultFieldUniversalIdentifier(
{
objectUniversalIdentifier: "c782b61c-70fd-4c88-9cd6-4e61ab8d7591",
fieldName: "createdBy",
},
),
position: 0,
isVisible: true,
size: 200,
},
],
});
```
|
||
|
|
30b8663a74 |
chore: remove IS_AI_ENABLED feature flag (#19916)
## Summary - AI is now GA, so the public/lab `IS_AI_ENABLED` flag is removed from `FeatureFlagKey`, the public flag catalog, and the dev seeder. - Drops every backend `@RequireFeatureFlag(IS_AI_ENABLED)` guard (agent, agent chat, chat subscription, role-to-agent assignment, workflow AI step creation) and the now-unused `FeatureFlagModule`/`FeatureFlagGuard` wiring in the AI and workflow modules. - Removes frontend gating from settings nav, role permissions/assignment/applicability, command menu hotkeys, side panel, mobile/drawer nav, and the agent chat provider so AI UI is always on. Tests and generated GraphQL/SDK schemas updated accordingly. ## Test plan - [x] `npx nx typecheck twenty-shared` - [x] `npx nx typecheck twenty-server` - [x] `npx nx typecheck twenty-front` - [x] `npx nx lint:diff-with-main twenty-server` - [x] `npx nx lint:diff-with-main twenty-front` - [x] `npx jest --config=packages/twenty-server/jest.config.mjs feature-flag` - [x] `npx jest --config=packages/twenty-server/jest.config.mjs workspace-entity-manager` - [ ] Manual smoke test: AI features still accessible without any flag row in `featureFlag` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
10c49a49c4 |
feat(sdk): support viewSorts in app manifests (#19881)
## Summary
Today the SDK lets apps declare `filters` on a view but not `sorts`, so
any view installed via an app manifest can never have a default
ordering. This PR adds declarative view sorts end-to-end: SDK manifest
type, `defineView` validation, CLI scaffold, and the application
install/sync pipeline that converts the manifest into the universal flat
entity used by workspace migrations. The persistence layer
(`ViewSortEntity`, resolvers, action handlers, builders…) already
existed server-side; the missing piece was the manifest → universal-flat
converter and the relation wiring on `view`.
## Changes
**`twenty-shared`**
- Add `ViewSortDirection` enum (`ASC` | `DESC`) and re-export it from
`twenty-shared/types`.
- Add `ViewSortManifest` type and an optional `sorts?:
ViewSortManifest[]` on `ViewManifest`, exported from
`twenty-shared/application`.
**`twenty-sdk`**
- Validate `sorts` entries in `defineView` (`universalIdentifier`,
`fieldMetadataUniversalIdentifier`, `direction` ∈ `ASC`/`DESC`).
- Add a commented `// sorts: [ ... ]` example to the CLI view scaffold
template + matching snapshot assertion.
**`twenty-server`**
- Re-export `ViewSortDirection` from `twenty-shared/types` in
`view-sort/enums/view-sort-direction.ts` (single source of truth,
backward compatible for existing imports).
- New converter `fromViewSortManifestToUniversalFlatViewSort` (+ unit
tests for `ASC` and `DESC`).
- Wire the converter into
`computeApplicationManifestAllUniversalFlatEntityMaps` so
`viewManifest.sorts` are added to `flatViewSortMaps`, mirroring how
filters are processed.
- Replace the `// @ts-expect-error TODO migrate viewSort to v2 /
viewSorts: null` placeholder in `ALL_ONE_TO_MANY_METADATA_RELATIONS`
with the proper relation (`viewSortIds` /
`viewSortUniversalIdentifiers`).
- Update affected snapshots (`get-metadata-related-metadata-names`,
`all-universal-flat-entity-foreign-key-aggregator-properties`).
## Example usage
\`\`\`ts
defineView({
name: 'All issues',
objectUniversalIdentifier: 'issue',
sorts: [
{
universalIdentifier: 'all-issues__sort-created-at',
fieldMetadataUniversalIdentifier: 'createdAt',
direction: 'DESC',
},
],
});
\`\`\`
|
||
|
|
eb1ca1b9ec |
perf(sdk): split twenty-sdk barrel into per-purpose subpaths to cut logic-function bundle ~700x (#19834)
## Summary
Logic-function bundles produced by the twenty-sdk CLI were ~1.18 MB even
for a one-line handler. Root cause: the SDK shipped as a single bundled
barrel (`twenty-sdk` → `dist/index.mjs`) that co-mingled server-side
definition factories with the front-component runtime, validation (zod),
and React. With no `\"sideEffects\"` declaration on the SDK package,
esbuild had to assume every module-level statement could have side
effects and refused to drop unused code.
This PR restructures the SDK so consumers' bundlers can tree-shake at
the leaf level:
- **Reorganized SDK source.** All server-side definition factories now
live under `src/sdk/define/` (agents, application, fields,
logic-functions, objects, page-layouts, roles, skills, views,
navigation-menu-items, etc.). All front-component runtime
(components, hooks, host APIs, command primitives) lives under
`src/sdk/front-component/`. The legacy bare `src/sdk/index.ts` is
removed; the bare `twenty-sdk` entry no longer exists.
- **Split the build configs by purpose / runtime env.** Replaced
`vite.config.sdk.ts` with two purpose-specific configs:
- `vite.config.define.ts` — node target, externals from package
`dependencies`, emits to `dist/define/**`
- `vite.config.front-component.ts` — browser/React target, emits to
`dist/front-component/**`
Both use `preserveModules: true` so each leaf ships as its own `.mjs`.
- **\`\"sideEffects\": false\`** on `twenty-sdk` so esbuild can drop
unreferenced re-exports.
- **\`package.json\` exports + \`typesVersions\`** updated: dropped the
bare \`.\` entry, added \`./front-component\`, and pointed \`./define\`
at the new per-module dist layout.
- **Migrated every internal/example/community app** to the new subpath
imports (`twenty-sdk/define`, `twenty-sdk/front-component`,
`twenty-sdk/ui`).
- **Added \`bundle-investigation\` internal app** that reproduces the
bundle bloat and demonstrates the fix.
- Cleaned up dead \`twenty-sdk/dist/sdk/...\` references in the
front-component story builder, the call-recording app, and the SDK
tsconfig.
## Bundle size impact
Measured with esbuild using the same options as the SDK CLI
(\`packages/twenty-apps/internal/bundle-investigation\`):
| Variant | Imports | Before | After |
| ----------------------- |
------------------------------------------------------- | ---------- |
--------- |
| \`01-bare\` | \`defineLogicFunction\` from \`twenty-sdk/define\` |
1177 KB | **1.6 KB** |
| \`02-with-sdk-client\` | + \`CoreApiClient\` from
\`twenty-client-sdk/core\` | 1177 KB | **1.9 KB** |
| \`03-fetch-issues\` | + GitHub GraphQL fetch + JWT signing + 2
mutations | 1181 KB | **5.8 KB** |
| \`05-via-define-subpath\` | same as \`01\`, via the public subpath |
1177 KB | **1.7 KB** |
That's a ~735× reduction on the bare baseline. Knock-on benefits for
Lambda warm + cold starts, S3 upload size, and \`/tmp\` disk usage in
warm containers.
## Test plan
- [x] \`npx nx run twenty-sdk:build\` succeeds
- [x] \`npx nx run twenty-sdk:typecheck\` passes
- [x] \`npx nx run twenty-sdk:test:unit\` passes (31 files / 257 tests)
- [x] \`npx nx run-many -t typecheck
--projects=twenty-front,twenty-server,twenty-front-component-renderer,twenty-sdk,twenty-shared,bundle-investigation\`
passes
- [x] \`node
packages/twenty-apps/internal/bundle-investigation/scripts/build-variants.mjs\`
produces the sizes above
- [ ] CI green
Made with [Cursor](https://cursor.com)
|
||
|
|
76ea0f37ed |
Surface structured validation errors during application install (#19787)
## Summary - Add `WorkspaceMigrationGraphqlApiExceptionInterceptor` to `MarketplaceResolver` and `ApplicationInstallResolver` so validation failures during app install return `METADATA_VALIDATION_FAILED` with structured `extensions.errors` instead of generic `INTERNAL_SERVER_ERROR` - Update SDK `installTarballApp()` to pass the full GraphQL error object (including extensions) through the install flow - Add `formatInstallValidationErrors` utility to format structured validation errors for CLI output - Add integration test verifying structured error responses for invalid navigation menu items and view fields --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
3463ee5dc4 |
Switch default remote to lastly added remote (#19697)
as title |
||
|
|
a88d1f4442 |
Introduce standalone page (#19675)
Add support for standalone pages: a new `PageLayout` type (`STANDALONE_PAGE`) that can be rendered independently at `/page/:pageLayoutId`, not tied to any record or object context. - New `STANDALONE_PAGE` page layout type - New `PAGE_LAYOUT` navigation menu item type: adds a `pageLayoutId` foreign key to `NavigationMenuItemEntity`, allowing sidebar items to link directly to standalone pages - New `GLOBAL_OBJECT_CONTEXT` command menu availability type: separates object-context-dependent commands (Create Record, Import, Export, See Deleted, Create View, Hide Deleted) from truly global ones, so standalone pages only show relevant commands - Frontend routing & rendering: adds a `/page/:pageLayoutId` route with its own page component, header, and command menu - Widget rendering refactor - Instance commands: two fast 1.22 migrations: `pageLayoutId` column + `STANDALONE_PAGE` enum, and `GLOBAL_OBJECT_CONTEXT` availability type enum - Workspace command: backfills existing command menu items from `GLOBAL` to `GLOBAL_OBJECT_CONTEXT` where appropriate - Dev seeds: adds a sample "Star History" standalone page with an iframe widget for local development |
||
|
|
63806b24fe |
Export field settings types from SDK public API (#19611)
## Summary - Follows up on #19610 which exported view/page-layout types but missed field settings types - Adds re-exports for `DateDisplayFormat`, `NumberDataType`, and `FieldMetadataSettingsOnClickAction` from the SDK public API - These enums are referenced in the return type of `defineObject` (via field settings types like `FieldMetadataSettingsDate`, `FieldMetadataSettingsNumber`, `FieldMetadataSettingsMultiItem`) but were not publicly exported - This causes TS4082 ("private name") errors for SDK consumers that have `declaration: true` in their tsconfig, specifically when using `defineObject` with fields that have settings (e.g. SELECT, DATE, NUMBER fields) |
||
|
|
7540bb064f |
Re-export missing types from SDK public API (#19610)
## Summary - Fixes TS4082 errors for SDK consumers with `declaration: true` in their tsconfig - The `define*` functions (e.g. `defineView`, `definePageLayout`, `defineLogicFunction`) return `ValidationResult<T>` where `T` references types from `twenty-shared` that were not re-exported from the SDK's public barrel - TypeScript cannot generate `.d.ts` files when the return type references "private names" — types that exist in the package but aren't publicly exported ### Added re-exports **Enums** (used in `ViewManifest`, `HttpRouteTriggerSettings`): - `ViewType`, `ViewFilterOperand`, `ViewFilterGroupLogicalOperator`, `ViewOpenRecordIn`, `ViewVisibility` - `HTTPMethod` **Types** (used in `PageLayoutWidgetManifest`, `LogicFunctionConfig`): - `GridPosition`, `PageLayoutWidgetConditionalDisplay` - `InputJsonSchema` |
||
|
|
ad1a4ecca0 |
Add isUnique support for application-defined fields (#19609)
## Summary - Adds `isUnique?: boolean` to `RegularFieldManifest` in `twenty-shared`, allowing SDK applications to declare unique constraints on fields - Updates the manifest-to-flat-field converter to read `isUnique` from the manifest instead of hardcoding `false` - Generates corresponding unique index metadata in `computeApplicationManifestAllUniversalFlatEntityMaps` when a field has `isUnique: true`, matching the behavior of the `CreateFieldInput` path - Adds SDK-side validation rejecting `isUnique` on RELATION, MORPH_RELATION, and FILES field types - Adds integration test verifying manifest sync creates a unique index for `isUnique` fields - Adds SDK unit tests for `isUnique` validation on unsupported field types ## Test plan - [x] SDK unit tests: `defineField` accepts `isUnique: true` on TEXT, rejects on RELATION and FILES - [ ] Integration test: manifest sync with `isUnique: true` creates the unique index in DB - [ ] Verify `isUnique` defaults to `false` when not specified (backward compatible) - [ ] Verify standalone manifest fields (not nested in objects) also generate unique indexes correctly |
||
|
|
4264741281 |
Clear stale SDK config on uninstall and invalid client (#19608)
## Summary - After a successful `uninstall`, clear all app registration config (`appRegistrationId`, `appRegistrationClientId`, `appAccessToken`, `appRefreshToken`) so the next `dev` run doesn't hit "app not found" errors from leftover credentials - On app re-registration (`ensureAppRegistration`), clear stale `appAccessToken`/`appRefreshToken` that belonged to the previous registration - On OAuth token refresh failure, only clear config when the server returns `invalid_client` (registration was deleted), not on transient failures like network issues - Extract a shared `parse-server-error` utility for consistently parsing both GraphQL error codes (`extensions.code`/`subCode`) and OAuth error responses (`error`/`error_description`) |
||
|
|
baf2fc4cc9 |
Fix sdk-e2e-test: ensure DB is ready before server starts (#19583)
## Summary
- The `sdk-e2e-test` CI job has been failing since at least April 9th
because the nx dependency graph runs `database:reset` and
`start:ci-if-needed` in **parallel** — neither depends on the other. The
server starts before the DB tables are created, crashes with `relation
"core.keyValuePair" does not exist`, and `wait-on` times out after 10
minutes.
- Replace the `nx-affected` orchestration for e2e with explicit
**sequential** CI steps: build → create DB → reset DB → start server →
wait for health → run tests.
- Add server log dump on failure for easier future debugging.
## Root cause
In `packages/twenty-sdk/project.json`, the `test:e2e` target has:
```json
"dependsOn": [
"build",
{ "target": "database:reset", "projects": "twenty-server" },
{ "target": "start:ci-if-needed", "projects": "twenty-server" }
]
```
Since `database:reset` and `start:ci-if-needed` don't depend on each
other, nx can (and does) run them concurrently. `start:ci-if-needed`
fires `nohup nest start &` and immediately completes. The server process
tries to query `core.keyValuePair` before `database:reset` creates it →
crash → wait-on timeout → job failure.
|
||
|
|
53065f241f |
Exchange clientSecret for tokens after app registration + bump canary (#19582)
## Summary - **Fix `createApplicationRegistration` flow**: The server's `createApplicationRegistration` mutation returns a `clientSecret`, not `accessToken`/`refreshToken` directly. The SDK now correctly requests `clientSecret` and immediately performs an OAuth `client_credentials` exchange to obtain `appAccessToken` and `appRefreshToken`, then stores them in config. - **New `exchangeCredentialsForTokens` helper**: Shared by both `dev` and `dev --once` flows. Takes `clientId` + `clientSecret`, calls `/oauth/token` with `client_credentials` grant, and persists the resulting tokens. - **Bump `twenty-sdk`, `twenty-client-sdk`, `create-twenty-app` to `1.22.0-canary.2`** ## Context The `1.22.0-canary.1` SDK release expected `createApplicationRegistration` to return `accessToken`/`refreshToken` directly, but the `v1.22.0` server returns `clientSecret`. This caused `yarn twenty dev` and `yarn twenty dev --once` to fail with "No registration found" errors. |
||
|
|
c26c0b9d71 |
Use app's own OAuth credentials for CoreApiClient generation (#19563)
## Summary - **SDK (`dev` & `dev --once`)**: After app registration, the CLI now obtains an `APPLICATION_ACCESS` token via `client_credentials` grant using the app's own `clientId`/`clientSecret`, and uses that token for CoreApiClient schema introspection — instead of the user's `config.accessToken` which returns the full unscoped schema. - **Config**: `oauthClientSecret` is now persisted alongside `oauthClientId` in `~/.twenty/config.json` when creating a new app registration, so subsequent `dev`/`dev --once` runs can obtain fresh app tokens without re-registration. - **CI action**: `spawn-twenty-app-dev-test` now outputs a proper `API_KEY` JWT (signed with the seeded dev workspace secret) instead of the previous hardcoded `ACCESS` token — giving consumers a real API key rather than a user session token. ## Motivation When developing Twenty apps, `yarn twenty dev` was using the CLI user's OAuth token for GraphQL schema introspection during CoreApiClient generation. This token (type `ACCESS`) has no `applicationId` claim, so the server returns the **full workspace schema** — including all objects — rather than the scoped schema the app should see at runtime (filtered by `applicationId`). This caused a discrepancy: the generated CoreApiClient contained fields the app couldn't actually query at runtime with its `APPLICATION_ACCESS` token. By switching to `client_credentials` grant, the SDK now introspects with the same token type the app will use in production, ensuring the generated client accurately reflects the app's runtime capabilities. |
||
|
|
b284c8323c |
Remove Favorite and FavoriteFolder from workspace schema (#19536)
## Summary - Removes all workspace schema definitions for `Favorite` and `FavoriteFolder` entities, which have been fully migrated to `NavigationMenuItems` - Deletes 26 standalone files including workspace entities, NestJS modules, services, listeners, jobs, standard application builders (field metadata, views, view fields, view field groups, indexes, page layouts), mocks, and integration tests - Cleans up ~40 modified files: removes `favorites` relation from 10 workspace entities and their field metadata utils, removes entries from all builder maps, shared constants (`STANDARD_OBJECTS`, `CoreObjectNameSingular`, `DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS`), SDK default relations, AI tool filtering, and standard object icons |
||
|
|
d2f51cc939 |
Fix pre post logic function not executed (#19462)
- removes pre-install function
- execute **asyncrhonously** post-install function at application
installation
- add optional `shouldRunOnVersionUpgrade` boolean value on post-install
function definition default false
- update PostInstallPayload to
```
export type PostInstallPayload = {
previousVersion?: string;
newVersion: string;
};
```
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
5eaabe95e7 |
Fix role synchronisation (#19469)
As title solves https://discord.com/channels/1130383047699738754/1491167098398052503 |
||
|
|
90de1d4a34 |
Add twenty sync command (#19413)
Add one shot app synchronisation `twenty sync command` command Complementary with `twenty app dev` command which is watch mode Fixes https://discord.com/channels/1130383047699738754/1489644493106839663 |
||
|
|
15eb3e7edc |
feat(sdk): use config file as single source of truth, remove env var fallbacks (#19409)
## Summary - **Config as source of truth**: `~/.twenty/config.json` is now the single source of truth for SDK authentication — env var fallbacks have been removed from the config resolution chain. - **Test instance support**: `twenty server start --test` spins up a dedicated Docker instance on port 2021 with its own config (`config.test.json`), so integration tests don't interfere with the dev environment. - **API key auth for marketplace**: Removed `UserAuthGuard` from `MarketplaceResolver` so API key tokens (workspace-scoped) can call `installMarketplaceApp`. - **CI for example apps**: Added monorepo CI workflows for `hello-world` and `postcard` example apps to catch regressions. - **Simplified CI**: All `ci-create-app-e2e` and example app workflows now use a shared `spawn-twenty-app-dev-test` action (Docker-based) instead of building the server from source. Consolidated auth env vars to `TWENTY_API_URL` + `TWENTY_API_KEY`. - **Template publishing fix**: `create-twenty-app` template now correctly preserves `.github/` and `.gitignore` through npm publish (stored without leading dot, renamed after copy). ## Test plan - [x] CI SDK (lint, typecheck, unit, integration, e2e) — all green - [x] CI Example App Hello World — green - [x] CI Example App Postcard — green - [x] CI Create App E2E minimal — green - [x] CI Front, CI Server, CI Shared — green |
||
|
|
5c8b6e395b |
App feedbacks front (#19416)
- Removes agents from app settings https://discord.com/channels/1130383047699738754/1488500960656490618 - fix public assets not properly pack in deploy and build commands - |
||
|
|
8702300b07 |
App feedbacks fix option id required in apps (#19386)
fixes https://discord.com/channels/1130383047699738754/1488226371032453292 |
||
|
|
119014f86d |
Improve apps (#19256)
- simplify the base application template - remove --exhaustive option and replace by a --example option like in next.js https://nextjs.org/docs/app/api-reference/cli - Fix some bugs and logs - add a post-card app in twenty-apps/examples/ |
||
|
|
2d6c8be7df |
[Apps] Fix - app-synced object should be searchable (#19206)
## Summary - **Make app-synced objects searchable**: `isSearchable` was hardcoded to `false` and the `searchVector` field was missing the `GENERATED ALWAYS AS (...)` expression, causing all records to have a `NULL` search vector and be excluded from search results. Fixed by defaulting `isSearchable` to `true` (configurable via the object manifest), computing the `asExpression` from the label identifier field, and allowing the update-field-action-handler to handle the `null` → defined `asExpression` transition. - **Make `isSearchable` updatable on an object**: The property had `toCompare: false` in the entity properties configuration, so updates via the API were silently ignored and never persisted. Fixed by setting `toCompare: true`. |
||
|
|
81f10c586f |
Add workspace DDL lock env var and maintenance mode UI (#19130)
## Summary - Add `WORKSPACE_SCHEMA_DDL_LOCKED` env-only boolean config variable that blocks all workspace schema DDL changes when set to `true`. This is intended for hot upgrades where logical replication cannot handle DDL changes. Enforced at two chokepoints: - `WorkspaceMigrationRunnerService.run` — blocks all metadata-driven DDL (object/field/index CRUD, app sync/uninstall, standard app sync, upgrade commands) - `WorkspaceDataSourceService.createWorkspaceDBSchema` / `deleteWorkspaceDBSchema` — blocks workspace creation (sign-up) and hard deletion. Uses a dedicated `WorkspaceDataSourceException` (not ForbiddenException) - Add maintenance mode feature with Admin Panel UI and user-facing banner: - **Backend**: `MaintenanceModeService` stores maintenance window (startAt, endAt, optional link) in `core.keyValuePair` as `CONFIG_VARIABLE`. Validates endAt > startAt. Uses `GraphQLISODateTime` scalar for date fields. Exposed via `clientConfig` REST endpoint and admin GraphQL mutations (`setMaintenanceMode`, `clearMaintenanceMode`) - **Admin Panel**: New "Maintenance Mode" section in Health tab with UTC datetime pickers and activate/deactivate controls - **Banner**: `InformationBannerMaintenance` displayed at the top of `DefaultLayout` for all users, using Temporal API for timezone-aware formatting with an optional "Learn more" link These two features are **independent** — the DDL lock is controlled via env var for operational use, while maintenance mode is a UI notification mechanism controlled from the admin panel. |
||
|
|
16e3e38b79 |
Improve getting started doc (#19138)
- improves `packages/twenty-docs/developers/extend/apps/getting-started.mdx` --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
9054d3aef6 |
[COMMAND MENU ITEMS] Resolve object metadata label in dynamic command menu item label (#19211)
- Adds a resolveObjectMetadataLabel utility that returns the singular or
plural label from object metadata based on the number of selected
records (e.g., "person" vs "people").
- Exposes a pre-computed objectMetadataLabel string on
CommandMenuContextApi, making it available for label interpolation
(e.g., Delete ${capitalize(objectMetadataLabel)}).
|
||
|
|
aa0ea96582 |
Improve app errors logs at sync (#19174)
1. Fix scrollbar Before https://github.com/user-attachments/assets/29792a71-b2dd-49f6-bb90-9d15feeb95aa After https://github.com/user-attachments/assets/939a000a-b787-4ea5-a9f0-61fbac886025 2. Introduce verbose vs non-verbose verbose = what we have today (very detailed) non-verbose = summarized (with a log to say add --verbose for full logs!) without --verbose <img width="1256" height="876" alt="updated_non_verbose" src="https://github.com/user-attachments/assets/d6194c41-2366-4297-a7ac-b3f3b27e08dd" /> with --verbose <img width="422" height="819" alt="verbose_logs" src="https://github.com/user-attachments/assets/409e2e88-ec3d-4bab-957c-ef319895f8c5" /> |
||
|
|
2612145436 | Fix sdk tests (#19172) | ||
|
|
37908114fc |
[SDK] Extract twenty-front-component-renderer outside of twenty-sdk ( 2.8MB ) (#19021)
Followup https://github.com/twentyhq/twenty/pull/19010 ## Dependency diagram ``` ┌─────────────────────┐ │ twenty-front │ │ (React frontend) │ └─────────┬───────────┘ │ imports runtime: │ FrontComponentRenderer │ FrontComponentRendererWithSdkClient │ useFrontComponentExecutionContext ▼ ┌──────────────────────────────────┐ ┌─────────────────────────┐ │ twenty-front-component-renderer │────────▶│ twenty-sdk │ │ (remote-dom host + worker) │ │ (app developer SDK) │ │ │ │ │ │ imports from twenty-sdk: │ │ Public API: │ │ • types only: │ │ defineFrontComponent │ │ FrontComponentExecutionContext│ │ navigate, closeSide… │ │ NavigateFunction │ │ useFrontComponent… │ │ CloseSidePanelFunction │ │ Command components │ │ CommandConfirmation… │ │ conditional avail. │ │ OpenCommandConfirmation… │ │ │ │ EnqueueSnackbarFunction │ │ Internal only: │ │ etc. │ │ frontComponentHost… │ │ │ │ front-component-build │ │ owns locally: │ │ esbuild plugins │ │ • ALLOWED_HTML_ELEMENTS │ │ │ │ • EVENT_TO_REACT │ └────────────┬────────────┘ │ • HTML_TAG_TO_CUSTOM_ELEMENT… │ │ │ • SerializedEventData │ │ types │ • PropertySchema │ ▼ │ • frontComponentHostComm… │ ┌─────────────────────────┐ │ (local ref to globalThis) │ │ twenty-shared │ │ • setFrontComponentExecution… │ │ (common types/utils) │ │ (local impl, same keys) │ │ AppPath, SidePanelP… │ │ │ │ EnqueueSnackbarParams │ └──────────────────────────────────┘ │ isDefined, … │ │ └─────────────────────────┘ │ also depends on ▼ twenty-shared (types) @remote-dom/* (runtime) @quilted/threads (runtime) react (runtime) ``` **Key points:** - **`twenty-front`** depends on the renderer, **not** on `twenty-sdk` directly (for rendering) - **`twenty-front-component-renderer`** depends on `twenty-sdk` for **types only** (function signatures, `FrontComponentExecutionContext`). The runtime bridge (`frontComponentHostCommunicationApi`) is shared via `globalThis` keys, not module imports - **`twenty-sdk`** has no dependency on the renderer — clean one-way dependency - The renderer owns all remote-dom infrastructure (element schemas, event mappings, custom element tags) that was previously leaking through the SDK's public API - The SDK's `./build` entry point was removed entirely (unused) |
||
|
|
8985dfbc5d |
Improve apps (#19120)
fixes https://discord.com/channels/1130383047699738754/1488094970241089586 |
||
|
|
fe1377f18b |
Provide applicatiion assets (#18973)
- improve backend - improve frontend <img width="1293" height="824" alt="image" src="https://github.com/user-attachments/assets/7a4633f1-85cd-4126-b058-dbeae6ba2218" /> |
||
|
|
c3a3cf9c67 |
[Apps SDK] Add error message if relationType is missing (#19006)
Before <img width="2138" height="1434" alt="image" src="https://github.com/user-attachments/assets/23aa04ae-9994-4ff9-bac8-9f245e6ca05f" /> After <img width="1029" height="610" alt="image" src="https://github.com/user-attachments/assets/7af48bc0-678d-4451-8cfb-a5e4028f2c2d" /> |
||
|
|
c7f6036a47 |
[SDK] twenty-ui/display selective re-export to avoid bloating icons (#19010)
<img width="1946" height="792" alt="image" src="https://github.com/user-attachments/assets/d0abf62b-85d4-4f5f-b6dc-f2cc1c691f7e" /> Avoid re-exporting twenty-ui icons bundle that are massive ~4MB As discussed with @charlesBochet the problem should rather be treated at twenty-ui level at some point, that's quite a quick workaround in order to avoid overloading the twenty-sdk build size When time comes, where twenty-ui is mature enough to get published we will work on its bundle size |
||
|
|
052aecccc7 |
Refactor dependency graph for SDK, client-sdk and create-app (#18963)
## Summary
### Externalize `twenty-client-sdk` from `twenty-sdk`
Previously, `twenty-client-sdk` was listed as a `devDependency` of
`twenty-sdk`, which caused Vite to bundle it inline into the dist
output. This meant end-user apps had two copies of `twenty-client-sdk`:
one hidden inside `twenty-sdk`'s bundle, and one installed explicitly in
their `node_modules`. These copies could drift apart since they weren't
guaranteed to be the same version.
**Change:** Moved `twenty-client-sdk` from `devDependencies` to
`dependencies` in `twenty-sdk/package.json`. Vite's `external` function
now recognizes it and keeps it as an external `require`/`import` in the
dist output. End users get a single deduplicated copy resolved by their
package manager.
### Externalize `twenty-sdk` from `create-twenty-app`
Similarly, `create-twenty-app` had `twenty-sdk` as a `devDependency`
(bundled inline). After refactoring `create-twenty-app` to
programmatically import operations from `twenty-sdk` (instead of
shelling out via `execSync`), it became a proper runtime dependency.
**Change:** Moved `twenty-sdk` from `devDependencies` to `dependencies`
in `create-twenty-app/package.json`.
### Switch E2E CI to `yarn npm publish`
The `workspace:*` protocol in `dependencies` is a Yarn-specific feature.
`npm publish` publishes it as-is (which breaks for consumers), while
`yarn npm publish` automatically replaces `workspace:*` with the
resolved version at publish time (e.g., `workspace:*` becomes `=1.2.3`).
**Change:** Replaced `npm publish` with `yarn npm publish` in
`.github/workflows/ci-create-app-e2e.yaml`.
### Replace `execSync` with programmatic SDK calls in
`create-twenty-app`
`create-twenty-app` was shelling out to `yarn twenty remote add` and
`yarn twenty server start` via `execSync`, which assumed the `twenty`
binary was already installed in the scaffolded app. This was fragile and
created an implicit circular dependency.
**Changes:**
- Replaced `execSync('yarn twenty remote add ...')` with a direct call
to `authLoginOAuth()` from `twenty-sdk/cli`
- Replaced `execSync('yarn twenty server start')` with a direct call to
`serverStart()` from `twenty-sdk/cli`
- Deleted the duplicated `setup-local-instance.ts` from
`create-twenty-app`
### Centralize `serverStart` as a dedicated operation
The Docker server start logic was previously inline in the `server
start` CLI command handler (`server.ts`), and `setup-local-instance.ts`
was shelling out to `yarn twenty server start` to invoke it -- meaning
`twenty-sdk` was calling itself via a child process.
**Changes:**
- Extracted the Docker container management logic into a new
`serverStart` operation (`cli/operations/server-start.ts`)
- Merged the detect-or-start flow from `setup-local-instance.ts` into
`serverStart` (detect across multiple ports, start Docker if needed,
poll for health)
- Deleted `setup-local-instance.ts` from `twenty-sdk`
- Added `onProgress` callback (consistent with other operations like
`appBuild`) instead of direct `console.log` calls
- Both the `server start` CLI command and `create-twenty-app` now call
`serverStart()` programmatically
related to https://github.com/twentyhq/twenty-infra/pull/525
|
||
|
|
4ea2e32366 |
Refactor twenty client sdk provisioning for logic function and front-component (#18544)
## 1. The `twenty-client-sdk` Package (Source of Truth)
The monorepo package at `packages/twenty-client-sdk` ships with:
- A **pre-built metadata client** (static, generated from a fixed
schema)
- A **stub core client** that throws at runtime (`CoreApiClient was not
generated...`)
- Both ESM (`.mjs`) and CJS (`.cjs`) bundles in `dist/`
- A `package.json` with proper `exports` map for
`twenty-client-sdk/core`, `twenty-client-sdk/metadata`, and
`twenty-client-sdk/generate`
## 2. Generation & Upload (Server-Side, at Migration Time)
**When**: `WorkspaceMigrationRunnerService.run()` executes after a
metadata schema change.
**What happens in `SdkClientGenerationService.generateAndStore()`**:
1. Copies the stub `twenty-client-sdk` package from the server's assets
(resolved via `SDK_CLIENT_PACKAGE_DIRNAME` — from
`dist/assets/twenty-client-sdk/` in production, or from `node_modules`
in dev)
2. Filters out `node_modules/` and `src/` during copy — only
`package.json` + `dist/` are kept (like an npm publish)
3. Calls `replaceCoreClient()` which uses `@genql/cli` to introspect the
**application-scoped** GraphQL schema and generates a real
`CoreApiClient`, then compiles it to ESM+CJS and overwrites
`dist/core.mjs` and `dist/core.cjs`
4. Archives the **entire package** (with `package.json` + `dist/`) into
`twenty-client-sdk.zip`
5. Uploads the single archive to S3 under
`FileFolder.GeneratedSdkClient`
6. Sets `isSdkLayerStale = true` on the `ApplicationEntity` in the
database
## 3. Invalidation Signal
The `isSdkLayerStale` boolean column on `ApplicationEntity` is the
invalidation mechanism:
- **Set to `true`** by `generateAndStore()` after uploading a new client
archive
- **Checked** by both logic function drivers before execution — if
`true`, they rebuild their local layer
- **Set back to `false`** by `markSdkLayerFresh()` after the driver has
successfully consumed the new archive
Default is `false` so existing applications without a generated client
aren't affected.
## 4a. Logic Functions — Local Driver
**`ensureSdkLayer()`** is called before every execution:
1. Checks if the local SDK layer directory exists AND `isSdkLayerStale`
is `false` → early return
2. Otherwise, cleans the local layer directory
3. Calls `downloadAndExtractToPackage()` which streams the zip from S3
directly to disk and extracts the full package into
`<tmpdir>/sdk/<workspaceId>-<appId>/node_modules/twenty-client-sdk/`
4. Calls `markSdkLayerFresh()` to set `isSdkLayerStale = false`
**At execution time**, `assembleNodeModules()` symlinks everything from
the deps layer's `node_modules/` **except** `twenty-client-sdk`, which
is symlinked from the SDK layer instead. This ensures the logic
function's `import ... from 'twenty-client-sdk/core'` resolves to the
generated client.
## 4b. Logic Functions — Lambda Driver
**`ensureSdkLayer()`** is called during `build()`:
1. Checks if `isSdkLayerStale` is `false` and an existing Lambda layer
ARN exists → early return
2. Otherwise, deletes all existing layer versions for this SDK layer
name
3. Calls `downloadArchiveBuffer()` to get the raw zip from S3 (no disk
extraction)
4. Calls `reprefixZipEntries()` which streams the zip entries into a
**new zip** with the path prefix
`nodejs/node_modules/twenty-client-sdk/` — this is the Lambda layer
convention path. All done in memory, no disk round-trip
5. Publishes the re-prefixed zip as a new Lambda layer via
`publishLayer()`
6. Calls `markSdkLayerFresh()`
**At function creation**, the Lambda is created with **two layers**:
`[depsLayerArn, sdkLayerArn]`. The SDK layer is listed last so it
overwrites the stub `twenty-client-sdk` from the deps layer (later
layers take precedence in Lambda's `/opt` merge).
## 5. Front Components
Front components are built by `app:build` with `twenty-client-sdk/core`
and `twenty-client-sdk/metadata` as **esbuild externals**. The stored
`.mjs` in S3 has unresolved bare import specifiers like `import {
CoreApiClient } from 'twenty-client-sdk/core'`.
SDK import resolution is split between the **frontend host** (fetching &
caching SDK modules) and the **Web Worker** (rewriting imports):
**Server endpoints**:
- `GET /rest/front-components/:id` —
`FrontComponentService.getBuiltComponentStream()` returns the **raw
`.mjs`** directly from file storage. No bundling, no SDK injection.
- `GET /rest/sdk-client/:applicationId/:moduleName` —
`SdkClientController` reads a single file (e.g. `dist/core.mjs`) from
the generated SDK archive via
`SdkClientGenerationService.readFileFromArchive()` and serves it as
JavaScript.
**Frontend host** (`FrontComponentRenderer` in `twenty-front`):
1. Queries `FindOneFrontComponent` which returns `applicationId`,
`builtComponentChecksum`, `usesSdkClient`, and `applicationTokenPair`
2. If `usesSdkClient` is `true`, renders
`FrontComponentRendererWithSdkClient` which calls the
`useApplicationSdkClient` hook
3. `useApplicationSdkClient({ applicationId, accessToken })` checks the
Jotai atom family cache for existing blob URLs. On cache miss, fetches
both SDK modules from `GET /rest/sdk-client/:applicationId/core` and
`/metadata`, creates **blob URLs** for each, and stores them in the atom
family
4. Once the blob URLs are cached, passes them as `sdkClientUrls`
(already blob URLs, not server URLs) to `SharedFrontComponentRenderer` →
`FrontComponentWorkerEffect` → worker's `render()` call via
`HostToWorkerRenderContext`
**Worker** (`remote-worker.ts` in `twenty-sdk`):
1. Fetches the raw component `.mjs` source as text
2. If `sdkClientUrls` are provided and the source contains SDK import
specifiers (`twenty-client-sdk/core`, `twenty-client-sdk/metadata`),
**rewrites** the bare specifiers to the blob URLs received from the host
(e.g. `'twenty-client-sdk/core'` → `'blob:...'`)
3. Creates a blob URL for the rewritten source and `import()`s it
4. Revokes only the component blob URL after the module is loaded — the
SDK blob URLs are owned and managed by the host's Jotai cache
This approach eliminates server-side esbuild bundling on every request,
caches SDK modules per application in the frontend, and keeps the
worker's job to a simple string rewrite.
## Summary Diagram
```
app:build (SDK)
└─ twenty-client-sdk stub (metadata=real, core=stub)
│
▼
WorkspaceMigrationRunnerService.run()
└─ SdkClientGenerationService.generateAndStore()
├─ Copy stub package (package.json + dist/)
├─ replaceCoreClient() → regenerate core.mjs/core.cjs
├─ Zip entire package → upload to S3
└─ Set isSdkLayerStale = true
│
┌────────┴────────────────────┐
▼ ▼
Logic Functions Front Components
│ │
├─ Local Driver ├─ GET /rest/sdk-client/:appId/core
│ └─ downloadAndExtract │ → core.mjs from archive
│ → symlink into │
│ node_modules ├─ Host (useApplicationSdkClient)
│ │ ├─ Fetch SDK modules
└─ Lambda Driver │ ├─ Create blob URLs
└─ downloadArchiveBuffer │ └─ Cache in Jotai atom family
→ reprefixZipEntries │
→ publish as Lambda ├─ GET /rest/front-components/:id
layer │ → raw .mjs (no bundling)
│
└─ Worker (browser)
├─ Fetch component .mjs
├─ Rewrite imports → blob URLs
└─ import() rewritten source
```
## Next PR
- Estimate perf improvement by implementing a redis caching for front
component client storage ( we don't even cache front comp initially )
- Implem frontent blob invalidation sse event from server
---------
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
|
||
|
|
96f3ff0e90 |
Add record table widget to dashboards (#18747)
## Demo https://github.com/user-attachments/assets/584de452-544a-41f8-ae9f-4be9e9d0cd9f ## Problem - Dashboards only supported chart widgets — tabular record data had no inline widget type - `RecordTable` was tightly coupled to the record index: HTML IDs, CSS variables, and hover portals were global strings with no per-instance scoping, so multiple tables on the same page would collide - `updateRecordTableCSSVariable`, `RECORD_TABLE_HTML_ID`, and cell portal IDs were hardcoded — placing two tables caused hover portals and CSS column widths to bleed across instances - Grid drag-select captured record UUIDs as cell IDs, producing `NaN` layout coordinates and a full-page freeze on second widget creation ## Fix - `RECORD_TABLE` is now a valid widget type across the full stack — server DTOs, DB enum migration, universal config mapping, GraphQL codegen, shared types (`RecordTableConfigurationDto`, `WidgetType`, `addRecordTableWidgetType` migration) - A record table widget can be placed on a dashboard and boots from a View ID with no record index dependency — `StandaloneRecordTableProvider` + `StandaloneRecordTableViewLoadEffect` (wraps existing `RecordTableWithWrappers` unchanged) - Selecting a data source auto-creates a dedicated View with up to 6 initial fields; switching source or deleting the widget cleans up the View — `useCreateViewForRecordTableWidget` + `useDeleteViewForRecordTableWidget` - The settings panel exposes source, field visibility/reorder, filter conditions, sort rules, and editable widget title — `SidePanelPageLayoutRecordTableSettings` + sub-pages, matching chart widget pattern - Filters, sorts, and aggregate operations update the table in real time but only persist to the View on explicit dashboard save — `useSaveRecordTableWidgetsViewDataOnDashboardSave` (diff + flush on save) - Headers are always non-interactive (no dropdown, no cursor pointer); columns are resizable only in edit mode; cells are non-editable in both modes — `isRecordTableColumnHeadersReadOnlyComponentState`, `isRecordTableColumnResizableComponentState`, `isRecordTableCellsNonEditableComponentState` (Jotai component states) - Hover portals and CSS column widths no longer bleed between multiple table widgets — `getRecordTableHtmlId(tableId)`, `getRecordTableCellId(tableId, …)`, `updateRecordTableCSSVariable(tableId, …)` scope all DOM IDs and CSS variables per instance - Clicking inside a widget's content area no longer opens the settings panel — `WidgetCardContent` stops click propagation when editable, limiting settings-open to the card header and chrome - Second widget creation no longer freezes the page — `PageLayoutGridLayout` drag-select filters by `cell-` prefix to exclude record UUIDs from grid cell detection ## Follow-up fixes **Widget save flow** - Saving a dashboard silently dropped record table widget changes (column visibility, order, filters, sorts, aggregates) because widget data save was bundled inside the layout save and only ran when layout structure changed - Widget data now persists independently via `useSavePageLayoutWidgetsData`, called in all save paths (dashboard save, record page save, layout customization save); saves are also skipped when nothing has changed **Drag-and-drop / checkbox columns in widget** - Record table widgets showed the drag handle column and checkbox selection column even though row reordering and multi-select are meaningless in a read-only widget - Two new component states (`isRecordTableDragColumnHiddenComponentState`, `isRecordTableCheckboxColumnHiddenComponentState`) hide each column independently; widget tables now display only data columns **Sticky column layout** - Sticky positioning of the first three columns used `:nth-of-type` CSS selectors — when drag or checkbox columns were hidden, the selector targeted the wrong column and the first data column didn't stick - Sticky CSS now targets semantic class names (`RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH_CLASS_NAME`, etc.) so sticky behavior is correct regardless of which columns are hidden **Save/Cancel buttons during edit mode** - Save and Cancel command-menu buttons were unpinned during dashboard edit mode because the pin logic excluded all items while `isPageInEditMode` was true - Items whose availability expression contains `isPageInEditMode` are now exempted from the unpin rule; Save/Cancel stay pinned during editing **Title input auto-focus** - Selecting "Record Table" as widget type auto-focused the title input, interrupting the configuration flow - `focusTitleInput` is now `false` when navigating to record table settings **Morph relation field error** - A field with missing `morphRelations` metadata crashed the page with a "refresh" error from `mapObjectMetadataToGraphQLQuery` - Now returns an empty array and silently omits the field from the query instead of crashing **`updateRecordMutation` prop removal** - `RecordTableWithWrappers` required callers to pass an `updateRecordMutation` callback, duplicating `useUpdateOneRecord` at every usage site - The mutation is now owned inside `RecordTableContextProvider` via `RecordTableUpdateContext`; the prop is gone **Standalone → Widget module rename** - `record-table-standalone` module renamed to `record-table-widget` — `StandaloneRecordTable` → `RecordTableWidget`, `StandaloneRecordTableViewLoadEffect` → `RecordTableWidgetViewLoadEffect`, etc. **RecordTableRow cell extraction** - Row rendering logic (`RecordTableCellDragAndDrop`, `RecordTableCellCheckbox`, `RecordTableFieldsCells`, hotkey/arrow-key effects) was duplicated between `RecordTableRow` and `RecordTableRowVirtualizedFullData` - Extracted `RecordTableRowCells` (shared cell content) and `RecordTableStaticTr` (non-draggable `<tr>` wrapper); when drag column is hidden, rows render inside a static `<tr>` instead of the draggable wrapper **View load effect metadata tracking** - `RecordTableWidgetViewLoadEffect` now tracks `objectMetadataItem.updatedAt` alongside `viewId` to re-load states when metadata changes (e.g. field additions), preventing stale column data **Data source dropdown deduplication** - Extracted `filterReadableActiveObjectMetadataItems` util, shared by both chart and record table data source dropdowns — removes duplicated permission-filtering logic **RECORD_TABLE view identifier mapping (server)** - Added `RECORD_TABLE` case to `fromPageLayoutWidgetConfigurationToUniversalConfiguration` and `fromUniversalConfigurationToFlatPageLayoutWidgetConfiguration` so widget views are properly mapped during workspace import/export **GraphQL error handler typing (server)** - `formatError` parameter changed from `any` to `unknown`; `workspaceQueryRunnerGraphqlApiExceptionHandler` broadened from `QueryFailedErrorWithCode` to `Error | QueryFailedError` — removes unsafe type casts **Save hook signature** - `useSaveRecordTableWidgetsViewDataOnDashboardSave` no longer takes `pageLayoutId` in constructor; receives it as a callback parameter, eliminating the need for `useAtomComponentStateCallbackState` **Customize Dashboard hidden during edit mode** - The "Customize Dashboard" command was still visible while already editing — its `conditionalAvailabilityExpression` now includes `not isPageInEditMode` **Fields dropdown split** - `RecordTableFieldsDropdownContent` (300+ lines) split into `RecordTableFieldsDropdownVisibleFieldsContent` and `RecordTableFieldsDropdownHiddenFieldsContent` **Checkbox placeholder cleanup** - Removed unnecessary `StyledRecordTableTdContainer` wrapper from `RecordTableCellCheckboxPlaceholder` |
||
|
|
49afe5dbc4 |
Refactor: Unify command menu item execution (#18908)
- Makes engineComponentKey non-nullable on CommandMenuItem. Adds two new engine component keys: TRIGGER_WORKFLOW_VERSION and FRONT_COMPONENT_RENDERER to cover the former standalone cases. - Unifies the previously separated engine, front component and workflow runners into a single `CommandRunner` component with a unified `useMountCommand` hook that handles all command types. |