UNPKG

@stackbit/utils

Version:
290 lines 12.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createContentSourceWithDrafts = void 0; const update_document_operation_1 = require("./update-document-operation"); /** * The `createContentSourceWithDrafts()` function enhances the provided content * source with "Content Drafts" capabilities, enabling editors to modify multiple * content fields on a page and then save all pending changes into the CMS in a * single action. This reduces the number of content writes in the CMS. * For instance, when using this with `GitContentSource`, all pending changes are * saved in a single commit. * * To retrieve draft content in the web framework and reflect changes in Create * before they are saved to the CMS (e.g., Git), run the web framework with the * `STACKBIT_API_KEY` environment variable set to the value of the `localId` * variable printed when running `stackbit dev` locally. Then, pass the * `STACKBIT_API_KEY` to the `getCSIDocuments()` method from the `@stackbit/utils` * package to retrieve the draft content from the Create server. * * ```javascript * const result = await getCSIDocuments({ * stackbitApiKey: process.env.STACKBIT_API_KEY * }); * ``` * * You can also pass additional data to filter out specific documents by ID: * * ``` * {"documentSpecs": [{srcType: "git", srcProjectId: "...", "srcDocumentId": "..."}]} * ``` * * For example, after running `stackbit dev`, the command will print: * * ``` * info: localId: ab1...9yz * ``` * * Use the `localId` value to set the `STACKBIT_API_KEY` environment variable * when running your web framework: * * ``` * STACKBIT_API_KEY=ab1...9yz npm run dev * ``` * * Then, use the `STACKBIT_API_KEY` to fetch documents with pending "draft" * changes and render them on a page. * * When the project runs in Create Cloud, a different `STACKBIT_API_KEY` will be * generated and injected into the environment variables. */ function createContentSourceWithDrafts(options) { const contentSource = options.contentSource; const contentSourceWithDrafts = new ContentSourceWithDrafts({ contentSource }); // Return proxied content source and override some of its methods. return new Proxy(contentSource, { get(target, prop) { switch (prop) { case 'init': return contentSourceWithDrafts.init; case 'getSchema': return contentSourceWithDrafts.getSchema; case 'getDocuments': return contentSourceWithDrafts.getDocuments; case 'updateDocument': return contentSourceWithDrafts.updateDocument; default: return target[prop]; } } }); } exports.createContentSourceWithDrafts = createContentSourceWithDrafts; class ContentSourceWithDrafts { constructor(options) { /** * Override the `cache.updateContent` function passed to the content source. * This override should ensure that when `cache.updateContent` called by the * wrapped content source, it updates the documents passed in * `contentChanges.documents` with cached operations, and removes the * document specified in `contentChanges.deletedDocumentIds` from the drafts * map. */ this.init = async (options) => { this.cache = options.cache; const newOptions = { ...options, cache: { ...options.cache, getDocuments: () => { // Documents in the cache may contain unsaved changes. // Map the documents with unsaved changes to the original documents. const documents = options.cache.getDocuments(); return documents.map((document) => { const draft = this.draftsByDocumentId[document.id]; if (draft) { return draft.origDocument; } return document; }); }, getDocumentById: (documentId) => { // Documents in the cache may contain unsaved changes. // Return the original document if the document in cache has unsaved changes. const draft = this.draftsByDocumentId[documentId]; if (draft) { return draft.origDocument; } return options.cache.getDocumentById(documentId); }, updateContent: async (contentChanges) => { if (Array.isArray(contentChanges.deletedDocumentIds)) { for (const deletedDocumentId of contentChanges.deletedDocumentIds) { delete this.draftsByDocumentId[deletedDocumentId]; } } // When the wrapped content source calls cache.updateContent(), // apply the unsaved operations for documents with unsaved content. if (Array.isArray(contentChanges.documents)) { contentChanges = { ...contentChanges, documents: contentChanges.documents.map((document) => { const draft = this.draftsByDocumentId[document.id]; if (!draft || draft.saving) { return document; } return (0, update_document_operation_1.updateDocumentWithOperations)({ document, operations: draft.operations, getModelByName: this.cache.getModelByName }); }) }; } await options.cache.updateContent(contentChanges); } } }; return this.contentSource.init(newOptions); }; /** * Override the schema returned by the content source, by adding "Save" and * "Cancel" actions to every "data" and "page" model. */ this.getSchema = async () => { const schema = await this.contentSource.getSchema(); return { ...schema, models: schema.models.map((model) => { if (model.type === 'data' || model.type === 'page') { const { actions = [], ...restModel } = model; model = { ...restModel, actions: actions.concat(createSaveAction({ draftsByDocumentId: this.draftsByDocumentId, contentSource: this.contentSource }), createCancelAction({ draftsByDocumentId: this.draftsByDocumentId, cache: this.cache })) }; } return model; }) }; }; /** * When Stackbit calls `getDocuments` to reload all content source documents, * update documents that have drafts with cached operations. */ this.getDocuments = async (options) => { const getDocumentsResult = await this.contentSource.getDocuments(options); const isArray = Array.isArray(getDocumentsResult); const documents = isArray ? getDocumentsResult : getDocumentsResult.documents; const documentsWithDrafts = documents.map((document) => { const draft = this.draftsByDocumentId[document.id]; if (!draft) { return document; } return (0, update_document_operation_1.updateDocumentWithOperations)({ document, operations: draft.operations, getModelByName: this.cache.getModelByName }); }); return isArray ? documentsWithDrafts : { ...getDocumentsResult, documents: documentsWithDrafts }; }; /** * When updating a document, accumulate the update operations and store * them in `draftsByDocumentId` instead of calling the `updateDocument` * method of the wrapped content source. */ this.updateDocument = async ({ document, operations }) => { let draft = this.draftsByDocumentId[document.id]; if (!draft) { draft = { origDocument: this.cache.getDocumentById(document.id), operations: [], saving: false }; } draft.operations = draft.operations.concat(operations); const updatedDocument = (0, update_document_operation_1.updateDocumentWithOperations)({ document, operations, getModelByName: this.cache.getModelByName }); this.draftsByDocumentId[document.id] = draft; await this.cache.updateContent({ documents: [updatedDocument] }).catch(() => { // TODO: log error }); }; this.contentSource = options.contentSource; this.draftsByDocumentId = {}; } } /** * Create a custom action that invokes the `updateDocument` method on the * wrapped content source with the cached update operations for a specific * document. */ function createSaveAction({ draftsByDocumentId, contentSource }) { return { name: 'save_draft', label: 'Save', icon: 'check', preferredStyle: 'button-primary', state: async ({ document }) => { return draftsByDocumentId[document.id] ? 'enabled' : 'disabled'; }, run: async ({ document, currentUser }) => { const draft = draftsByDocumentId[document.id]; if (!draft) { return; } try { // Update documents in content-source (CMS) // Mark the draft as being saved, in case `updateDocument()` // calls `cache.updateContent()` after updating the document // with operations, and so the overriden cache won't re-apply // the same operations again. draft.saving = true; await contentSource.updateDocument({ document: draft.origDocument, operations: draft.operations, userContext: currentUser }); delete draftsByDocumentId[document.id]; } catch (error) { // If the update was unsuccessful, don't delete the draft and // revert `saving` back to false, so the user won't lose their // unsaved changes. draft.saving = false; throw error; } } }; } /** * Create a custom action that discards any cached operations for a specific document. */ function createCancelAction({ draftsByDocumentId, cache }) { return { name: 'cancel_draft', label: 'Cancel', icon: 'xmark', preferredStyle: 'button-secondary', state: async ({ document }) => { return draftsByDocumentId[document.id] ? 'enabled' : 'disabled'; }, run: async ({ document, currentUser }) => { const draft = draftsByDocumentId[document.id]; if (draft) { delete draftsByDocumentId[document.id]; // update documents in content-store await cache.updateContent({ documents: [draft.origDocument] }); } } }; } //# sourceMappingURL=content-source-with-drafts.js.map