From ff61963bdcbbf3cebb975eae9cab556966a8c585 Mon Sep 17 00:00:00 2001 From: Padmaja Date: Tue, 30 Jul 2024 14:03:58 +0530 Subject: [PATCH] Introduce last modified at works only for manual publish (#2429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Introduce custom lastModifiedAt #1868 * 🔨 Modified script to optimize metadata creation #1868 * 🚨 Fix lint warning #1868 * 🔥 Ignore lastModifiedAt for magazine as its not necessary #1868 --- sanityv3/actions/CustomDuplicateAction.ts | 46 +++---- sanityv3/actions/CustomPublishAction.ts | 58 +++----- sanityv3/sanity.config.tsx | 2 +- .../documents/news/sharedNewsFields.ts | 8 ++ .../scripts/issue-1868/createMetadataN.mjs | 127 ++++++++++++++++++ .../scripts/issue-1868/renameLangField.mjs | 8 +- .../scripts/issue-1868/retainLastModified.mjs | 66 +++++++++ web/lib/queries/common/pageContentFields.ts | 6 +- web/lib/queries/common/publishDateTime.ts | 4 + web/lib/queries/localNews.ts | 4 +- web/lib/queries/news.ts | 8 +- 11 files changed, 258 insertions(+), 79 deletions(-) create mode 100644 sanityv3/scripts/issue-1868/createMetadataN.mjs create mode 100644 sanityv3/scripts/issue-1868/retainLastModified.mjs diff --git a/sanityv3/actions/CustomDuplicateAction.ts b/sanityv3/actions/CustomDuplicateAction.ts index b37647ed4..60ea3d364 100644 --- a/sanityv3/actions/CustomDuplicateAction.ts +++ b/sanityv3/actions/CustomDuplicateAction.ts @@ -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.', }) + } }, } } diff --git a/sanityv3/actions/CustomPublishAction.ts b/sanityv3/actions/CustomPublishAction.ts index cb56c43bb..a5426c991 100644 --- a/sanityv3/actions/CustomPublishAction.ts +++ b/sanityv3/actions/CustomPublishAction.ts @@ -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) { @@ -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({ diff --git a/sanityv3/sanity.config.tsx b/sanityv3/sanity.config.tsx index 35e8e4e3e..3a3531430 100644 --- a/sanityv3/sanity.config.tsx +++ b/sanityv3/sanity.config.tsx @@ -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 } diff --git a/sanityv3/schemas/documents/news/sharedNewsFields.ts b/sanityv3/schemas/documents/news/sharedNewsFields.ts index f70a16c78..e51639c61 100644 --- a/sanityv3/schemas/documents/news/sharedNewsFields.ts +++ b/sanityv3/schemas/documents/news/sharedNewsFields.ts @@ -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 = { diff --git a/sanityv3/scripts/issue-1868/createMetadataN.mjs b/sanityv3/scripts/issue-1868/createMetadataN.mjs new file mode 100644 index 000000000..64962ab41 --- /dev/null +++ b/sanityv3/scripts/issue-1868/createMetadataN.mjs @@ -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 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) +}) diff --git a/sanityv3/scripts/issue-1868/renameLangField.mjs b/sanityv3/scripts/issue-1868/renameLangField.mjs index 7fc48d1f8..ddfb017ca 100644 --- a/sanityv3/scripts/issue-1868/renameLangField.mjs +++ b/sanityv3/scripts/issue-1868/renameLangField.mjs @@ -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, @@ -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, diff --git a/sanityv3/scripts/issue-1868/retainLastModified.mjs b/sanityv3/scripts/issue-1868/retainLastModified.mjs new file mode 100644 index 000000000..a1791c40c --- /dev/null +++ b/sanityv3/scripts/issue-1868/retainLastModified.mjs @@ -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) +}) diff --git a/web/lib/queries/common/pageContentFields.ts b/web/lib/queries/common/pageContentFields.ts index c61b110da..3d48d2019 100644 --- a/web/lib/queries/common/pageContentFields.ts +++ b/web/lib/queries/common/pageContentFields.ts @@ -13,7 +13,7 @@ import { imageCarouselFields } from './imageCarouselFields' import { keyNumbersFields } from './keyNumbersFields' import { noDrafts, sameLang } from './langAndDrafts' import promoteMagazine from './promotions/promoteMagazine' -import { publishDateTimeQuery } from './publishDateTime' +import { lastUpdatedTimeQuery, publishDateTimeQuery } from './publishDateTime' const pageContentFields = /* groq */ ` _type == "keyNumbers" =>{ @@ -274,7 +274,7 @@ _type == "keyNumbers" =>{ ] | order(${publishDateTimeQuery} desc)[0...3]{ "type": _type, "id": _id, - "updatedAt": _updatedAt, + "updatedAt": ${lastUpdatedTimeQuery}, title, heroImage, "publishDateTime": ${publishDateTimeQuery}, @@ -449,7 +449,7 @@ _type == "keyNumbers" =>{ ] | order(${publishDateTimeQuery} desc){ "type": _type, "id": _id, - "updatedAt": _updatedAt, + "updatedAt": ${lastUpdatedTimeQuery}, title, heroImage, "publishDateTime": ${publishDateTimeQuery}, diff --git a/web/lib/queries/common/publishDateTime.ts b/web/lib/queries/common/publishDateTime.ts index 7c0b024c1..7a3133cdc 100644 --- a/web/lib/queries/common/publishDateTime.ts +++ b/web/lib/queries/common/publishDateTime.ts @@ -5,3 +5,7 @@ export const publishDateTimeQuery = /* groq */ ` coalesce(firstPublishedAt, _createdAt) ) ` + +export const lastUpdatedTimeQuery = /* groq */ ` + coalesce(lastModifiedAt,_updatedAt) +` diff --git a/web/lib/queries/localNews.ts b/web/lib/queries/localNews.ts index 3015a62d2..de9715b86 100644 --- a/web/lib/queries/localNews.ts +++ b/web/lib/queries/localNews.ts @@ -5,12 +5,12 @@ import { ingressForNewsQuery, relatedLinksForNewsQuery, } from './common/newsSubqueries' -import { publishDateTimeQuery } from './common/publishDateTime' +import { publishDateTimeQuery, lastUpdatedTimeQuery } from './common/publishDateTime' import { fixPreviewForDrafts } from './common/langAndDrafts' const localNewsFields = /* groq */ ` "id": _id, - "updatedAt": _updatedAt, + "updatedAt": ${lastUpdatedTimeQuery}, title, heroImage, ${slugsForNewsAndMagazine}, diff --git a/web/lib/queries/news.ts b/web/lib/queries/news.ts index 191f70210..51d75876f 100644 --- a/web/lib/queries/news.ts +++ b/web/lib/queries/news.ts @@ -7,14 +7,14 @@ import { ingressForNewsQuery, relatedLinksForNewsQuery, } from './common/newsSubqueries' -import { publishDateTimeQuery } from './common/publishDateTime' +import { lastUpdatedTimeQuery, publishDateTimeQuery } from './common/publishDateTime' export const excludeCrudeOilAssays = Flags.IS_DEV || Flags.IS_GLOBAL_PROD ? /* groq */ `!('crude-oil-assays' in tags[]->key.current) &&` : '' const latestNewsFields = /* groq */ ` "id": _id, - "updatedAt": _updatedAt, + "updatedAt": ${lastUpdatedTimeQuery}, title, heroImage, ${slugsForNewsAndMagazine}, @@ -26,7 +26,7 @@ const latestNewsFields = /* groq */ ` const newsFields = /* groq */ ` "id": _id, - "updatedAt": _updatedAt, + "updatedAt": ${lastUpdatedTimeQuery}, title, heroImage, "publishDateTime": ${publishDateTimeQuery}, @@ -70,7 +70,7 @@ export const newsPromotionQuery = /* groq */ ` ] | order(${publishDateTimeQuery} desc)[0...3]{ "type": _type, "id": _id, - "updatedAt": _updatedAt, + "updatedAt": ${lastUpdatedTimeQuery}, title, heroImage, "publishDateTime": ${publishDateTimeQuery},