UNPKG

@craftercms/studio-ui

Version:

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

1,246 lines (1,245 loc) 54.5 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 React, { useEffect, useRef, useState } from 'react'; import { changeCurrentUrl, clearSelectedZones, clearSelectForEdit, contentTypeDropTargetsResponse, contentTypesResponse, deleteItemOperation, deleteItemOperationComplete, deleteItemOperationFailed, duplicateItemOperation, duplicateItemOperationComplete, duplicateItemOperationFailed, fetchContentTypes, fetchGuestModel, fetchGuestModelComplete, fetchPrimaryGuestModelComplete, guestCheckIn, guestCheckOut, guestModelUpdated, guestSiteLoad, hostCheckIn, hotKey, iceZoneSelected, initRichTextEditorConfig, insertComponentOperation, insertItemOperation, insertItemOperationComplete, insertItemOperationFailed, insertOperationComplete, insertOperationFailed, instanceDragBegun, instanceDragEnded, moveItemOperation, moveItemOperationComplete, moveItemOperationFailed, reloadRequest, requestEdit, requestWorkflowCancellationDialog, requestWorkflowCancellationDialogOnResult, selectForEdit, setContentTypeDropTargets, setEditModePadding, setHighlightMode, setItemBeingDragged, setPreviewEditMode, showEditDialog as showEditDialogAction, sortItemOperation, sortItemOperationComplete, sortItemOperationFailed, toggleEditModePadding, trashed, updateFieldValueOperation, updateFieldValueOperationComplete, updateFieldValueOperationFailed, updateRteConfig, snackGuestMessage } from '../../state/actions/preview'; import { writeInstance, deleteItem, duplicateItem, fetchContentInstance, fetchContentInstanceDescriptor, fetchItemsByPath, fetchSandboxItem as fetchSandboxItemService, fetchWorkflowAffectedItems, insertComponent, insertInstance, insertItem, moveItem, sortItem, updateField } from '../../services/content'; import { filter, map, switchMap, takeUntil } from 'rxjs/operators'; import { forkJoin, of } from 'rxjs'; import { FormattedMessage, useIntl } from 'react-intl'; import { getGuestToHostBus, getHostToGuestBus, getHostToHostBus } from '../../utils/subjects'; import { useDispatch, useStore } from 'react-redux'; import { nnou } from '../../utils/object'; import { findParentModelId, getModelIdFromInheritedField, isInheritedField } from '../../utils/model'; import RubbishBin from '../RubbishBin/RubbishBin'; import { useSnackbar } from 'notistack'; import { getStoredClipboard, getStoredEditModeChoice, getStoredEditModePadding, getStoredHighlightModeChoice, removeStoredClipboard, getOutdatedXBValidationDate, setOutdatedXBValidationDate } from '../../utils/state'; import { fetchSandboxItem, reloadDetailedItem, restoreClipboard, unlockItem, updateItemsByPath } from '../../state/actions/content'; import EditFormPanel from '../EditFormPanel/EditFormPanel'; import { createModelHierarchyDescriptorMap, getComputedEditMode, getNumOfMenuOptionsForItem, hasEditAction, isItemLockedForMe, normalizeModel, normalizeModelsLookup, parseContentXML } from '../../utils/content'; import moment from 'moment-timezone'; import Snackbar from '@mui/material/Snackbar'; import CloseRounded from '@mui/icons-material/CloseRounded'; import IconButton from '@mui/material/IconButton'; import { useSelection } from '../../hooks/useSelection'; import { usePreviewState } from '../../hooks/usePreviewState'; import { useContentTypes } from '../../hooks/useContentTypes'; import { useActiveUser } from '../../hooks/useActiveUser'; import { useMount } from '../../hooks/useMount'; import { usePreviewNavigation } from '../../hooks/usePreviewNavigation'; import { useActiveSite } from '../../hooks/useActiveSite'; import { getPathFromPreviewURL, processPathMacros, withIndex } from '../../utils/path'; import { closeItemMegaMenu, closeSingleFileUploadDialog, rtePickerActionResult, showEditDialog, showItemMegaMenu, showRtePickerActions, showSingleFileUploadDialog, showWorkflowCancellationDialog, workflowCancellationDialogClosed } from '../../state/actions/dialogs'; import { UNDEFINED } from '../../utils/constants'; import { useCurrentPreviewItem } from '../../hooks/useCurrentPreviewItem'; import { useSiteUIConfig } from '../../hooks/useSiteUIConfig'; import { useRTEConfig } from '../../hooks/useRTEConfig'; import { guestMessages } from '../../assets/guestMessages'; import { useEnhancedDialogState } from '../../hooks/useEnhancedDialogState'; import KeyboardShortcutsDialog from '../KeyboardShortcutsDialog'; import { previewKeyboardShortcuts } from '../../assets/keyboardShortcuts'; import { contentEvent, contentTypeCreated, contentTypeDeleted, contentTypeUpdated, lockContentEvent, pluginInstalled, pluginUninstalled, showSystemNotification } from '../../state/actions/system'; import useSpreadState from '../../hooks/useSpreadState'; import useUpdateRefs from '../../hooks/useUpdateRefs'; import { useHotkeys } from 'react-hotkeys-hook'; import { batchActions, dispatchDOMEvent, editContentTypeTemplate } from '../../state/actions/misc'; import { RefreshRounded } from '@mui/icons-material'; import { getPersonFullName } from '../SiteDashboard'; import { useTheme } from '@mui/material/styles'; import { createCustomDocumentEventListener } from '../../utils/dom'; import BrowseFilesDialog from '../BrowseFilesDialog'; import DataSourcesActionsList from '../DataSourcesActionsList/DataSourcesActionsList'; import { editControllerActionCreator, itemActionDispatcher } from '../../utils/itemActions'; import useEnv from '../../hooks/useEnv'; import useAuth from '../../hooks/useAuth'; import { getOffsetLeft, getOffsetTop } from '@mui/material/Popover'; import { isSameDay } from '../../utils/datetime'; const originalDocDomain = document.domain; const startCommunicationDetectionTimeout = (timeoutRef, setShowSnackbar, timeout = 5000) => { clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(() => setShowSnackbar(true), timeout); }; // region const issueDescriptorRequest = () => {...} const issueDescriptorRequest = (props) => { const { site, path, contentTypes, requestedSourceMapPaths, flatten = true, dispatch, completeAction } = props; const hostToGuest$ = getHostToGuestBus(); const guestToHost$ = getGuestToHostBus(); fetchContentInstanceDescriptor(site, path, { flatten }, contentTypes) .pipe( // If another check in comes while loading, this request should be cancelled. // This may happen if navigating rapidly from one page to another (guest-side). takeUntil(guestToHost$.pipe(filter(({ type }) => [guestCheckIn.type, guestCheckOut.type].includes(type)))), switchMap((obj) => { let requests = []; let sandboxItemPaths = []; let sandboxItemPathLookup = {}; Object.values(obj.modelLookup).forEach((model) => { if (model.craftercms.path) { sandboxItemPaths.push(model.craftercms.path); sandboxItemPathLookup[model.craftercms.path] = true; Object.values(model.craftercms.sourceMap).forEach((path) => { if (!sandboxItemPathLookup[path]) { sandboxItemPathLookup[path] = true; sandboxItemPaths.push(path); } if (!requestedSourceMapPaths.current[path]) { requestedSourceMapPaths.current[path] = true; requests.push(fetchContentInstance(site, path, contentTypes)); } }); } }); return forkJoin({ sandboxItems: fetchItemsByPath(site, sandboxItemPaths), models: requests.length ? forkJoin(requests).pipe( map((response) => { let lookup = obj.modelLookup; response.forEach((contentInstance) => { lookup = Object.assign(Object.assign({}, lookup), { [contentInstance.craftercms.id]: contentInstance }); }); return Object.assign(Object.assign({}, obj), { modelLookup: lookup }); }) ) : of(obj) }); }) ) .subscribe(({ sandboxItems, models: { model, modelLookup } }) => { const normalizedModels = normalizeModelsLookup(modelLookup); const hierarchyMap = createModelHierarchyDescriptorMap(normalizedModels, contentTypes); const normalizedModel = normalizedModels[model.craftercms.id]; const modelIdByPath = {}; Object.values(modelLookup).forEach((model) => { // Embedded components don't have a path. if (model.craftercms.path) { modelIdByPath[model.craftercms.path] = model.craftercms.id; } }); dispatch( batchActions([ completeAction({ model: normalizedModel, modelLookup: normalizedModels, modelIdByPath: modelIdByPath, hierarchyMap }), updateItemsByPath({ items: sandboxItems }) ]) ); hostToGuest$.next({ type: 'FETCH_GUEST_MODEL_COMPLETE', payload: { path, model: normalizedModel, modelLookup: normalizedModels, hierarchyMap, modelIdByPath: modelIdByPath, sandboxItems } }); }); }; // endregion const dataSourceActionsListInitialState = { show: false, rect: null, items: [] }; export function PreviewConcierge(props) { var _a; const dispatch = useDispatch(); const store = useStore(); const { id: siteId, uuid } = (_a = useActiveSite()) !== null && _a !== void 0 ? _a : {}; const user = useActiveUser(); const { guest, editMode, highlightMode, editModePadding, icePanelWidth, toolsPanelWidth, hostSize, showToolsPanel } = usePreviewState(); const item = useCurrentPreviewItem(); const { currentUrlPath } = usePreviewNavigation(); const contentTypes = useContentTypes(); const { authoringBase, guestBase, xsrfArgument } = useSelection((state) => state.env); const priorState = useRef({ site: siteId }); const { enqueueSnackbar } = useSnackbar(); const { formatMessage } = useIntl(); const { active: authActive } = useAuth(); const models = guest === null || guest === void 0 ? void 0 : guest.models; const modelIdByPath = guest === null || guest === void 0 ? void 0 : guest.modelIdByPath; const hierarchyMap = guest === null || guest === void 0 ? void 0 : guest.hierarchyMap; const requestedSourceMapPaths = useRef({}); const guestDetectionTimeoutRef = useRef(); const [guestDetectionSnackbarOpen, setGuestDetectionSnackbarOpen] = useState(false); const { socketConnected } = useEnv(); const socketConnectionTimeoutRef = useRef(); const [socketConnectionSnackbarOpen, setSocketConnectionSnackbarOpen] = useState(false); const currentItemPath = guest === null || guest === void 0 ? void 0 : guest.path; const uiConfig = useSiteUIConfig(); const { cdataEscapedFieldPatterns } = uiConfig; const rteConfig = useRTEConfig(); const keyboardShortcutsDialogState = useEnhancedDialogState(); const theme = useTheme(); const browseFilesDialogState = useEnhancedDialogState(); const [browseFilesDialogPath, setBrowseFilesDialogPath] = useState('/'); const [browseFilesDialogMimeTypes, setBrowseFilesDialogMimeTypes] = useState([]); const [dataSourceActionsListState, setDataSourceActionsListState] = useSpreadState(dataSourceActionsListInitialState); const env = useEnv(); const conditionallyToggleEditMode = (nextHighlightMode) => { if (item && !isItemLockedForMe(item, user.username) && hasEditAction(item.availableActions)) { dispatch( setPreviewEditMode({ // If switching from highlight modes (all vs move), we just want to switch modes without turning off edit mode. editMode: nextHighlightMode !== highlightMode ? true : !editMode, highlightMode: nextHighlightMode }) ); } }; const upToDateRefs = useUpdateRefs({ store, item, theme, guest, models, user, siteId, dispatch, guestBase, rteConfig, contentTypes, xsrfArgument, hierarchyMap, highlightMode, modelIdByPath, formatMessage, authoringBase, currentUrlPath, enqueueSnackbar, editModePadding, cdataEscapedFieldPatterns, conditionallyToggleEditMode, keyboardShortcutsDialogState, setDataSourceActionsListState, showToolsPanel, toolsPanelWidth, browseFilesDialogState, onShortCutKeypress(event) { const key = event.key; switch (key) { case 'e': upToDateRefs.current.conditionallyToggleEditMode('all'); break; case 'm': upToDateRefs.current.conditionallyToggleEditMode('move'); break; case 'p': upToDateRefs.current.dispatch(toggleEditModePadding()); break; case '?': upToDateRefs.current.keyboardShortcutsDialogState.onOpen(); break; case 'r': getHostToGuestBus().next(reloadRequest()); break; case 'E': dispatch( showEditDialog({ site: upToDateRefs.current.siteId, path: upToDateRefs.current.guest.path, readonly: isItemLockedForMe(upToDateRefs.current.item, upToDateRefs.current.user.username), authoringBase: upToDateRefs.current.authoringBase }) ); break; case 'a': if (store.getState().dialogs.itemMegaMenu.open) { dispatch(closeItemMegaMenu()); } else if (upToDateRefs.current.item) { let top, left; let menuButton = document.querySelector('#previewAddressBarActionsMenuButton'); if (menuButton) { let anchorRect = menuButton.getBoundingClientRect(); top = anchorRect.top + getOffsetTop(anchorRect, 'top'); left = anchorRect.left + getOffsetLeft(anchorRect, 'left'); } else { top = 80; left = (upToDateRefs.current.showToolsPanel ? upToDateRefs.current.toolsPanelWidth : 0) + 20; } let path = upToDateRefs.current.item.path; if (path === '/site/website') { path = withIndex(path); } dispatch( showItemMegaMenu({ path: path, anchorReference: 'anchorPosition', anchorPosition: { top, left }, loaderItems: getNumOfMenuOptionsForItem(item) }) ); } break; } }, env }); const onRtePickerResult = (payload) => { const hostToGuest$ = getHostToGuestBus(); hostToGuest$.next({ type: rtePickerActionResult.type, payload }); }; useEffect(() => { if (!socketConnected && authActive) { startCommunicationDetectionTimeout(socketConnectionTimeoutRef, setSocketConnectionSnackbarOpen); } else { clearTimeout(socketConnectionTimeoutRef.current); setSocketConnectionSnackbarOpen(false); } }, [socketConnected, authActive]); // Legacy Guest pencil repaint - When the guest screen size changes, pencils need to be repainted. useEffect(() => { if (editMode) { let timeout = setTimeout(() => { getHostToGuestBus().next({ type: 'REPAINT_PENCILS' }); }, 500); return () => { clearTimeout(timeout); }; } }, [icePanelWidth, toolsPanelWidth, hostSize, editMode, showToolsPanel]); // Send editMode changes to guest useEffect(() => { // FYI. Path navigator refresh triggers this effect too due to item changing. if (item) { const mode = getComputedEditMode({ item, username: user.username, editMode }); getHostToGuestBus().next(setPreviewEditMode({ editMode: mode })); } }, [item, editMode, user.username, dispatch]); // Fetch active item useEffect(() => { if (currentItemPath && siteId) { dispatch(fetchSandboxItem({ path: currentItemPath })); } }, [dispatch, currentItemPath, siteId]); // Update rte config useEffect(() => { if (rteConfig) { // @ts-ignore - TODO: type action accordingly getHostToGuestBus().next(updateRteConfig({ rteConfig })); } }, [rteConfig]); // Guest detection, document domain restoring, editMode/highlightMode preference retrieval, // and guest key up/down notifications. useMount(() => { const localEditMode = getStoredEditModeChoice(user.username); if (nnou(localEditMode) && editMode !== localEditMode) { dispatch(setPreviewEditMode({ editMode: localEditMode })); } const localHighlightMode = getStoredHighlightModeChoice(user.username); if (nnou(localHighlightMode) && highlightMode !== localHighlightMode) { dispatch(setHighlightMode({ highlightMode: localHighlightMode })); } const localPaddingMode = getStoredEditModePadding(user.username); if (nnou(localPaddingMode) && editModePadding !== localPaddingMode) { dispatch(setEditModePadding({ editModePadding: localPaddingMode })); } startCommunicationDetectionTimeout(guestDetectionTimeoutRef, setGuestDetectionSnackbarOpen); return () => { document.domain = originalDocDomain; }; }); // Retrieve stored site clipboard, retrieve stored tools panel page. useEffect(() => { const localClipboard = getStoredClipboard(uuid, user.username); if (localClipboard) { let hours = moment().diff(moment(localClipboard.timestamp), 'hours'); if (hours >= 24) { removeStoredClipboard(uuid, user.username); } else { dispatch( restoreClipboard({ type: localClipboard.type, paths: localClipboard.paths, sourcePath: localClipboard.sourcePath }) ); } } }, [dispatch, uuid, user.username]); // Post content types useEffect(() => { contentTypes && getHostToGuestBus().next(contentTypesResponse({ contentTypes: Object.values(contentTypes) })); }, [contentTypes]); // region guestToHost$ subscription useEffect(() => { const hostToGuest$ = getHostToGuestBus(); const guestToHost$ = getGuestToHostBus(); const hostToHost$ = getHostToHostBus(); const updatedModifiedItem = (path) => { upToDateRefs.current.dispatch( reloadDetailedItem({ path }) ); }; const guestToHostSubscription = guestToHost$.subscribe((action) => { var _a, _b, _c, _d; const { siteId, models, dispatch, guestBase, contentTypes, hierarchyMap, authoringBase, formatMessage, modelIdByPath, enqueueSnackbar, env, user } = upToDateRefs.current; const { type, payload } = action; switch (type) { case guestSiteLoad.type: case guestCheckIn.type: const { version: guestVersion } = payload; const studioVersion = env.version; if ( type === guestCheckIn.type && (guestVersion === null || guestVersion === void 0 ? void 0 : guestVersion.substr(0, 5)) !== studioVersion ) { const xbOutdatedValidationDate = getOutdatedXBValidationDate(siteId, user.username); // If message has not been shown today or not shown at all if (!xbOutdatedValidationDate || !isSameDay(xbOutdatedValidationDate, new Date())) { enqueueSnackbar(formatMessage(guestMessages.outdatedExpBuilderVersion)); setOutdatedXBValidationDate(siteId, user.username, new Date()); } } clearTimeout(guestDetectionTimeoutRef.current); setGuestDetectionSnackbarOpen(false); break; } switch (type) { // region Legacy preview sites messages case guestSiteLoad.type: { const { url, location } = payload; const path = getPathFromPreviewURL(url); dispatch(guestCheckIn({ location, site: siteId, path })); issueDescriptorRequest({ site: siteId, path, contentTypes, requestedSourceMapPaths, dispatch, completeAction: fetchPrimaryGuestModelComplete }); break; } case 'ICE_ZONE_ON': { dispatch( showEditDialog({ path: payload.itemId, authoringBase, site: siteId, iceGroupId: payload.iceId || UNDEFINED, modelId: payload.embeddedItemId || UNDEFINED, isHidden: Boolean(payload.embeddedItemId) }) ); break; } case 'IS_REVIEWER': { getHostToGuestBus().next({ type: 'REPAINT_PENCILS' }); break; } case 'CHECK_OUT_GUEST': { const path = getPathFromPreviewURL(payload.url); dispatch(guestCheckOut({ path })); break; } // endregion case guestCheckIn.type: case fetchGuestModel.type: { if (type === guestCheckIn.type) { getHostToGuestBus().next( hostCheckIn({ editMode: false, username: upToDateRefs.current.user.username, highlightMode: upToDateRefs.current.highlightMode, authoringBase: upToDateRefs.current.authoringBase, site: upToDateRefs.current.siteId, editModePadding: upToDateRefs.current.editModePadding, rteConfig: (_a = upToDateRefs.current.rteConfig) !== null && _a !== void 0 ? _a : {} }) ); dispatch(guestCheckIn(payload)); if (payload.documentDomain) { try { document.domain = payload.documentDomain; } catch (e) { console.error(e); } } else if (document.domain !== originalDocDomain) { document.domain = originalDocDomain; } if (payload.__CRAFTERCMS_GUEST_LANDING__) { nnou(siteId) && dispatch(changeCurrentUrl('/')); } else { const path = payload.path; contentTypes && hostToGuest$.next(contentTypesResponse({ contentTypes: Object.values(contentTypes) })); issueDescriptorRequest({ site: siteId, path, contentTypes, requestedSourceMapPaths, dispatch, completeAction: fetchPrimaryGuestModelComplete }); } } /* else if (type === FETCH_GUEST_MODEL) */ else { if ((_b = payload.path) === null || _b === void 0 ? void 0 : _b.startsWith('/')) { issueDescriptorRequest({ site: siteId, path: payload.path, contentTypes, requestedSourceMapPaths, dispatch, completeAction: fetchGuestModelComplete }); } else { return console.warn(`Ignoring FETCH_GUEST_MODEL request since "${payload.path}" is not a valid path.`); } } break; } case guestCheckOut.type: { requestedSourceMapPaths.current = {}; dispatch(action); startCommunicationDetectionTimeout(guestDetectionTimeoutRef, setGuestDetectionSnackbarOpen); break; } case sortItemOperation.type: { const { fieldId, currentIndex, targetIndex } = payload; let { modelId, parentModelId } = payload; const path = models[modelId !== null && modelId !== void 0 ? modelId : parentModelId].craftercms.path; if (isInheritedField(models[modelId], fieldId)) { modelId = getModelIdFromInheritedField(models[modelId], fieldId, upToDateRefs.current.modelIdByPath); parentModelId = findParentModelId(modelId, upToDateRefs.current.hierarchyMap, models); } sortItem( siteId, modelId, fieldId, currentIndex, targetIndex, models[parentModelId ? parentModelId : modelId].craftercms.path ).subscribe({ next({ updatedDocument }) { const updatedModels = {}; parseContentXML( updatedDocument, parentModelId ? models[parentModelId].craftercms.path : models[modelId].craftercms.path, contentTypes, updatedModels ); dispatch(guestModelUpdated({ model: normalizeModel(updatedModels[modelId]) })); issueDescriptorRequest({ site: siteId, path: path !== null && path !== void 0 ? path : models[parentModelId].craftercms.path, contentTypes, requestedSourceMapPaths, dispatch, completeAction: fetchGuestModelComplete }); hostToHost$.next(sortItemOperationComplete(payload)); updatedModifiedItem(path); enqueueSnackbar(formatMessage(guestMessages.sortOperationComplete)); }, error(error) { console.error(`${type} failed`, error); hostToHost$.next(sortItemOperationFailed()); // If write operation fails the items remains locked, so we need to dispatch unlockItem dispatch(unlockItem({ path })); enqueueSnackbar(formatMessage(guestMessages.sortOperationFailed), { variant: 'error' }); } }); break; } case insertComponentOperation.type: { const { fieldId, targetIndex, instance, shared = false, create = false } = payload; let { modelId, parentModelId } = payload; const path = models[modelId !== null && modelId !== void 0 ? modelId : parentModelId].craftercms.path; const contentType = contentTypes[instance.craftercms.contentTypeId]; if (isInheritedField(models[modelId], fieldId)) { modelId = getModelIdFromInheritedField(models[modelId], fieldId, modelIdByPath); parentModelId = findParentModelId(modelId, hierarchyMap, models); } const shouldSerializeFn = (instanceFieldId) => upToDateRefs.current.cdataEscapedFieldPatterns.some((pattern) => Boolean(instanceFieldId.match(pattern))); // Cases: // - Shared new - shared: true, create: true -> insertComponent // - Shared existing - shared: true, create: false -> insertInstance // - Embedded new - shared: false, create: true -> insertComponent // * Embedded existing - shared: false, create: false -> insertInstance <- This case doesn't go through here, it goes by the move/sort operation. let serviceObservable = create ? // region insertComponent insertComponent( siteId, modelId, fieldId, targetIndex, contentType, instance, models[parentModelId ? parentModelId : modelId].craftercms.path, shared, shouldSerializeFn ) : // endregion // region insertInstance insertInstance( siteId, modelId, fieldId, targetIndex, instance, models[parentModelId ? parentModelId : modelId].craftercms.path ); // endregion // Writing the xml document for the component being inserted only applies to new & shared. if (shared && create) { let postWriteObs = serviceObservable; serviceObservable = writeInstance(siteId, instance, contentType, shouldSerializeFn).pipe( switchMap(() => postWriteObs) ); } serviceObservable.subscribe({ next() { issueDescriptorRequest({ site: siteId, path: path !== null && path !== void 0 ? path : models[parentModelId].craftercms.path, contentTypes, requestedSourceMapPaths, dispatch, completeAction: fetchGuestModelComplete }); hostToGuest$.next( insertOperationComplete( Object.assign(Object.assign({}, payload), { currentFullUrl: `${guestBase}${upToDateRefs.current.currentUrlPath}` }) ) ); updatedModifiedItem(path); enqueueSnackbar(formatMessage(guestMessages.insertOperationComplete)); }, error(error) { console.error(`${type} failed`, error); hostToGuest$.next(insertOperationFailed()); // If write operation fails the items remains locked, so we need to dispatch unlockItem dispatch(unlockItem({ path })); enqueueSnackbar(formatMessage(guestMessages.insertOperationFailed), { variant: 'error' }); } }); break; } case insertItemOperation.type: { const { modelId, parentModelId, fieldId, index, instance } = payload; const path = models[parentModelId !== null && parentModelId !== void 0 ? parentModelId : modelId].craftercms.path; insertItem(siteId, modelId, fieldId, index, instance, path, (instanceFieldId) => upToDateRefs.current.cdataEscapedFieldPatterns.some((pattern) => Boolean(instanceFieldId.match(pattern))) ).subscribe({ next() { hostToGuest$.next(insertItemOperationComplete()); enqueueSnackbar(formatMessage(guestMessages.insertItemOperationComplete)); }, error(error) { console.error(`${type} failed`, error); hostToGuest$.next(insertItemOperationFailed()); // If write operation fails the items remains locked, so we need to dispatch unlockItem dispatch(unlockItem({ path })); enqueueSnackbar(formatMessage(guestMessages.insertItemOperationFailed), { variant: 'error' }); } }); break; } case duplicateItemOperation.type: { const { modelId, parentModelId, fieldId, index } = payload; const path = models[parentModelId !== null && parentModelId !== void 0 ? parentModelId : modelId].craftercms.path; duplicateItem(siteId, modelId, fieldId, index, path).subscribe({ next({ newItem }) { issueDescriptorRequest({ site: siteId, path: newItem.path, contentTypes, requestedSourceMapPaths, dispatch, completeAction: fetchPrimaryGuestModelComplete }); hostToGuest$.next(duplicateItemOperationComplete()); enqueueSnackbar(formatMessage(guestMessages.duplicateItemOperationComplete)); }, error(error) { console.error(`${type} failed`, error); hostToGuest$.next(duplicateItemOperationFailed()); // If write operation fails the items remains locked, so we need to dispatch unlockItem dispatch(unlockItem({ path })); enqueueSnackbar(formatMessage(guestMessages.duplicateItemOperationFailed), { variant: 'error' }); } }); break; } case moveItemOperation.type: { const { originalFieldId, originalIndex, targetFieldId, targetIndex } = payload; let { originalModelId, originalParentModelId, targetModelId, targetParentModelId } = payload; const originPath = models[originalParentModelId ? originalParentModelId : originalModelId].craftercms.path; const targetPath = models[targetParentModelId ? targetParentModelId : targetModelId].craftercms.path; if (isInheritedField(models[originalModelId], originalFieldId)) { originalModelId = getModelIdFromInheritedField(models[originalModelId], originalFieldId, modelIdByPath); originalParentModelId = findParentModelId(originalModelId, hierarchyMap, models); } if (isInheritedField(models[targetModelId], targetFieldId)) { targetModelId = getModelIdFromInheritedField(models[targetModelId], targetFieldId, modelIdByPath); targetParentModelId = findParentModelId(targetModelId, hierarchyMap, models); } moveItem( siteId, originalModelId, originalFieldId, originalIndex, targetModelId, targetFieldId, targetIndex, originPath, targetPath ).subscribe({ next() { hostToGuest$.next(moveItemOperationComplete()); dispatch( batchActions([ reloadDetailedItem({ path: originPath }), reloadDetailedItem({ path: targetPath }) ]) ); enqueueSnackbar(formatMessage(guestMessages.moveOperationComplete)); }, error(error) { console.error(`${type} failed`, error); hostToGuest$.next(moveItemOperationFailed()); // If write operation fails the items remains locked, so we need to dispatch unlockItem dispatch(batchActions([unlockItem({ path: originPath }), unlockItem({ path: targetPath })])); enqueueSnackbar(formatMessage(guestMessages.moveOperationFailed), { variant: 'error' }); } }); break; } case deleteItemOperation.type: { const { fieldId, index } = payload; let { modelId, parentModelId } = payload; const path = models[modelId !== null && modelId !== void 0 ? modelId : parentModelId].craftercms.path; if (isInheritedField(models[modelId], fieldId)) { modelId = getModelIdFromInheritedField(models[modelId], fieldId, modelIdByPath); parentModelId = findParentModelId(modelId, hierarchyMap, models); } deleteItem( siteId, modelId, fieldId, index, models[parentModelId ? parentModelId : modelId].craftercms.path ).subscribe({ next: () => { issueDescriptorRequest({ site: siteId, path: path !== null && path !== void 0 ? path : models[parentModelId].craftercms.path, contentTypes, requestedSourceMapPaths, dispatch, completeAction: fetchGuestModelComplete }); hostToHost$.next(deleteItemOperationComplete(payload)); updatedModifiedItem(path); enqueueSnackbar(formatMessage(guestMessages.deleteOperationComplete)); }, error: (error) => { console.error(`${type} failed`, error); hostToHost$.next(deleteItemOperationFailed()); // If write operation fails the items remains locked, so we need to dispatch unlockItem dispatch(unlockItem({ path })); enqueueSnackbar(formatMessage(guestMessages.deleteOperationFailed), { variant: 'error' }); } }); break; } case updateFieldValueOperation.type: { const { fieldId, index, value } = payload; let { modelId, parentModelId } = payload; const path = models[parentModelId ? parentModelId : modelId].craftercms.path; if (isInheritedField(models[modelId], fieldId)) { modelId = getModelIdFromInheritedField(models[modelId], fieldId, modelIdByPath); parentModelId = findParentModelId(modelId, hierarchyMap, models); } updateField( siteId, modelId, fieldId, index, path, value, upToDateRefs.current.cdataEscapedFieldPatterns.some((pattern) => Boolean(fieldId.match(pattern))) ) .pipe(switchMap(() => fetchSandboxItemService(siteId, path))) .subscribe({ next(item) { hostToGuest$.next(updateFieldValueOperationComplete({ item })); updatedModifiedItem(path); enqueueSnackbar(formatMessage(guestMessages.updateOperationComplete)); }, error(error) { console.error(`${type} failed`, error); hostToGuest$.next(updateFieldValueOperationFailed()); enqueueSnackbar(formatMessage(guestMessages.updateOperationFailed), { variant: 'error' }); } }); break; } case iceZoneSelected.type: { dispatch(selectForEdit(payload)); break; } case clearSelectedZones.type: { dispatch(clearSelectForEdit()); break; } case instanceDragBegun.type: case instanceDragEnded.type: { dispatch(setItemBeingDragged(type === instanceDragBegun.type ? payload : null)); break; } case contentTypeDropTargetsResponse.type: { dispatch(setContentTypeDropTargets(payload)); break; } case snackGuestMessage.type: { enqueueSnackbar( payload.id in guestMessages ? formatMessage(guestMessages[payload.id], (_c = payload.values) !== null && _c !== void 0 ? _c : {}) : payload.id, { variant: payload.level ? payload.level === 'required' ? 'error' : payload.level === 'suggestion' ? 'warning' : 'info' : null } ); break; } case hotKey.type: { upToDateRefs.current.onShortCutKeypress(payload); break; } case showEditDialogAction.type: { dispatch( showEditDialog({ authoringBase, path: upToDateRefs.current.guest.path, selectedFields: payload.selectedFields, site: siteId }) ); break; } case updateRteConfig.type: { // @ts-ignore - TODO: type action accordingly getHostToGuestBus().next( updateRteConfig({ rteConfig: (_d = upToDateRefs.current.rteConfig) !== null && _d !== void 0 ? _d : {} }) ); break; } case requestEdit.type: { let { store } = upToDateRefs.current; const { modelId, parentModelId, fields, typeOfEdit: type, index } = payload; const path = models[parentModelId ? parentModelId : modelId].craftercms.path; let item = store.getState().content.itemsByPath[path]; const model = models[modelId]; const contentType = contentTypes[model.craftercms.contentTypeId]; if (type === 'content') { // Not quite sure if it ever happens that the item isn't already loaded. (item ? of(item) : fetchSandboxItemService(siteId, path, { castAsDetailedItem: true })).subscribe( (item) => { itemActionDispatcher({ item, site: siteId, option: 'edit', dispatch, authoringBase, formatMessage, extraPayload: { modelId: parentModelId ? modelId : null, selectedFields: fields, index } }); } ); } else if (type === 'template') { dispatch(editContentTypeTemplate({ contentTypeId: contentType.id })); } else { dispatch(editControllerActionCreator(contentType.type, contentType.id)); } break; } case requestWorkflowCancellationDialog.type: { fetchWorkflowAffectedItems(payload.siteId, payload.path).subscribe((items) => { dispatch( showWorkflowCancellationDialog({ items, onClosed: batchActions([ workflowCancellationDialogClosed(), requestWorkflowCancellationDialogOnResult({ type: 'close' }) ]), onContinue: requestWorkflowCancellationDialogOnResult({ type: 'continue' }) }) ); }); break; } // region actions whitelisted case unlockItem.type: { dispatch(action); break; } // endregion case showRtePickerActions.type: { const typedPayload = payload; const { setDataSourceActionsListState, showToolsPanel, toolsPanelWidth, browseFilesDialogState } = upToDateRefs.current; const onShowSingleFileUploadDialog = (path, type) => { setDataSourceActionsListState(dataSourceActionsListInitialState); if (path) { dispatch( showSingleFileUploadDialog({ site: siteId, path, fileTypes: type === 'image' ? ['image/*'] : ['video/*'], onClose: batchActions([ closeSingleFileUploadDialog(), dispatchDOMEvent({ id: 'fileUploadCanceled' }) ]), onUploadComplete: batchActions([ closeSingleFileUploadDialog(), dispatchDOMEvent({ id: 'fileUploaded' }) ]) }) ); let unsubscribe, cancelUnsubscribe; unsubscribe = createCustomDocumentEventListener('fileUploaded', ({ successful: response }) => { const file = response[0]; const filePath = `${file.meta.path}${file.meta.path.endsWith('/') ? '' : '/'}${file.meta.name}`; onRtePickerResult({ path: filePath, name: file.meta.name }); cancelUnsubscribe(); }); cancelUnsubscribe = createCustomDocumentEventListener('fileUploadCanceled', () => { onRtePickerResult(); unsubscribe(); }); } else { dispatch( showSystemNotification({ message: formatMessage(guestMessages.noPathSetInDataSource) }) ); } }; const onShowBrowseFilesDialog = (path, type) => { const mimeTypes = type === 'image' ? ['image/png', 'image/jpeg', 'image/gif', 'image/jpg'] : ['video/mp4']; setDataSourceActionsListState(dataSourceActionsListInitialState); if (path) { setBrowseFilesDialogPath(path); setBrowseFilesDialogMimeTypes(mimeTypes); browseFilesDialogState.onOpen(); } else { dispatch( showSystemNotification({ message: formatMessage(guestMessages.noPathSetInDataSource) }) ); } }; const dataSourcesByType = { image: ['allowImageUpload', 'allowImagesFromRepo'], media: ['allowVideoUpload', 'allowVideosFromRepo'] }; // filter data sources to only the ones that match the type const dataSourcesKeys = Object.keys(typedPayload.datasources).filter((datasourceId) => { var _a; return (_a = dataSourcesByType[typedPayload.type]) === null || _a === void 0 ? void 0 : _a.includes(datasourceId); }); // directly open corresponding dialog if (dataSourcesKeys.length === 1) { // determine if upload or browse const key = dataSourcesKeys[0]; const processedPath = processPathMacros({ path: typedPayload.datasources[key].value, objectId: typedPayload.model.craftercms.id, objectGroupId: typedPayload.model.objectGroupId }); if (key === 'allowImageUpload' || key === 'allowVideoUpload') { onShowSingleFileUploadDialog(processedPath, typedPayload.type); } else { onShowBrowseFilesDialog(processedPath, typedPayload.type); } } else if (dataSourcesKeys.length > 1) { // create items for DataSourcesActionsList const dataSourcesItems = []; dataSourcesKeys.forEach((dataSourceKey) => { dataSourcesItems.push({ label: formatMessage(guestMessages[dataSourceKey]), path: processPathMacros({ path: typedPayload.datasources[dataSourceKey].value, objectId: typedPayload.model.objectId, objectGroupId: typedPayload.model.objectGroupId }), action: dataSourceKey === 'allowImageUpload' || dataSourceKey === 'allowVideoUpload' ? onShowSingleFileUploadDialog : onShowBrowseFilesDialog, type: typedPayload.type }); }); const { left, top, height } = typedPayload.rect; setDataSourceActionsListState({ show: true, items: dataSourcesItems, rect: Object.assign(Object.assign({}, typedPayload.rect), { left: left + (showToolsPanel ? toolsPanelWidth : 0), top: top + height * 3 // To position correctly under the button }) }); } else if (dataSourcesKeys.length === 0) { dispatch( showSystemNotification({ message: formatMessage(guestMessages.noDataSourcesSet) }) ); } } } }); return () => { guestToHostSubscription.unsubscribe(); }; }, [upToDateRefs]); // hostToHost$ subscription useEffect(() => { const hostToHost$ = getHostToHostBus(); const hostToGuest$ = getHostToGuestBus(); const hostToHostSubscription = hostToHost$.subscribe(({ type, payload }) => { const { guest, user, enqueueSnackbar, formatMessage } = upToDateRefs.current; switch (type) { case pluginUninstalled.type: case contentTypeCreated.type: case contentTypeUpdated.type: case contentTypeDeleted.type: case pluginInstalled.type: { dispatch(fetchContentTypes()); break; } case contentEvent.type: { const { user: person, targetPath } = payload; const { theme } = upToDateRefs.current; if ( person.username !== user.username && guest && (guest.path === targetPath || guest.modelIdByPath[targetPath]) ) { enqueueSnackbar( formatMessage(guestMessages.contentWasChangedByAnotherUser, { name: getPersonFullName(person) }), { action: React.createElement( IconButton, { size: 'small', onClick: () => hostToGuest$.next(reloadRequest()), sx: { color: `common.${theme.palette.mode === 'light' ? 'white' : 'black'}` } }, React.createElement(RefreshRounded, null) ) } ); } break; } case lockContentEvent.type: { const { user: person, targetPath, locked } = payload; if ( locked && (guest === null || guest === void 0 ? void 0 : guest.path) === targetPath && person.username !== user.username ) { enqueueSnackbar( formatMessage(guestMessages.contentWasLockedByAnotherUser, { name: getPersonFullName(person) }) ); } break; } } }); return () => { hostToHostSubscription.unsubsc