@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
1,074 lines (1,072 loc) • 37.1 kB
JavaScript
/*
* 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);
}
})
);
}