From 614bc7b7e65ed94f39786830db19fcb298ec693c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Wed, 24 Jun 2026 15:57:01 +0200 Subject: [PATCH] feat: serve HTTP logic functions on isolated *.withtwenty.com domain (#22045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Implements [core-team-issues#2473](https://github.com/twentyhq/core-team-issues/issues/2473): serve HTTP-triggered logic functions from a dedicated, **cookieless** public domain (`{workspaceSubdomain}.withtwenty.com`) instead of the same-site `/s/` route, so functions can safely return **arbitrary headers** — custom headers, `Permissions-Policy` (camera/mic/geolocation), `Cross-Origin-Opener-Policy: same-origin`, `Cross-Origin-Embedder-Policy: require-corp`, `Set-Cookie`, etc. The `/s/` route stays the strict, same-site path it is today. **Self-hosting is unchanged** — everything new is gated on `PUBLIC_DOMAIN_URL` being set. ### Why Today user-authored function responses are served same-site with the Twenty app, so the response-header allow-list is restricted to 5 safe headers and request headers are limited to a per-function allow-list. Serving from an origin that shares nothing with `*.twenty.com` removes that constraint safely — the same "user content domain" pattern as GitHub (`*.githubusercontent.com`) and CodeSandbox (`*.csb.app`). ## What's in here **Routing** - The **root-path → `/s` rewrite happens at the nginx ingress**, not in app code. The existing `api-ingress.yaml` already rewrites root paths onto `/s` (host-agnostically) when the edge sets `X-Twenty-Public-Domain: true`, so `*.withtwenty.com` and registered custom public domains are handled by the same mechanism. (An earlier in-app middleware was removed as a redundant, wrong-layer duplicate.) - `WorkspaceDomainsService.resolveWorkspaceAndPublicDomain` recognizes `*.` subdomains, resolves the workspace by subdomain, and returns `isIsolatedOrigin`. Explicitly registered public-domain rows still take precedence and keep their application scoping. The ingress preserves the `Host` header, so this resolution still fires. **Headers (server)** - Isolated origin → all response headers pass through and all request headers are forwarded. Same-site `/s/` keeps the strict allow-lists. (Global CORS already handles preflight/ACAO.) **`/s/` deprecation for new routes (cloud only)** - New `LOGIC_FUNCTION_LEGACY_ROUTE_CUTOFF` config var (ISO date, optional). When `PUBLIC_DOMAIN_URL` is set, functions created on/after the cutoff return **410 Gone** on `/s/` with the new URL. Existing routes and self-hosted instances are untouched. **Frontend education** - `publicFunctionDomain` added to `ClientConfig` (from `PUBLIC_DOMAIN_URL`). - The logic-function **Live URL** now resolves to `https://{workspaceSubdomain}.{publicFunctionDomain}{path}` on cloud, falling back to `/s/` for self-hosting. - Front components call their functions through the SDK (`RestApiClient`), which now targets the isolated domain via the injected `TWENTY_FUNCTIONS_URL`. - New **"Public URL"** section on the application **Settings** tab explaining the isolated domain (shown when the app exposes HTTP-triggered functions). **Docs**: note the `withtwenty.com` domain for external callers in the apps guide. ## Infra prerequisites (not code — needs dashboard work) - Wildcard DNS `*.withtwenty.com` (proxied) + wildcard TLS in the public-domain Cloudflare zone. - Edge (Cloudflare) sets `X-Twenty-Public-Domain: true` for `*.withtwenty.com` requests, so the existing nginx ingress rewrites them onto `/s` (same header the custom-domain flow already relies on). - Set `PUBLIC_DOMAIN_URL=https://withtwenty.com` on cloud. - Submit `withtwenty.com` to the **Public Suffix List** (required for cross-tenant cookie isolation before relying on `Set-Cookie`). ## Test plan - [x] `nx typecheck twenty-server`, `nx typecheck twenty-front` - [x] `lint:diff-with-main` + oxfmt clean (server + front) - [x] `npx jest route-trigger public-function-domain domain-server-config workspace-domains build-logic-function-event client-config` → server unit tests passing (resolution tiers, header passthrough vs allow-list, `/s/` cutoff 410) - [x] `npx jest getLogicFunctionHttpUrl` (front) and `nx test twenty-client-sdk` (RestApiClient routing) passing - [x] CI green (server, front, sdk, renderer, ui, zapier, example apps) - [ ] Manual: hit `{subdomain}.withtwenty.com/` end-to-end once infra is provisioned ``<img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">`` --- .../src/metadata/generated/schema.graphql | 5 +- .../src/metadata/generated/schema.ts | 9 +- .../src/metadata/generated/types.ts | 14 +-- .../extend/apps/layout/front-components.mdx | 38 ++++--- .../components/FrontComponentRenderer.tsx | 4 + .../components/FrontComponentWorkerEffect.tsx | 4 + .../src/remote/worker/remote-worker.ts | 6 + .../src/types/HostToWorkerRenderContext.ts | 1 + .../src/generated-admin/graphql.ts | 1 - .../src/generated-metadata/graphql.ts | 24 +--- .../client-config/hooks/useClientConfig.ts | 1 + .../client-config/types/ClientConfig.ts | 1 + .../states/domainConfigurationState.ts | 6 +- .../components/FrontComponentRenderer.tsx | 15 +++ .../FrontComponentRendererWithSdkClient.tsx | 3 + .../components/SettingPublicDomain.tsx | 87 ++------------- .../SettingsPublicDomainsListCard.tsx | 31 ++++-- .../graphql/mutations/createPublicDomain.ts | 2 +- .../graphql/mutations/updatePublicDomain.ts | 13 --- ...lectedApplicationIdForPublicDomainState.ts | 8 ++ ...ettingsLogicFunctionHttpTriggerSection.tsx | 7 +- .../hooks/useGetLogicFunctionHttpUrl.ts | 31 ++++++ .../__tests__/getLogicFunctionHttpUrl.test.ts | 79 +++++++++++++ .../utils/getLogicFunctionHttpUrl.ts | 43 ++++++++ .../SettingsApplicationDetails.tsx | 7 +- .../SettingsApplicationDetailSettingsTab.tsx | 16 ++- ...ttingsApplicationFunctionDomainSection.tsx | 63 +++++++++++ .../tabs/SettingsApplicationsDeveloperTab.tsx | 15 --- .../applicationHasHttpTriggeredFunctions.ts | 10 ++ ...dd-primary-public-domain-to-application.ts | 25 +++++ ...e-public-domain-application-id-not-null.ts | 27 +++++ .../instance-commands.constant.ts | 4 + .../application/application.entity.ts | 24 ++++ .../application/application.service.ts | 15 +++ ...ion-entity-relation-properties.constant.ts | 2 + .../client-config.controller.spec.ts | 1 + .../client-config/client-config.entity.ts | 3 + .../services/client-config.service.spec.ts | 2 + .../services/client-config.service.ts | 3 + .../services/domain-server-config.service.ts | 48 +++++++- .../public-function-domain.util.spec.ts | 104 ++++++++++++++++++ .../utils/public-function-domain.util.ts | 32 ++++++ .../services/workspace-domains.service.ts | 84 +++++++++++++- .../logic-function-trigger.service.ts | 3 + ...route-trigger-rest-api-exception-filter.ts | 6 + .../exceptions/route-trigger.exception.ts | 3 + .../triggers/route/route-trigger.service.ts | 75 ++++++++++++- .../build-logic-function-event.util.spec.ts | 23 ++++ .../utils/build-logic-function-event.util.ts | 28 ++++- .../utils/route-trigger-response.util.ts | 3 +- .../dtos/create-public-domain.input.ts | 7 +- .../dtos/update-public-domain.input.ts | 16 --- .../public-domain/public-domain.entity.ts | 18 +-- .../public-domain/public-domain.resolver.ts | 15 +-- .../public-domain/public-domain.service.ts | 56 +--------- .../twenty-config/config-variables.ts | 11 ++ ...-to-flat-field-metadatas-to-create.spec.ts | 1 + ...-relation-flat-field-metadata-pair.spec.ts | 1 + .../front-component/front-component.module.ts | 2 + .../route-trigger.controller.spec.ts | 97 ++++++++++++---- .../route-trigger/route-trigger.controller.ts | 53 +++------ .../workspace-entity-manager.spec.ts | 2 - .../core/utils/seed-feature-flags.util.ts | 5 - .../constants/DefaultFunctionsUrlName.ts | 1 + .../twenty-shared/src/application/index.ts | 1 + .../twenty-shared/src/types/FeatureFlagKey.ts | 1 - 66 files changed, 996 insertions(+), 350 deletions(-) delete mode 100644 packages/twenty-front/src/modules/settings/domains/graphql/mutations/updatePublicDomain.ts create mode 100644 packages/twenty-front/src/modules/settings/domains/states/selectedApplicationIdForPublicDomainState.ts create mode 100644 packages/twenty-front/src/modules/settings/logic-functions/hooks/useGetLogicFunctionHttpUrl.ts create mode 100644 packages/twenty-front/src/modules/settings/logic-functions/utils/__tests__/getLogicFunctionHttpUrl.test.ts create mode 100644 packages/twenty-front/src/modules/settings/logic-functions/utils/getLogicFunctionHttpUrl.ts create mode 100644 packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationFunctionDomainSection.tsx create mode 100644 packages/twenty-front/src/pages/settings/applications/utils/applicationHasHttpTriggeredFunctions.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-16/2-16-instance-command-fast-1782281874768-add-primary-public-domain-to-application.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-16/2-16-instance-command-slow-1782281874769-make-public-domain-application-id-not-null.ts create mode 100644 packages/twenty-server/src/engine/core-modules/domain/domain-server-config/utils/__tests__/public-function-domain.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/domain/domain-server-config/utils/public-function-domain.util.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/public-domain/dtos/update-public-domain.input.ts create mode 100644 packages/twenty-shared/src/application/constants/DefaultFunctionsUrlName.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 6fd54f11c3..2cde22f2b3 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -1772,7 +1772,6 @@ enum FeatureFlagKey { IS_UNIQUE_INDEXES_ENABLED IS_JSON_FILTER_ENABLED IS_MARKETPLACE_SETTING_TAB_VISIBLE - IS_PUBLIC_DOMAIN_ENABLED IS_EMAIL_GROUP_ENABLED IS_JUNCTION_RELATIONS_ENABLED IS_REST_METADATA_API_NEW_FORMAT_DIRECT @@ -1947,6 +1946,7 @@ type ClientConfig { isEmailVerificationRequired: Boolean! defaultSubdomain: String frontDomain: String! + publicFunctionDomain: String analyticsEnabled: Boolean! support: Support! isAttachmentPreviewEnabled: Boolean! @@ -3450,8 +3450,7 @@ type Mutation { startChannelSync(connectedAccountId: UUID!): ChannelSyncSuccess! saveImapSmtpCaldavAccount(handle: String!, connectionParameters: EmailAccountConnectionParameters!, id: UUID): ImapSmtpCaldavConnectionSuccess! updateLabPublicFeatureFlag(input: UpdateLabPublicFeatureFlagInput!): FeatureFlag! - createPublicDomain(domain: String!, applicationId: String): PublicDomain! - updatePublicDomain(domain: String!, applicationId: String): PublicDomain! + createPublicDomain(domain: String!, applicationId: String!): PublicDomain! deletePublicDomain(domain: String!): Boolean! checkPublicDomainValidRecords(domain: String!): DomainValidRecords createOneAppToken(input: CreateOneAppTokenInput!): AppToken! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 86e59d26c2..9885eb70ab 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -1408,7 +1408,7 @@ export interface FeatureFlag { __typename: 'FeatureFlag' } -export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' | 'IS_CALL_RECORDING_ENABLED' | 'IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED' +export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' | 'IS_CALL_RECORDING_ENABLED' | 'IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED' export interface WorkspaceUrls { customUrl?: Scalars['String'] @@ -1574,6 +1574,7 @@ export interface ClientConfig { isEmailVerificationRequired: Scalars['Boolean'] defaultSubdomain?: Scalars['String'] frontDomain: Scalars['String'] + publicFunctionDomain?: Scalars['String'] analyticsEnabled: Scalars['Boolean'] support: Support isAttachmentPreviewEnabled: Scalars['Boolean'] @@ -2968,7 +2969,6 @@ export interface Mutation { saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess updateLabPublicFeatureFlag: FeatureFlag createPublicDomain: PublicDomain - updatePublicDomain: PublicDomain deletePublicDomain: Scalars['Boolean'] checkPublicDomainValidRecords?: DomainValidRecords createOneAppToken: AppToken @@ -4633,6 +4633,7 @@ export interface ClientConfigGenqlSelection{ isEmailVerificationRequired?: boolean | number defaultSubdomain?: boolean | number frontDomain?: boolean | number + publicFunctionDomain?: boolean | number analyticsEnabled?: boolean | number support?: SupportGenqlSelection isAttachmentPreviewEnabled?: boolean | number @@ -6153,8 +6154,7 @@ export interface MutationGenqlSelection{ startChannelSync?: (ChannelSyncSuccessGenqlSelection & { __args: {connectedAccountId: Scalars['UUID']} }) saveImapSmtpCaldavAccount?: (ImapSmtpCaldavConnectionSuccessGenqlSelection & { __args: {handle: Scalars['String'], connectionParameters: EmailAccountConnectionParameters, id?: (Scalars['UUID'] | null)} }) updateLabPublicFeatureFlag?: (FeatureFlagGenqlSelection & { __args: {input: UpdateLabPublicFeatureFlagInput} }) - createPublicDomain?: (PublicDomainGenqlSelection & { __args: {domain: Scalars['String'], applicationId?: (Scalars['String'] | null)} }) - updatePublicDomain?: (PublicDomainGenqlSelection & { __args: {domain: Scalars['String'], applicationId?: (Scalars['String'] | null)} }) + createPublicDomain?: (PublicDomainGenqlSelection & { __args: {domain: Scalars['String'], applicationId: Scalars['String']} }) deletePublicDomain?: { __args: {domain: Scalars['String']} } checkPublicDomainValidRecords?: (DomainValidRecordsGenqlSelection & { __args: {domain: Scalars['String']} }) createOneAppToken?: (AppTokenGenqlSelection & { __args: {input: CreateOneAppTokenInput} }) @@ -9079,7 +9079,6 @@ export const enumFeatureFlagKey = { IS_UNIQUE_INDEXES_ENABLED: 'IS_UNIQUE_INDEXES_ENABLED' as const, IS_JSON_FILTER_ENABLED: 'IS_JSON_FILTER_ENABLED' as const, IS_MARKETPLACE_SETTING_TAB_VISIBLE: 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' as const, - IS_PUBLIC_DOMAIN_ENABLED: 'IS_PUBLIC_DOMAIN_ENABLED' as const, IS_EMAIL_GROUP_ENABLED: 'IS_EMAIL_GROUP_ENABLED' as const, IS_JUNCTION_RELATIONS_ENABLED: 'IS_JUNCTION_RELATIONS_ENABLED' as const, IS_REST_METADATA_API_NEW_FORMAT_DIRECT: 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' as const, diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index 5a904fc228..28db6c1c2f 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -3784,6 +3784,9 @@ export default { "frontDomain": [ 1 ], + "publicFunctionDomain": [ + 1 + ], "analyticsEnabled": [ 6 ], @@ -8950,19 +8953,8 @@ export default { "String!" ], "applicationId": [ - 1 - ] - } - ], - "updatePublicDomain": [ - 270, - { - "domain": [ 1, "String!" - ], - "applicationId": [ - 1 ] } ], diff --git a/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx b/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx index 0ded788038..ee4ecd240b 100644 --- a/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx +++ b/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx @@ -200,24 +200,29 @@ export default defineFrontComponent({ Front components run browser-side in a sandboxed Web Worker, while [logic functions](/developers/extend/apps/logic/logic-functions) run server-side. There is no direct in-process call between the two — instead, a front component reaches a logic function over HTTP. -A logic function declared with `httpRouteTriggerSettings` is exposed under the `/s/` endpoint at `${TWENTY_API_URL}/s`. Your front component calls that route with the `RestApiClient` from `twenty-client-sdk/rest`, which authenticates with the `TWENTY_APP_ACCESS_TOKEN` that Twenty injects into the worker. +A logic function declared with `httpRouteTriggerSettings` is reachable over HTTP at its route path. Twenty injects the base URL your functions are served from into the worker as `TWENTY_FUNCTIONS_URL`, together with the `TWENTY_APP_ACCESS_TOKEN` that authenticates the call. There is no dedicated SDK client for invoking your own functions yet, so call them with a plain `fetch`: -The `RestApiClient` is built for exactly this. It reads `TWENTY_API_URL` and `TWENTY_APP_ACCESS_TOKEN` from the worker environment, attaches the `Authorization: Bearer` header, serializes and parses JSON, and throws a `RestApiClientError` when the token or URL is missing or the response is non-2xx — so you don't reimplement that boilerplate in every component. +> **On Twenty Cloud, HTTP-triggered logic functions are served on a dedicated per-workspace domain** at `https://.twenty.com` — this is exactly what `TWENTY_FUNCTIONS_URL` resolves to. For external callers, copy the exact URL from the function's **HTTP trigger** settings or the application's **Settings** tab. + + + The legacy `/s/` function route is **deprecated** and will be **deactivated on 2026-07-24**. Use `TWENTY_FUNCTIONS_URL` (above) instead, and migrate any hard-coded `/s/` URLs before that date. The `/s/` route remains available for self-hosting. + A headless front component can run the call on mount via the `Command` component, then unmount automatically: ```tsx src/front-components/sync-prs.tsx import { defineFrontComponent } from 'twenty-sdk/define'; import { Command } from 'twenty-sdk/command'; -import { RestApiClient } from 'twenty-client-sdk/rest'; const SyncPrs = () => { const execute = async () => { - const client = new RestApiClient(); - - await client.post('/s/github/fetch-prs', { - owner: 'twentyhq', - repo: 'twenty', + await fetch(`${process.env.TWENTY_FUNCTIONS_URL}/github/fetch-prs`, { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.TWENTY_APP_ACCESS_TOKEN}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ owner: 'twentyhq', repo: 'twenty' }), }); }; @@ -233,7 +238,7 @@ export default defineFrontComponent({ }); ``` -The path passed to the client is the route's public path — the logic function's `httpRouteTriggerSettings.path` prefixed with `/s`. Keep `isAuthRequired: true`; the client supplies the app access token Twenty mints for your component: +The path appended to `TWENTY_FUNCTIONS_URL` is the logic function's `httpRouteTriggerSettings.path`. Keep `isAuthRequired: true`; the `TWENTY_APP_ACCESS_TOKEN` Twenty mints for your component authenticates the request: ```ts src/logic-functions/fetch-prs.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; @@ -258,12 +263,12 @@ export default defineLogicFunction({ ``` -`TWENTY_API_URL` and `TWENTY_APP_ACCESS_TOKEN` are injected automatically — see [Application variables](#application-variables). Because secret application variables are never exposed to front components, keep API keys and other sensitive logic in the logic function, not in the front component. +`TWENTY_FUNCTIONS_URL` and `TWENTY_APP_ACCESS_TOKEN` are injected automatically — see [Application variables](#application-variables). Because secret application variables are never exposed to front components, keep API keys and other sensitive logic in the logic function, not in the front component. -### RestApiClient reference +### Calling the Twenty REST API -Import `RestApiClient` from `twenty-client-sdk/rest`. It belongs to the same client family as `CoreApiClient` and `MetadataApiClient`, but targets your app's HTTP routes instead of the GraphQL API. +To read or write Twenty records from a front component, use `RestApiClient` from `twenty-client-sdk/rest`. It belongs to the same client family as `CoreApiClient` and `MetadataApiClient`, but targets the Twenty REST API (`/rest/...`) instead of the GraphQL API, reading its base URL from `TWENTY_API_URL`. | Method | Description | |--------|-------------| @@ -280,7 +285,7 @@ The base URL and token are resolved from the environment by default. Pass overri ```ts const client = new RestApiClient({ - baseUrl: 'https://api.example.com', + baseUrl: 'https://myworkspace.twenty.com', token: 'my-token', }); ``` @@ -293,8 +298,8 @@ import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest'; const client = new RestApiClient(); try { - const prs = await client.get('/s/github/fetch-prs', { - query: { state: 'open' }, + const people = await client.get('/rest/people', { + query: { limit: 10 }, }); } catch (error) { if (error instanceof RestApiClientError) { @@ -376,7 +381,8 @@ The following system variables are always available via `process.env`: | Variable | Description | |----------|-------------| -| `TWENTY_API_URL` | Base URL of the Twenty API | +| `TWENTY_FUNCTIONS_URL` | Base URL your app's HTTP logic functions are served from (used by `RestApiClient`) | +| `TWENTY_API_URL` | Base URL of the Twenty core API | | `TWENTY_APP_ACCESS_TOKEN` | Short-lived token scoped to your app's role | ## Host communication API diff --git a/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx b/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx index 5707497cc8..b808243b1b 100644 --- a/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx +++ b/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx @@ -22,6 +22,7 @@ type FrontComponentContentProps = { componentUrl: string; applicationAccessToken?: string; apiUrl?: string; + functionsBaseUrl?: string; sdkClientUrls?: SdkClientUrls; applicationVariables?: Record; executionContext: FrontComponentExecutionContext; @@ -34,6 +35,7 @@ export const FrontComponentRenderer = ({ componentUrl, applicationAccessToken, apiUrl, + functionsBaseUrl, sdkClientUrls, applicationVariables, executionContext, @@ -56,6 +58,7 @@ export const FrontComponentRenderer = ({ componentUrl={componentUrl} applicationAccessToken={applicationAccessToken} apiUrl={apiUrl} + functionsBaseUrl={functionsBaseUrl} sdkClientUrls={sdkClientUrls} applicationVariables={applicationVariables} frontComponentId={executionContext.frontComponentId} @@ -71,6 +74,7 @@ export const FrontComponentRenderer = ({ setThread, applicationAccessToken, apiUrl, + functionsBaseUrl, sdkClientUrls, applicationVariables, executionContext.frontComponentId, diff --git a/packages/twenty-front-component-renderer/src/remote/components/FrontComponentWorkerEffect.tsx b/packages/twenty-front-component-renderer/src/remote/components/FrontComponentWorkerEffect.tsx index 3ae2356661..8b5baab1d0 100644 --- a/packages/twenty-front-component-renderer/src/remote/components/FrontComponentWorkerEffect.tsx +++ b/packages/twenty-front-component-renderer/src/remote/components/FrontComponentWorkerEffect.tsx @@ -36,6 +36,7 @@ type FrontComponentWorkerEffectProps = { componentUrl: string; applicationAccessToken?: string; apiUrl?: string; + functionsBaseUrl?: string; sdkClientUrls?: SdkClientUrls; applicationVariables?: Record; frontComponentId: string; @@ -53,6 +54,7 @@ export const FrontComponentWorkerEffect = ({ componentUrl, applicationAccessToken, apiUrl, + functionsBaseUrl, sdkClientUrls, applicationVariables, frontComponentId, @@ -123,6 +125,7 @@ export const FrontComponentWorkerEffect = ({ componentUrl, applicationAccessToken, apiUrl, + functionsBaseUrl, sdkClientUrls, applicationVariables, }) @@ -146,6 +149,7 @@ export const FrontComponentWorkerEffect = ({ componentUrl, applicationAccessToken, apiUrl, + functionsBaseUrl, sdkClientUrls, applicationVariables, frontComponentId, diff --git a/packages/twenty-front-component-renderer/src/remote/worker/remote-worker.ts b/packages/twenty-front-component-renderer/src/remote/worker/remote-worker.ts index 5044c95ce4..089a8e1b16 100644 --- a/packages/twenty-front-component-renderer/src/remote/worker/remote-worker.ts +++ b/packages/twenty-front-component-renderer/src/remote/worker/remote-worker.ts @@ -104,6 +104,12 @@ const render: WorkerExports['render'] = async ( }); } + if (isDefined(renderContext.functionsBaseUrl)) { + setWorkerEnv({ + TWENTY_FUNCTIONS_URL: renderContext.functionsBaseUrl, + }); + } + if (isDefined(renderContext.applicationAccessToken)) { setWorkerEnv({ TWENTY_APP_ACCESS_TOKEN: renderContext.applicationAccessToken, diff --git a/packages/twenty-front-component-renderer/src/types/HostToWorkerRenderContext.ts b/packages/twenty-front-component-renderer/src/types/HostToWorkerRenderContext.ts index 83c99e644c..8236610a9b 100644 --- a/packages/twenty-front-component-renderer/src/types/HostToWorkerRenderContext.ts +++ b/packages/twenty-front-component-renderer/src/types/HostToWorkerRenderContext.ts @@ -7,6 +7,7 @@ export type HostToWorkerRenderContext = { componentUrl: string; applicationAccessToken?: string; apiUrl?: string; + functionsBaseUrl?: string; sdkClientUrls?: SdkClientUrls; applicationVariables?: Record; }; diff --git a/packages/twenty-front/src/generated-admin/graphql.ts b/packages/twenty-front/src/generated-admin/graphql.ts index 111e1fbaea..66f99241e8 100644 --- a/packages/twenty-front/src/generated-admin/graphql.ts +++ b/packages/twenty-front/src/generated-admin/graphql.ts @@ -304,7 +304,6 @@ export enum FeatureFlagKey { IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED = 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED', IS_MARKETPLACE_SETTING_TAB_VISIBLE = 'IS_MARKETPLACE_SETTING_TAB_VISIBLE', IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED = 'IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED', - IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED', IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT', IS_SETTINGS_DISCOVERY_HERO_ENABLED = 'IS_SETTINGS_DISCOVERY_HERO_ENABLED', IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED' diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index ce4f905622..f4b80eae8d 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -918,6 +918,7 @@ export type ClientConfig = { isWorkspaceSchemaDDLLocked: Scalars['Boolean']['output']; maintenance?: Maybe; publicFeatureFlags: Array; + publicFunctionDomain?: Maybe; sentry: Sentry; signInPrefilled: Scalars['Boolean']['output']; support: Support; @@ -1713,7 +1714,6 @@ export enum FeatureFlagKey { IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED = 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED', IS_MARKETPLACE_SETTING_TAB_VISIBLE = 'IS_MARKETPLACE_SETTING_TAB_VISIBLE', IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED = 'IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED', - IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED', IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT', IS_SETTINGS_DISCOVERY_HERO_ENABLED = 'IS_SETTINGS_DISCOVERY_HERO_ENABLED', IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED' @@ -2635,7 +2635,6 @@ export type Mutation = { updatePageLayoutWidget: PageLayoutWidget; updatePageLayoutWithTabsAndWidgets: PageLayout; updatePasswordViaResetToken: InvalidatePassword; - updatePublicDomain: PublicDomain; updateSkill: Skill; updateUnsubscribeTopic: UnsubscribeTopic; updateUserEmail: Scalars['Boolean']['output']; @@ -2864,7 +2863,7 @@ export type MutationCreatePageLayoutWidgetArgs = { export type MutationCreatePublicDomainArgs = { - applicationId?: InputMaybe; + applicationId: Scalars['String']['input']; domain: Scalars['String']['input']; }; @@ -3576,12 +3575,6 @@ export type MutationUpdatePasswordViaResetTokenArgs = { }; -export type MutationUpdatePublicDomainArgs = { - applicationId?: InputMaybe; - domain: Scalars['String']['input']; -}; - - export type MutationUpdateSkillArgs = { input: UpdateSkillInput; }; @@ -7887,7 +7880,7 @@ export type CheckPublicDomainValidRecordsMutation = { __typename?: 'Mutation', c export type CreatePublicDomainMutationVariables = Exact<{ domain: Scalars['String']['input']; - applicationId?: InputMaybe; + applicationId: Scalars['String']['input']; }>; @@ -7900,14 +7893,6 @@ export type DeletePublicDomainMutationVariables = Exact<{ export type DeletePublicDomainMutation = { __typename?: 'Mutation', deletePublicDomain: boolean }; -export type UpdatePublicDomainMutationVariables = Exact<{ - domain: Scalars['String']['input']; - 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; }>; @@ -8857,9 +8842,8 @@ 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"}}}},{"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 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":"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"}}},{"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 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 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":"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/client-config/hooks/useClientConfig.ts b/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts index 04644f87df..3145a91d92 100644 --- a/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts @@ -178,6 +178,7 @@ export const useClientConfig = (): UseClientConfigResult => { setDomainConfiguration({ defaultSubdomain: clientConfig?.defaultSubdomain, frontDomain: clientConfig?.frontDomain, + publicFunctionDomain: clientConfig?.publicFunctionDomain, }); setCanManageFeatureFlags(clientConfig?.canManageFeatureFlags); setLabPublicFeatureFlags(clientConfig?.publicFeatureFlags); diff --git a/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts b/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts index d3f991210e..0bb5eb2765 100644 --- a/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts @@ -22,6 +22,7 @@ export type ClientConfig = { captcha: Captcha; defaultSubdomain?: string; frontDomain: string; + publicFunctionDomain?: string | null; isAttachmentPreviewEnabled: boolean; isConfigVariablesInDbEnabled: boolean; isEmailVerificationRequired: boolean; diff --git a/packages/twenty-front/src/modules/domain-manager/states/domainConfigurationState.ts b/packages/twenty-front/src/modules/domain-manager/states/domainConfigurationState.ts index 67937c5a42..38eb1823cc 100644 --- a/packages/twenty-front/src/modules/domain-manager/states/domainConfigurationState.ts +++ b/packages/twenty-front/src/modules/domain-manager/states/domainConfigurationState.ts @@ -2,11 +2,15 @@ import { type ClientConfig } from '@/client-config/types/ClientConfig'; import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState'; export const domainConfigurationState = createAtomState< - Pick + Pick< + ClientConfig, + 'frontDomain' | 'defaultSubdomain' | 'publicFunctionDomain' + > >({ key: 'domainConfiguration', defaultValue: { frontDomain: '', defaultSubdomain: undefined, + publicFunctionDomain: undefined, }, }); diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx index f7da2616e1..16f16cfed9 100644 --- a/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx +++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx @@ -1,5 +1,9 @@ +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; import { FrontComponentRendererProvider } from '@/front-components/components/FrontComponentRendererProvider'; import { FrontComponentRendererWithSdkClient } from '@/front-components/components/FrontComponentRendererWithSdkClient'; +import { getFunctionsBaseUrl } from '@/settings/logic-functions/utils/getLogicFunctionHttpUrl'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useFrontComponentExecutionContext } from '@/front-components/hooks/useFrontComponentExecutionContext'; import { useOnFrontComponentUpdated } from '@/front-components/hooks/useOnFrontComponentUpdated'; import { frontComponentApplicationTokenPairComponentState } from '@/front-components/states/frontComponentApplicationTokenPairComponentState'; @@ -29,6 +33,9 @@ export const FrontComponentRenderer = ({ const { colorScheme } = useContext(ThemeContext); const { enqueueErrorSnackBar } = useSnackBar(); + const { publicFunctionDomain } = useAtomStateValue(domainConfigurationState); + const currentWorkspace = useAtomStateValue(currentWorkspaceState); + const setFrontComponentApplicationTokenPair = useSetAtomComponentState( frontComponentApplicationTokenPairComponentState, frontComponentId, @@ -104,6 +111,12 @@ export const FrontComponentRenderer = ({ const applicationVariables = data.frontComponent.applicationVariables ?? undefined; + const functionsBaseUrl = + getFunctionsBaseUrl({ + publicFunctionDomain, + workspaceSubdomain: currentWorkspace?.subdomain, + }) ?? `${REACT_APP_SERVER_BASE_URL}/s`; + if (usesSdkClient) { return ( @@ -112,6 +125,7 @@ export const FrontComponentRenderer = ({ componentUrl={componentUrl} applicationAccessToken={accessToken} applicationId={data.frontComponent.applicationId} + functionsBaseUrl={functionsBaseUrl} executionContext={executionContext} frontComponentHostCommunicationApi={ frontComponentHostCommunicationApi @@ -130,6 +144,7 @@ export const FrontComponentRenderer = ({ componentUrl={componentUrl} applicationAccessToken={accessToken} apiUrl={REACT_APP_SERVER_BASE_URL} + functionsBaseUrl={functionsBaseUrl} executionContext={executionContext} frontComponentHostCommunicationApi={frontComponentHostCommunicationApi} applicationVariables={applicationVariables} diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentRendererWithSdkClient.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentRendererWithSdkClient.tsx index 0d2402a7cd..9bdfc3edca 100644 --- a/packages/twenty-front/src/modules/front-components/components/FrontComponentRendererWithSdkClient.tsx +++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentRendererWithSdkClient.tsx @@ -14,6 +14,7 @@ type FrontComponentRendererWithSdkClientProps = { componentUrl: string; applicationAccessToken: string; applicationId: string; + functionsBaseUrl?: string; executionContext: FrontComponentExecutionContext; frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi; applicationVariables?: Record; @@ -25,6 +26,7 @@ export const FrontComponentRendererWithSdkClient = ({ componentUrl, applicationAccessToken, applicationId, + functionsBaseUrl, executionContext, frontComponentHostCommunicationApi, applicationVariables, @@ -47,6 +49,7 @@ export const FrontComponentRendererWithSdkClient = ({ componentUrl={componentUrl} applicationAccessToken={applicationAccessToken} apiUrl={REACT_APP_SERVER_BASE_URL} + functionsBaseUrl={functionsBaseUrl} sdkClientUrls={sdkClientState.blobUrls} executionContext={executionContext} frontComponentHostCommunicationApi={ diff --git a/packages/twenty-front/src/modules/settings/domains/components/SettingPublicDomain.tsx b/packages/twenty-front/src/modules/settings/domains/components/SettingPublicDomain.tsx index e8867adfbf..1989127496 100644 --- a/packages/twenty-front/src/modules/settings/domains/components/SettingPublicDomain.tsx +++ b/packages/twenty-front/src/modules/settings/domains/components/SettingPublicDomain.tsx @@ -6,10 +6,9 @@ import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { SettingsPath } from 'twenty-shared/types'; import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; -import { Select } from '@/ui/input/components/Select'; import { TextInput } from '@/ui/input/components/TextInput'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; -import { Button, ButtonGroup, type SelectOption } from 'twenty-ui/input'; +import { Button, ButtonGroup } from 'twenty-ui/input'; import { styled } from '@linaria/react'; import { SettingsDomainRecords } from '@/settings/domains/components/SettingsDomainRecords'; import { useCheckPublicDomainValidRecords } from '@/settings/domains/hooks/useCheckPublicDomainValidRecords'; @@ -17,14 +16,14 @@ import { useMutation, useQuery } from '@apollo/client/react'; import { CreatePublicDomainDocument, DeletePublicDomainDocument, - FindManyApplicationsDocument, FindManyPublicDomainsDocument, - UpdatePublicDomainDocument, } from '~/generated-metadata/graphql'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { CheckPublicDomainValidRecordsEffect } from '@/settings/domains/components/CheckPublicDomainValidRecordsEffect'; +import { selectedApplicationIdForPublicDomainState } from '@/settings/domains/states/selectedApplicationIdForPublicDomainState'; import { selectedPublicDomainState } from '@/settings/domains/states/selectedPublicDomainState'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useState } from 'react'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { getDomainValidationSchema } from '@/settings/domains/utils/getDomainValidationSchema'; @@ -57,6 +56,9 @@ export const SettingPublicDomain = () => { const [selectedPublicDomain, setSelectedPublicDomain] = useAtomState( selectedPublicDomainState, ); + const selectedApplicationIdForPublicDomain = useAtomStateValue( + selectedApplicationIdForPublicDomainState, + ); const { t } = useLingui(); const navigate = useNavigateSettings(); const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); @@ -65,22 +67,10 @@ export const SettingPublicDomain = () => { 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); @@ -89,20 +79,6 @@ 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 } = @@ -132,7 +108,10 @@ export const SettingPublicDomain = () => { const validationSchema = getDomainValidationSchema(); const onCreate = async () => { - if (!isDefined(newPublicDomain)) { + if ( + !isDefined(newPublicDomain) || + !isDefined(selectedApplicationIdForPublicDomain) + ) { return; } @@ -148,7 +127,7 @@ export const SettingPublicDomain = () => { await createPublicDomain({ variables: { domain: newPublicDomain, - applicationId: draftApplicationId, + applicationId: selectedApplicationIdForPublicDomain, }, onCompleted: (data) => { setSelectedPublicDomain(data.createPublicDomain); @@ -165,35 +144,6 @@ 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 ( { )} -
- -