UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

1,074 lines (1,072 loc) 37.1 kB
/* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { errorSelectorApi1, get, getBinary, getGlobalHeaders, getText, post, postJSON } from '../utils/ajax'; import { catchError, map, pluck, switchMap, tap } from 'rxjs/operators'; import { forkJoin, Observable, of, zip } from 'rxjs'; import { cdataWrap, createElement, createElements, fromString, getInnerHtml, newXMLDocument, serialize } from '../utils/xml'; import { createLookupTable, nnou, nou, toQueryString } from '../utils/object'; import $ from 'jquery/dist/jquery.slim'; import { dataUriToBlob, isBlank, popPiece, removeLastPiece } from '../utils/string'; import Core from '@uppy/core'; import XHRUpload from '@uppy/xhr-upload'; import { getRequestForgeryToken } from '../utils/auth'; import { generateComponentPath, parseContentXML, parseSandBoxItemToDetailedItem, prepareVirtualItemProps } from '../utils/content'; import { fetchContentTypes } from './contentTypes'; import { getFileNameFromPath, getParentPath, getPasteItemFromPath } from '../utils/path'; import { v4 as uuid } from 'uuid'; import { isPdfDocument, isMediaContent, isTextContent } from '../components/PathNavigator/utils'; export function fetchComponentInstanceHTML(path) { return getText(`/crafter-controller/component.html${toQueryString({ path })}`).pipe(pluck('response')); } export function fetchContentXML(site, path, options) { options = Object.assign({ lock: false }, options); const qs = toQueryString({ site_id: site, path, edit: options.lock }); return get(`/studio/api/1/services/api/1/content/get-content.json${qs}`).pipe(pluck('response', 'content')); } export function fetchContentDOM(site, path) { return fetchContentXML(site, path).pipe(map(fromString)); } export function fetchDescriptorXML(site, path, options) { const qs = toQueryString(Object.assign({ siteId: site, path, flatten: true }, options)); return get(`/studio/api/2/content/descriptor${qs}`).pipe(pluck('response', 'xml')); } export function fetchDescriptorDOM(site, path, options) { return fetchDescriptorXML(site, path, options).pipe(map(fromString)); } export function fetchSandboxItem(site, path, options) { return fetchItemsByPath(site, [path], options).pipe(pluck(0)); } // endregion export function fetchDetailedItem(siteId, path, options) { const { preferContent } = Object.assign({ preferContent: true }, options); const qs = toQueryString({ siteId, path, preferContent }); return get(`/studio/api/2/content/item_by_path${qs}`).pipe( pluck('response', 'item'), map((item) => prepareVirtualItemProps(item)) ); } export function fetchDetailedItems(site, paths) { return forkJoin(paths.map((path) => fetchDetailedItem(site, path))); } export function fetchContentInstanceLookup(site, path, contentTypesLookup) { return fetchContentDOM(site, path).pipe( map((doc) => { const lookup = {}; parseContentXML(doc, path, contentTypesLookup, lookup); return lookup; }) ); } export function fetchContentInstance(site, path, contentTypesLookup) { return fetchContentDOM(site, path).pipe(map((doc) => parseContentXML(doc, path, contentTypesLookup, {}))); } export function writeContent(site, path, content, options) { options = Object.assign({ unlock: true }, options); return post( writeContentUrl({ site, path: getParentPath(path), unlock: options.unlock ? 'true' : 'false', fileName: getFileNameFromPath(path) }), content ).pipe( map((ajaxResponse) => { var _a; if ((_a = ajaxResponse.response.result) === null || _a === void 0 ? void 0 : _a.error) { // eslint-disable-next-line no-throw-literal throw Object.assign(Object.assign({}, ajaxResponse), { status: 500, response: { message: ajaxResponse.response.result.error.message } }); } else return true; }) ); } export function fetchContentInstanceDescriptor(site, path, options, contentTypeLookup) { return ( contentTypeLookup ? of(contentTypeLookup) : fetchContentTypes(site).pipe(map((contentTypes) => createLookupTable(contentTypes))) ).pipe( switchMap((contentTypeLookup) => fetchDescriptorDOM(site, path, options).pipe( map((doc) => { const modelLookup = {}; const model = parseContentXML(doc, path, contentTypeLookup, modelLookup); return { model, modelLookup }; }) ) ) ); } function writeContentUrl(qs) { qs = new URLSearchParams(qs); return `/studio/api/1/services/api/1/content/write-content.json?${qs.toString()}`; } function createComponentObject(instance, contentType, shouldSerializeValueFn) { var _a, _b; const id = (instance.craftercms.id = (_a = instance.craftercms.id) !== null && _a !== void 0 ? _a : uuid()); const path = (instance.craftercms.path = (_b = instance.craftercms.path) !== null && _b !== void 0 ? _b : generateComponentPath(id, instance.craftercms.contentTypeId)); const fileName = getFileNameFromPath(path); const serializedInstance = {}; for (let key in instance) { if (key !== 'craftercms' && key !== 'fileName' && key !== 'internalName') { let value = instance[key]; serializedInstance[key] = nnou(value) && (typeof value !== 'string' || !isBlank(value)) && (shouldSerializeValueFn === null || shouldSerializeValueFn === void 0 ? void 0 : shouldSerializeValueFn(key)) ? cdataWrap(`${value}`) : value; } } return mergeContentDocumentProps( 'component', Object.assign( { '@attributes': { id }, 'content-type': contentType.id, 'display-template': contentType.displayTemplate, // TODO: per this, at this point, internal-name is always cdata wrapped, not driven by config. 'internal-name': cdataWrap(instance.craftercms.label), 'file-name': fileName, objectId: id }, serializedInstance ) ); } // region writeInstance /** * Creates a new content item xml document and writes it to the repo. */ export function writeInstance(site, instance, contentType, shouldSerializeValueFn) { const doc = newXMLDocument('component'); const transferObj = createComponentObject(instance, contentType, shouldSerializeValueFn); createElements(doc.documentElement, transferObj); return writeContent(site, instance.craftercms.path, serialize(doc)); } // endregion // region updateField export function updateField(site, modelId, fieldId, indexToUpdate, path, value, serializeValue = false) { return performMutation( site, path, (element) => { let node = extractNode(element, removeLastPiece(fieldId) || fieldId, indexToUpdate); if (fieldId.includes('.')) { // node is <item /> inside collection const fieldToUpdate = popPiece(fieldId); let fieldNode = node.querySelector(`:scope > ${fieldToUpdate}`); if (nou(fieldNode)) { fieldNode = createElement(fieldToUpdate); node.appendChild(fieldNode); } node = fieldNode; } else if (!node) { // node is <fieldId /> inside the element node = createElement(fieldId); element.appendChild(node); } node.innerHTML = typeof serializeValue === 'function' ? serializeValue(value) : Boolean(serializeValue) ? cdataWrap(value) : value; }, modelId ); } // endregion // region performMutation function performMutation(site, path, mutation, modelId = null) { return fetchContentDOM(site, path).pipe( switchMap((doc) => { const documentModelId = doc.querySelector(':scope > objectId').innerHTML.trim(); if (nnou(modelId) && documentModelId !== modelId) { const component = doc.querySelector(`[id="${modelId}"]`); mutation(component); updateModifiedDateElement(component); } else { mutation(doc.documentElement); } updateModifiedDateElement(doc.documentElement); return post( writeContentUrl({ site, path: path, unlock: 'true', fileName: getInnerHtml(doc.querySelector(':scope > file-name')) }), serialize(doc) ).pipe(map(() => ({ updatedDocument: doc }))); }) ); } // endregion // region insertComponent /** * Inserts a *new* component item on the specified component collection field. In case of shared components, only * updates the target content item field to include the reference, does not create/write the shared component document. * */ export function insertComponent( site, modelId, fieldId, targetIndex, contentType, instance, path, shared = false, shouldSerializeValueFn ) { return performMutation( site, path, (element) => { var _a; const id = instance.craftercms.id; const path = shared ? (_a = instance.craftercms.path) !== null && _a !== void 0 ? _a : generateComponentPath(id, instance.craftercms.contentTypeId) : null; // Create the new `item` that holds or references (embedded vs shared) the component. const newItem = createElement('item'); // Add the child elements into the `item` node createElements( newItem, Object.assign( { '@attributes': { inline: !shared }, key: shared ? path : id, value: cdataWrap(instance.craftercms.label) }, shared ? { include: path, disableFlattening: 'false' } : { component: createComponentObject(instance, contentType, shouldSerializeValueFn) } ) ); insertCollectionItem(element, fieldId, targetIndex, newItem); }, modelId ); } // endregion // region insertInstance /** * Insert an *existing* (i.e. shared) component on to the document * */ export function insertInstance(site, modelId, fieldId, targetIndex, instance, path, datasource) { return performMutation( site, path, (element) => { const path = instance.craftercms.path; const newItem = createElement('item'); createElements(newItem, { '@attributes': { // TODO: Review datasource persistence. datasource: datasource !== null && datasource !== void 0 ? datasource : '' }, key: path, value: cdataWrap(instance.craftercms.label), include: path, disableFlattening: 'false' }); insertCollectionItem(element, fieldId, targetIndex, newItem); }, modelId ); } // endregion // region insertItem export function insertItem(site, modelId, fieldId, index, instance, path, shouldSerializeValueFn) { return performMutation( site, path, (element) => { let node = extractNode(element, removeLastPiece(fieldId) || fieldId, index); const newItem = createElement('item'); const serializedInstance = {}; for (let key in instance) { if (key !== 'craftercms') { let value = instance[key]; serializedInstance[key] = nnou(value) && (typeof value !== 'string' || !isBlank(value)) && (shouldSerializeValueFn === null || shouldSerializeValueFn === void 0 ? void 0 : shouldSerializeValueFn(key)) ? cdataWrap(`${value}`) : value; } } createElements(newItem, serializedInstance); node.appendChild(newItem); }, modelId ); } // endregion export function duplicateItem(site, modelId, fieldId, targetIndex, path) { return fetchContentDOM(site, path).pipe( switchMap((doc) => { var _a; const documentModelId = doc.querySelector(':scope > objectId').innerHTML.trim(); let parentElement = doc.documentElement; if (nnou(modelId) && documentModelId !== modelId) { parentElement = doc.querySelector(`[id="${modelId}"]`); } const item = extractNode(parentElement, fieldId, targetIndex).cloneNode(true); const itemPath = item.querySelector(':scope > key').textContent.trim(); const isEmbedded = Boolean(item.querySelector(':scope > component')); // removing last piece to get the parent of the item const field = extractNode(parentElement, fieldId, removeLastPiece(`${targetIndex}`)); const newItemData = updateItemId(item); newItemData.path = (_a = newItemData.path) !== null && _a !== void 0 ? _a : path; updateModifiedDateElement(parentElement); field.appendChild(item); const returnValue = { updatedDocument: doc, newItem: newItemData }; if (isEmbedded) { return post( writeContentUrl({ site, path: path, unlock: 'true', fileName: getInnerHtml(doc.querySelector(':scope > file-name')) }), serialize(doc) ).pipe(map(() => returnValue)); } else { return fetchContentDOM(site, itemPath).pipe( switchMap((componentDoc) => { // update new shared component info (ids/date) updateComponentId(componentDoc.documentElement, newItemData.modelId); updateModifiedDateElement(componentDoc.documentElement); return forkJoin([ post( writeContentUrl({ site, path, unlock: 'true', fileName: getInnerHtml(doc.querySelector(':scope > file-name')) }), serialize(doc) ), post( writeContentUrl({ site, path: newItemData.path, unlock: 'true', fileName: getInnerHtml(componentDoc.querySelector(':scope > file-name')) }), serialize(componentDoc) ) ]).pipe( map(() => { returnValue.newItem.path += `/${returnValue.newItem.modelId}.xml`; return returnValue; }) ); }) ); } }) ); } export function sortItem(site, modelId, fieldId, currentIndex, targetIndex, path) { return performMutation( site, path, (element) => { const item = extractNode(element, fieldId, currentIndex); insertCollectionItem(element, fieldId, targetIndex, item, currentIndex); }, modelId ); } export function moveItem( site, originalModelId, originalFieldId, originalIndex, targetModelId, targetFieldId, targetIndex, originalParentPath, targetParentPath ) { // TODO Warning: cannot perform as transaction whilst the UI is the one to do all this. // const isOriginalEmbedded = nnou(originalParentPath); // const isTargetEmbedded = nnou(targetParentPath); // When moving between inherited dropzone to other dropzone, the modelsIds will be different but in some cases the // parentId will be null for both targets in that case we need to add a nnou validation to parentsModelId; const isSameModel = originalModelId === targetModelId; const isSameDocument = originalParentPath === targetParentPath; if (isSameDocument || isSameModel) { // Moving items between two fields of the same document or model... return performMutation(site, originalParentPath, (element) => { // Item may be moving... // - from parent model to an embedded model // - from an embedded model to the parent model // - from an embedded model to another embedded model // - from a field to another WITHIN the same model (parent or embedded) const parentDocumentModelId = getInnerHtml(element.querySelector(':scope > objectId')); const sourceModelElement = parentDocumentModelId === originalModelId ? element : element.querySelector(`[id="${originalModelId}"]`); const targetModelElement = parentDocumentModelId === targetModelId ? element : element.querySelector(`[id="${targetModelId}"]`); const item = extractNode(sourceModelElement, originalFieldId, originalIndex); const targetField = extractNode(targetModelElement, targetFieldId, removeLastPiece(`${targetIndex}`)); const targetFieldItems = targetField.querySelectorAll(':scope > item'); const parsedTargetIndex = parseInt(popPiece(`${targetIndex}`)); if (targetFieldItems.length === parsedTargetIndex) { targetField.appendChild(item); } else { targetField.insertBefore(item, targetFieldItems[parsedTargetIndex]); } }); } else { let removedItemHTML; return performMutation( site, originalParentPath, (element) => { const item = extractNode(element, originalFieldId, originalIndex); const field = extractNode(element, originalFieldId, removeLastPiece(`${originalIndex}`)); removedItemHTML = item.outerHTML; field.removeChild(item); }, originalModelId ).pipe( switchMap(() => performMutation( site, targetParentPath, (element) => { const item = extractNode(element, targetFieldId, targetIndex); const field = extractNode(element, targetFieldId, removeLastPiece(`${targetIndex}`)); const auxElement = createElement('hold'); auxElement.innerHTML = removedItemHTML; field.insertBefore(auxElement.querySelector(':scope > item'), item); }, targetModelId ) ) ); } } export function deleteItem(site, modelId, fieldId, indexToDelete, path) { return performMutation( site, path, (element) => { let index = indexToDelete; let fieldNode = element.querySelector(`:scope > ${fieldId}`); if (typeof indexToDelete === 'string') { index = parseInt(popPiece(indexToDelete)); // A fieldId can be in the form of `a.b`, which translates to `a > item > b` on the XML. // In terms of index, since all it should ever arrive here is collection items, // this assumes the index path points to the item itself, not the collection. // By calling removeLastPiece(indexToDelete), we should get the collection node here. fieldNode = extractNode(element, fieldId, removeLastPiece(`${indexToDelete}`)); } const $fieldNode = $(fieldNode); $fieldNode.children().eq(index).remove(); if ($fieldNode.children().length === 0) { // If the node isn't completely blank, the xml formatter won't do it's job in converting to a self-closing tag. // Also, later on, when retrieved, some *legacy* functions would impaired as the deserializing into JSON had unexpected content $fieldNode.html(''); } }, modelId ); } export function fetchItemsByContentType(site, contentTypes, contentTypesLookup, options) { if (typeof contentTypes === 'string') { contentTypes = [contentTypes]; } return postJSON( `/studio/api/2/search/search.json?siteId=${site}`, Object.assign(Object.assign({}, options), { filters: { 'content-type': contentTypes } }) ).pipe( map(({ response }) => ({ count: response.result.total, paths: response.result.items.map((item) => item.path) })), switchMap(({ paths, count }) => zip( of(count), paths.length ? forkJoin( paths.reduce((array, path) => { array.push(fetchContentInstance(site, path, contentTypesLookup)); return array; }, []) ) : of([]) ) ), map(([count, array]) => { return { count, lookup: array.reduce( (hash, contentInstance) => Object.assign(hash, { [contentInstance.craftercms.path]: contentInstance }), {} ) }; }) ); } export function formatXML(site, path) { return fetchContentDOM(site, path).pipe( switchMap((doc) => post( writeContentUrl({ site, path: path, unlock: 'true', fileName: getInnerHtml(doc.querySelector(':scope > file-name')) }), serialize(doc) ) ), map(() => true) ); } // Updates a component's parent id (the item that contains the component). // If the component is embedded, update its ids too. When shared it needs to be done separately because the item // needs to be fetched. function updateItemId(item, skipShared = false) { const component = item.querySelector(':scope > component'); const key = item.querySelector(':scope > key'); const id = uuid(); if (component) { // embedded component updateComponentId(component, id); key.innerHTML = id; return { modelId: id, path: null }; } else if (!skipShared) { // shared component const originalPath = key.textContent; const basePath = originalPath.split('/').slice(0, -1).join('/'); const newPath = `${basePath}/${id}.xml`; const include = item.querySelector(':scope > include'); key.innerHTML = newPath; include.innerHTML = newPath; return { modelId: id, path: basePath }; } } // Updates the ids of a component (shared/embedded) function updateComponentId(component, id) { const objectId = component.querySelector(':scope > objectId'); const fileName = component.querySelector(':scope > file-name'); component.id = id; fileName.innerHTML = `${id}.xml`; objectId.innerHTML = id; updateElementComponentsId(component); } // Updates the ids of the embedded components inside an element // It looks for items inside a component and update its ids (skipping shared). function updateElementComponentsId(element) { element.querySelectorAll('item').forEach((item) => { updateItemId(item, true); }); } function extractNode(doc, fieldId, index) { var _a; const indexes = index === '' || nou(index) ? [] : `${index}`.split('.').map((i) => parseInt(i, 10)); let aux = (_a = doc.documentElement) !== null && _a !== void 0 ? _a : doc; if (nou(index) || isBlank(`${index}`)) { return aux.querySelector(`:scope > ${fieldId}`); } const fields = fieldId.split('.'); if (indexes.length > fields.length) { // There's more indexes than fields throw new Error( '[content/extractNode] Path not handled: indexes.length > fields.length. Indexes ' + `is ${indexes} and fields is ${fields}` ); } indexes.forEach((_index, i) => { const field = fields[i]; aux = aux.querySelectorAll(`:scope > ${field} > item`)[_index]; }); if (indexes.length === fields.length) { return aux; } else if (indexes.length < fields.length) { // There's one more field to use as there were less indexes // than there were fields. For example: fieldId: `items_o.content_o`, index: 0 // At this point, aux would be `items_o[0]` and we need to extract `content_o` const field = fields[fields.length - 1]; return aux.querySelector(`:scope > ${field}`); } } function mergeContentDocumentProps(type, data) { // Dasherized props... // content-type, display-template, no-template-required, internal-name, file-name // merge-strategy, folder-name, parent-descriptor const now = data.lastModifiedDate_dt && data.createdDate_dt ? null : createModifiedDate(); const dateCreated = data.createdDate_dt ? data.createdDate_dt : now; const dateModified = data.lastModifiedDate_dt ? data.lastModifiedDate_dt : now; return Object.assign( { 'content-type': '', 'display-template': '', 'internal-name': '', 'file-name': '', 'merge-strategy': 'inherit-levels', createdDate_dt: dateCreated, lastModifiedDate_dt: dateModified, objectId: '' }, type === 'page' ? { placeInNav: 'false' } : {}, data ); } function createModifiedDate() { return new Date().toISOString(); } function updateModifiedDateElement(doc) { doc.querySelector(':scope > lastModifiedDate_dt').innerHTML = createModifiedDate(); } function insertCollectionItem(element, fieldId, targetIndex, newItem, currentIndex) { let fieldNode = extractNode(element, fieldId, removeLastPiece(`${targetIndex}`)); let index = typeof targetIndex === 'string' ? parseInt(popPiece(targetIndex)) : targetIndex; // If currentIndex it means the op is a 'sort', and the index(targetIndex) needs to plus 1 or no if (nnou(currentIndex)) { let currentIndexParsed = typeof currentIndex === 'string' ? parseInt(popPiece(currentIndex)) : currentIndex; let targetIndexParsed = typeof targetIndex === 'string' ? parseInt(popPiece(targetIndex)) : targetIndex; if (currentIndexParsed > targetIndexParsed) { index = typeof targetIndex === 'string' ? parseInt(popPiece(targetIndex)) : targetIndex; } else { index = typeof targetIndex === 'string' ? parseInt(popPiece(targetIndex)) + 1 : targetIndex + 1; } } if (nou(fieldNode)) { fieldNode = createElement(fieldId); fieldNode.setAttribute('item-list', 'true'); element.appendChild(fieldNode); } const itemList = fieldNode.querySelectorAll(`:scope > item`); if (itemList.length === index) { fieldNode.appendChild(newItem); } else { fieldNode.insertBefore(newItem, itemList[index]); } } export function createFileUpload(uploadUrl, file, path, metaData, xsrfArgumentName) { const qs = toQueryString({ [xsrfArgumentName]: getRequestForgeryToken() }); return new Observable((subscriber) => { const uppy = new Core({ autoProceed: true }); uppy.use(XHRUpload, { endpoint: `${uploadUrl}${qs}`, headers: getGlobalHeaders() }); uppy.setMeta(metaData); const blob = dataUriToBlob(file.dataUrl); uppy.on('upload-success', (file, response) => { subscriber.next({ type: 'complete', payload: response }); subscriber.complete(); }); uppy.on('upload-progress', (file, progress) => { let type = 'progress'; subscriber.next({ type, payload: { file, progress } }); }); uppy.on('upload-error', (file, error) => { subscriber.error(error); }); uppy.addFile({ name: file.name, type: file.type, data: blob }); return () => { uppy.cancelAll(); }; }); } export function uploadDataUrl(site, file, path, xsrfArgumentName) { return createFileUpload( '/studio/api/1/services/api/1/content/write-content.json', file, path, { site, name: file.name, type: file.type, path }, xsrfArgumentName ); } export function uploadToS3(site, file, path, profileId, xsrfArgumentName) { return createFileUpload( '/studio/api/2/aws/s3/upload.json', file, path, { name: file.name, type: file.type, siteId: site, path, profileId: profileId }, xsrfArgumentName ); } export function uploadToWebDAV(site, file, path, profileId, xsrfArgumentName) { return createFileUpload( '/studio/api/2/webdav/upload', file, path, { name: file.name, type: file.type, siteId: site, path, profileId: profileId }, xsrfArgumentName ); } export function uploadToCMIS(site, file, path, repositoryId, xsrfArgumentName) { return createFileUpload( '/studio/api/2/cmis/upload', file, path, { name: file.name, type: file.type, siteId: site, cmisPath: path, cmisRepoId: repositoryId }, xsrfArgumentName ); } export function getBulkUploadUrl(site, path) { const qs = toQueryString({ site, path, contentType: 'folder', createFolders: true, draft: false, duplicate: false, unlock: true, _csrf: getRequestForgeryToken() }); return `/studio/api/1/services/api/1/content/write-content.json${qs}`; } export function fetchQuickCreateList(site) { return get(`/studio/api/2/content/list_quick_create_content.json${toQueryString({ siteId: site })}`).pipe( pluck('response', 'items') ); } export function fetchItemHistory(site, path) { return get(`/studio/api/1/services/api/1/content/get-item-versions.json${toQueryString({ site, path })}`).pipe( pluck('response'), catchError(errorSelectorApi1) ); } export function revertTo(site, path, versionNumber) { return get( `/studio/api/1/services/api/1/content/revert-content.json${toQueryString({ site, path, version: versionNumber })}` ).pipe( pluck('response'), catchError((ajaxError) => { ajaxError.response = { response: { code: 1000, message: 'Unable to revert content at this time.', remedialAction: 'Content may be locked. Try again later.' } }; throw ajaxError; }) ); } export function fetchItemVersion(site, path, versionNumber) { return of({ site, path, versionNumber, content: null }); } export function fetchVersions(site, path, versionNumbers, contentTypes) { return of([ { site, path, versionNumber: versionNumbers[0], content: null }, { site, path, versionNumber: versionNumbers[1], content: null } ]); } export function fetchChildrenByPath(siteId, path, options) { return postJSON('/studio/api/2/content/children_by_path', Object.assign({ siteId, path }, options)).pipe( pluck('response'), map(({ children, levelDescriptor, total, offset, limit }) => Object.assign(children ? children.map((child) => prepareVirtualItemProps(child)) : [], { levelDescriptor: levelDescriptor ? prepareVirtualItemProps(levelDescriptor) : null, total, offset, limit }) ) ); } export function fetchChildrenByPaths(siteId, fetchOptionsByPath, options) { const paths = Object.keys(fetchOptionsByPath); if (paths.length === 0) { return of({}); } const requests = paths.map((path) => fetchChildrenByPath(siteId, path, Object.assign(Object.assign({}, options), fetchOptionsByPath[path])).pipe( catchError((error) => { if (error.status === 404) { return of([]); } else { throw error; } }) ) ); return forkJoin(requests).pipe( map((responses) => { const data = {}; Object.keys(fetchOptionsByPath).forEach((path, i) => (data[path] = responses[i])); return data; }) ); } export function fetchItemsByPath(siteId, paths, options) { if (!(paths === null || paths === void 0 ? void 0 : paths.length)) { return of([]); } const { castAsDetailedItem = false, preferContent = true } = options !== null && options !== void 0 ? options : {}; return postJSON('/studio/api/2/content/sandbox_items_by_path', { siteId, paths, preferContent }).pipe( pluck('response'), map(({ items, missingItems }) => Object.assign( items.map((item) => prepareVirtualItemProps(castAsDetailedItem ? parseSandBoxItemToDetailedItem(item) : item)), { missingItems } ) ) ); } export function fetchItemByPath(siteId, path, options) { return fetchItemsByPath(siteId, [path], options).pipe( tap((items) => { if (items[0] === void 0) { // Fake out the 404 which the backend won't return for this bulk API // eslint-disable-next-line no-throw-literal throw { name: 'AjaxError', status: 404, response: { response: { code: 7000, message: 'Content not found', remedialAction: `Check that path '${path}' is correct and it exists in site '${siteId}'`, documentationUrl: '' } } }; } }), pluck(0) ); } export function fetchItemWithChildrenByPath(siteId, path, options) { return forkJoin({ item: fetchItemByPath(siteId, path, { castAsDetailedItem: true }), children: fetchChildrenByPath(siteId, path, options) }); } export function paste(siteId, targetPath, clipboard) { return postJSON('/studio/api/2/content/paste', { siteId, operation: clipboard.type, targetPath, item: getPasteItemFromPath(clipboard.sourcePath, clipboard.paths) }).pipe(pluck('response')); } export function duplicate(siteId, path) { return postJSON('/studio/api/2/content/duplicate', { siteId, path }).pipe(pluck('response')); } export function deleteItems(siteId, items, comment, optionalDependencies) { return postJSON('/studio/api/2/content/delete', { siteId, items, optionalDependencies, comment }).pipe(map(() => true)); } export function lock(siteId, path) { return postJSON('/studio/api/2/content/item_lock_by_path', { siteId, path }).pipe(map(() => true)); } export function unlock(siteId, path) { return postJSON('/studio/api/2/content/item_unlock_by_path', { siteId, path }).pipe( map(() => true), // Do not throw/report 409 (item is already unlocked) as an error. catchError((error) => { if (error.status === 409) { return of(false); } else { throw new Error(error); } }) ); } export function fetchWorkflowAffectedItems(site, path) { return get( `/studio/api/2/workflow/affected_paths${toQueryString({ siteId: site, path })}` ).pipe(pluck('response', 'items')); } export function createFolder(site, path, name) { return post(`/studio/api/1/services/api/1/content/create-folder.json${toQueryString({ site, path, name })}`).pipe( pluck('response'), catchError(errorSelectorApi1) ); } export function createFile(site, path, fileName) { return post( `/studio/api/1/services/api/1/content/write-content.json${toQueryString({ site, path, phase: 'onSave', fileName, unlock: true })}` ).pipe(pluck('response'), catchError(errorSelectorApi1)); } export function renameFolder(site, path, name) { return post(`/studio/api/1/services/api/1/content/rename-folder.json${toQueryString({ site, path, name })}`).pipe( pluck('response'), catchError(errorSelectorApi1) ); } export function renameContent(siteId, path, name) { return postJSON(`/studio/api/2/content/rename`, { siteId, path, name }).pipe(pluck('response')); } export function changeContentType(site, path, contentType) { return post( `/studio/api/1/services/api/1/content/change-content-type.json${toQueryString({ site, path, contentType: contentType })}` ).pipe(pluck('response'), catchError(errorSelectorApi1)); } export function checkPathExistence(site, path) { return get(`/studio/api/1/services/api/1/content/content-exists.json${toQueryString({ site_id: site, path })}`).pipe( pluck('response', 'content'), catchError(errorSelectorApi1) ); } export function fetchLegacyItem(site, path) { return get(`/studio/api/1/services/api/1/content/get-item.json${toQueryString({ site_id: site, path })}`).pipe( pluck('response', 'item'), catchError(errorSelectorApi1) ); } export function fetchLegacyItemsTree(site, path, options) { return get( `/studio/api/1/services/api/1/content/get-items-tree.json${toQueryString( Object.assign({ site_id: site, path }, options) )}` ).pipe(pluck('response', 'item'), catchError(errorSelectorApi1)); } export function fetchContentByCommitId(site, path, commitId) { return getBinary( `/studio/api/2/content/get_content_by_commit_id${toQueryString({ siteId: site, path, commitId })}`, void 0, 'blob' ).pipe( switchMap((ajax) => { const blob = ajax.response; const type = ajax.xhr.getResponseHeader('content-type'); if (isMediaContent(type) || isPdfDocument(type)) { return of(URL.createObjectURL(blob)); } else if (isTextContent(type)) { return blob.text(); } else { return of(blob); } }) ); }