Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core): stabilize studio usage of actions API #6782

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {type SanityClient} from '@sanity/client'
import {type Action, type SanityClient} from '@sanity/client'
import {type Mutation} from '@sanity/mutator'
import {type SanityDocument} from '@sanity/types'
import {omit} from 'lodash'
Expand All @@ -14,7 +14,6 @@ import {
} from '../buffered-doc'
import {getPairListener, type ListenerEvent} from '../getPairListener'
import {type IdPair, type PendingMutationsEvent, type ReconnectEvent} from '../types'
import {type HttpAction} from './actionTypes'

const isMutationEventForDocId =
(id: string) =>
Expand Down Expand Up @@ -95,12 +94,8 @@ function isLiveEditMutation(mutationParams: Mutation['params'], publishedId: str
return patchTargets.every((target) => target === publishedId)
}

function toActions(idPair: IdPair, mutationParams: Mutation['params']) {
return mutationParams.mutations.flatMap<HttpAction>((mutations) => {
if (Object.keys(mutations).length > 1) {
// todo: this might be a bit too strict, but I'm (lazily) trying to check if we ever get more than one mutation in a payload
throw new Error('Did not expect multiple mutations in the same payload')
}
function toActions(idPair: IdPair, mutationParams: Mutation['params']): Action[] {
return mutationParams.mutations.flatMap((mutations) => {
// This action is not always interoperable with the equivalent mutation. It will fail if the
// published version of the document already exists.
if (mutations.createIfNotExists) {
Expand All @@ -125,30 +120,18 @@ function toActions(idPair: IdPair, mutationParams: Mutation['params']) {
patch: omit(mutations.patch, 'id'),
}
}
throw new Error('Todo: implement')
throw new Error('Cannot map mutation to action')
})
}

function commitActions(
defaultClient: SanityClient,
idPair: IdPair,
mutationParams: Mutation['params'],
) {
function commitActions(client: SanityClient, idPair: IdPair, mutationParams: Mutation['params']) {
if (isLiveEditMutation(mutationParams, idPair.publishedId)) {
return commitMutations(defaultClient, mutationParams)
return commitMutations(client, mutationParams)
}

const vXClient = defaultClient.withConfig({apiVersion: 'X'})
const {dataset} = defaultClient.config()

return vXClient.observable.request({
url: `/data/actions/${dataset}`,
method: 'post',
return client.observable.action(toActions(idPair, mutationParams), {
tag: 'document.commit',
body: {
transactionId: mutationParams.transactionId,
actions: toActions(idPair, mutationParams),
},
transactionId: mutationParams.transactionId,
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,38 @@ import {isLiveEditEnabled} from '../utils/isLiveEditEnabled'

export const del: OperationImpl<[], 'NOTHING_TO_DELETE'> = {
disabled: ({snapshots}) => (snapshots.draft || snapshots.published ? false : 'NOTHING_TO_DELETE'),
execute: ({client: globalClient, schema, idPair, typeName, snapshots}) => {
execute: ({client, schema, idPair, typeName, snapshots}) => {
if (isLiveEditEnabled(schema, typeName)) {
const tx = globalClient.observable.transaction().delete(idPair.publishedId)
const tx = client.observable.transaction().delete(idPair.publishedId)
return tx.commit({tag: 'document.delete'})
}

const vXClient = globalClient.withConfig({apiVersion: 'X'})
const {dataset} = globalClient.config()

//the delete action requires a published doc -- discard if not present
if (!snapshots.published) {
return vXClient.observable.request({
url: `/data/actions/${dataset}`,
method: 'post',
tag: 'document.discard',
body: {
actions: [
{
actionType: 'sanity.action.document.discard',
draftId: idPair.draftId,
publishedId: idPair.publishedId,
},
],
return client.observable.action(
{
actionType: 'sanity.action.document.discard',
draftId: idPair.draftId,
purge: false,
},
})
{tag: 'document.delete'},
)
}

return vXClient.observable.request({
url: `/data/actions/${dataset}`,
method: 'post',
tag: 'document.delete',
// this disables referential integrity for cross-dataset references. we
// have this set because we warn against deletes in the `ConfirmDeleteDialog`
// UI. This operation is run when "delete anyway" is clicked
query: {skipCrossDatasetReferenceValidation: 'true'},
body: {
actions: [
{
actionType: 'sanity.action.document.delete',
draftId: idPair.draftId,
publishedId: idPair.publishedId,
},
],
return client.observable.action(
{
actionType: 'sanity.action.document.delete',
includeDrafts: [idPair.draftId],
publishedId: idPair.publishedId,
purge: false,
},
{
tag: 'document.delete',
// this disables referential integrity for cross-dataset references. we
// have this set because we warn against deletes in the `ConfirmDeleteDialog`
// UI. This operation is run when "delete anyway" is clicked
skipCrossDatasetReferenceValidation: true,
},
})
)
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,14 @@ export const discardChanges: OperationImpl<[], DisabledReason> = {
}
return false
},
execute: ({client: globalClient, idPair}) => {
const vXClient = globalClient.withConfig({apiVersion: 'X'})
const {dataset} = globalClient.config()

return vXClient.observable.request({
url: `/data/actions/${dataset}`,
method: 'post',
tag: 'document.discard-changes',
body: {
actions: [
{
actionType: 'sanity.action.document.discard',
draftId: idPair.draftId,
publishedId: idPair.publishedId,
},
],
execute: ({client, idPair}) => {
return client.observable.action(
{
actionType: 'sanity.action.document.discard',
draftId: idPair.draftId,
purge: false,
},
})
{tag: 'document.discard-changes'},
)
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,28 @@ export const publish: OperationImpl<[], DisabledReason> = {
}
return false
},
execute: ({client: globalClient, idPair, snapshots}) => {
const vXClient = globalClient.withConfig({apiVersion: 'X'})
const {dataset} = globalClient.config()

execute: ({client, idPair, snapshots}) => {
// The editor must be able to see the draft they are choosing to publish.
if (!snapshots.draft) {
throw new Error('cannot execute "publish" when draft is missing')
}

return vXClient.observable.request({
url: `/data/actions/${dataset}`,
method: 'post',
tag: 'document.publish',
body: {
actions: [
{
actionType: 'sanity.action.document.publish',
draftId: idPair.draftId,
publishedId: idPair.publishedId,
// The editor must be able to see the latest state of both the draft document they are
// publishing, and the published document they are choosing to replace. Optimistic
// locking using `ifDraftRevisionId` and `ifPublishedRevisionId` ensures the client and
// server are synchronised.
ifDraftRevisionId: snapshots.draft._rev,
ifPublishedRevisionId: snapshots.published?._rev,
},
],
return client.observable.action(
{
actionType: 'sanity.action.document.publish',
draftId: idPair.draftId,
publishedId: idPair.publishedId,
// The editor must be able to see the latest state of both the draft document they are
// publishing, and the published document they are choosing to replace. Optimistic
// locking using `ifDraftRevisionId` and `ifPublishedRevisionId` ensures the client and
// server are synchronised.
ifDraftRevisionId: snapshots.draft._rev,
// @ts-expect-error FIXME
ifPublishedRevisionId: snapshots.published?._rev,
},
{
tag: 'document.publish',
},
})
)
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,20 @@ export const unpublish: OperationImpl<[], DisabledReason> = {
}
return snapshots.published ? false : 'NOT_PUBLISHED'
},
execute: ({client: globalClient, idPair}) => {
const vXClient = globalClient.withConfig({apiVersion: 'X'})
const {dataset} = globalClient.config()

return vXClient.observable.request({
url: `/data/actions/${dataset}`,
method: 'post',
// this disables referential integrity for cross-dataset references. we
// have this set because we warn against unpublishes in the `ConfirmDeleteDialog`
// UI. This operation is run when "unpublish anyway" is clicked
query: {skipCrossDatasetReferenceValidation: 'true'},
tag: 'document.unpublish',
body: {
actions: [
{
actionType: 'sanity.action.document.unpublish',
draftId: idPair.draftId,
publishedId: idPair.publishedId,
},
],
execute: ({client, idPair}) =>
client.observable.action(
{
// This operation is run when "unpublish anyway" is clicked
actionType: 'sanity.action.document.unpublish',
draftId: idPair.draftId,
publishedId: idPair.publishedId,
},
})
},
{
tag: 'document.unpublish',
// this disables referential integrity for cross-dataset references. we
// have this set because we warn against unpublishes in the `ConfirmDeleteDialog`
// UI.
skipCrossDatasetReferenceValidation: true,
},
),
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,35 @@
import {type SanityClient} from '@sanity/client'
import {map, type Observable, of, ReplaySubject, timeout, timer} from 'rxjs'
import {catchError, share} from 'rxjs/operators'
import {catchError, concatMap, 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<boolean> => {
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),
export const fetchFeatureToggle = (client: SanityClient): Observable<boolean> => {
const dataset = client.config().dataset
return timer(0, 1000 * 120).pipe(
concatMap(() =>
client.observable.request({
uri: `/data/actions/${dataset}`,
withCredentials: true,
}),
)
),
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),
}),
)
}
Loading
Loading