From 34b927ff233be152d9ac6219ba1db72d55b893e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Sun, 10 May 2026 20:17:28 +0200 Subject: [PATCH] feat(public-domain): bind public domains to apps + reorganize settings (#20360) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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) --- .../src/metadata/generated/schema.graphql | 4 +- .../src/metadata/generated/schema.ts | 6 +- .../src/metadata/generated/types.ts | 18 + .../twenty-docs/developers/extend/api.mdx | 2 +- .../developers/extend/capabilities/apis.mdx | 2 +- .../permissions-access-control.mdx | 2 +- .../user-guide/ai/how-tos/ai-faq.mdx | 2 +- .../twenty-docs/user-guide/ai/overview.mdx | 2 +- .../how-tos/migrating-from-other-crms.mdx | 2 +- .../migrating-from-self-hosted-to-cloud.mdx | 2 +- .../capabilities/permissions.mdx | 14 +- .../how-tos/permissions-faq.mdx | 4 +- .../capabilities/domains-settings.mdx | 15 +- .../capabilities/member-management.mdx | 2 +- .../settings/how-tos/settings-faq.mdx | 4 +- .../user-guide/settings/overview.mdx | 2 +- .../show-expected-amount-in-pipeline.mdx | 2 +- .../how-tos/track-time-in-stage.mdx | 2 +- .../capabilities/workflow-actions.mdx | 2 +- .../how-tos/need-more-help/workflows-faq.mdx | 2 +- .../src/generated-metadata/graphql.ts | 27 +- .../modules/app/components/SettingsRoutes.tsx | 43 +- .../components/SettingPublicDomain.tsx | 88 ++- .../components/SettingsCustomDomain.tsx | 6 +- .../domains/components/SettingsSubdomain.tsx | 6 +- .../graphql/mutations/createPublicDomain.ts | 5 +- .../graphql/mutations/updatePublicDomain.ts | 13 + .../graphql/queries/findManyPublicDomains.ts | 1 + .../hooks/useSettingsNavigationItems.tsx | 14 - .../components/SettingsRolesContainer.tsx | 39 -- ...gsRolePermissionsObjectLevelObjectForm.tsx | 4 + .../roles/role/components/SettingsRole.tsx | 4 + .../useCreateWorkspaceInvitation.test.tsx | 5 +- .../useDeleteWorkspaceInvitation.test.tsx | 3 +- .../useResendWorkspaceInvitation.test.tsx | 3 +- .../hooks/useCreateWorkspaceInvitation.ts | 11 +- .../hooks/useDeleteWorkspaceInvitation.ts | 12 +- .../hooks/useResendWorkspaceInvitation.ts | 13 +- .../states/workspaceInvitationsStates.ts | 9 - .../src/pages/settings/SettingsWorkspace.tsx | 17 +- .../tabs/SettingsApplicationsDeveloperTab.tsx | 29 + .../settings/domains/SettingsDomains.tsx | 98 --- .../SettingsEmailingDomainDetail.tsx | 2 +- .../SettingsNewEmailingDomain.tsx | 8 +- .../members/SettingsWorkspaceMembers.tsx | 576 ++---------------- .../roles/SettingsRoleAddObjectLevel.tsx | 8 + .../roles/SettingsRoleCreate.tsx | 0 .../{ => members}/roles/SettingsRoleEdit.tsx | 0 .../roles/SettingsRoleObjectLevel.tsx | 0 .../SettingsRoleCreate.stories.tsx | 4 +- .../__stories__/SettingsRoleEdit.stories.tsx | 4 +- .../SettingsWorkspaceMembersAccessTab.tsx | 249 ++++++++ .../tabs/SettingsWorkspaceMembersRolesTab.tsx | 15 + .../tabs/SettingsWorkspaceMembersTeamTab.tsx | 313 ++++++++++ .../pages/settings/roles/SettingsRoles.tsx | 11 - .../__stories__/SettingsRoles.stories.tsx | 47 -- .../SettingsSecurityApprovedAccessDomain.tsx | 8 +- ...000-add-application-id-to-public-domain.ts | 33 + .../instance-commands.constant.ts | 2 + .../approved-access-domain.service.ts | 2 +- .../services/approved-access-domain.spec.ts | 2 +- .../services/workspace-domains.service.ts | 49 +- .../triggers/route/route-trigger.service.ts | 10 +- .../dtos/create-public-domain.input.ts | 16 + .../public-domain/dtos/public-domain.dto.ts | 3 + .../dtos/update-public-domain.input.ts | 16 + .../public-domain-exception-filter.ts | 1 + .../public-domain/public-domain.entity.ts | 16 + .../public-domain/public-domain.exception.ts | 3 + .../public-domain/public-domain.module.ts | 7 +- .../public-domain/public-domain.resolver.ts | 17 +- .../public-domain/public-domain.service.ts | 87 ++- .../twenty-shared/src/types/SettingsPath.ts | 23 +- 73 files changed, 1168 insertions(+), 905 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/domains/graphql/mutations/updatePublicDomain.ts delete mode 100644 packages/twenty-front/src/modules/settings/roles/components/SettingsRolesContainer.tsx delete mode 100644 packages/twenty-front/src/modules/workspace-invitation/states/workspaceInvitationsStates.ts delete mode 100644 packages/twenty-front/src/pages/settings/domains/SettingsDomains.tsx rename packages/twenty-front/src/pages/settings/{ => members}/roles/SettingsRoleAddObjectLevel.tsx (92%) rename packages/twenty-front/src/pages/settings/{ => members}/roles/SettingsRoleCreate.tsx (100%) rename packages/twenty-front/src/pages/settings/{ => members}/roles/SettingsRoleEdit.tsx (100%) rename packages/twenty-front/src/pages/settings/{ => members}/roles/SettingsRoleObjectLevel.tsx (100%) rename packages/twenty-front/src/pages/settings/{ => members}/roles/__stories__/SettingsRoleCreate.stories.tsx (80%) rename packages/twenty-front/src/pages/settings/{ => members}/roles/__stories__/SettingsRoleEdit.stories.tsx (81%) create mode 100644 packages/twenty-front/src/pages/settings/members/tabs/SettingsWorkspaceMembersAccessTab.tsx create mode 100644 packages/twenty-front/src/pages/settings/members/tabs/SettingsWorkspaceMembersRolesTab.tsx create mode 100644 packages/twenty-front/src/pages/settings/members/tabs/SettingsWorkspaceMembersTeamTab.tsx delete mode 100644 packages/twenty-front/src/pages/settings/roles/SettingsRoles.tsx delete mode 100644 packages/twenty-front/src/pages/settings/roles/__stories__/SettingsRoles.stories.tsx create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1798000003000-add-application-id-to-public-domain.ts create mode 100644 packages/twenty-server/src/engine/core-modules/public-domain/dtos/create-public-domain.input.ts create mode 100644 packages/twenty-server/src/engine/core-modules/public-domain/dtos/update-public-domain.input.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 0f2ab51ce1..32241f9645 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -2356,6 +2356,7 @@ type PublicDomain { id: UUID! domain: String! isValidated: Boolean! + applicationId: UUID createdAt: DateTime! } @@ -3327,7 +3328,8 @@ type Mutation { updateLabPublicFeatureFlag(input: UpdateLabPublicFeatureFlagInput!): FeatureFlag! enablePostgresProxy: PostgresCredentials! disablePostgresProxy: PostgresCredentials! - createPublicDomain(domain: String!): PublicDomain! + createPublicDomain(domain: String!, applicationId: String): PublicDomain! + updatePublicDomain(domain: String!, applicationId: String): PublicDomain! deletePublicDomain(domain: String!): Boolean! checkPublicDomainValidRecords(domain: String!): DomainValidRecords createEmailingDomain(domain: String!, driver: EmailingDomainDriver!): EmailingDomain! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 4919e7ef7f..38dec16073 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -2026,6 +2026,7 @@ export interface PublicDomain { id: Scalars['UUID'] domain: Scalars['String'] isValidated: Scalars['Boolean'] + applicationId?: Scalars['UUID'] createdAt: Scalars['DateTime'] __typename: 'PublicDomain' } @@ -2856,6 +2857,7 @@ export interface Mutation { enablePostgresProxy: PostgresCredentials disablePostgresProxy: PostgresCredentials createPublicDomain: PublicDomain + updatePublicDomain: PublicDomain deletePublicDomain: Scalars['Boolean'] checkPublicDomainValidRecords?: DomainValidRecords createEmailingDomain: EmailingDomain @@ -5035,6 +5037,7 @@ export interface PublicDomainGenqlSelection{ id?: boolean | number domain?: boolean | number isValidated?: boolean | number + applicationId?: boolean | number createdAt?: boolean | number __typename?: boolean | number __scalar?: boolean | number @@ -5922,7 +5925,8 @@ export interface MutationGenqlSelection{ updateLabPublicFeatureFlag?: (FeatureFlagGenqlSelection & { __args: {input: UpdateLabPublicFeatureFlagInput} }) enablePostgresProxy?: PostgresCredentialsGenqlSelection disablePostgresProxy?: PostgresCredentialsGenqlSelection - createPublicDomain?: (PublicDomainGenqlSelection & { __args: {domain: Scalars['String']} }) + createPublicDomain?: (PublicDomainGenqlSelection & { __args: {domain: Scalars['String'], applicationId?: (Scalars['String'] | null)} }) + updatePublicDomain?: (PublicDomainGenqlSelection & { __args: {domain: Scalars['String'], applicationId?: (Scalars['String'] | null)} }) deletePublicDomain?: { __args: {domain: Scalars['String']} } checkPublicDomainValidRecords?: (DomainValidRecordsGenqlSelection & { __args: {domain: Scalars['String']} }) createEmailingDomain?: (EmailingDomainGenqlSelection & { __args: {domain: Scalars['String'], driver: EmailingDomainDriver} }) diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index d715db5293..8ae7417cd7 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -4616,6 +4616,9 @@ export default { "isValidated": [ 6 ], + "applicationId": [ + 3 + ], "createdAt": [ 4 ], @@ -8573,6 +8576,21 @@ export default { "domain": [ 1, "String!" + ], + "applicationId": [ + 1 + ] + } + ], + "updatePublicDomain": [ + 258, + { + "domain": [ + 1, + "String!" + ], + "applicationId": [ + 1 ] } ], diff --git a/packages/twenty-docs/developers/extend/api.mdx b/packages/twenty-docs/developers/extend/api.mdx index 1fcb95f917..e450972f04 100644 --- a/packages/twenty-docs/developers/extend/api.mdx +++ b/packages/twenty-docs/developers/extend/api.mdx @@ -37,7 +37,7 @@ Both are available as REST and GraphQL. GraphQL adds batch upserts and the abili Authorization: Bearer YOUR_API_KEY ``` -Create an API key in **Settings → API & Webhooks → + Create key**. Copy it immediately — it's shown once. Keys can be scoped to a specific role under **Settings → Roles → Assignment tab** to limit what they can access. +Create an API key in **Settings → API & Webhooks → + Create key**. Copy it immediately — it's shown once. Keys can be scoped to a specific role under **Settings → Members → Roles → Assignment tab** to limit what they can access. diff --git a/packages/twenty-docs/developers/extend/capabilities/apis.mdx b/packages/twenty-docs/developers/extend/capabilities/apis.mdx index e3b9b8e37b..08216f0110 100644 --- a/packages/twenty-docs/developers/extend/capabilities/apis.mdx +++ b/packages/twenty-docs/developers/extend/capabilities/apis.mdx @@ -83,7 +83,7 @@ Your API key grants access to sensitive data. Don't share it with untrusted serv For better security, assign a specific role to limit access: -1. Go to **Settings → Roles** +1. Go to **Settings → Members → Roles** 2. Click on the role to assign 3. Open the **Assignment** tab 4. Under **API Keys**, click **+ Assign to API key** diff --git a/packages/twenty-docs/user-guide/ai/capabilities/permissions-access-control.mdx b/packages/twenty-docs/user-guide/ai/capabilities/permissions-access-control.mdx index afa8063e51..17c3fc4ba7 100644 --- a/packages/twenty-docs/user-guide/ai/capabilities/permissions-access-control.mdx +++ b/packages/twenty-docs/user-guide/ai/capabilities/permissions-access-control.mdx @@ -9,7 +9,7 @@ AI agents respect your existing permission structure. This is particularly impor ## Assign a Role to an AI Agent -1. Go to **Settings → Roles** +1. Go to **Settings → Members → Roles** 2. Click on the role you want to assign 3. Open the **Assignment** tab 4. Under **AI Agents**, click **+ Assign to AI agent** diff --git a/packages/twenty-docs/user-guide/ai/how-tos/ai-faq.mdx b/packages/twenty-docs/user-guide/ai/how-tos/ai-faq.mdx index e0c03e2fb9..c36d3540f1 100644 --- a/packages/twenty-docs/user-guide/ai/how-tos/ai-faq.mdx +++ b/packages/twenty-docs/user-guide/ai/how-tos/ai-faq.mdx @@ -17,7 +17,7 @@ description: Frequently asked questions about AI features in Twenty. - AI agents will operate under the permission system. You can assign specific roles to AI agents under **Settings → Roles**, giving you full control over what data they can access and what actions they can perform. + AI agents will operate under the permission system. You can assign specific roles to AI agents under **Settings → Members → Roles**, giving you full control over what data they can access and what actions they can perform. diff --git a/packages/twenty-docs/user-guide/ai/overview.mdx b/packages/twenty-docs/user-guide/ai/overview.mdx index 5d7aa93f91..a9ecd46329 100644 --- a/packages/twenty-docs/user-guide/ai/overview.mdx +++ b/packages/twenty-docs/user-guide/ai/overview.mdx @@ -44,7 +44,7 @@ Extend your workflows with AI-powered actions and autonomous agents. AI agents will be managed through the existing permissions system: -1. Go to **Settings → Roles** +1. Go to **Settings → Members → Roles** 2. Configure which data each AI agent can access 3. Set read/write permissions per object diff --git a/packages/twenty-docs/user-guide/data-migration/how-tos/migrating-from-other-crms.mdx b/packages/twenty-docs/user-guide/data-migration/how-tos/migrating-from-other-crms.mdx index 5b261ff33b..42de0b9bf5 100644 --- a/packages/twenty-docs/user-guide/data-migration/how-tos/migrating-from-other-crms.mdx +++ b/packages/twenty-docs/user-guide/data-migration/how-tos/migrating-from-other-crms.mdx @@ -204,7 +204,7 @@ After importing data, complete your workspace configuration: - Test each one before relying on it ### Configure Roles and Permissions -- Set up roles in **Settings → Roles** +- Set up roles in **Settings → Members → Roles** - Assign users to appropriate roles ### Connect Email and Calendar diff --git a/packages/twenty-docs/user-guide/data-migration/how-tos/migrating-from-self-hosted-to-cloud.mdx b/packages/twenty-docs/user-guide/data-migration/how-tos/migrating-from-self-hosted-to-cloud.mdx index 4694aef5c0..1dbb113a06 100644 --- a/packages/twenty-docs/user-guide/data-migration/how-tos/migrating-from-self-hosted-to-cloud.mdx +++ b/packages/twenty-docs/user-guide/data-migration/how-tos/migrating-from-self-hosted-to-cloud.mdx @@ -121,7 +121,7 @@ After importing data, manually recreate: - Test each workflow before relying on it ### Roles and Permissions -- Configure roles in **Settings → Roles** +- Configure roles in **Settings → Members → Roles** - Assign users to appropriate roles ### Integrations diff --git a/packages/twenty-docs/user-guide/permissions-access/capabilities/permissions.mdx b/packages/twenty-docs/user-guide/permissions-access/capabilities/permissions.mdx index 217453aee8..3cb8840592 100644 --- a/packages/twenty-docs/user-guide/permissions-access/capabilities/permissions.mdx +++ b/packages/twenty-docs/user-guide/permissions-access/capabilities/permissions.mdx @@ -13,7 +13,7 @@ Twenty's permission system allows you to control access to three main areas: To create a new role: -1. Go to **Settings → Roles** +1. Go to **Settings → Members → Roles** 2. Under **All Roles**, click on **+ Create Role** 3. Enter a role name 4. In the default **Permissions** tab, [configure permissions](#customize-permissions) @@ -23,7 +23,7 @@ To create a new role: To delete a role: -1. Go to **Settings → Roles** +1. Go to **Settings → Members → Roles** 2. Click on the role you want to remove 3. Open the **Settings** tab, then click **Delete Role** 4. Click **Confirm** in the modal @@ -35,12 +35,12 @@ If a role is deleted, any workspace member assigned to it will be automatically ## Assign Roles to Members ### View Current Assignments -- Go to **Settings → Roles** +- Go to **Settings → Members → Roles** - See all roles and how many members are assigned to each - View which members have which roles ### Assign a Role to a Member -1. Go to **Settings → Roles** +1. Go to **Settings → Members → Roles** 2. Click on the role you want to assign 3. Open the **Assignment** tab 4. Click **+ Assign to member** @@ -48,7 +48,7 @@ If a role is deleted, any workspace member assigned to it will be automatically 6. Confirm the assignment ### Set Default Role -1. Go to **Settings → Roles** +1. Go to **Settings → Members → Roles** 2. In the **Options** section, find **Default Role** 3. Select which role new members should automatically receive 4. New workspace members will be assigned this role when they join @@ -164,7 +164,7 @@ Beyond workspace members, roles can also be assigned to **API Keys** and **AI Ag ### Assign a Role to an API Key -1. Go to **Settings → Roles** +1. Go to **Settings → Members → Roles** 2. Click on the role you want to assign 3. Open the **Assignment** tab 4. Under **API Keys**, click **+ Assign to API key** @@ -179,7 +179,7 @@ API keys without an assigned role use default permissions. For tighter security, ### Assign a Role to an AI Agent -1. Go to **Settings → Roles** +1. Go to **Settings → Members → Roles** 2. Click on the role you want to assign 3. Open the **Assignment** tab 4. Under **AI Agents**, click **+ Assign to AI agent** diff --git a/packages/twenty-docs/user-guide/permissions-access/how-tos/permissions-faq.mdx b/packages/twenty-docs/user-guide/permissions-access/how-tos/permissions-faq.mdx index ff9a3e55a7..0d5510508a 100644 --- a/packages/twenty-docs/user-guide/permissions-access/how-tos/permissions-faq.mdx +++ b/packages/twenty-docs/user-guide/permissions-access/how-tos/permissions-faq.mdx @@ -19,7 +19,7 @@ Any workspace member assigned to that role will be automatically reassigned to t -Go to **Settings → Roles**, find the **Default Role** option, and select which role new members should automatically receive when they join. +Go to **Settings → Members → Roles**, find the **Default Role** option, and select which role new members should automatically receive when they join. @@ -64,7 +64,7 @@ Row-level permissions will be available on the **Organization** plan by Q1 2026. -1. Go to **Settings → Roles** +1. Go to **Settings → Members → Roles** 2. Select the role 3. Navigate to the object containing the field 4. Set the field permission to **See Field** (without Edit Field) diff --git a/packages/twenty-docs/user-guide/settings/capabilities/domains-settings.mdx b/packages/twenty-docs/user-guide/settings/capabilities/domains-settings.mdx index 3909033033..de369d3dc4 100644 --- a/packages/twenty-docs/user-guide/settings/capabilities/domains-settings.mdx +++ b/packages/twenty-docs/user-guide/settings/capabilities/domains-settings.mdx @@ -3,10 +3,12 @@ title: Domain Settings description: Configure workspace domain, approved access domains, and public domains. --- -Configure domain settings under **Settings → Domains**. +Domain settings live in three places, depending on what you're configuring. ## Workspace Domain +Configure under **Settings → General → Workspace Domain**. + Edit your subdomain name or set a custom domain for your workspace. ### Customize Domain @@ -18,6 +20,8 @@ For custom domains, you'll need to configure DNS settings with your domain provi ## Approved Domains +Configure under **Settings → Members → Access**. + Anyone with an email address at these domains is allowed to sign up for this workspace automatically. ### Add Approved Access Domain @@ -33,12 +37,15 @@ This is useful for allowing your entire team to self-register while keeping the ## Public Domains -Provision a complete and secure hosting environment on these domains. +Configure under **Settings → Apps → Developer**. + +Provision a complete and secure hosting environment on these domains. A public domain can be bound to a specific app — when bound, only that app's HTTP-routed logic functions are reachable on the domain. Leave the binding empty to expose all workspace HTTP routes. ### Add Public Domain 1. Click **Add Public Domain** 2. Enter the domain you want to use -3. Configure DNS settings as instructed -4. Verify the domain +3. Optionally bind it to an app +4. Configure DNS settings as instructed +5. Verify the domain SSL certificates are automatically provisioned for public domains. diff --git a/packages/twenty-docs/user-guide/settings/capabilities/member-management.mdx b/packages/twenty-docs/user-guide/settings/capabilities/member-management.mdx index ade88d72e0..2483a24338 100644 --- a/packages/twenty-docs/user-guide/settings/capabilities/member-management.mdx +++ b/packages/twenty-docs/user-guide/settings/capabilities/member-management.mdx @@ -67,7 +67,7 @@ Manage invitations that haven't been accepted: Allow team members to join automatically based on their email domain: -1. Go to **Settings → Domains** +1. Go to **Settings → Members → Access** 2. Add your company domain (e.g., `yourcompany.com`) 3. Anyone with that email domain can join without an invitation diff --git a/packages/twenty-docs/user-guide/settings/how-tos/settings-faq.mdx b/packages/twenty-docs/user-guide/settings/how-tos/settings-faq.mdx index c52ee3a131..f72a05cef8 100644 --- a/packages/twenty-docs/user-guide/settings/how-tos/settings-faq.mdx +++ b/packages/twenty-docs/user-guide/settings/how-tos/settings-faq.mdx @@ -137,7 +137,7 @@ Yes, you can connect multiple email accounts. Go to **Settings → Accounts** an -Yes! Go to **Settings → Domains** and click **Customize Domain**. You have two options: +Yes! Go to **Settings → General → Workspace Domain** and click **Customize Domain**. You have two options: - **Subdomain**: Use a Twenty subdomain like `yourcompany.twenty.com` - **Custom domain**: Use your own domain like `crm.yourcompany.com` (requires DNS configuration) @@ -146,7 +146,7 @@ A subdomain is quick to set up, while a custom domain provides a fully branded e -You can configure approved access domains so team members with company email addresses can automatically join your workspace. Go to **Settings → Domains** and add your company domain (e.g., `yourcompany.com`). +You can configure approved access domains so team members with company email addresses can automatically join your workspace. Go to **Settings → Members → Access** and add your company domain (e.g., `yourcompany.com`). diff --git a/packages/twenty-docs/user-guide/settings/overview.mdx b/packages/twenty-docs/user-guide/settings/overview.mdx index b9ad2faddd..85962decbc 100644 --- a/packages/twenty-docs/user-guide/settings/overview.mdx +++ b/packages/twenty-docs/user-guide/settings/overview.mdx @@ -39,7 +39,7 @@ Add team members to your workspace: 4. Assign appropriate roles -Before inviting your team, check the default role under **Settings → Roles**. New members are automatically assigned this role when they join. +Before inviting your team, check the default role under **Settings → Members → Roles**. New members are automatically assigned this role when they join. ## Workspace Settings Checklist diff --git a/packages/twenty-docs/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline.mdx b/packages/twenty-docs/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline.mdx index 0d4aaf49a2..ff5ae9183f 100644 --- a/packages/twenty-docs/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline.mdx +++ b/packages/twenty-docs/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline.mdx @@ -38,7 +38,7 @@ You need two custom fields on the Opportunities object. If you don't want users manually editing these calculated fields: -1. Go to **Settings → Roles** +1. Go to **Settings → Members → Roles** 2. Select the role to configure 3. Find the Opportunities object 4. Set **Probability** and **Expected Amount** fields to read-only diff --git a/packages/twenty-docs/user-guide/views-pipelines/how-tos/track-time-in-stage.mdx b/packages/twenty-docs/user-guide/views-pipelines/how-tos/track-time-in-stage.mdx index dc685fd2d8..43034c0c27 100644 --- a/packages/twenty-docs/user-guide/views-pipelines/how-tos/track-time-in-stage.mdx +++ b/packages/twenty-docs/user-guide/views-pipelines/how-tos/track-time-in-stage.mdx @@ -58,7 +58,7 @@ You don't need "Days in" fields for Closed Won and Closed Lost since those are f If you don't want users manually editing these calculated fields: -1. Go to **Settings → Roles** +1. Go to **Settings → Members → Roles** 2. Select the role to configure 3. Find the Opportunities object 4. Set the "Last Entered" and "Days in" fields to read-only diff --git a/packages/twenty-docs/user-guide/workflows/capabilities/workflow-actions.mdx b/packages/twenty-docs/user-guide/workflows/capabilities/workflow-actions.mdx index 70bdc239e5..0459776ef4 100644 --- a/packages/twenty-docs/user-guide/workflows/capabilities/workflow-actions.mdx +++ b/packages/twenty-docs/user-guide/workflows/capabilities/workflow-actions.mdx @@ -261,5 +261,5 @@ AI Agent actions consume workflow credits based on the AI model used. See [Workf -AI agents respect role-based permissions. You can assign specific roles to agents under **Settings → Roles** to control what data they can access. See [Permissions](/user-guide/permissions-access/capabilities/permissions) for details. +AI agents respect role-based permissions. You can assign specific roles to agents under **Settings → Members → Roles** to control what data they can access. See [Permissions](/user-guide/permissions-access/capabilities/permissions) for details. diff --git a/packages/twenty-docs/user-guide/workflows/how-tos/need-more-help/workflows-faq.mdx b/packages/twenty-docs/user-guide/workflows/how-tos/need-more-help/workflows-faq.mdx index 7e71a5c4a4..0270d7cf07 100644 --- a/packages/twenty-docs/user-guide/workflows/how-tos/need-more-help/workflows-faq.mdx +++ b/packages/twenty-docs/user-guide/workflows/how-tos/need-more-help/workflows-faq.mdx @@ -8,7 +8,7 @@ description: Frequently asked questions about workflows in Twenty. This is likely a permissions issue. You need access to workflows to create and activate them. - **Solution**: Contact your workspace administrator to grant you workflow access under **Settings → Roles**. + **Solution**: Contact your workspace administrator to grant you workflow access under **Settings → Members → Roles**. If you don't see the Workflows section at all in your sidebar, this confirms it's a permissions issue. diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 39f9d29ca5..c6de43a5a8 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -2528,6 +2528,7 @@ export type Mutation = { updatePageLayoutWidget: PageLayoutWidget; updatePageLayoutWithTabsAndWidgets: PageLayout; updatePasswordViaResetToken: InvalidatePassword; + updatePublicDomain: PublicDomain; updateSkill: Skill; updateUserEmail: Scalars['Boolean']; updateView: View; @@ -2750,6 +2751,7 @@ export type MutationCreatePageLayoutWidgetArgs = { export type MutationCreatePublicDomainArgs = { + applicationId?: InputMaybe; domain: Scalars['String']; }; @@ -3412,6 +3414,12 @@ export type MutationUpdatePasswordViaResetTokenArgs = { }; +export type MutationUpdatePublicDomainArgs = { + applicationId?: InputMaybe; + domain: Scalars['String']; +}; + + export type MutationUpdateSkillArgs = { input: UpdateSkillInput; }; @@ -4041,6 +4049,7 @@ export type PublicConnectionParametersOutput = { export type PublicDomain = { __typename?: 'PublicDomain'; + applicationId?: Maybe; createdAt: Scalars['DateTime']; domain: Scalars['String']; id: Scalars['UUID']; @@ -7196,10 +7205,11 @@ export type CheckPublicDomainValidRecordsMutation = { __typename?: 'Mutation', c export type CreatePublicDomainMutationVariables = Exact<{ domain: Scalars['String']; + applicationId?: InputMaybe; }>; -export type CreatePublicDomainMutation = { __typename?: 'Mutation', createPublicDomain: { __typename?: 'PublicDomain', id: string, domain: string, isValidated: boolean, createdAt: string } }; +export type CreatePublicDomainMutation = { __typename?: 'Mutation', createPublicDomain: { __typename?: 'PublicDomain', id: string, domain: string, isValidated: boolean, applicationId?: string | null, createdAt: string } }; export type DeletePublicDomainMutationVariables = Exact<{ domain: Scalars['String']; @@ -7208,10 +7218,18 @@ export type DeletePublicDomainMutationVariables = Exact<{ export type DeletePublicDomainMutation = { __typename?: 'Mutation', deletePublicDomain: boolean }; +export type UpdatePublicDomainMutationVariables = Exact<{ + domain: Scalars['String']; + applicationId?: InputMaybe; +}>; + + +export type UpdatePublicDomainMutation = { __typename?: 'Mutation', updatePublicDomain: { __typename?: 'PublicDomain', id: string, domain: string, isValidated: boolean, applicationId?: string | null, createdAt: string } }; + export type FindManyPublicDomainsQueryVariables = Exact<{ [key: string]: never; }>; -export type FindManyPublicDomainsQuery = { __typename?: 'Query', findManyPublicDomains: Array<{ __typename?: 'PublicDomain', id: string, domain: string, isValidated: boolean, createdAt: string }> }; +export type FindManyPublicDomainsQuery = { __typename?: 'Query', findManyPublicDomains: Array<{ __typename?: 'PublicDomain', id: string, domain: string, isValidated: boolean, applicationId?: string | null, createdAt: string }> }; export type CreateEmailingDomainMutationVariables = Exact<{ domain: Scalars['String']; @@ -8105,9 +8123,10 @@ export const GetApiKeysDocument = {"kind":"Document","definitions":[{"kind":"Ope export const GetWebhookDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWebhook"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"webhook"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WebhookFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WebhookFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Webhook"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"targetUrl"}},{"kind":"Field","name":{"kind":"Name","value":"operations"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"secret"}}]}}]} as unknown as DocumentNode; export const GetWebhooksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWebhooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"webhooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WebhookFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WebhookFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Webhook"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"targetUrl"}},{"kind":"Field","name":{"kind":"Name","value":"operations"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"secret"}}]}}]} as unknown as DocumentNode; export const CheckPublicDomainValidRecordsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CheckPublicDomainValidRecords"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"domain"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"checkPublicDomainValidRecords"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"domain"},"value":{"kind":"Variable","name":{"kind":"Name","value":"domain"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"validationType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode; -export const CreatePublicDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreatePublicDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"domain"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createPublicDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"domain"},"value":{"kind":"Variable","name":{"kind":"Name","value":"domain"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"isValidated"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode; +export const CreatePublicDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreatePublicDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"domain"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createPublicDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"domain"},"value":{"kind":"Variable","name":{"kind":"Name","value":"domain"}}},{"kind":"Argument","name":{"kind":"Name","value":"applicationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"isValidated"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode; export const DeletePublicDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeletePublicDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"domain"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deletePublicDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"domain"},"value":{"kind":"Variable","name":{"kind":"Name","value":"domain"}}}]}]}}]} as unknown as DocumentNode; -export const FindManyPublicDomainsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyPublicDomains"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyPublicDomains"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"isValidated"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode; +export const UpdatePublicDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdatePublicDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"domain"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updatePublicDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"domain"},"value":{"kind":"Variable","name":{"kind":"Name","value":"domain"}}},{"kind":"Argument","name":{"kind":"Name","value":"applicationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"isValidated"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode; +export const FindManyPublicDomainsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyPublicDomains"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyPublicDomains"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"isValidated"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode; export const CreateEmailingDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateEmailingDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"domain"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"driver"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EmailingDomainDriver"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createEmailingDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"domain"},"value":{"kind":"Variable","name":{"kind":"Name","value":"domain"}}},{"kind":"Argument","name":{"kind":"Name","value":"driver"},"value":{"kind":"Variable","name":{"kind":"Name","value":"driver"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"driver"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"verifiedAt"}},{"kind":"Field","name":{"kind":"Name","value":"verificationRecords"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; export const DeleteEmailingDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteEmailingDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteEmailingDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode; export const VerifyEmailingDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"VerifyEmailingDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"verifyEmailingDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"driver"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"verifiedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 29f43a47be..e6f560d102 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -136,12 +136,6 @@ const SettingsWorkspaceEmailGroupChannelDetail = lazy(() => })), ); -const SettingsDomains = lazy(() => - import('~/pages/settings/domains/SettingsDomains').then((module) => ({ - default: module.SettingsDomains, - })), -); - const SettingsSubdomainPage = lazy(() => import('~/pages/settings/domains/SettingsSubdomainPage').then((module) => ({ default: module.SettingsSubdomainPage, @@ -549,32 +543,30 @@ const SettingsUpdates = lazy(() => })), ); -const SettingsRoles = lazy(() => - import('~/pages/settings/roles/SettingsRoles').then((module) => ({ - default: module.SettingsRoles, - })), -); - const SettingsRoleCreate = lazy(() => - import('~/pages/settings/roles/SettingsRoleCreate').then((module) => ({ - default: module.SettingsRoleCreate, - })), + import('~/pages/settings/members/roles/SettingsRoleCreate').then( + (module) => ({ + default: module.SettingsRoleCreate, + }), + ), ); const SettingsRoleEdit = lazy(() => - import('~/pages/settings/roles/SettingsRoleEdit').then((module) => ({ + import('~/pages/settings/members/roles/SettingsRoleEdit').then((module) => ({ default: module.SettingsRoleEdit, })), ); const SettingsRoleObjectLevel = lazy(() => - import('~/pages/settings/roles/SettingsRoleObjectLevel').then((module) => ({ - default: module.SettingsRoleObjectLevel, - })), + import('~/pages/settings/members/roles/SettingsRoleObjectLevel').then( + (module) => ({ + default: module.SettingsRoleObjectLevel, + }), + ), ); const SettingsRoleAddObjectLevel = lazy(() => - import('~/pages/settings/roles/SettingsRoleAddObjectLevel').then( + import('~/pages/settings/members/roles/SettingsRoleAddObjectLevel').then( (module) => ({ default: module.SettingsRoleAddObjectLevel, }), @@ -644,7 +636,6 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => ( path={SettingsPath.EmailGroupChannelDetail} element={} /> - } /> } @@ -763,7 +754,15 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => ( /> } > - } /> + + } + /> } /> { CreatePublicDomainDocument, ); + const [updatePublicDomain] = useMutation(UpdatePublicDomainDocument); + const [newPublicDomain, setNewPublicDomain] = useState( selectedPublicDomain?.domain ?? '', ); + // Holds the chosen application before the public domain is created. + // Once selectedPublicDomain exists, the dropdown reads from it directly. + const [draftApplicationId, setDraftApplicationId] = useState( + null, + ); + + const selectedApplicationId = isDefined(selectedPublicDomain) + ? (selectedPublicDomain.applicationId ?? null) + : draftApplicationId; + const [newPublicDomainError, setNewPublicDomainError] = useState< string | undefined >(undefined); @@ -73,6 +88,20 @@ export const SettingPublicDomain = () => { FindManyPublicDomainsDocument, ); + const { data: applicationsData } = useQuery(FindManyApplicationsDocument); + + const applicationPinnedOption: SelectOption = { + value: null, + label: t`Workspace (all apps)`, + }; + + const applicationOptions: SelectOption[] = ( + applicationsData?.findManyApplications ?? [] + ).map((application) => ({ + value: application.id, + label: application.name, + })); + const [deletePublicDomain] = useMutation(DeletePublicDomainDocument); const { isLoading, publicDomainRecords, checkPublicDomainRecords } = @@ -89,7 +118,7 @@ export const SettingPublicDomain = () => { enqueueSuccessSnackBar({ message: t`Public domain successfully deleted`, }); - navigate(SettingsPath.Domains); + navigate(SettingsPath.Applications); refetchPublicDomains(); }, onError: (error) => @@ -116,7 +145,10 @@ export const SettingPublicDomain = () => { setNewPublicDomainError(undefined); await createPublicDomain({ - variables: { domain: newPublicDomain }, + variables: { + domain: newPublicDomain, + applicationId: draftApplicationId, + }, onCompleted: (data) => { setSelectedPublicDomain(data.createPublicDomain); enqueueSuccessSnackBar({ @@ -132,6 +164,35 @@ export const SettingPublicDomain = () => { }); }; + const onApplicationChange = async (nextApplicationId: string | null) => { + if (!isDefined(selectedPublicDomain)) { + setDraftApplicationId(nextApplicationId); + return; + } + + if (nextApplicationId === (selectedPublicDomain.applicationId ?? null)) { + return; + } + + await updatePublicDomain({ + variables: { + domain: selectedPublicDomain.domain, + applicationId: nextApplicationId, + }, + onCompleted: (data) => { + setSelectedPublicDomain(data.updatePublicDomain); + enqueueSuccessSnackBar({ + message: t`Public domain updated successfully`, + }); + refetchPublicDomains(); + }, + onError: (error) => + enqueueErrorSnackBar({ + apolloError: error, + }), + }); + }; + return ( { href: getSettingsPath(SettingsPath.Workspace), }, { - children: Domains, - href: getSettingsPath(SettingsPath.Domains), + children: Apps, + href: getSettingsPath(SettingsPath.Applications), }, { children: Public Domain }, ]} actionButton={ navigate(SettingsPath.Domains)} + onCancel={() => navigate(SettingsPath.Applications)} isSaveDisabled={loading || isDefined(selectedPublicDomain)} onSave={onCreate} /> @@ -209,6 +270,21 @@ export const SettingPublicDomain = () => { )} +
+ +