Skip to content

Commit

Permalink
Introduce last modified at works only for manual publish (#2429)
Browse files Browse the repository at this point in the history
* ✨ Introduce custom lastModifiedAt #1868

* πŸ”¨ Modified script to optimize metadata creation #1868

* 🚨 Fix lint warning #1868

* πŸ”₯ Ignore lastModifiedAt for magazine as its not necessary #1868
  • Loading branch information
padms authored Jul 30, 2024
1 parent fcff077 commit ff61963
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 79 deletions.
46 changes: 22 additions & 24 deletions sanityv3/actions/CustomDuplicateAction.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,36 @@
import { useToast } from '@sanity/ui'
import { DocumentActionComponent, DocumentActionDescription, DocumentActionProps, DocumentActionsContext } from 'sanity'
import { DocumentActionComponent, DocumentActionDescription, DocumentActionProps } from 'sanity'
import { defaultLanguage } from '../languages'
import { apiVersion } from '../sanity.client'

export function createCustomDuplicateAction(originalAction: DocumentActionComponent, context: DocumentActionsContext) {
export function createCustomDuplicateAction(originalAction: DocumentActionComponent) {
const CustumDuplicateAction = (props: DocumentActionProps) => {
const originalResult = originalAction(props) as DocumentActionDescription
const toast = useToast()
const { draft, published } = props
const lang = draft?.lang || published?.lang

return {
...originalResult,
onHandle: () => {
context
.getClient({ apiVersion: apiVersion })
.fetch(/* groq */ `*[_id match '*'+$id][0]{lang}`, { id: context.documentId })
.then((result) => {
if (result?.lang == defaultLanguage.name) {
// allow duplicate action only on base language
originalResult.onHandle && originalResult.onHandle()
} else {
toast.push({
duration: 7000,
status: 'error',
title: 'Cannot duplicate the translation.',
})
}
if (!lang) {
toast.push({
duration: 7000,
status: 'error',
title: 'Failed to duplicate. Missing language.',
})
.catch((error) => {
console.log(error)
toast.push({
duration: 7000,
status: 'error',
title: 'Failed to duplicate',
})
return null
}

if (lang == defaultLanguage.name) {
// allow duplicate action only on base language
originalResult.onHandle && originalResult.onHandle()
} else {
toast.push({
duration: 7000,
status: 'error',
title: 'Cannot duplicate the translation.',
})
}
},
}
}
Expand Down
58 changes: 17 additions & 41 deletions sanityv3/actions/CustomPublishAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,23 @@ import {
DocumentActionsContext,
SanityClient,
} from 'sanity'
import { dataset, apiVersion } from '../sanity.client'
import { apiVersion } from '../sanity.client'
import { useToast } from '@sanity/ui'

const projectId = import.meta.env.SANITY_STUDIO_API_PROJECT_ID || 'h61q9gi9'
/** Secret site already exposes the mutation token. So we can reuse it instead. */
const token = import.meta.env.SANITY_STUDIO_HISTORY_API_TOKEN || import.meta.env.SANITY_STUDIO_MUTATION_TOKEN

const FIRST_PUBLISHED_AT_FIELD_NAME = 'firstPublishedAt'
const LAST_MODIFIED_AT_FIELD_NAME = 'lastModifiedAt'

const requiresConfirm = ['news', 'localNews']
const requiresFirstPublished = ['news', 'localNews', 'magazine']

const shouldAddFirstPublishedAt = async (props: DocumentActionProps) => {
if (!requiresFirstPublished.includes(props.type)) return false
let error = false
// https://github.com/sanity-io/sanity/issues/2179
const revisions = await fetch(
`https://${projectId}.api.sanity.io/${apiVersion}/data/history/${dataset}/transactions/${props.id}?excludeContent=true`,
{
method: 'GET',
headers: new Headers({
Authorization: `Bearer ${token}`,
}),
},
)
.then((res) => res.text())
.catch((err: Error) => {
console.error(err)
error = true
})

if (error) throw 'Failed retrieving history of document.'
const requiresFirstPublished = ['news', 'localNews']

const hasBeenPublished = !!revisions
const updateCustomPublishFields = async (id: string, client: SanityClient, setFirstPublish: boolean) => {
const currentTimeStamp = new Date().toISOString()
const patch = client.patch(id).set({ [LAST_MODIFIED_AT_FIELD_NAME]: currentTimeStamp })
if (setFirstPublish) patch.set({ [FIRST_PUBLISHED_AT_FIELD_NAME]: currentTimeStamp })

return !hasBeenPublished || !props.published?.[FIRST_PUBLISHED_AT_FIELD_NAME]
}

const addFirstPublishedAtField = async (id: string, client: SanityClient) => {
await client
.patch(id)
.set({ [FIRST_PUBLISHED_AT_FIELD_NAME]: new Date().toISOString() })
.commit()
.catch((e) => {
throw e
})
await patch.commit().catch((e) => {
throw e
})
}

export function createCustomPublishAction(originalAction: DocumentActionComponent, context: DocumentActionsContext) {
Expand All @@ -64,10 +35,15 @@ export function createCustomPublishAction(originalAction: DocumentActionComponen

const handlePublish = async () => {
try {
if (await shouldAddFirstPublishedAt(props)) {
await addFirstPublishedAtField(props.draft?._id || props.id, client)
if (requiresFirstPublished.includes(props.type)) {
await updateCustomPublishFields(
props.draft?._id || props.id,
client,
!props.published?.[FIRST_PUBLISHED_AT_FIELD_NAME],
)
}
originalResult.onHandle && originalResult.onHandle()
setDialogOpen(false)
} catch (e) {
console.error(e)
toast.push({
Expand Down
2 changes: 1 addition & 1 deletion sanityv3/sanity.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ const getConfig = (datasetParam: string, projectIdParam: string, isSecret = fals
case 'publish':
return createCustomPublishAction(originalAction, context)
case 'duplicate':
return createCustomDuplicateAction(originalAction, context)
return createCustomDuplicateAction(originalAction)
default:
return originalAction
}
Expand Down
8 changes: 8 additions & 0 deletions sanityv3/schemas/documents/news/sharedNewsFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ export const publishDateTime = [
readOnly: true,
hidden: true,
},
{
// Set automatically in the custom action "ConfirmPublishWithi18n"
title: 'Date and time of when the document was last updated at',
name: 'lastModifiedAt',
type: 'datetime',
readOnly: true,
hidden: true,
},
]

export const tags = {
Expand Down
127 changes: 127 additions & 0 deletions sanityv3/scripts/issue-1868/createMetadataN.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { sanityClients } from './getSanityClients.mjs'
import { testDocs, SCHEMA_TYPE } from './testDocument.mjs'

/**
* This migration script creates new `translation.metadata` documents for all
* documents that have a `__i18n_refs` field that is an array of references.
*
* This migration is necessary for the new version of the plugin to work.
*
* 1. Take a backup of your dataset with:
* `npx sanity@latest dataset export`
*
* 2. Copy this file to the root of your Sanity Studio project
*
* 3. Update the `UNSET_REFS_FIELD`, `UNSET_BASE_FIELD`,
* and `SCHEMA_TYPE` constants to match your use
*
* 4. Run the script (replace <schema-type> with the name of your schema type):
* npx sanity@latest exec ./createMetadata.ts --with-user-token
*
* 5. Repeat for every schema type and dataset using the updated plugin
*/

const client = sanityClients[0]
// Values in this field will be used to create meta documents
const UNSET_REFS_FIELD = `_langRefs`

// This field will NOT be modified in this script
// Run the `renameLanguageField.ts` script after if you want to change this
const LANGUAGE_FIELD = `_lang`
// eslint-disable-next-line no-console
console.log(
`Finding "${SCHEMA_TYPE}" documents with translation references in a "${UNSET_REFS_FIELD}" field to create "translation.metadata" documents.`,
)

const fetchDocuments = () =>
client.fetch(
`*[
_type in $type
&& (defined(${UNSET_REFS_FIELD}) )
&& defined(${LANGUAGE_FIELD})
][0...100] {
_id,
_rev,
${LANGUAGE_FIELD},
${UNSET_REFS_FIELD},
}`,
{ type: SCHEMA_TYPE, testDocs: testDocs },
)

const buildMetadata = (docs) => {
return docs
.filter((doc) => doc?.[UNSET_REFS_FIELD]?.length)
.map((doc) => {
return {
create: {
_type: 'translation.metadata',
translations: [
{
_key: doc[LANGUAGE_FIELD],
value: {
_type: 'reference',
_ref: doc._id.replace(`drafts.`, ``),
...(doc[UNSET_REFS_FIELD].some((ref) => typeof ref._weak !== 'undefined')
? { _weak: doc[UNSET_REFS_FIELD].find((ref) => ref._weak)?._weak }
: {}),
},
},
...doc[UNSET_REFS_FIELD].map(({ _ref, _key, _weak }) => ({
_key,
value: {
_type: 'reference',
_ref,
...(typeof _weak === 'undefined' ? {} : { _weak }),
},
})),
],
},
patch: {
id: doc._id,
patch: {
unset: [UNSET_REFS_FIELD],
// this will cause the migration to fail if any of the documents has been
// modified since it was fetched.
ifRevisionID: doc._rev,
},
},
}
})
}

const commitTransaction = (tx) => tx.commit()

const migrateNextBatch = async () => {
// Get all docs that match query
const documents = await fetchDocuments()
console.log('Found ' + documents.length + ' docs')

// Create new metadata documents before unsetting
const metadatas = buildMetadata(documents)

if (metadatas.length) {
const tx = client.transaction()
metadatas.forEach((metadata) => {
console.log(JSON.stringify(metadata.patch) + '\n')
return tx.create(metadata.create).patch(metadata.patch.id, metadata.patch.patch)
})
await commitTransaction(tx)
}
if (documents.length === 0) {
// eslint-disable-next-line no-console
console.debug('No more documents to create or patch!')
// eslint-disable-next-line no-console
console.debug(
'Be sure to migrate your "language" field using the "renameLanguageField.ts" script or update your plugin configuration\'s "Langage Field" setting',
)
return null
}
return migrateNextBatch()
}

migrateNextBatch().catch((err) => {
console.error(err)
// eslint-disable-next-line no-process-exit
process.exit(1)
})
8 changes: 4 additions & 4 deletions sanityv3/scripts/issue-1868/renameLangField.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ import { testDocs, SCHEMA_TYPE } from './testDocument.mjs'

const UNSET_FIELD_NAME = `_lang`
const NEW_FIELD_NAME = `lang`
//const SCHEMA_TYPE = [`magazine`, `localNews`]
// This field will be unset from all documents that contain it
const UNSET_BASE_FIELD = `__i18n_base`

// This will use the client configured in ./sanity.cli.ts
const client = sanityClients[0]

//&& _id in $testDocs
const query = `*[_type in $type && defined(${UNSET_FIELD_NAME})
const query = `*[_type in $type && defined(${UNSET_FIELD_NAME}) || defined(${UNSET_BASE_FIELD})
][0...100] {
_id,
_rev,
Expand All @@ -49,7 +49,7 @@ const buildPatches = (docs) =>
id: doc._id,
patch: {
set: { [NEW_FIELD_NAME]: doc[UNSET_FIELD_NAME] },
unset: [UNSET_FIELD_NAME],
unset: [UNSET_FIELD_NAME, UNSET_BASE_FIELD],
// this will cause the migration to fail if any of the
// documents have been modified since the original fetch.
ifRevisionID: doc._rev,
Expand Down
66 changes: 66 additions & 0 deletions sanityv3/scripts/issue-1868/retainLastModified.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { sanityClients } from './getSanityClients.mjs'

/**
* This migration script adds `lastModifiedAt` and sets it to the current _updatedAt field.
* Applicable to news, localNews and magazine.
*/

const UPDATED_AT = `_updatedAt`
const LAST_MODIFIED_AT = `lastModifiedAt`
const SCHEMA_TYPE = [`news`, `localNews`]

// This will use the client configured in ./sanity.cli.ts
const client = sanityClients[0]

//&& _id in $testDocs
const query = `*[_type in $type && !defined(${LAST_MODIFIED_AT}) ][0...100] {
_id,
_rev,
${UPDATED_AT}
}`
const fetchDocuments = () =>
client.fetch(query, {
type: SCHEMA_TYPE,
})

const buildPatches = (docs) =>
docs.map((doc) => ({
id: doc._id,
patch: {
setIfMissing: { [LAST_MODIFIED_AT]: doc[UPDATED_AT] },
// this will cause the migration to fail if any of the
// documents have been modified since the original fetch.
ifRevisionID: doc._rev,
},
}))

const createTransaction = (patches) =>
patches.reduce((tx, patch) => tx.patch(patch.id, patch.patch), client.transaction())

const commitTransaction = (tx) => tx.commit()

const migrateNextBatch = async () => {
const documents = await fetchDocuments()
console.log(`Found ${documents.length} documents to migrate\n`)

const patches = buildPatches(documents)
if (patches.length === 0) {
// eslint-disable-next-line no-console
console.debug('No more documents to migrate!')
return null
}
// eslint-disable-next-line no-console
console.debug(
`Migrating batch:\n %s`,
patches.map((patch) => `${patch.id} => ${JSON.stringify(patch.patch)}`).join('\n'),
)
const transaction = createTransaction(patches)
await commitTransaction(transaction)
return migrateNextBatch()
}

migrateNextBatch().catch((err) => {
console.error(err)
// eslint-disable-next-line no-process-exit
process.exit(1)
})
Loading

0 comments on commit ff61963

Please sign in to comment.