From 8610fc6a3af6fd9ba12cfac665f294c62235e168 Mon Sep 17 00:00:00 2001 From: Carolina Gonzalez Date: Wed, 15 May 2024 09:31:11 -0400 Subject: [PATCH] feat: fetch feature toggle to enable serverside document actions (#6418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature: scaffold feature flag function * revert back operations for now until caching issue is fixed * refactor: try memoizing featureToggle * refactor: use shareReplay * feat: add feature toggle requests to operations events etc. * refactor: rename featureToggleRequest * fix: do not pass canUseServerActions to snapshot pair * refactor(document-store): pass in server actions config as observable (#6582) * feat: use real endpoint in fetchFeatureToggle * test: update checkoutPair and add featureToggle e2e test * chore: remove comment now that proper endpoint is in place * fix(document-store): timeout feature flag req after 2s and treat as 'false' * fix(sanity): make config flag override server document actions (#6634) --------- Co-authored-by: Bjørge Næss --- dev/studio-e2e-testing/sanity.config.ts | 3 ++ dev/test-studio/sanity.config.ts | 5 +- .../sanity/src/core/config/prepareConfig.ts | 3 +- packages/sanity/src/core/config/types.ts | 13 ++--- .../src/core/store/_legacy/datastores.ts | 17 ++++-- .../document-pair/checkoutPair.test.ts | 34 ++++++++---- .../document/document-pair/checkoutPair.ts | 11 ++-- .../document-pair/consistencyStatus.ts | 9 +++- .../document/document-pair/documentEvents.ts | 2 +- .../document/document-pair/editOperations.ts | 4 +- .../document/document-pair/editState.ts | 4 +- .../document/document-pair/memoizeKeyGen.ts | 9 +--- .../document/document-pair/memoizedPair.ts | 4 +- .../document/document-pair/operationArgs.ts | 16 ++++-- .../document/document-pair/operationEvents.ts | 4 +- .../document/document-pair/remoteSnapshots.ts | 2 +- .../document/document-pair/snapshotPair.ts | 2 +- .../document-pair/utils/fetchFeatureToggle.ts | 36 +++++++++++++ .../document/document-pair/validation.ts | 4 +- .../store/_legacy/document/document-store.ts | 12 +++-- .../_legacy/grants/documentPairPermissions.ts | 10 +++- .../store/_legacy/history/useTimelineStore.ts | 9 +++- .../fetchFeatureToggle.spec.ts | 53 +++++++++++++++++++ 23 files changed, 207 insertions(+), 59 deletions(-) create mode 100644 packages/sanity/src/core/store/_legacy/document/document-pair/utils/fetchFeatureToggle.ts create mode 100644 test/e2e/tests/document-actions/fetchFeatureToggle.spec.ts diff --git a/dev/studio-e2e-testing/sanity.config.ts b/dev/studio-e2e-testing/sanity.config.ts index 72d7eedb55a..40b3283a607 100644 --- a/dev/studio-e2e-testing/sanity.config.ts +++ b/dev/studio-e2e-testing/sanity.config.ts @@ -95,4 +95,7 @@ export default defineConfig({ plugins: [sharedSettings()], basePath: '/test', + unstable_serverActions: { + enabled: true, + }, }) diff --git a/dev/test-studio/sanity.config.ts b/dev/test-studio/sanity.config.ts index bee4d308a75..1c233d13e9c 100644 --- a/dev/test-studio/sanity.config.ts +++ b/dev/test-studio/sanity.config.ts @@ -148,9 +148,8 @@ export default defineConfig([ plugins: [sharedSettings()], basePath: '/test', icon: SanityMonogram, - unstable_serverActions: { - enabled: true, - }, + // eslint-disable-next-line camelcase + __internal_serverDocumentActions: {}, scheduledPublishing: { enabled: true, inputDateTimeFormat: 'MM/dd/yy h:mm a', diff --git a/packages/sanity/src/core/config/prepareConfig.ts b/packages/sanity/src/core/config/prepareConfig.ts index 829464a5c8b..cd0462ac14f 100644 --- a/packages/sanity/src/core/config/prepareConfig.ts +++ b/packages/sanity/src/core/config/prepareConfig.ts @@ -204,7 +204,8 @@ export function prepareConfig( __internal: { sources: resolvedSources, }, - serverActions: rawWorkspace.unstable_serverActions ?? {enabled: false}, + // eslint-disable-next-line camelcase + __internal_serverDocumentActions: rawWorkspace.__internal_serverDocumentActions, ...defaultPluginsOptions, } preparedWorkspaces.set(rawWorkspace, workspaceSummary) diff --git a/packages/sanity/src/core/config/types.ts b/packages/sanity/src/core/config/types.ts index 885fa0524d1..6488309f980 100644 --- a/packages/sanity/src/core/config/types.ts +++ b/packages/sanity/src/core/config/types.ts @@ -448,10 +448,10 @@ export interface WorkspaceOptions extends SourceOptions { /** * @hidden - * @beta + * @internal */ - unstable_serverActions?: { - enabled: boolean + __internal_serverDocumentActions?: { + enabled?: boolean } scheduledPublishing?: DefaultPluginsWorkspaceOptions['scheduledPublishing'] @@ -770,8 +770,9 @@ export interface Source { } /** @beta */ tasks?: WorkspaceOptions['tasks'] - /** @beta */ - serverActions?: WorkspaceOptions['unstable_serverActions'] + + /** @internal */ + __internal_serverDocumentActions?: WorkspaceOptions['__internal_serverDocumentActions'] } /** @internal */ @@ -810,7 +811,7 @@ export interface WorkspaceSummary extends DefaultPluginsWorkspaceOptions { source: Observable }> } - serverActions: WorkspaceOptions['unstable_serverActions'] + __internal_serverDocumentActions: WorkspaceOptions['__internal_serverDocumentActions'] } /** diff --git a/packages/sanity/src/core/store/_legacy/datastores.ts b/packages/sanity/src/core/store/_legacy/datastores.ts index f20b8aa9774..c86b76bcede 100644 --- a/packages/sanity/src/core/store/_legacy/datastores.ts +++ b/packages/sanity/src/core/store/_legacy/datastores.ts @@ -1,6 +1,7 @@ /* eslint-disable camelcase */ import {useMemo} from 'react' +import {of} from 'rxjs' import {useClient, useSchema, useTemplates} from '../../hooks' import {createDocumentPreviewStore, type DocumentPreviewStore} from '../../preview' @@ -13,6 +14,7 @@ import { createConnectionStatusStore, } from './connection-status/connection-status-store' import {createDocumentStore, type DocumentStore} from './document' +import {fetchFeatureToggle} from './document/document-pair/utils/fetchFeatureToggle' import {createGrantsStore, type GrantsStore} from './grants' import {createHistoryStore, type HistoryStore} from './history' import {__tmp_wrap_presenceStore, type PresenceStore} from './presence/presence-store' @@ -131,6 +133,14 @@ export function useDocumentStore(): DocumentStore { const documentPreviewStore = useDocumentPreviewStore() const workspace = useWorkspace() + const serverActionsEnabled = useMemo(() => { + const configFlag = workspace.__internal_serverDocumentActions?.enabled + // If it's explicitly set, let it override the feature toggle + return typeof configFlag === 'boolean' + ? of(configFlag as boolean) + : fetchFeatureToggle(getClient(DEFAULT_STUDIO_CLIENT_OPTIONS)) + }, [getClient, workspace.__internal_serverDocumentActions?.enabled]) + return useMemo(() => { const documentStore = resourceCache.get({ @@ -144,7 +154,7 @@ export function useDocumentStore(): DocumentStore { initialValueTemplates: templates, schema, i18n, - serverActionsEnabled: !!workspace.serverActions?.enabled, + serverActionsEnabled, }) resourceCache.set({ @@ -155,14 +165,15 @@ export function useDocumentStore(): DocumentStore { return documentStore }, [ + resourceCache, getClient, documentPreviewStore, historyStore, - resourceCache, schema, - templates, i18n, workspace, + templates, + serverActionsEnabled, ]) } diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.test.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.test.ts index 694a5b03a0d..ea11805416b 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.test.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.test.ts @@ -37,7 +37,7 @@ beforeEach(() => { describe('checkoutPair -- local actions', () => { test('patch', async () => { - const {draft, published} = checkoutPair(client as any as SanityClient, idPair, false) + const {draft, published} = checkoutPair(client as any as SanityClient, idPair, of(false)) const combined = merge(draft.events, published.events) const sub = combined.subscribe() await new Promise((resolve) => setTimeout(resolve, 0)) @@ -62,7 +62,7 @@ describe('checkoutPair -- local actions', () => { }) test('createIfNotExists', async () => { - const {draft, published} = checkoutPair(client as any as SanityClient, idPair, false) + const {draft, published} = checkoutPair(client as any as SanityClient, idPair, of(false)) const combined = merge(draft.events, published.events) const sub = combined.subscribe() await new Promise((resolve) => setTimeout(resolve, 0)) @@ -104,7 +104,7 @@ describe('checkoutPair -- local actions', () => { }) test('create', async () => { - const {draft, published} = checkoutPair(client as any as SanityClient, idPair, false) + const {draft, published} = checkoutPair(client as any as SanityClient, idPair, of(false)) const combined = merge(draft.events, published.events) const sub = combined.subscribe() await new Promise((resolve) => setTimeout(resolve, 0)) @@ -146,7 +146,7 @@ describe('checkoutPair -- local actions', () => { }) test('createOrReplace', async () => { - const {draft, published} = checkoutPair(client as any as SanityClient, idPair, false) + const {draft, published} = checkoutPair(client as any as SanityClient, idPair, of(false)) const combined = merge(draft.events, published.events) const sub = combined.subscribe() await new Promise((resolve) => setTimeout(resolve, 0)) @@ -189,7 +189,7 @@ describe('checkoutPair -- local actions', () => { }) test('delete', async () => { - const {draft, published} = checkoutPair(client as any as SanityClient, idPair, false) + const {draft, published} = checkoutPair(client as any as SanityClient, idPair, of(false)) const combined = merge(draft.events, published.events) const sub = combined.subscribe() await new Promise((resolve) => setTimeout(resolve, 0)) @@ -223,7 +223,11 @@ describe('checkoutPair -- local actions', () => { describe('checkoutPair -- server actions', () => { test('patch', async () => { - const {draft, published} = checkoutPair(clientWithConfig as any as SanityClient, idPair, true) + const {draft, published} = checkoutPair( + clientWithConfig as any as SanityClient, + idPair, + of(true), + ) const combined = merge(draft.events, published.events) const sub = combined.subscribe() await new Promise((resolve) => setTimeout(resolve, 0)) @@ -257,7 +261,11 @@ describe('checkoutPair -- server actions', () => { }) test('published patch uses mutation endpoint', async () => { - const {draft, published} = checkoutPair(clientWithConfig as any as SanityClient, idPair, true) + const {draft, published} = checkoutPair( + clientWithConfig as any as SanityClient, + idPair, + of(true), + ) const combined = merge(draft.events, published.events) const sub = combined.subscribe() await new Promise((resolve) => setTimeout(resolve, 0)) @@ -286,7 +294,11 @@ describe('checkoutPair -- server actions', () => { }) test('create', async () => { - const {draft, published} = checkoutPair(clientWithConfig as any as SanityClient, idPair, true) + const {draft, published} = checkoutPair( + clientWithConfig as any as SanityClient, + idPair, + of(true), + ) const combined = merge(draft.events, published.events) const sub = combined.subscribe() await new Promise((resolve) => setTimeout(resolve, 0)) @@ -327,7 +339,11 @@ describe('checkoutPair -- server actions', () => { }) test('createIfNotExists', async () => { - const {draft, published} = checkoutPair(clientWithConfig as any as SanityClient, idPair, true) + const {draft, published} = checkoutPair( + clientWithConfig as any as SanityClient, + idPair, + of(true), + ) const combined = merge(draft.events, published.events) const sub = combined.subscribe() await new Promise((resolve) => setTimeout(resolve, 0)) diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.ts index 114abf5a7d8..0542de8be51 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.ts @@ -2,7 +2,7 @@ import {type SanityClient} from '@sanity/client' import {type Mutation} from '@sanity/mutator' import {type SanityDocument} from '@sanity/types' import {EMPTY, from, merge, type Observable, Subject} from 'rxjs' -import {filter, map, mergeMap, share, tap} from 'rxjs/operators' +import {filter, map, mergeMap, share, take, tap} from 'rxjs/operators' import { type BufferedDocumentEvent, @@ -196,7 +196,7 @@ function submitCommitRequest( export function checkoutPair( client: SanityClient, idPair: IdPair, - serverActionsEnabled: boolean, + serverActionsEnabled: Observable, ): Pair { const {publishedId, draftId} = idPair @@ -226,7 +226,12 @@ export function checkoutPair( const commits$ = merge(draft.commitRequest$, published.commitRequest$).pipe( mergeMap((commitRequest) => - submitCommitRequest(client, idPair, commitRequest, serverActionsEnabled), + serverActionsEnabled.pipe( + take(1), + mergeMap((canUseServerActions) => + submitCommitRequest(client, idPair, commitRequest, canUseServerActions), + ), + ), ), mergeMap(() => EMPTY), share(), diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/consistencyStatus.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/consistencyStatus.ts index fee5ae6def1..f101dc81c2f 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/consistencyStatus.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/consistencyStatus.ts @@ -13,9 +13,14 @@ export const consistencyStatus: ( client: SanityClient, idPair: IdPair, typeName: string, - serverActionsEnabled: boolean, + serverActionsEnabled: Observable, ) => Observable = memoize( - (client: SanityClient, idPair: IdPair, typeName: string, serverActionsEnabled: boolean) => { + ( + client: SanityClient, + idPair: IdPair, + typeName: string, + serverActionsEnabled: Observable, + ) => { return memoizedPair(client, idPair, typeName, serverActionsEnabled).pipe( switchMap(({draft, published}) => combineLatest([draft.consistency$, published.consistency$]), diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/documentEvents.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/documentEvents.ts index 78fdbe335a4..6e127a213cc 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/documentEvents.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/documentEvents.ts @@ -15,7 +15,7 @@ export const documentEvents = memoize( client: SanityClient, idPair: IdPair, typeName: string, - serverActionsEnabled: boolean, + serverActionsEnabled: Observable, ): Observable => { return memoizedPair(client, idPair, typeName, serverActionsEnabled).pipe( switchMap(({draft, published}) => merge(draft.events, published.events)), diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/editOperations.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/editOperations.ts index 78da2bfbc1b..45f41e33cfb 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/editOperations.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/editOperations.ts @@ -18,7 +18,7 @@ export const editOperations = memoize( client: SanityClient historyStore: HistoryStore schema: Schema - serverActionsEnabled: boolean + serverActionsEnabled: Observable }, idPair: IdPair, typeName: string, @@ -34,5 +34,5 @@ export const editOperations = memoize( merge(operationEvents$.pipe(mergeMap(() => EMPTY)), operations$), ).pipe(shareReplay({refCount: true, bufferSize: 1})) }, - (ctx, idPair, typeName) => memoizeKeyGen(ctx.client, idPair, typeName, ctx.serverActionsEnabled), + (ctx, idPair, typeName) => memoizeKeyGen(ctx.client, idPair, typeName), ) diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts index a300f87c827..adba9e7588f 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts @@ -34,7 +34,7 @@ export const editState = memoize( ctx: { client: SanityClient schema: Schema - serverActionsEnabled: boolean + serverActionsEnabled: Observable }, idPair: IdPair, typeName: string, @@ -73,5 +73,5 @@ export const editState = memoize( refCount(), ) }, - (ctx, idPair, typeName) => memoizeKeyGen(ctx.client, idPair, typeName, ctx.serverActionsEnabled), + (ctx, idPair, typeName) => memoizeKeyGen(ctx.client, idPair, typeName), ) diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/memoizeKeyGen.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/memoizeKeyGen.ts index c8253af8155..25ff846510d 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/memoizeKeyGen.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/memoizeKeyGen.ts @@ -2,12 +2,7 @@ import {type SanityClient} from '@sanity/client' import {type IdPair} from '../types' -export function memoizeKeyGen( - client: SanityClient, - idPair: IdPair, - typeName: string, - serverActionsEnabled: boolean, -) { +export function memoizeKeyGen(client: SanityClient, idPair: IdPair, typeName: string) { const config = client.config() - return `${config.dataset ?? ''}-${config.projectId ?? ''}-${idPair.publishedId}-${typeName}${serverActionsEnabled ? '-serverActionsEnabled' : ''}` + return `${config.dataset ?? ''}-${config.projectId ?? ''}-${idPair.publishedId}-${typeName}` } diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/memoizedPair.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/memoizedPair.ts index 52d8032388a..6a940c01df1 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/memoizedPair.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/memoizedPair.ts @@ -11,13 +11,13 @@ export const memoizedPair: ( client: SanityClient, idPair: IdPair, typeName: string, - serverActionsEnabled: boolean, + serverActionsEnabled: Observable, ) => Observable = memoize( ( client: SanityClient, idPair: IdPair, _typeName: string, - serverActionsEnabled: boolean, + serverActionsEnabled: Observable, ): Observable => { return new Observable((subscriber) => { const pair = checkoutPair(client, idPair, serverActionsEnabled) diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/operationArgs.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/operationArgs.ts index f5cf44f203c..de0b0b5ab29 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/operationArgs.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/operationArgs.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable max-nested-callbacks */ import {type SanityClient} from '@sanity/client' import {type Schema} from '@sanity/types' @@ -18,19 +19,24 @@ export const operationArgs = memoize( client: SanityClient historyStore: HistoryStore schema: Schema - serverActionsEnabled: boolean + serverActionsEnabled: Observable }, idPair: IdPair, typeName: string, ): Observable => { return snapshotPair(ctx.client, idPair, typeName, ctx.serverActionsEnabled).pipe( switchMap((versions) => - combineLatest([versions.draft.snapshots$, versions.published.snapshots$]).pipe( + combineLatest([ + versions.draft.snapshots$, + versions.published.snapshots$, + ctx.serverActionsEnabled, + ]).pipe( map( - ([draft, published]): OperationArgs => ({ + ([draft, published, canUseServerActions]): OperationArgs => ({ ...ctx, + serverActionsEnabled: canUseServerActions, idPair, - typeName: typeName, + typeName, snapshots: {draft, published}, draft: versions.draft, published: versions.published, @@ -43,6 +49,6 @@ export const operationArgs = memoize( ) }, (ctx, idPair, typeName) => { - return memoizeKeyGen(ctx.client, idPair, typeName, ctx.serverActionsEnabled) + return memoizeKeyGen(ctx.client, idPair, typeName) }, ) diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/operationEvents.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/operationEvents.ts index e91e501fe5d..c43162be5c3 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/operationEvents.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/operationEvents.ts @@ -139,7 +139,7 @@ export const operationEvents = memoize( client: SanityClient historyStore: HistoryStore schema: Schema - serverActionsEnabled: boolean + serverActionsEnabled: Observable }) => { const result$: Observable = operationCalls$.pipe( groupBy((op) => op.idPair.publishedId), @@ -170,7 +170,7 @@ export const operationEvents = memoize( args.operationName, operationArguments, args.extraArgs, - ctx.serverActionsEnabled, + operationArguments.serverActionsEnabled, ), ), ) diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/remoteSnapshots.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/remoteSnapshots.ts index 788a41f80d6..91db8f91fde 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/remoteSnapshots.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/remoteSnapshots.ts @@ -14,7 +14,7 @@ export const remoteSnapshots = memoize( client: SanityClient, idPair: IdPair, typeName: string, - serverActionsEnabled: boolean, + serverActionsEnabled: Observable, ): Observable => { return memoizedPair(client, idPair, typeName, serverActionsEnabled).pipe( switchMap(({published, draft}) => merge(published.remoteSnapshot$, draft.remoteSnapshot$)), diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/snapshotPair.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/snapshotPair.ts index 146d7481a7a..0efc43a5461 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/snapshotPair.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/snapshotPair.ts @@ -65,7 +65,7 @@ export const snapshotPair = memoize( client: SanityClient, idPair: IdPair, typeName: string, - serverActionsEnabled: boolean, + serverActionsEnabled: Observable, ): Observable => { return memoizedPair(client, idPair, typeName, serverActionsEnabled).pipe( map(({published, draft, transactionsPendingEvents$}): SnapshotPair => { diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/utils/fetchFeatureToggle.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/utils/fetchFeatureToggle.ts new file mode 100644 index 00000000000..448bdaba4f4 --- /dev/null +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/utils/fetchFeatureToggle.ts @@ -0,0 +1,36 @@ +import {type SanityClient} from '@sanity/client' +import {map, type Observable, of, ReplaySubject, timeout, timer} from 'rxjs' +import {catchError, share} from 'rxjs/operators' + +interface ActionsFeatureToggle { + actions: boolean +} + +//in the "real" code, this would be observable.request, to a URI +export const fetchFeatureToggle = (defaultClient: SanityClient): Observable => { + const client = defaultClient.withConfig({apiVersion: 'X'}) + const {dataset} = defaultClient.config() + + return client.observable + .request({ + uri: `/data/actions/${dataset}`, + withCredentials: true, + }) + .pipe( + map((res: ActionsFeatureToggle) => res.actions), + timeout({first: 2000, with: () => of(false)}), + catchError(() => + // If we fail to fetch the feature toggle, we'll just assume it's disabled and fallback to legacy mutations + of(false), + ), + share({ + // replay latest known state to new subscribers + connector: () => new ReplaySubject(1), + // this will typically be completed and unsubscribed from right after the answer is received, so we don't want to reset + resetOnRefCountZero: false, + // once the fetch has completed, we'll wait for 2 minutes before resetting the state. + // we'll then check again once a new subscriber comes in + resetOnComplete: () => timer(1000 * 120), + }), + ) +} diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts index 61722628513..4ad8d322267 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts @@ -95,7 +95,7 @@ export const validation = memoize( observeDocumentPairAvailability: ObserveDocumentPairAvailability schema: Schema i18n: LocaleSource - serverActionsEnabled: boolean + serverActionsEnabled: Observable }, {draftId, publishedId}: IdPair, typeName: string, @@ -191,6 +191,6 @@ export const validation = memoize( ) }, (ctx, idPair, typeName) => { - return memoizeKeyGen(ctx.client, idPair, typeName, ctx.serverActionsEnabled) + return memoizeKeyGen(ctx.client, idPair, typeName) }, ) diff --git a/packages/sanity/src/core/store/_legacy/document/document-store.ts b/packages/sanity/src/core/store/_legacy/document/document-store.ts index dd8f23fc015..64ed02d48cc 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-store.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-store.ts @@ -80,7 +80,7 @@ export interface DocumentStoreOptions { schema: Schema initialValueTemplates: Template[] i18n: LocaleSource - serverActionsEnabled: boolean + serverActionsEnabled: Observable } /** @internal */ @@ -91,7 +91,7 @@ export function createDocumentStore({ initialValueTemplates, schema, i18n, - serverActionsEnabled = false, + serverActionsEnabled, }: DocumentStoreOptions): DocumentStore { const observeDocumentPairAvailability = documentPreviewStore.unstable_observeDocumentPairAvailability @@ -100,6 +100,7 @@ export function createDocumentStore({ // internal operations, and a `getClient` method that we expose to user-land // for things like validations const client = getClient(DEFAULT_STUDIO_CLIENT_OPTIONS) + const ctx = { client, getClient, @@ -154,7 +155,12 @@ export function createDocumentStore({ return editState(ctx, getIdPairFromPublished(publishedId), type) }, operationEvents(publishedId, type) { - return operationEvents({client, historyStore, schema, serverActionsEnabled}).pipe( + return operationEvents({ + client, + historyStore, + schema, + serverActionsEnabled, + }).pipe( filter( (result) => result.args.idPair.publishedId === publishedId && result.args.typeName === type, diff --git a/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts b/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts index 0c8ba6cb092..5c84b123398 100644 --- a/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts +++ b/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts @@ -15,6 +15,7 @@ import { } from '../../../util' import {useGrantsStore} from '../datastores' import {snapshotPair} from '../document' +import {fetchFeatureToggle} from '../document/document-pair/utils/fetchFeatureToggle' import {type GrantsStore, type PermissionCheckResult} from './types' function getSchemaType(schema: Schema, typeName: string): SchemaType { @@ -168,7 +169,7 @@ export interface DocumentPairPermissionsOptions { id: string type: string permission: DocumentPermission - serverActionsEnabled: boolean + serverActionsEnabled: Observable } /** @@ -296,7 +297,12 @@ export function useDocumentPairPermissions({ () => overrideGrantsStore || defaultGrantsStore, [defaultGrantsStore, overrideGrantsStore], ) - const serverActionsEnabled = useMemo(() => !!workspace.serverActions?.enabled, [workspace]) + + const serverActionsEnabled = useMemo(() => { + const configFlag = workspace.__internal_serverDocumentActions?.enabled + // If it's explicitly set, let it override the feature toggle + return typeof configFlag === 'boolean' ? of(configFlag as boolean) : fetchFeatureToggle(client) + }, [client, workspace.__internal_serverDocumentActions?.enabled]) return useDocumentPairPermissionsFromHookFactory( useMemo( diff --git a/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts b/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts index c8aad898c17..403a755fbb7 100644 --- a/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts +++ b/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts @@ -22,6 +22,7 @@ import { import {useClient} from '../../../hooks' import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../../studioClient' import {remoteSnapshots, type RemoteSnapshotVersionEvent} from '../document' +import {fetchFeatureToggle} from '../document/document-pair/utils/fetchFeatureToggle' interface UseTimelineControllerOpts { documentId: string @@ -103,8 +104,6 @@ export function useTimelineStore({ const client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS) const workspace = useWorkspace() - const serverActionsEnabled = useMemo(() => !!workspace.serverActions?.enabled, [workspace]) - /** * The mutable TimelineController, used internally */ @@ -160,6 +159,12 @@ export function useTimelineStore({ return () => controller.suspend() }, [rev, since, controller, timelineController$]) + const serverActionsEnabled = useMemo(() => { + const configFlag = workspace.__internal_serverDocumentActions?.enabled + // If it's explicitly set, let it override the feature toggle + return typeof configFlag === 'boolean' ? of(configFlag as boolean) : fetchFeatureToggle(client) + }, [client, workspace.__internal_serverDocumentActions?.enabled]) + /** * Fetch document snapshots and update the mutable controller. * Unsubscribes on clean up, preventing double fetches in strict mode. diff --git a/test/e2e/tests/document-actions/fetchFeatureToggle.spec.ts b/test/e2e/tests/document-actions/fetchFeatureToggle.spec.ts new file mode 100644 index 00000000000..f445865bb9d --- /dev/null +++ b/test/e2e/tests/document-actions/fetchFeatureToggle.spec.ts @@ -0,0 +1,53 @@ +import {expect} from '@playwright/test' +import {test} from '@sanity/test' + +interface ActionsFeatureToggle { + actions: boolean +} + +test(`document actions follow appropriate logic after receiving response from feature toggle endpoint`, async ({ + page, + createDraftDocument, +}) => { + await createDraftDocument('/test/content/book') + + const featureToggleRequest = page.waitForResponse(async (response) => { + return response.url().includes('/data/actions') && response.request().method() === 'GET' + }) + + const featureToggleResponse: ActionsFeatureToggle = await (await featureToggleRequest).json() + + await page.getByTestId('field-title').getByTestId('string-input').fill('Test title') + + if (featureToggleResponse.actions) { + const actionsEditRequest = page.waitForResponse(async (response) => { + return response.url().includes('/data/actions') && response.request().method() === 'POST' + }) + + const actionsEditResponse = await (await actionsEditRequest).json() + + expect(actionsEditResponse).toEqual( + expect.objectContaining({ + transactionId: expect.any(String), + }), + ) + } else { + const mutateEditRequest = page.waitForResponse(async (response) => { + return response.url().includes('/data/mutate') && response.request().method() === 'POST' + }) + + const mutateEditResponse = await (await mutateEditRequest).json() + + expect(mutateEditResponse).toEqual( + expect.objectContaining({ + transactionId: expect.any(String), + results: [ + { + id: expect.any(String), + operation: expect.any(String), + }, + ], + }), + ) + } +})