e6cc783a23fdedf8d7cfbbdf24cce99d30e96d74
530
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
76e144e85a |
Deprecate messageChannel messageFolder calendarChannel standard objects (#20836)
# Introduction Removing old standard objects `messageChannel` and `messageFolder` and `calendarChannel` --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
068d365731 |
feat(sdk): error on incompatible view filter operand at sync time (#20763)
view filters with mismatched operand + field type now error at sync -- was silently failing before |
||
|
|
323e66433e |
lint: migrate prettier to oxfmt (#20783)
Most changes are `implements` being unwrapped this is not a oxfmt regression Prettier in 3.7 (we're on 3.1) changed this behaviour prettier blog [post](https://prettier.io/blog/2025/11/27/3.7.0#change-18094) This unifies our linting tooling --------- Co-authored-by: github-actions <github-actions@twenty.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
138eb5a74a |
Add empty operands to UUID filter type in workflow filter action (#20821)
## Summary - Adds `IS_EMPTY` and `IS_NOT_EMPTY` operands to the UUID entry in `getStepFilterOperands`, aligning the workflow filter action with the find records (search) action which already includes these operands for ID-type fields. ## Test plan - [ ] Open a workflow with a filter action, select an ID-type field, and verify the operand dropdown now includes "Is empty" and "Is not empty" - [ ] Open a workflow with a find records action, select an ID-type field, and verify the operand dropdown is consistent with the filter action --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
9b9c97a049 |
Deprecate and backfill delete ConnectedAccount twenty standard object (#20752)
# Introduction Following connected account permissions refactor and encryption Removing the old workspace schema twenty standard application connectedAccount objects and related standard fields and index - a lot of deadcode - instance command backfill cleaning the connected account object from workspaces |
||
|
|
b454ad2aea |
fix(workflow): restore initial input fields on code step creation (#20756)
## Summary - Fixes a regression from #20208 where creating a new CODE workflow step shows no input fields - The split-triggers PR removed `SEED_LOGIC_FUNCTION_INPUT_SCHEMA` and replaced `toolInputSchema` with `workflowActionTriggerSettings`, but `CodeStepBuildService.createCodeStepLogicFunction` was not updated to pass the seed schema — causing `logicFunctionInput` to default to `{}` and no fields to render - Adds `SEED_WORKFLOW_ACTION_TRIGGER_SETTINGS` constant (matching the seed template's `{ a: string, b: number }` params) and passes it when creating the seed logic function ## Test plan - [x] Unit test updated to assert `logicFunctionInput` contains `{ a: null, b: null }` on code step creation - [x] Create a new CODE step in the workflow builder and verify input fields `a` and `b` appear immediately Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
a26fe3bb65 |
docs(sdk): document DatabaseEventPayload and simplify its type (#20754)
closes https://discord.com/channels/1130383047699738754/1505967920163983502 Update logic-function docs to match the real `DatabaseEventPayload` shape. The docs now show database event payloads as record-level events with `recordId` and `properties.before/after/diff/updatedFields`, including compact examples for created, updated, and destroyed events. Route payload type imports now use the preferred `twenty-sdk/logic-function` surface. Also clean up the shared payload type wrapper so it models event metadata without over-promising actor fields; `userId`, `userWorkspaceId`, and `workspaceMemberId` remain optional through the underlying event type. |
||
|
|
7ab6f5719f |
Update default widget gridPosition (#20740)
move DEFAULT_WIDGET_SIZE to twenty-shared and use it in sync application manifest |
||
|
|
1a9f786e42 |
refactor(filters): pass fieldMetadataItems array to dispatcher (#20737)
## Summary Alternative to #20717. Same goal (clean up the filter dispatcher API after #20670) but smaller and follows the codebase's "pass data, not behavior" style. The dispatcher takes a `fieldMetadataItems: FieldShared[]` array directly instead of a `findFieldMetadataItemById: (id) => FieldShared | undefined` callback. The util builds the id lookup internally — once per call, used for both source-field and relation-target-field lookups. No new types, no separate hydration step. ## What changes **`twenty-shared`** - `computeRecordGqlOperationFilter` / `turnRecordFilterIntoRecordGqlOperationFilter` / `turnRecordFilterGroupsIntoGqlOperationFilter`: replace `findFieldMetadataItemById` param with `fieldMetadataItems` / `fieldMetadataItemById` (internal Map). - Remove the exported `FindFieldMetadataItemById` type. - `turnAnyFieldFilterIntoRecordGqlFilter`: rename its internal `fieldById` Map for consistency. - Tests updated to pass arrays. **Frontend (15 call sites)** - Switch from `fieldMetadataItemByIdMapSelector` to `flattenedFieldMetadataItemsSelector`. - Pass `fieldMetadataItems: flattenedFieldMetadataItems` to the dispatcher. - `useFindManyRecordsSelectedInContextStore` keeps the Map selector because it still does a per-filter lookup for the soft-delete check. **Server (5 call sites)** - Pass `Object.values(flatFieldMetadataMaps.byUniversalIdentifier).filter(isDefined)`. ## Why this over #20717 #20717 moves resolution into a separate hydration step + introduces a `HydratedRecordFilter` type. The bug that #20717 originally surfaced was Sentry catching 4 critical runtime errors during review (`fieldMetadataItemByIdMap` declared but not passed). The added type and the explicit hydration boundary are extra surface area for not much benefit — the existing API was a callback wrapping a Map at every call site, and the natural simplification is to just pass the Map (or its array) directly. Net diff: **196 insertions, 203 deletions** (~7 lines net removed). 32 files. ## Test plan - [x] Shared filter unit tests pass (461 tests) - [x] Frontend filter/context-store tests pass (13 tests) - [x] Frontend typecheck passes - [x] Server typecheck passes - [x] Lint passes (frontend + server) - [ ] Integration tests on #20670 still pass — workflow find-records + chart-data with relation-traversal filter still work end-to-end through the new array param |
||
|
|
83b10ad698 |
fix(server): sync command menu item availability expressions on existing workspaces (#20719)
Two fixes via one workspace command: 1. Gates 5 standard command menu items behind `pageType == "INDEX_PAGE"` -- `importRecords`, `exportView`, `seeDeletedRecords`, `createNewView`, `hideDeletedRecords`. They currently appear (and crash or do nothing) on RECORD_PAGE. 2. Fixes Edit Layout missing from older workspaces -- root cause is `conditionalAvailabilityExpression` drift between source-of-truth constants and the workspace DB (e.g. #20556 removed a feature flag from the expression without syncing existing workspaces). The 2-6 workspace command iterates all `STANDARD_COMMAND_MENU_ITEMS` and reconciles any `conditionalAvailabilityExpression` that differs from the constant. Idempotent -- already-correct rows are skipped. Deferred: `deleteRecords` doesn't refetch the current record after deletion on RECORD_PAGE (mutation fires but UI shows stale state until refresh) -- different fix shape (frontend handler), separate PR. |
||
|
|
08e7e4819b |
use declared outputSchema for logic-function steps (#20679)
When a logic function declares `workflowActionTriggerSettings.outputSchema`, use it as the step's initial output schema so downstream steps can pick variables without first running the Test tab. A successful test run still overrides the schema with the inferred shape, preserving "test wins" behavior. Falls back to the existing "Generate Function Output" LINK placeholder when no schema is declared (custom code steps, older functions). https://github.com/user-attachments/assets/af9c45ed-d623-4234-be9f-46812fd06e2e |
||
|
|
57f13c9b92 |
[CONNECTED_ACCOUNT_BREAKING_CHANGE] Encrypt ConnectedAccount connectionParameters (#20673)
# Introduction Prevent any cross user `connectedAccount` `connectionParamaters` leak Also encrypt in db all `connectionParameters` password Never return any password through `DTO` anymore The settings now allow update mutation without providing the password in edition mode Verified all `connectionParameters.password` interaction ## Integration tests - Added more coverage for both failing and successful paths - Introduced a new env var that allow bypass the provider connection test ## Legacy connected Account decryption support Stop allowing non encrypted decryption on `accessToken` and `refreshToken`, only allow legacy decryption on refactored `connectionParameters` ## Upsert ownership Completely got rid of the connected workspace schema context which is legacy Also now a user can only upsert a connected account for him only.. ## New UI <img width="1770" height="1852" alt="image" src="https://github.com/user-attachments/assets/55c1dc89-42ff-4084-95e2-cc5f9e23753b" /> If in edition the password is by default disabled It needs to be selected as being edited to be enabled ## Next - Refactor tool permissions flag not to include connected accounts - Remove the legacy connected standard object - Refactor and improve connected account resolver auth |
||
|
|
291ce5ccdb |
fix(filters): make filter dispatcher own relation-target resolution (#20670)
## Summary Two relation-traversal bugs surfaced post-merge of #20533, both rooted in the same architectural smell: the GraphQL filter dispatcher took a flat `fields: FieldShared[]` array and silently dropped any filter whose `relationTargetFieldMetadataId` wasn't in that array. Callers had to remember to pre-augment the list with relation targets — and 16+ call sites did not all know this. This PR fixes both bugs and removes the smell. ### Bug 1 — Save as new view loses the relation target `useCreateViewFromCurrentView` built the create-filter input without `relationTargetFieldMetadataId`. The saved view's filter persisted without the traversal — on reload the chip showed "Company contains 'air'" instead of "Company → Name contains 'air'". Discarded at save time, not at read time. Fix: include `relationTargetFieldMetadataId` in the create input. (Commit 1.) ### Bug 2 — Workflow Search Records drops one-hop traversals `FindRecordsWorkflowAction` built its fields list from `flatObjectMetadata.fieldIds` only (source object's fields). The shared dispatcher then couldn't resolve the relation target field on the related object and silently dropped the filter — a configured "People where Company → Name Contains 'Airbnb'" came through as `{ and: [] }`. This was the same shape as bugs already fixed in 5 other call sites (chart filters, view filters, record table, etc.). The pattern was: caller forgets to augment fields → dispatcher silently drops the filter. Fix (commit 2): change the dispatcher to take a `findFieldMetadataItemById: (id) => FieldShared | undefined` resolver callback. Both source-field and relation-target-field lookups go through the same resolver, so callers no longer need to know about the augmentation requirement. Frontend callers pass a workspace-wide resolver built from `flattenedFieldMetadataItemsSelector`; server callers wrap `findFlatEntityByIdInFlatEntityMaps` on `flatFieldMetadataMaps`. In both cases relation-target lookups just work, because the resolver can see fields on related objects. ## Why this matters Before: "if you call the dispatcher, pre-augment your fields list with relation targets, or filters get silently dropped." An invariant only enforceable by code review, broken often enough to ship two user-visible bugs in one week. After: the dispatcher resolves field ids itself. There's no list to forget to augment. The failure mode (filter silently dropped) becomes structurally impossible at the dispatcher boundary. Net diff: 240 insertions, 319 deletions. Removed `augmentFieldsWithRelationTargets` (frontend) and the workflow whack-a-mole code (server). ## Test plan - [ ] Save view: create an advanced filter using a one-hop relation traversal, click "Save as new view", reload, confirm the chip still reads "Source → Target operator value" - [ ] Workflow: configure a Search Records action with a relation-traversal filter, run the workflow, confirm the filter is actually applied - [ ] Dashboard chart: configure a chart with a relation-traversal filter, confirm the chart data respects it - [ ] Record table, group-by, calendar, total count, footer aggregates: all continue to work with both plain and relation-traversal filters |
||
|
|
db0547f503 |
[1/3] Rename permissionFlag to rolePermissionFlag + add permissionFlag catalog/backfill (#20481)
Split of #20377. ## Summary This PR separates available permission flags from per-role permission flag grants. Previously, `core.permissionFlag` stored the role assignment directly: `roleId + flag`. This PR renames that legacy grant table to `core.rolePermissionFlag`, then recreates `core.permissionFlag` as the catalog of available permission flags. ## What changed - Rename the existing `core.permissionFlag` grant table to `core.rolePermissionFlag`. - Add the new syncable `core.permissionFlag` catalog entity with key, label, description, icon, permission type, relevance flags, and custom/standard metadata. - Add stable `SystemPermissionFlag` universal identifiers for the built-in `PermissionFlagType` values. - Seed the standard permission flags for every workspace under the Twenty standard application. - Backfill existing role grants: - create missing catalog rows for existing grant keys, - add `rolePermissionFlag.permissionFlagId`, - migrate grants from the old string `flag` column to the new catalog FK, - replace the old `(flag, roleId)` uniqueness with `(permissionFlagId, roleId)`. - Rewire role permission flag caches, permission checks, role DTO mapping, and `upsertPermissionFlags` to resolve through the catalog. - Keep the existing public role permission API shape: product/app surfaces still talk about `permissionFlags` and return `{ id, roleId, flag }`. - Update metadata flat-entity machinery, migration builders, validators, action handlers, snapshots, generated schemas, docs, and app fixtures for the new `permissionFlag` / `rolePermissionFlag` split. ## Behavior after this PR - Existing permission flag grants keep working. - Existing GraphQL role permission flows keep the same public naming. - Standard permission flags are represented as catalog rows. - Permission checks now compare grants through catalog universal identifiers instead of the legacy `flag` column. - Workspace deletion cleanup now verifies both `permissionFlag` and `rolePermissionFlag`. ## What is not in this PR - Public GraphQL CRUD for custom permission flags. - App manifest support for declaring new custom permission flags. - Frontend UI for creating or assigning custom permission flags beyond the existing role permission flow. --------- Co-authored-by: Weiko <corentin@twenty.com> |
||
|
|
c938fbf4d6 |
feat(twenty-front): relation traversal in filter dropdown (stacked) (#20533)
**Stacked on #20527** https://github.com/user-attachments/assets/48995655-401a-4c35-8094-e88da8408bdd ## Summary Surfaces the one-hop relation traversal added in #20527 through the existing **composite sub-field dropdown pattern**. Clicking a MANY_TO_ONE relation field in the "+ Filter" picker now opens the same second-level dropdown that composite fields (FULL_NAME, ADDRESS, CURRENCY, etc.) already use — populated with the target object's filterable fields. Picking one (e.g. `Company → Name`) builds a filter that serializes to the nested GraphQL filter the backend now accepts: `{ company: { name: { ilike: "%X%" } } }`. No new components. The whole feature reuses `AdvancedFilterSubFieldSelectMenu` + the existing `subFieldNameUsedInDropdownComponentState` + the existing `MenuItem hasSubMenu` indicator. Only the conditions that gate the sub-menu (and the sub-menu's content for relations) were broadened. ## What landed | File | Change | |---|---| | `ObjectFilterDropdownFilterSelectMenuItem` | Sub-menu chevron now shows on MANY_TO_ONE relations (`isManyToOneRelationField` util). | | `AdvancedFilterFieldSelectMenu` | Relation clicks open the sub-menu alongside composite clicks. | | `AdvancedFilterSubFieldSelectMenu` | New branch: when the sub-menu type is `'RELATION'`, render the target object's filterable fields via `useFilterableFieldMetadataItems(targetObjectMetadataId)`. Composite logic untouched. | | `objectFilterDropdownSubMenuFieldType` state | Widened to accept a `'RELATION'` sentinel. Role-permissions sub-field menu narrows it back out (it doesn't traverse relations). | | `useSelectFieldUsedInAdvancedFilterDropdown` | New optional `targetFieldMetadataItem` arg. When present, the stored RecordFilter's `type` is the target field's type so the operand picker and value input render the target's operands (`'TEXT'` operators when filtering `company.name`, etc.). | | `turnRecordFilterIntoGqlOperationFilter` (shared) | When the filter targets a `RELATION` field with a `subFieldName`, synthesize a field-metadata for the target, recurse to build the inner filter, then wrap it under the relation field's name → `{ relationName: { targetFieldName: { ...operator } } }`. | `RecordFilter.subFieldName` stays narrowly typed as `CompositeFieldSubFieldName` so the wide downstream consumers (`shouldShowFilterTextInput`, composite handlers in the serializer, etc.) don't change. The relation target field's name is stored through a narrowly-scoped cast at the dropdown's storage point — the serializer checks `filter.type === 'RELATION'` before interpreting it as a target field name, so the cast can't be mis-read by composite-only code paths. ## Test plan - [ ] Open a table view on People, click "+ Filter", click "Company" → sub-menu opens with Company's filterable fields - [ ] Pick "Name" → operand picker shows TEXT operators (Contains, Equals, …) - [ ] Type "Airbnb" → filter applies, table shows people whose company name contains "Airbnb" - [ ] Verify network tab: the GraphQL filter variable is `{ company: { name: { ilike: "%Airbnb%" } } }` - [ ] Same flow with a composite target field (e.g. `Company → annualRecurringRevenue → amountMicros`) — should work end-to-end (backend supports composite-within-relation; #20527 has an integration test covering this) - [ ] Composite fields (FULL_NAME, ADDRESS) still open their normal sub-menu and filter correctly — no regression - [ ] Role-permissions field-select sub-field menu is unaffected (it bails out early on the RELATION sentinel) ## Out of scope - ONE_TO_MANY traversal (no backend support yet) - Aggregates (`people.count > 5`) - Persisting relation-traversal filters into a saved view (ViewFilter has no `relationPath` column yet; that's a separate slice) - REST API DSL changes - AI Tools 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
0d5617d446 |
chore(server): drop unused postgresCredentials feature (#20573)
## Summary Drops the `postgresCredentials` legacy feature: a never-finished "postgres proxy" that would have let users query their workspace data over a standard Postgres connection. Nothing — frontend, e2e, Zapier, docs, other server code — calls these mutations/query. ## History - **Introduced** June 2024 (#5767, Thomas Trompette) as "first step for creating credentials for database proxy", alongside the Postgres FDW / remote-server work and the custom `twenty-postgres-spilo` image. Planned follow-ups (provisioning a DB on the proxy, mapping users, exposing it as a remote server) never landed. - **Abandoned** January 2026 (#17001, Weiko) when the sibling "remote integration" feature was removed as a BREAKING CHANGE — "not maintained for more than a year and never officially launched". The spilo image was then replaced with vanilla `postgres:16` (#19182, March 2026), retiring the FDW infrastructure entirely. - This PR finishes the cleanup: removes the orphaned module, the `allPostgresCredentials` relation, `JwtTokenTypeEnum.POSTGRES_PROXY` + payload, the reserved metadata keywords, and adds a 2.5.0 fast instance command that drops `core.postgresCredentials` (reversible `down`). Regenerated frontend GraphQL types + SDK metadata client. ## Test plan - [x] `tsgo --noEmit` clean on twenty-server + twenty-front; lint + prettier clean on touched files. - [x] `database:migrate:generate` reports no pending schema diff; server boots and serves the new schema. |
||
|
|
04eb913551 |
chore(page-layout): remove IS_RECORD_PAGE_LAYOUT_* feature flags (#20556)
## Summary
- Both \`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED\` and
\`IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED\` are force-enabled on
every existing workspace by the 1.23.0 upgrade command
\`BackfillRecordPageLayoutsCommand\` and seeded enabled for new
workspaces via \`DEFAULT_FEATURE_FLAGS\` +
\`seed-feature-flags.util.ts\`. They are no longer load-bearing.
- Unwrap all \`if (flag) { … }\` conditionals to their enabled branch on
both server and front.
- Delete legacy fallback files that only the disabled branch reached:
\`PageLayoutRelationWidgetsSyncEffect\`,
\`usePageLayoutWithRelationWidgets\`,
\`reInjectDynamicRelationWidgetsFromDraft\`,
\`injectRelationWidgetsIntoLayout\`, \`isDynamicRelationWidget\` (and
their tests).
- Strip the two \`enableFeatureFlags\` calls from the 1.23 upgrade
command — the page-layout backfill data logic itself is kept intact
since old workspaces upgrading from < 1.23 still need it.
- No DB cleanup migration: stale \`featureFlag\` rows are left in place,
matching the precedent set by #20531 and #20460.
Net diff: 37 files, +106 / -1727.
## Test plan
- [x] \`npx nx typecheck twenty-shared twenty-server twenty-front\` —
all pass
- [x] \`npx nx lint:diff-with-main twenty-server twenty-front\` — all
pass
- [x] \`cd packages/twenty-front && npx jest page-layout\` — 1240 tests,
all pass
- [x] \`cd packages/twenty-server && npx jest
workspace-entity-manager.spec\` — pass
- [ ] Manual smoke: open a record page, verify tabs render and \"Edit
Layout\" command-menu action is available
- [ ] Manual smoke: Settings → Data model → object → Layout tab is
visible (and hidden for remote / Dashboard objects)
- [ ] Manual smoke: edit a tab title, save, reload — confirm persistence
|
||
|
|
e16977f97b |
[breaking: deploy server before front] feat(view-sort): pick sort sub-field inline on the chip (#20445)
## Summary Lets users choose which sub-field of a composite column to sort by — directly from the sort chip — by clicking the sub-field label and picking from a dropdown. Persists per view via a new nullable \`subFieldName\` column on \`ViewSort\`. Replaces #20438, which proposed a field-settings (admin) configuration for the same problem. The chip-level approach is more discoverable (the option lives where the user is looking) and per-view, so different views on the same object can sort by different sub-fields. ### What changes for users - **FullName columns**: previously sorted by \`firstName\` and \`lastName\` together as a stable dual-key sort. Now the user can pick which sub-field is primary (the other is the tie-breaker). Default remains \`firstName\` primary, \`lastName\` tie-breaker. - **Address columns**: previously not sortable at all (not in \`SORTABLE_FIELD_METADATA_TYPES\`). Now sortable, with a chip dropdown listing each enabled sub-field. Default is \`addressCity\` if enabled, else the first enabled sub-field. Disabling a sub-field at the field-metadata level (existing setting) removes it from the dropdown. - **Other composite types** (Currency, Phones, Emails, Links, Actor) and scalar fields keep their existing single-key sort behavior. ### UX ``` ┌─────────────────────────┐ ┌─────────────────────────┐ │ ↑ Name · Last name ✕ │ │ ↑ Address · City ✕ │ └────────┬────────────────┘ └────────┬────────────────┘ ▼ (click sub-field) ▼ ┌────────────┐ ┌────────────┐ │ First name │ │ Address 1 │ │ Last name ✓│ │ Address 2 │ └────────────┘ │ City ✓│ │ State │ │ Postcode │ │ Country │ └────────────┘ ``` The chip body still toggles direction on click — the \`Dropdown\`'s internal wrapper calls \`stopPropagation\` so the sub-field click doesn't bubble to the chip's onClick. ## What changed **Backend:** - \`ViewSortEntity\` — new nullable \`subFieldName: varchar\` column - \`ViewSortDTO\`, \`CreateViewSortInput\`, \`UpdateViewSortInputUpdates\` — new \`@Field(() => String, { nullable: true })\` - \`FLAT_VIEW_SORT_EDITABLE_PROPERTIES\` — \`'subFieldName'\` added so the property flows through the update merge path - \`ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME.viewSort\` — new \`subFieldName\` entry with \`toCompare: true\` so cache diffs notice it - \`fromCreateViewSortInputToFlatViewSortToCreate\` — threads \`subFieldName\` through - Instance command migration (\`add-sub-field-name-to-view-sort\`) — single \`ALTER TABLE core.viewSort ADD subFieldName varchar\` / \`DROP\` **Frontend:** - \`RecordSort\` and \`ViewSort\` types — \`subFieldName?: string | null\` - \`VIEW_SORT_FRAGMENT\` — adds \`subFieldName\` so the field round-trips - \`mapRecordSortToViewSort\` + \`areViewSortsEqual\` — carry the new field through, include it in the diff so the usual \`useSaveRecordSortsToViewSorts\` create/update flow fires when it changes - \`useSaveRecordSortsToViewSorts\` — passes \`subFieldName\` in both \`CreateViewSortInput\` and \`UpdateViewSortInputUpdates\` - \`getOrderByForFieldMetadataType(field, direction, subFieldName?)\` — new optional third arg. \`turnSortsIntoOrderBy\` threads \`sort.subFieldName\` into it. - \`Address\` added to \`SORTABLE_FIELD_METADATA_TYPES\` - New helpers: \`getEnabledAddressSubFields\` (filters by the field's \`subFields\` setting, falls back to the 6 default visible address sub-fields), \`getDefaultSortSubFieldForAddress\`, \`getDefaultSortSubFieldForFullName\` - New shared types/constants: \`AllowedFullNameSubField\`, \`ALLOWED_FULL_NAME_SUBFIELDS\`, \`DEFAULT_VISIBLE_ADDRESS_SUBFIELDS\` - \`SortOrFilterChip\` — new \`labelSubField?: ReactNode\` slot; renders as \` · {sub-field}\` with subdued weight after the main label - \`EditableSortChip\` — builds options from field metadata (\`ALLOWED_FULL_NAME_SUBFIELDS\` for FullName, \`getEnabledAddressSubFields\` for Address), uses i18n-wrapped labels, persists picks via \`upsertRecordSort\` ## Test plan - [x] \`npx nx typecheck\` passes for twenty-shared, twenty-front, twenty-server - [x] \`oxlint --type-aware\` on all 19 frontend + 9 server changed files: 0 errors - [x] \`prettier --check\`: clean - [x] 16 unit tests pass — \`getOrderByForFieldMetadataType\` covers the new \`subFieldName\` override branch for FULL_NAME and ADDRESS; \`getDefaultSortSubFieldForAddress\` covers the city/first-enabled fallback path; \`getDefaultSortSubFieldForFullName\` exercises its constant - [ ] Manual: sort a People view by Full Name → click the chip's sub-field label → switch between First name and Last name → reload page → choice is preserved - [ ] Manual: sort a Company view by Address → confirm dropdown lists only enabled sub-fields → disable Address \`addressCity\` in field settings → confirm dropdown options update and runtime falls back to the first enabled sub-field 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
fcd2d586ee |
chore(billing) - remove feature flag (#20531)
- remove feature flag - remove old enforce cap usage logic |
||
|
|
aecfe699f4 |
feat(ai-chat) - Stop ai thinking if credits exhausted (#20526)
Billing is now decremented per-step, not per-turn. The onStepFinish callback in chat-execution.service.ts calls a new decrementAndCheckAvailableCredits method on each model step, so Redis is debited incrementally as the agent runs rather than all at once at the end. Credit exhaustion stops the stream mid-run. When a step depletes the remaining credits, a hasNoMoreAvailableCredits flag is set and passed into the stopWhen predicate of streamText, causing the agent to halt before starting the next step. A new credits-exhausted event is introduced. After the stream drains and the response is persisted, if credits ran out the job publishes a dedicated credits-exhausted event to the frontend instead of the normal message-persisted event. The frontend handles this new event. useAgentChatSubscription has a new credits-exhausted case that sets a BILLING_CREDITS_EXHAUSTED-coded error on the atom, closes the writer, and stops the streaming state — triggering the existing AiChatCreditsExhaustedMessage UI. |
||
|
|
27fd124c2e |
Dedicated REST controllers for object & field metadata (#20364)
## Summary
- Replace the dynamic `RestApiMetadataController` (which parsed
`/rest/metadata/*path` and proxied to internal GraphQL) with two
dedicated controllers: `ObjectMetadataController` and
`FieldMetadataController`.
- Drop the GraphQL hop: reads hit Postgres directly via TypeORM
repositories; writes call the existing
`{create,update,delete}One{Object,Field}` service methods.
- Introduce a new clean response shape behind a workspace feature flag
(`IS_REST_METADATA_API_NEW_FORMAT_DIRECT`) — see grace period below.
- Update the OpenAPI spec so the REST playground reflects the (default)
legacy shape during the grace period.
## Why
The legacy metadata controller was over-complex: it routed every method
through a path parser, a set of GraphQL query-builder factories, an
internal GraphQL call, and a
`cleanGraphQLResponse` post-processor. Operation names from GraphQL
(`createOneObject`, `updateOneField`, …) leaked straight into REST
responses. The internal-GraphQL hop also gave us
nothing on metadata reads — pagination, filtering, and serialization all
happen against the same Postgres tables either way.
## Feature flag & grace period
`IS_REST_METADATA_API_NEW_FORMAT_DIRECT` (workspace-scoped):
- **Existing workspaces:** flag absent → resolves to `false` → **legacy
response shape** (no behavior change).
- **Newly created workspaces:** flag seeded to `true` via
`DEFAULT_FEATURE_FLAGS` → **new response shape** from day one.
- **Toggle:** support-assisted (no frontend); customers contact us to
opt into the new shape early.
- **Removal:** the flag, the legacy adapter utils
(`to-legacy-{object,field}-metadata-response.util.ts`), and the
parametrized test wrapper get deleted after the grace window. New shape
becomes the only shape; OpenAPI flips to new shape; POST loses the
conditional and reverts to a declarative response.
## Response shapes
| Operation | Legacy (flag OFF, default for existing) | New (flag ON) |
|-----------|-----------------------------------------|---------------|
| `GET /rest/metadata/objects` | `{ data: { objects: [...] }, pageInfo,
totalCount }` | `{ data: [...], pageInfo, totalCount }` |
| `GET /rest/metadata/objects/:id` | `{ data: { object: {...} } }` | `{
... }` |
| `POST /rest/metadata/objects` | `201 { data: { createOneObject: {...}
} }` | `201 { ... }` |
| `PATCH/PUT /rest/metadata/objects/:id` | `{ data: { updateOneObject:
{...} } }` | `{ ... }` |
| `DELETE /rest/metadata/objects/:id` | `{ data: { deleteOneObject: {
... } } }` | `{ ... }` |
Same matrix for `/rest/metadata/fields`. Cursor params
(`starting_after`, `ending_before`, `limit`) and `totalCount` are
preserved across both shapes. POST returns `201` in both (old
controller already did — the doc on main saying `200` was wrong).
## Implementation notes
- Reads go straight to Postgres with TypeORM cursor pagination
(`paginateByIdCursor` util, mutually-exclusive `starting_after` /
`ending_before`). No cache on this path — caching +
filterable pagination didn't combine cleanly.
- Object endpoints inline `fields[]` via a single follow-up `WHERE
objectMetadataId IN (...)` query.
- Controllers read the flag via `FeatureFlagService.isFeatureEnabled`
and conditionally pass the result through a legacy-shape adapter util
before returning.
- Per-domain REST exception filters
(`{Object,Field}MetadataRestApiExceptionFilter`); the `exceptionCode →
httpStatus` switch is extracted to a util so it can be merged with the
existing GraphQL handler later.
- New controllers live inside the metadata domain modules
(`metadata-modules/{object,field}-metadata/controllers/`) to match
existing precedent (view-field, view, page-layout, …).
- Removes: `RestApiMetadataController`, `RestApiMetadataService`,
`metadata/query-builder/`, `clean-graphql-response.utils.ts`.
- Integration tests are parametrized over both flag values via
`describe.each` — both shapes are asserted in CI.
- OpenAPI fixes inherited from the migration (kept as-is): documents
flat `fields: [...]` rather than the obsolete `{edges:{node:[...]}}`
wrapping; always emits `totalCount`; POST
status `201`. These match what customers actually receive on both
shapes.
Note: Next goal is to implement something similar for graphql and remove
nestjs-query dependency for those 2 entities, then generalise it.
Note2: We have the same issue with Core Rest API such as
```json
{
"data": {
"createCompany": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"createdAt": "2026-05-07T12:14:52.769Z",
"updatedAt": "2026-05-07T12:14:52.769Z",
"deletedAt": "2026-05-07T12:14:52.769Z",
...
```
with "createCompany" here which is odd compared to REST standards (FYI
@etiennejouan @charlesBochet)
## Before (Without feature flag)
<img width="1346" height="712" alt="Screenshot 2026-05-12 at 20 50 38"
src="https://github.com/user-attachments/assets/316ce225-1045-4aac-97a9-60fd537eb1ec"
/>
<img width="1378" height="729" alt="Screenshot 2026-05-12 at 20 52 24"
src="https://github.com/user-attachments/assets/a621ab6f-e4f8-44d5-817c-1efd25d33c30"
/>
## After (With feature flag)
<img width="1376" height="728" alt="Screenshot 2026-05-12 at 20 50 46"
src="https://github.com/user-attachments/assets/2424d9c5-e4ed-497c-8e5c-6b54d78675e4"
/>
<img width="1375" height="727" alt="Screenshot 2026-05-12 at 20 51 47"
src="https://github.com/user-attachments/assets/101d957f-38ed-45d9-ab7b-f4f4eb983397"
/>
---------
Co-authored-by: prastoin <paul@twenty.com>
|
||
|
|
bbc55193f5 |
Fix phone unique constraints (#20261)
## Summary Closes #20195 Fix phone field unique constraints so phone numbers are considered unique by both `primaryPhoneNumber` and `primaryPhoneCallingCode`. - Include `primaryPhoneCallingCode` in the shared phone composite unique constraint metadata - Align the frontend settings composite field config with the backend metadata - Return all included unique composite subfields when building create-many conflict fields - Match composite unique conflict fields as a group during create-many upserts ## Root Cause Phone composite metadata only marked `primaryPhoneNumber` as part of the unique constraint. That made different international phone numbers with the same national number conflict, for example `+1 123456789` and `+32 123456789`. ## Test Plan - `yarn workspace twenty-shared build` - `jest --runTestsByPath <index action handler and create-many utility specs>` - `prettier --check <touched files>` - `oxlint --type-aware <touched files>` - `nx run twenty-shared:typecheck` - `nx run twenty-server:typecheck` - `nx run twenty-front:typecheck` --------- Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
5003fcbbf2 |
chore: remove dead feature flags (#20460)
## Summary Two related cleanups, following the same pattern as #19916 and #19074. ### Dead feature flags Drops four feature flags whose only references are the enum entry and the generated GraphQL/SDK files: - `IS_COMMAND_MENU_ITEM_ENABLED` — never read anywhere. - `IS_DATASOURCE_MIGRATED` — already commented `@deprecated`. Zero non-generated consumers. - `IS_RICH_TEXT_V1_MIGRATED` — the 1-19 migration that gated it was removed in #19074; the flag became dead at that point. - `IS_CONNECTED_ACCOUNT_MIGRATED` — only read by the 1-21 `migrate-messaging-infrastructure-to-metadata` command as an early-return guard, but the flag was never written anywhere in the codebase, so the guard never fired (and that workspace command is now removed entirely — see below). Generated GraphQL/SDK schemas and the `workspace-entity-manager` test mock are trimmed to match. ### 1-21 workspace commands Same pattern as #19074 (which removed workspace commands ≤ 1.18). Twenty is now on 2-5; the 1-21 workspace commands have long since run on every active workspace and are dead code. Removes: - All 14 workspace commands under `upgrade-version-command/1-21/` (compose-email menu item, key-value-pair index, datasource backfill, message-thread backfill, dedup engine commands, select-all fixes, AI response format migration, edit-layout label, drop messaging FKs, folder parent-id migration, messaging-infra-to-metadata, navigation refactor, message-thread label fix, search-menu-item label). - The `1-21-upgrade-version-command.module.ts` registration and the `V1_21_UpgradeVersionCommandModule` import from `WorkspaceCommandProviderModule`. **Kept** (intentionally): the 3 `1-21-instance-command-fast-*` files. Unlike workspace commands (which mutate data), instance commands carry **schema deltas** still required by current entity definitions (`AddViewFieldGroupIdIndex`, `MigrateMessagingCalendarToCore`, `AddEmailThreadWidgetType`). They remain registered in `INSTANCE_COMMANDS` and `'1.21.0'` stays in `TWENTY_PREVIOUS_VERSIONS`. They will fold away naturally on a future version bump when a `CoreMigrationCheck`-style snapshot picks them up. ## Test plan - [x] `npx nx typecheck twenty-shared` - [x] `npx nx typecheck twenty-server` - [x] `npx nx typecheck twenty-front` - [x] `npx prettier --check` on the changed files - [x] `npx oxlint` on the changed server files - [x] `npx jest feature-flag` (4 suites, 20 tests pass) - [x] `npx jest workspace-entity-manager` (1 suite, 5 tests pass) |
||
|
|
1adb59f7f8 |
Support optional labels on logic-function input schema fields (#20471)
Adds an optional label field to logic-function input schema properties (InputSchemaProperty and InputJsonSchema). When set, the workflow builder renders the label in place of the raw property key for both leaf inputs and nested sections; when unset, it falls back to the key. jsonSchemaToInputSchema propagates the label so app authors can declare it in their JSON schema. Payload paths, the variable picker, and saved workflow inputs continue to use the property key — labels are display-only. |
||
|
|
ed75fc8a25 |
Use workflow inputSchema to render boolean, number, and enum fields in code/logic function steps (#20439)
<img width="415" height="772" alt="Screenshot 2026-05-11 at 4 44 08 PM" src="https://github.com/user-attachments/assets/32dbdd3c-e60b-4c43-90bc-18be05f22dcf" /> <img width="414" height="371" alt="Screenshot 2026-05-11 at 4 48 24 PM" src="https://github.com/user-attachments/assets/83be062c-7ed3-4953-98bb-e4290865040b" /> |
||
|
|
34b927ff23 |
feat(public-domain): bind public domains to apps + reorganize settings (#20360)
## Summary - **Public domains can now be bound to a specific app.** When a request hits an app-bound public domain, route resolution restricts logic-function matching to that app's HTTP-routed functions only — isolating each app's routes to its own domain instead of letting routes from other apps in the workspace match nondeterministically. - **Settings sidebar reorganized.** Removed the standalone Domains page. Workspace Domain → General. Approved Domains + Invitations → Members "Access" tab. Emailing Domains + Public Domains → Apps "Developer" tab. Roles → Members "Roles" tab. ## Why The use case: someone building a partner portal app or a lead-collection app declares private objects (leads, partners…) plus a few public HTTP routes. Each app needs its own domain (`partners.acme.com`, `leads.acme.com`) without those domains exposing every other app's routes in the same workspace. Today's PublicDomainEntity is workspace-scoped only, so all HTTP-routed logic functions in a workspace compete for any public domain — first match wins nondeterministically. ## Backend - Added nullable `applicationId` FK to `PublicDomainEntity` (cascade-deleted with the app); indexed for the route-trigger lookup. - New fast instance command `2-4-instance-command-fast-1798000003000-add-application-id-to-public-domain` adds the column, index, and FK constraint. - `createPublicDomain(domain, applicationId)` accepts an optional app binding; new `updatePublicDomain(domain, applicationId)` mutation rebinds/unbinds an existing domain. Both validate the application belongs to the workspace. - `WorkspaceDomainsService.resolveWorkspaceAndPublicDomain(origin)` returns both the workspace and the matched public domain in one query — replacing the old back-to-back lookups in the route-trigger hot path. `getWorkspaceByOriginOrDefaultWorkspace` is preserved as a thin wrapper. - `RouteTriggerService` filters `logicFunction` by `applicationId` when the matched public domain is app-scoped; falls back to workspace-wide when unbound. - Three sequential validation queries in `createPublicDomain` now run in parallel via `Promise.all`. ## Frontend | Old location | New location | |---|---| | Settings sidebar → Domains (standalone page) | Removed | | Domains page → Workspace Domain | General page | | Domains page → Approved Domains | Members → Access tab | | Domains page → Emailing Domains | Apps → Developer tab | | Domains page → Public Domains | Apps → Developer tab | | Settings sidebar → Roles (standalone) | Members → Roles tab | | `pages/settings/roles/` | `pages/settings/members/roles/` | - The Public Domain detail page has an Application picker that uses `Select`'s native `emptyOption` + `null` value pattern (matches `SettingsDataModelObjectIdentifiersForm`). - Members page tabs use the existing `TabListFromUrlOptionalEffect` mechanism (rendered automatically by `TabList`) for hash-based tab activation. - `/settings/members/roles` redirects to `/settings/members#roles` so role sub-pages' `navigate(SettingsPath.Roles)` lands on the Members page with the Roles tab pre-selected. - All affected breadcrumbs updated to nest under their new parents. - `SettingsPath.Roles` and friends now nest under `members/`; `Subdomain` and `CustomDomain` under `general/`; `PublicDomain` and `EmailingDomain` under `applications/`. ## Test plan - [x] `nx typecheck twenty-front` passes - [x] `nx typecheck twenty-server` passes - [x] `oxlint --type-aware` clean on all touched files - [x] `prettier --check` clean on all touched files - [x] Migration applied locally; `publicDomain.applicationId` (uuid, nullable) confirmed in DB - [x] GraphQL schema exposes `PublicDomain.applicationId`, `createPublicDomain.applicationId`, `updatePublicDomain` mutation - [x] **End-to-end route resolution scenarios verified locally:** - Domain bound to App A, function in App A → route matches ✅ - Domain bound to App B, function in App A → route does NOT match (HTTP 404 `TRIGGER_NOT_FOUND`) ✅ - Domain unbound (`applicationId = NULL`) → route matches workspace-wide ✅ - Unknown path on bound domain → returns 404 cleanly ✅ - [x] UI sanity (browser-tested at `apple.localhost:3001`): - General page shows Workspace Domain card - Members page shows Team / Access / Roles tabs - Access tab combines Invite by link + by email + Approved Domains - Roles tab embeds the role list - `/settings/members/roles` direct URL → redirects + Roles tab pre-selected - Apps Developer tab shows Emailing Domains + Public Domains sections - Public Domain detail page has Application picker dropdown listing workspace apps - Sidebar nav: "Domains" and "Roles" no longer present (now folded into General/Members) ## Notes for reviewers - Creating a public domain via the UI still requires Cloudflare credentials in the dev `.env` (`CLOUDFLARE_API_KEY`, `CLOUDFLARE_PUBLIC_DOMAIN_ZONE_ID`, `PUBLIC_DOMAIN_URL`). The DNS step is unchanged from main. - The `applicationId` column is nullable, so existing public-domain rows continue to work workspace-wide — no data backfill required. - `SettingsRolesContainer` was deleted (no longer referenced after `SettingsRoles` index page was removed). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a15bf1e608 | Reserve inbound subdomain for SES (#20414) | ||
|
|
4da8878697 |
feat: add email forwarding message channel (#19535)
## Summary - Add email forwarding as a new message channel type, allowing users to forward emails from addresses like `support@mycompany.com` into Twenty - Inbound emails arrive via S3 (SES → S3 bucket), are polled by a cron job, parsed, routed to the correct workspace/channel, and persisted as messages - Dedicated settings page at `/settings/accounts/new-email-forwarding` where users provide their source email handle and receive a unique forwarding address - Forwarding channels bypass the IMAP/mailbox sync state machine — they skip cron-driven sync, relaunch, and message-list-fetch lifecycle stages - Forwarding address section shown at the top of the Emails settings page so users can find/copy their addresses after initial setup - Tab names for forwarding channels display the user-provided handle (e.g. `support@mycompany.com`) instead of the internal routing address - Shared utilities extracted from IMAP driver: `extractThreadId`, `extractParticipants`, `extractAddresses` to avoid code duplication - Uses the existing S3 bucket (STORAGE_S3_*) with `inbound-email/` prefix — no separate bucket needed - Feature gated behind `isEmailForwardingEnabled` client config (requires `INBOUND_EMAIL_DOMAIN` + S3 storage) ## New backend modules - `InboundEmailS3ClientProvider` — lazy-initialized S3 client using existing storage config - `InboundEmailStorageService` — S3 operations (get, move to processed/unmatched/failed) - `InboundEmailParserService` — RFC 822 parsing via `postal-mime`, builds `MessageWithParticipants` - `InboundEmailImportService` — orchestrates download → parse → route → persist → archive - `MessagingInboundEmailPollCronJob` — polls S3 `incoming/` prefix, enqueues import jobs - `CreateEmailForwardingChannelInput` DTO — accepts user-provided `handle` ## New frontend components - `SettingsAccountsNewEmailForwardingChannel` — dedicated page with handle input form + forwarding address result - `SettingsAccountsEmailForwardingSection` — forwarding address list on the Emails settings page - `useConnectedAccountHandleMap` — shared hook for account ID → handle lookup - `useCreateEmailForwardingChannel` — mutation hook accepting handle parameter ## Test plan - [x] 17 unit tests for inbound email import service (all outcomes: imported, unmatched, loop_dropped, unconfigured, parse_failed, persist_failed) - [x] 16 tests for `computeSyncStatus` including EMAIL_FORWARDING cases - [x] 11 tests for `extractEnvelopeRecipient` utility - [x] TypeScript typechecks pass for both twenty-server and twenty-front - [x] Lint passes for both packages - [ ] Manual: create forwarding channel, verify forwarding address generated - [ ] Manual: send email to forwarding address, verify it appears in Twenty https://claude.ai/code/session_01KpyF6p4cUEnuaT4h8DP5Pm --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com> Co-authored-by: neo773 <neo773@protonmail.com> |
||
|
|
7999cd3dde |
fix: Use settings table rows and detail page for app connections (#20257)
## Summary Closes #20220 - Replace app connection-provider `SettingsListCard` rows with settings table rows that link to a per-connection detail page. - Add a connection detail page with inline display-name editing, provider and handle metadata, visibility, scopes, timestamps, reconnect, and confirmed disconnect. - Add a scoped connected-account rename mutation and persist visibility when reconnecting an existing app OAuth account. ## Tests - `./node_modules/.bin/jest packages/twenty-front/src/pages/settings/applications/__tests__/SettingsApplicationConnectionDetail.test.tsx --config packages/twenty-front/jest.config.mjs --runInBand` - `./node_modules/.bin/jest packages/twenty-front/src/pages/settings/applications/tabs/__tests__/SettingsApplicationConnectionsSection.test.tsx --config packages/twenty-front/jest.config.mjs --runInBand` - `./node_modules/.bin/jest packages/twenty-shared/src/utils/navigation/__tests__/getSettingsPath.test.ts --config packages/twenty-shared/jest.config.mjs --runInBand` - `./node_modules/.bin/jest packages/twenty-server/src/engine/metadata-modules/connected-account/resolvers/__tests__/connected-account.resolver.spec.ts --config packages/twenty-server/jest.config.mjs --runInBand` - `./node_modules/.bin/jest packages/twenty-server/src/engine/core-modules/application/application-oauth-provider/__tests__/application-oauth-provider-flow.service.spec.ts --config packages/twenty-server/jest.config.mjs --runInBand` - `./node_modules/.bin/oxlint ...` - `./node_modules/.bin/prettier --check ...` - `./node_modules/.bin/tsgo -p packages/twenty-shared/tsconfig.json` ## Notes Full frontend and server typechecks are currently blocked by unrelated existing workspace issues: - frontend implicit `any` errors in `useFrontComponentExecutionContext.ts` - server missing workspace/dependency modules such as `twenty-emails`, `twenty-client-sdk/generate`, and `@ai-sdk/azure` |
||
|
|
95bc8aea28 |
i18n - docs translations (#20366)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
9fc5be1c4c |
Billing - Migrate from Stripe metering (#20298)
**Overall strategy** **1. Introduce “Billing V2” behind a workspace flag** Gate the new model with FeatureFlagKey.IS_BILLING_V2_ENABLED so existing workspaces stay on the old behavior until they’re migrated or explicitly on V2. **2. Replace workflow metered SKUs with a resource-credit product** Conceptually, billable “workflow execution” usage is not the primary subscription line item anymore. Add a RESOURCE_CREDIT product (and keep WORKFLOW_NODE_EXECUTION as deprecated for the transition). Usage and limits are expressed through credit buckets (e.g. price metadata like credit_amount), so one product can represent pooled credits instead of a narrow workflow-only meter. **3. Migrate subscriptions in two layers** Schema/catalog: persist extra price metadata (instance upgrade) so the server knows credit amounts and can match Stripe prices to the new model. Per workspace: the registered workspace command upgrade:2-2:migrate-to-billing-v2 finds subscriptions that still have WORKFLOW_NODE_EXECUTION, swaps those items to the right RESOURCE_CREDIT prices (using existing Stripe schedule + BillingSubscriptionUpdateService stack), then treats the workspace as V2 (flag). Workspaces without that legacy item or without a subscription are skipped. **4. Unify subscription lifecycle + usage on the server** **5. Refresh the product surface in Settings** Test : - [x] Subscribe v1 + Update subscribe + Migrate - [x] Subscribe v2 + Update subscribe |
||
|
|
10876138d2 |
refactor: stop reading joinColumnName from relation field settings (#20304)
## Summary `joinColumnName` on relation field settings is always derivable from the field name (and the target object name for morph relations). This PR stops reading it from settings anywhere in production code; the stored value is no longer used. The settings field is **not** removed from data yet — a follow-up can drop it once we are confident nothing depends on the stored value. ## Helpers The helpers are split by layer because frontend and backend hold morph relations differently: the frontend has a base name plus a `morphRelations[]` array, the backend has one row per target with the name already morph-resolved. | Helper | Layer | When to use | |---|---|---| | `computeRelationGqlFieldJoinColumnName` | Shared / frontend (`gqlField`) | Non-morph relation on the frontend. | | `computeMorphRelationGqlFieldName` | Shared / frontend (`gqlField`) | Need the per-target morph gqlField name (e.g. `targetCompany`). | | `computeMorphRelationGqlFieldJoinColumnName` | Shared / frontend (`gqlField`) | Per-target morph join column on the frontend. Prefer over the non-morph helper for any morph field — it forces the per-target inputs. | | `computeMorphOrRelationFieldJoinColumnName` | Backend (`FlatFieldMetadata.name`) | Any backend read or write — the flat name is already morph-resolved, so one helper covers both cases. | | `computeMorphRelationFlatFieldName` | Backend (`FlatFieldMetadata.name`) | **Mutation paths only** (create / update / object rename). Reads consume the stored `field.name` and never call this. | ## Test plan - [x] Typecheck and lint (front, server, shared) - [x] Existing unit tests pass - [ ] CI green |
||
|
|
617f571400 |
20215 convert application variable to a syncable entity (#20269)
## Summary - Converts applicationVariable from a bespoke sync path to a proper SyncableEntity, unifying it with the workspace migration pipeline used by all other manifest-managed entities (agent, skill, frontComponent, webhook, etc.) - Removes the upsertManyApplicationVariableEntities method and its direct-DB-mutation approach in favor of the standard validate → build → run action handler pipeline - Adds universalIdentifier, deletedAt columns and makes applicationId NOT NULL via an instance command migration ## Motivation Before this change, applicationVariable was the only manifest-managed entity that bypassed ApplicationManifestMigrationService.syncMetadataFromManifest(). It used a bespoke service method called directly from syncApplication(), creating two mental models, two validation styles, and two cache invalidation patterns. Now there's one unified pipeline for all manifest entities. ## What changed ### Entity refactor: - ApplicationVariableEntity now extends SyncableEntity (gains universalIdentifier, non-nullable applicationId with CASCADE, soft-delete via deletedAt) ### New flat entity layer (flat-application-variable/): - Type, maps type, editable properties constant, entity-to-flat converter, cache service, module ### New migration pipeline wiring: - Manifest converter (fromApplicationVariableManifestToUniversalFlatApplicationVariable) - Validator service (FlatApplicationVariableValidatorService) - Builder service (WorkspaceMigrationApplicationVariableActionsBuilderService) - Create/Update/Delete action handlers with secret encryption hooks - Registered in orchestrator, builder module, runner module, and all type registries ### Removed bespoke path: - Deleted upsertManyApplicationVariableEntities from ApplicationVariableEntityService - Removed its call from ApplicationSyncService.syncApplication() - Kept update() (operator-set value at runtime) and getDisplayValue() (runtime display) ### Database migration: - Instance command to add columns, backfill universalIdentifier, enforce NOT NULL constraints, and update indexes ## Test plan - npx nx typecheck twenty-server passes (0 errors) - Unit tests pass (application-variable.service.spec.ts, build-env-var.spec.ts) - Install an app with applicationVariables in its manifest → variables appear with correct universalIdentifier - Update app manifest (add/remove/modify a variable) → migration pipeline handles diff correctly - Operator-set value via update endpoint persists correctly with encryption - Uninstall app → variables cascade-deleted - app dev --once on example app syncs without errors |
||
|
|
633553f729 |
feat(sdk): add defineCommandMenuItem (#20256)
## Summary - Add `defineCommandMenuItem` and `definePageLayoutWidget` as standalone SDK defines, mirroring the existing `definePageLayoutTab` pattern. Both entities can still be declared nested inside their parent (`defineFrontComponent.command` / `definePageLayout.tabs[].widgets[]`). - Add `CommandMenuItem` and `PageLayoutWidget` to the `SyncableEntity` enum and the dev-mode UI labels. - Wire the SDK manifest-build to extract the two new defines into top-level `commandMenuItems` / `pageLayoutWidgets` arrays on the manifest, and the server aggregator to consume them through the existing flat-entity converters. - On the server, expose `Application.commandMenuItems` (relation + DTO + service hydration in `findOneApplication`). - On the front, list command menu items in the application content tab and add a dedicated detail page with a settings tab, mirroring how `frontComponents` are surfaced. - Add `twenty add` templates and Vitest unit tests for both new defines. - Document the standalone-vs-nested pattern in `packages/twenty-sdk/README.md`. ### Why Until now, command menu items could only be declared as the nested `command:` field on `defineFrontComponent` — there was no way to register a command menu item from a separate file or from another package. The `SyncableEntity` enum had 12 values, while the server already synced 18 (including `commandMenuItem` and `pageLayoutWidget`). The same gap existed for `pageLayoutWidget`, which had no top-level define despite being synced server-side. This PR closes both gaps and aligns the SDK surface with what the server actually accepts. The standalone defines coexist with the nested form — pick one per entity, never both with the same `universalIdentifier` (the manifest aggregator will throw on duplicates). The README now documents this. ## Test plan - [x] `npx nx typecheck twenty-sdk` / `twenty-server` / `twenty-front` - [x] `npx nx lint:diff-with-main twenty-front` / `twenty-server` - [x] `npx nx lint twenty-sdk` / `twenty-shared` - [x] New unit tests: `define-command-menu-item.spec.ts`, `define-page-layout-widget.spec.ts` - [x] Existing manifest extract config tests still pass - [ ] Codegen `npx nx run twenty-front:graphql:generate --configuration=metadata` should be re-run after merge — the generated `graphql.ts` was patched manually to include `commandMenuItems` on `Application` and the `FindOneApplication` document. - [ ] Smoke test: scaffold an app with `twenty add` for both new entity types, run `twenty dev`, confirm the dev UI shows them in the sync list and the settings page surfaces command menu items in the content tab. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
d040756fcf |
remove direction from messages (#20026)
This was a leftover column removed in https://github.com/twentyhq/twenty/pull/6743 but was accidentally added again when we migrated to `buildMessageStandardFlatFieldMetadatas` from workspace decorator /closes #20011 |
||
|
|
53fdac1417 |
feat(apps): split AI tool and workflow action triggers in LogicFunction manifest (#20208)
## Summary Replaces the bolted-on `isTool` + `toolInputSchema` fields on `LogicFunctionManifest` with two distinct, opt-in triggers that align with the existing `cron` / `databaseEvent` / `httpRoute` trigger pattern: - **`toolTriggerSettings`** — exposes the function as an AI tool (chat / MCP / function calling). Uses standard JSON Schema (the format LLMs natively understand). - **`workflowActionTriggerSettings`** — exposes the function as a step in the visual workflow builder. Uses Twenty's rich `InputSchema` so the builder can render proper `FieldMetadataType`-aware editors, variable pickers, labels, and an optional `outputSchema`. A function can opt into none, one, or both. Each surface gets the schema format appropriate for it. ### Why `isTool: true` previously exposed the function as both an AI tool AND a workflow node, with the same JSON Schema feeding both — but the workflow builder really wants Twenty's `InputSchema` (with `CURRENCY`, `RELATION`, `EMAILS`, etc.) and the AI surface really wants standard JSON Schema. Today the workflow builder hacks around this by treating JSON Schema as `InputSchema`, which silently breaks for any non-primitive field type. Splitting the triggers fixes that and lets each surface evolve independently. ### Migration - **Fast** instance command adds the two new nullable columns. - **Slow** instance command backfills `toolTriggerSettings` + `workflowActionTriggerSettings` from `isTool=true` rows (preserving today's both-surfaces behaviour) then drops the legacy columns. ### Stacked Stacked on top of #20181. Merge that first, then this. ## Test plan - [ ] CI green (oxlint, typecheck, jest, vitest) - [ ] Run `--include-slow` upgrade against a workspace with existing `isTool=true` logic functions; verify both new columns populated and old columns dropped - [ ] Verify AI chat sees migrated tool functions (Linear create-issue, Exa search) and can call them with the JSON Schema - [ ] Add an AI-tool function from the Settings UI (toggles `toolTriggerSettings`) and verify it shows up in chat - [ ] Add a workflow-action function from the Settings UI (toggles `workflowActionTriggerSettings`) and verify it appears in the workflow node picker - [ ] In the workflow builder, edit a `LOGIC_FUNCTION` step and verify input fields render (no more JSON-Schema-as-InputSchema hack) - [ ] Try defining a function with no triggers in the SDK and verify `defineLogicFunction` rejects it 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: martmull <martmull@hotmail.fr> |
||
|
|
e3be1f4971 |
Make ConnectionProvider a true SyncableEntity (#20232)
## Summary PR #20181 left `ConnectionProvider` in the `SyncableEntity` enum but bypassing the standard sync pipeline — manifest sync called the bespoke `ApplicationOAuthProviderService.upsertManyFromManifest()` instead of going through the workspace-migration orchestrator like every other SyncableEntity. Anything that assumed *"all SyncableEntity values flow through the same pipeline"* (dev UI sync tracking, verification tooling) was wrong about ConnectionProvider — that's the inconsistency this PR closes. This PR follows the `.cursor/skills/syncable-entity-*` guides religiously, all six steps. ## What changes **Step 1 — Types & Constants** (`@syncable-entity-types-and-constants`) - Add `connectionProvider` to `ALL_METADATA_NAME` (twenty-shared) - Make `ApplicationOAuthProviderEntity` extend `SyncableEntity` (drops the ad-hoc columns since the base class provides them, adds `deletedAt`, drops the old `(applicationId, universalIdentifier)` unique in favour of SyncableEntity's `(workspaceId, universalIdentifier)`) - `FlatConnectionProvider`, `FlatConnectionProviderMaps`, `FLAT_CONNECTION_PROVIDER_EDITABLE_PROPERTIES`, `UniversalFlatConnectionProvider`, six action types - Register in **all** the central registries: `AllFlatEntityTypesByMetadataName`, `ALL_METADATA_ENTITY_BY_METADATA_NAME`, `ALL_ENTITY_PROPERTIES_CONFIGURATION`, `ALL_MANY_TO_ONE_*`, `ALL_ONE_TO_MANY_*`, `ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION`, `ALL_METADATA_SERIALIZED_RELATION`, `ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION`, `WORKSPACE_CACHE_KEYS_V2` (`flatConnectionProviderMaps`), `METADATA_EVENTS_TO_EMIT` - `case 'connectionProvider':` in seven discriminated-union switches (`derive-metadata-events-*`, `optimistically-apply-*`, `enrich-create-*`) **Step 2 — Cache & Transform** (`@syncable-entity-cache-and-transform`) - `WorkspaceFlatConnectionProviderMapCacheService` (extends `WorkspaceCacheProvider`, decorated with `@WorkspaceCache`, soft-delete-aware) - `fromConnectionProviderEntityToFlatConnectionProvider` util - `fromConnectionProviderManifestToUniversalFlatConnectionProvider` util - `FlatConnectionProviderModule` wires the cache service - Wired the manifest converter into `compute-application-manifest-all-universal-flat-entity-maps` **Step 3 — Builder & Validation** (`@syncable-entity-builder-and-validation`) - `FlatConnectionProviderValidatorService` — never throws, returns error arrays; uses indexed `byUniversalIdentifier` for the (name, applicationUniversalIdentifier) uniqueness check (no `Object.values().find()` on the hot path) - `WorkspaceMigrationConnectionProviderActionsBuilderService` - Registered in both validators-module + builder-module - **Wired into the orchestrator** (the most-commonly-forgotten step per the rule) — constructor inject, destructure `flatConnectionProviderMaps`, `validateAndBuild`, append actions to the final migration **Step 4 — Runner & Actions** (`@syncable-entity-runner-and-actions`) - Three handlers (create / update / delete) using the canonical `WorkspaceMigrationRunnerActionHandler` mixin - Registered in `WorkspaceSchemaMigrationRunnerActionHandlersModule` **Step 5 — Integration** (`@syncable-entity-integration`) - Delete the `upsertManyFromManifest` bypass on `ApplicationOAuthProviderService` - Remove the bypass call from `ApplicationSyncService` — manifest sync now flows through the standard pipeline - Drop `ApplicationOAuthProviderModule` from `ApplicationManifestModule` (no longer needed) - Import `FlatConnectionProviderModule` from `ApplicationOAuthProviderModule` to keep the cache discoverable - 3 new exception codes: `INVALID_CONNECTION_PROVIDER_INPUT`, `CONNECTION_PROVIDER_NOT_FOUND`, `CONNECTION_PROVIDER_NAME_ALREADY_EXISTS` **Migration** - Generated via `database:migrate:generate` (instance command `1777896012579`): drops the old `(applicationId, universalIdentifier)` unique constraint, adds `deletedAt` column, adds the `(workspaceId, universalIdentifier)` unique index that `SyncableEntity` requires. - Verified clean — a second `migrate:generate` pass produces zero drift. **Step 6 — Tests** (`@syncable-entity-testing`) - 3 new specs for the manifest converter (defaults, optional fields, all-fields) - All 32 existing OAuth-provider tests still pass - ConnectionProvider has no end-user GraphQL CRUD (it's manifest-driven only), so the GraphQL integration suite that other SyncableEntities ship doesn't apply here **Codegen** - Regenerated GraphQL artifacts (twenty-front + twenty-client-sdk) against the live schema ## Why this matters Before: - `ConnectionProvider` claimed to be a `SyncableEntity` (in the enum) - But the entity didn't extend `SyncableEntity` - And the manifest sync bypassed the standard pipeline - → Verification tooling, dev UI sync tracking, anything iterating over `ALL_METADATA_NAME` got inconsistent behaviour After: - `ConnectionProvider` is a `SyncableEntity` end-to-end - Single sync path through the workspace-migration orchestrator (same as `agent`, `skill`, `frontComponent`, `webhook`, …) - One mental model ## Out of scope (deliberate) - **Renaming the table** from `applicationOAuthProvider` to `connectionProvider` — the `metadataName` is `connectionProvider` (what consumers see in code); the table name is internal. A rename would balloon this PR with mechanical churn unrelated to the sync-pipeline wiring. Worth doing as a follow-up. - **`applicationVariable` SyncableEntity conversion** — the other manifest-sync holdout. Tracked in #20215. ## Test plan - [ ] Migration up/down clean against fresh DB - [ ] Install an app whose manifest declares connection providers — providers appear in the workspace - [ ] Re-deploy the app with one provider added, one removed, one renamed → all reconciled correctly via the sync pipeline - [ ] Verify the dev-UI sync-tracking page shows ConnectionProvider entries the same way it shows agents/skills/etc - [ ] OAuth flow still works (existing connections, new connections, reconnect, list/get from SDK) — should be unchanged since the runtime code path didn't move 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4852ac401a |
Add server upgrade status on admin panel (#20107)
## Summary Adds an admin upgrade-status panel that surfaces per-instance and per-workspace migration health, backed by a Redis-cached aggregate to keep the page snappy on large fleets. <img width="827" height="880" alt="Screenshot 2026-04-28 at 10 21 03" src="https://github.com/user-attachments/assets/8f88baa9-7268-4eff-bf6a-906a7f06ca91" /> <img width="804" height="892" alt="Screenshot 2026-04-28 at 10 21 11" src="https://github.com/user-attachments/assets/1e6decf8-766a-4d0e-96b1-03a9962bba3c" /> ## Computed metrics **Instance** (`InstanceUpgradeStatus`) - `inferredVersion` — version derived from the latest non-initial instance command name - `health` — `upToDate` | `behind` | `failed`, derived from the latest attempt vs. the last expected instance step in the upgrade sequence - `latestCommand` — `{ name, status, executedByVersion, errorMessage, createdAt }` from the most recent attempt **Per-workspace** (`WorkspaceUpgradeStatus`) - `workspaceId`, `displayName` - `inferredVersion`, `health`, `latestCommand` (same shape as instance), computed against the latest expected step in the sequence **Aggregate** (`AllWorkspacesUpgradeStatus`, only across `ACTIVE` / `SUSPENDED` workspaces) - `instanceUpgradeStatus` - `totalCount`, `upToDateCount`, `behindCount`, `failedCount` - `workspacesBehindIds[]`, `workspacesFailedIds[]` - `computedAt` ## Fetching strategy All reads go through `UpgradeStatusCacheService` (cache namespace: `EngineHealth`). - **Aggregate read** (`getAllWorkspacesStatus` → `getAllWorkspacesUpgradeStatus` query): reads summary + behind-ids + failed-ids in parallel; if any of the three keys is missing, full recompute (`recomputeAllWorkspaces`) is triggered, which also primes per-workspace entries. - **Per-workspace read** (`getWorkspacesStatus(ids)` → `getUpgradeStatus(ids)` query): `mget` on workspace keys; misses are recomputed individually (`recomputeWorkspace`), and aggregates are reconciled in place (count + id list deltas) without a full recompute. - **Recompute on demand**: `refreshUpgradeStatus` mutation calls `recomputeAllWorkspaces` to bypass cache and rewrite all keys. - **Auto-invalidation**: `InstanceCommandRunnerService` (fast + slow paths) and `WorkspaceCommandRunnerService` invalidate after every run via `safeInvalidateUpgradeStatusCache()` (`flushByPattern('upgrade-status:*')`). Failures in cache invalidation are swallowed and logged so they never break the migration runner. - **TTL**: `60 * 60 * 1000` ms (1 hour) on every key — protects against stale data even if a runner crashes before invalidating. ## Introduced cache keys All under the `EngineHealth` cache-storage namespace: | Key | Type | Purpose | | --- | --- | --- | | `upgrade-status:all-workspaces:summary` | `CachedAllWorkspacesStatusSummary` | Counts + instance status + `computedAt` | | `upgrade-status:all-workspaces:behind-ids` | `string[]` | Workspace ids in `behind` state | | `upgrade-status:all-workspaces:failed-ids` | `string[]` | Workspace ids in `failed` state | | `upgrade-status:workspace:<workspaceId>` | `CachedWorkspaceUpgradeStatus` | Per-workspace status (one key per workspace) | Full invalidation uses the pattern `upgrade-status:*`. ## Index added on `upgradeMigration` (already added on prod) Migration `2-2-instance-command-fast-1777308014234-addUpgradeMigrationWorkspaceIdIndex.ts`: ```sql CREATE INDEX "IDX_upgradeMigration_workspaceId_name_attempt" ON "core"."upgradeMigration" ("workspaceId", "name", "attempt") WHERE "workspaceId" IS NOT NULL; |
||
|
|
91124a3cb8 |
AI - Add azure foundry provider (#20170)
[Merge this before](https://github.com/twentyhq/twenty-infra/pull/655) Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
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>
|
||
|
|
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" /> |
||
|
|
8f362186ce |
Redesign application content tab + logic function settings; add Layout detail pages (#20056)
## Summary Iterative redesign of two related areas in settings, plus a new `pages/settings/layout/` folder for read-only entity detail pages. ### Application content tab - **Grouped into three sections** — Data / Layout / Logic — each with one H2 + multiple `TableSection`-wrapped sub-tables (mirrors the role-permissions pattern). Replaces six per-category table/row components with one uniform `<SettingsApplicationContentSubtable>` + `ApplicationContentRow` shape (net **−~700 lines** across the refactor). - **All 10 row categories now clickable** for installed apps: - Objects / Fields / Logic functions / Front components → existing detail pages - Agents → existing `AiAgentDetail` - Skills → existing `AiSkillDetail` (looked up by `Skill.applicationId + name`) - Roles → existing `RoleDetail` (looked up by `Role.universalIdentifier`) - Views / Page layouts / Navigation menu items → **new** detail pages (see below) - **Lifecycle hooks visible** — `pre-install` / `post-install` logic functions are surfaced in the Trigger column instead of appearing as empty/misconfigured. ### Logic function settings (Triggers + Test tabs) - Triggers tab is now editable (HTTP / Cron / Database event / AI tool) with a `<SettingsLogicFunctionTriggerSection>` wrapper that owns the toggle, header, and read-only short-circuit. - HTTP section gets a Live URL field with copy-to-clipboard. - Each section shows a **Sample input** preview (the JSON the function will receive) using the same payload builders the Test tab uses. - Test tab: **Simulate trigger** buttons that prefill the JSON input from the configured trigger's schema. Replaces an unclickable `<Select>` (which auto-disables when there's only one option — the typical case). - Read-only behavior for installed-app functions: explicit `<Callout>` notice when there's no trigger; trigger sections render as disabled controls when there is one. - Removed the empty Environment Variables section from the Settings tab (it just told the user to go elsewhere). ### New `pages/settings/layout/` folder Three new app-scoped detail pages so users can drill into entities the GraphQL `Application` type doesn't expose by id (keyed by manifest `universalIdentifier`): - `ApplicationViewDetail` — type, object, visibility + Fields / Filters / Sorts subsections (field UIDs resolved to readable labels via `useFieldLabelByUid`) - `ApplicationPageLayoutDetail` — type, object + per-tab subsections listing widgets - `ApplicationNavigationMenuItemDetail` — type, destination (resolved), icon, color, position Each page reads from the marketplace manifest the parent app page already loads (no extra queries). Folder set up so a future "Layout" settings tab can grow here (analogous to the existing `data-model/` folder under the Data tab). ### Other consistency fixes - Breadcrumbs on every app-scoped entity detail page now include a category crumb so users know what they're looking at: `Workspace / Applications / Timely / Navigation menu items / Time entry`. - Title fallback for nav menu items uses the resolved destination (`"Time entry"`) instead of the raw enum (`"OBJECT"`). - New shared utils: `getNavigationMenuItemDestination`, `resolveManifestObjectLabel`, `getLogicFunctionTriggerLabel`, `<MonoText>`. ## Backend changes Only one minor schema-shape change (additive): added `applicationId` to the `SkillFields` GraphQL fragment and `universalIdentifier` to the `RoleFragment` so the new lookups have what they need. Generated metadata schema patched in-tree to match — regenerate with `nx run twenty-front:graphql:generate --configuration=metadata` if it drifts. ## Test plan - [ ] Application content tab on an installed app shows the 3 grouped sections; rows in each section are clickable - [ ] Click an Object → existing object detail page - [ ] Click a Field → existing field-edit page - [ ] Click an Agent / Skill / Role → existing detail page - [ ] Click a View / Page layout / Navigation menu item → new read-only detail page; subsections (Fields/Filters/Sorts for views, per-tab widgets for page layouts) populate correctly - [ ] Breadcrumbs on every entity detail page have 5 crumbs ending in `<Category> / <Entity name>` - [ ] Logic function Triggers tab: toggle each trigger type on/off, see the Sample input preview update; for installed apps, sections render as read-only - [ ] Test tab: each "Simulate trigger" button prefills the JSON editor with the matching payload shape - [ ] Functions list: a function configured as `post-install` shows "Post-install" in the Trigger column 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: claude[bot] <claude[bot]@users.noreply.github.com> |
||
|
|
a5cd64daf5 |
refactor: standardize JsonStringified casing (#20101)
## Summary - Rename safeParseRelativeDateFilterJSONStringified to safeParseRelativeDateFilterJsonStringified - Update the matching utility file, exports, tests, and workflow usages Part of #19839. ## Validation - CI passed |
||
|
|
a90895e167 |
[Website] locale-segment routing and shared Lingui factory (#20079)
**1. Shared Lingui factory in `twenty-shared`**
- Extracted `createI18nInstanceFactory` into
`packages/twenty-shared/src/i18n/create-i18n-instance-factory.ts` so
every package gets the same per-render Lingui bootstrap with a
per-locale singleton cache and a `SOURCE_LOCALE` fallback.
- `twenty-emails/src/utils/i18n.utils.ts` now consumes the shared
factory.
**2. `twenty-website-new` Lingui bootstrap + Crowdin wiring**
- `lingui.config.ts`, `src/lib/i18n/*`, `nx run
twenty-website-new:lingui:{extract,compile}`.
- 31 locale PO files generated; minified compiled output kept out of
Prettier and Oxlint.
- `i18n-{push,pull}.yaml` workflows updated to include
`twenty-website-new` in Crowdin sync.
**3. `app/[locale]/...` segment routing with English at the root**
- All marketing routes moved under `src/app/[locale]/`; static
generation preserved (15 routes × 31 locales = 465 prerendered URLs).
- Middleware behavior:
- `/{en}/...` → 301 redirect to unprefixed canonical.
- `/{non-en}/...` → pass through, set `NEXT_LOCALE` cookie.
### What this PR explicitly does not do (deferred)
- Lingui-wrapping the actual marketing copy. Keys, build pipeline, and
runtime are wired; copy migration is a separate, reviewer-friendlier
PR.
|
||
|
|
3db1af9a17 |
fix(logic-function): forward raw request body for HMAC signature verification (#20061)
## Summary
- Add optional `rawBody?: string` to `LogicFunctionEvent` and forward it
from the route trigger so HMAC-based webhook signatures (GitHub's
`X-Hub-Signature-256`, Stripe, …) can be verified by user logic
functions.
- Update `github-connector`'s `getRawBodyForSignature` to prefer
`event.rawBody` (with the existing string/base64/null fallbacks kept for
older runtimes).
## Why
GitHub computes `X-Hub-Signature-256` over the **raw bytes** of the
request body. The receiver must verify against those exact bytes — key
order, whitespace and unicode escaping all matter, so the parsed JSON
body cannot be re-serialized to them.
Today the route trigger calls `extractBody(request)` which returns the
parsed object only. NestJS already preserves the raw body on
`request.rawBody` (the app is bootstrapped with `rawBody: true` in
`main.ts`), but it was never propagated into `LogicFunctionEvent`.
As a result the github-connector's webhook handler always took the "raw
body unavailable" branch and rejected every delivery (after #19961 /
|
||
|
|
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" /> |
||
|
|
6c1c0737b0 |
Clarify registry tools vs native model tool binding (#20022)
## Intent This is a small foundation cleanup for the tool architecture. The main decision is: registry tools and native SDK/model tools are different things. - Registry tools have descriptors, schemas, catalog entries, and execute through `ToolExecutorService` - Native model tools are opaque AI SDK objects, bound directly into the model `ToolSet` - Surfaces still own their policy: chat, MCP, and workflow agents decide what they expose ## What changed - Removed `NATIVE_MODEL` from `ToolCategory` - Kept `ToolRegistryService` focused on registry-backed tools only - Moved native model tool binding through `NativeToolBinderService` - Reused native binding from chat instead of duplicating provider-specific web-search logic - Kept MCP local execution exclusions in a dedicated constant - Moved surface-specific constants into dedicated constant files ## What comes next - Move hardcoded chat app preloads, like Exa web search, into app/manifest metadata - Decide a clearer policy for local runtime tools like code interpreter and HTTP request - Gradually document the three tool shapes: registry tools, native model tools, and local runtime tools --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
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.
|