import { pick, uniq, groupBy } from 'lodash/fp'; import { contentTypes, async } from '@strapi/utils'; const { getScalarAttributes, getMediaAttributes } = contentTypes; // Scalar fields that can be used in a DB `select` const AVAILABLE_STATUS_SCALAR_FIELDS = [ 'id', 'documentId', 'locale', 'updatedAt', 'createdAt', 'publishedAt' ]; // Relation populate shared by both the fast path and the full path const AVAILABLE_STATUS_POPULATE = { createdBy: { select: [ 'id', 'firstname', 'lastname', 'email' ] }, updatedBy: { select: [ 'id', 'firstname', 'lastname', 'email' ] } }; // All fields to pick from a hydrated result (scalars + populated relations + virtual) const AVAILABLE_STATUS_FIELDS = [ ...AVAILABLE_STATUS_SCALAR_FIELDS, 'createdBy', 'updatedBy', 'status' ]; const AVAILABLE_LOCALES_FIELDS = [ 'id', 'documentId', 'locale', 'updatedAt', 'createdAt', 'publishedAt' ]; /** Returns a DB filter that matches the opposite publish status. */ const oppositePublishStatus = (publishedAt)=>publishedAt !== null ? { $null: true } : { $notNull: true }; const CONTENT_MANAGER_STATUS = { PUBLISHED: 'published', DRAFT: 'draft', MODIFIED: 'modified' }; /** * Checks if the provided document version has been modified after all other versions. */ const getIsVersionLatestModification = (version, otherVersion)=>{ if (!version || !version.updatedAt) { return false; } const versionUpdatedAt = version?.updatedAt ? new Date(version.updatedAt).getTime() : 0; const otherUpdatedAt = otherVersion?.updatedAt ? new Date(otherVersion.updatedAt).getTime() : 0; return versionUpdatedAt > otherUpdatedAt; }; var documentMetadata = (({ strapi })=>({ /** * Returns available locales of a document for the current status. * * The result is sorted with the default locale (as defined by the i18n plugin) * at index 0 when present. This is the canonical-source invariant relied on by * `useDocument.getInitialFormValues` in the admin: when creating a new locale * draft, non-localized scalar/media values are inherited from * `availableLocales[0]`. Putting the default locale first means inheritance * stays predictable when sibling locales have drifted on non-localized fields * (which can happen because the server only syncs non-localized fields at * locale-creation time, not on subsequent updates — see * `copyNonLocalizedFields` in document-service/internationalization.ts). */ async getAvailableLocales (uid, version, allVersions) { // Group all versions by locale const versionsByLocale = groupBy('locale', allVersions); // Delete the current locale if (version.locale) { delete versionsByLocale[version.locale]; } // For each locale, get the ones with the same status // There will not be a draft and a version counterpart if the content // type does not have draft and publish const model = strapi.getModel(uid); const mappingResult = await async.map(Object.values(versionsByLocale), async (localeVersions)=>{ if (!contentTypes.hasDraftAndPublish(model)) { return localeVersions[0]; } const draftVersion = localeVersions.find((v)=>v.publishedAt === null); const otherVersions = localeVersions.filter((v)=>v.id !== draftVersion?.id); if (!draftVersion) { return; } return { ...draftVersion, status: this.getStatus(draftVersion, otherVersions) }; }); const filtered = mappingResult.filter(Boolean); // Sort the default locale first so `availableLocales[0]` is the canonical // source for non-localized field inheritance in the admin. Guarded so that // we no-op if the i18n plugin or its locales service is unavailable. let defaultLocaleCode; try { defaultLocaleCode = await strapi.plugin('i18n')?.service('locales')?.getDefaultLocale(); } catch { // i18n plugin disabled or service errored — leave order untouched. } if (!defaultLocaleCode) { return filtered; } // Stable partition: move the default locale entry to the front, keep the // rest of the order intact. const defaultEntries = filtered.filter((entry)=>entry.locale === defaultLocaleCode); const otherEntries = filtered.filter((entry)=>entry.locale !== defaultLocaleCode); return [ ...defaultEntries, ...otherEntries ]; }, /** * Returns available status of a document for the current locale */ getAvailableStatus (version, allVersions) { // Find the other status of the document const status = version.publishedAt !== null ? CONTENT_MANAGER_STATUS.DRAFT : CONTENT_MANAGER_STATUS.PUBLISHED; // Get version that match the current locale and not match the current status const availableStatus = allVersions.find((v)=>{ const matchLocale = v.locale === version.locale; const matchStatus = status === 'published' ? v.publishedAt !== null : v.publishedAt === null; return matchLocale && matchStatus; }); if (!availableStatus) return availableStatus; // Pick status fields (at fields, status, by fields), use lodash fp return pick(AVAILABLE_STATUS_FIELDS, availableStatus); }, /** * Get the available status of many documents, useful for batch operations * @param uid * @param documents * @returns */ async getManyAvailableStatus (uid, documents) { if (!documents.length) return []; // The status and locale of all documents should be the same const status = documents[0].publishedAt !== null ? 'published' : 'draft'; const locales = documents.map((d)=>d.locale).filter(Boolean); const where = { documentId: { $in: documents.map((d)=>d.documentId).filter(Boolean) }, publishedAt: { $null: status === 'published' } }; // If there is any locale to filter (if i18n is enabled) if (locales.length) { where.locale = { $in: locales }; } return strapi.query(uid).findMany({ where, select: AVAILABLE_STATUS_SCALAR_FIELDS }); }, getStatus (version, otherDocumentStatuses) { let draftVersion; let publishedVersion; if (version.publishedAt) { publishedVersion = version; } else { draftVersion = version; } const otherVersion = otherDocumentStatuses?.at(0); if (otherVersion?.publishedAt) { publishedVersion = otherVersion; } else if (otherVersion) { draftVersion = otherVersion; } if (!draftVersion) return CONTENT_MANAGER_STATUS.PUBLISHED; if (!publishedVersion) return CONTENT_MANAGER_STATUS.DRAFT; /* * The document is modified if the draft version has been updated more * recently than the published version. */ const isDraftModified = getIsVersionLatestModification(draftVersion, publishedVersion); return isDraftModified ? CONTENT_MANAGER_STATUS.MODIFIED : CONTENT_MANAGER_STATUS.PUBLISHED; }, // TODO is it necessary to return metadata on every page of the CM // We could refactor this so the locales are only loaded when they're // needed. e.g. in the bulk locale action modal. async getMetadata (uid, version, { availableLocales = true, availableStatus = true } = {}) { const model = strapi.getModel(uid); const hasDnP = contentTypes.hasDraftAndPublish(model); const isLocalized = model.pluginOptions?.i18n?.localized === true; if (!availableLocales && !availableStatus) { // Nothing to compute. return { availableLocales: [], availableStatus: [], versions: [] }; } if (!isLocalized && !hasDnP) { // If there are no locales and no draft/publish, there's only ever 1 version of any document. return { availableLocales: [], availableStatus: [], versions: [] }; } const onlyStatusIsRelevant = hasDnP && (!isLocalized || !availableLocales); if (onlyStatusIsRelevant) { const otherVersion = availableStatus ? await strapi.db.query(uid).findOne({ where: { documentId: version.documentId, ...version.locale ? { locale: version.locale } : {}, publishedAt: oppositePublishStatus(version.publishedAt) }, select: AVAILABLE_STATUS_SCALAR_FIELDS, populate: AVAILABLE_STATUS_POPULATE }) : null; return { availableLocales: [], availableStatus: otherVersion ? [ pick(AVAILABLE_STATUS_FIELDS, otherVersion) ] : [], versions: [] }; } // Full path for localized content types // TODO: Ignore publishedAt if availableStatus=false, and ignore locale if // i18n is disabled // Include non-translatable scalar and media fields in availableLocales for i18n prefilling let nonLocalizedFields = []; let nonLocalizedMediaFields = []; try { const i18nPlugin = strapi.plugin('i18n'); if (i18nPlugin) { const i18nService = i18nPlugin.service('content-types'); if (i18nService?.getNonLocalizedAttributes) { if (model?.attributes) { const allNonLocalized = i18nService.getNonLocalizedAttributes(model); // Get scalar and media attributes separately const scalarAttrs = getScalarAttributes(model); const mediaAttrs = getMediaAttributes(model); // Separate scalar fields (can be in fields array) from media fields (need to be populated) nonLocalizedFields = allNonLocalized.filter((field)=>field in model.attributes && scalarAttrs.includes(field)); nonLocalizedMediaFields = allNonLocalized.filter((field)=>field in model.attributes && mediaAttrs.includes(field)); } } } } catch (error) { // i18n plugin might not be enabled or might error, ignore silently } // Build populate object for non-localized media fields const mediaPopulate = nonLocalizedMediaFields.reduce((acc, field)=>{ acc[field] = { populate: { folder: true } }; return acc; }, {}); const params = { populate: { ...mediaPopulate, ...AVAILABLE_STATUS_POPULATE }, fields: uniq([ ...AVAILABLE_LOCALES_FIELDS, ...nonLocalizedFields ]), filters: { documentId: version.documentId } }; const dbParams = strapi.get('query-params').transform(uid, params); const versions = await strapi.db.query(uid).findMany(dbParams); // TODO: Remove use of available locales and use localizations instead const availableLocalesResult = availableLocales ? await this.getAvailableLocales(uid, version, versions) : []; const availableStatusResult = availableStatus ? this.getAvailableStatus(version, versions) : null; return { availableLocales: availableLocalesResult, availableStatus: availableStatusResult ? [ availableStatusResult ] : [], versions }; }, /** * Returns associated metadata of a document: * - Available locales of the document for the current status * - Available status of the document for the current locale */ async formatDocumentWithMetadata (uid, document, opts = {}) { if (!document) { return { data: document, meta: { availableLocales: [], availableStatus: [] } }; } const hasDraftAndPublish = contentTypes.hasDraftAndPublish(strapi.getModel(uid)); // Ignore available status if the content type does not have draft and publish if (!hasDraftAndPublish) { opts.availableStatus = false; } const { versions, ...meta } = await this.getMetadata(uid, document, opts); // Populate localization statuses if (document.localizations?.length) { document.localizations = document.localizations.map((d)=>{ // Find the counterpart version (same documentId + locale, opposite publishedAt) from // the already-fetched versions array, avoiding an extra DB query. const counterpart = versions.find((v)=>v.documentId === d.documentId && v.locale === d.locale && d.publishedAt === null !== (v.publishedAt === null)); return { ...d, status: this.getStatus(d, counterpart ? [ counterpart ] : []) }; }); } return { data: { ...document, // Add status to the document only if draft and publish is enabled status: hasDraftAndPublish ? this.getStatus(document, meta.availableStatus) : undefined }, meta }; } })); export { documentMetadata as default }; //# sourceMappingURL=document-metadata.mjs.map