@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
605 lines (603 loc) • 21.2 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 ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import SystemIcon from '../SystemIcon';
import ListItemText from '@mui/material/ListItemText';
import { usePossibleTranslation } from '../../hooks/usePossibleTranslation';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import React, { useCallback, useEffect, useState } from 'react';
import Switch from '@mui/material/Switch';
import { fetchConfigurationJSON } from '../../services/configuration';
import { useActiveSiteId } from '../../hooks/useActiveSiteId';
import { usePreviewState } from '../../hooks/usePreviewState';
import { getGuestToHostBus, getHostToGuestBus } from '../../utils/subjects';
import { getStoredLegacyComponentPanel, setStoredLegacyComponentPanel } from '../../utils/state';
import { useActiveUser } from '../../hooks/useActiveUser';
import { useEditMode } from '../../hooks/useEditMode';
import { useIntl } from 'react-intl';
import { translations } from './translations';
import BrowseFilesDialog from '../BrowseFilesDialog';
import { deleteItem, fetchContentDOM, fetchLegacyItem, sortItem } from '../../services/content';
import { useDispatch } from 'react-redux';
import { showConfirmDialog, showEditDialog } from '../../state/actions/dialogs';
import { dragAndDropMessages } from '../../env/i18n-legacy';
import { fetchAndInsertContentInstance, legacyLoadFormDefinition, legacyXmlModelToMap } from './utils';
import { getPathFromPreviewURL } from '../../utils/path';
import { useContentTypes } from '../../hooks/useContentTypes';
import { filter } from 'rxjs/operators';
import { showSystemNotification } from '../../state/actions/system';
import { nou } from '../../utils/object';
import { forEach } from '../../utils/array';
import { useEnv } from '../../hooks/useEnv';
import { createCustomDocumentEventListener } from '../../utils/dom';
import { guestMessages } from '../../assets/guestMessages';
import { useEnhancedDialogState } from '../../hooks/useEnhancedDialogState';
export function LegacyComponentsPanel(props) {
const { title, icon } = props;
const siteId = useActiveSiteId();
const user = useActiveUser();
const [open, setOpen] = useState(false);
const hostToGuest$ = getHostToGuestBus();
const guestToHost$ = getGuestToHostBus();
const editMode = useEditMode();
const [config, setConfig] = useState(null);
const contentTypesLookup = useContentTypes();
const { guest } = usePreviewState();
const guestPath = guest?.path;
const { formatMessage } = useIntl();
const [browsePath, setBrowsePath] = useState(null);
const dispatch = useDispatch();
const { authoringBase, activeEnvironment } = useEnv();
const [legacyContentModel, setLegacyContentModel] = useState(null);
const browseFilesDialogState = useEnhancedDialogState();
const startDnD = useCallback(
(path) => {
fetchContentDOM(siteId, path ? path : guestPath).subscribe((content) => {
let contentModel = legacyXmlModelToMap(content.documentElement);
setLegacyContentModel(contentModel);
hostToGuest$.next({
type: 'START_DRAG_AND_DROP',
payload: {
browse: config.browse,
components: config.components,
contentModel,
translation: {
addComponent: formatMessage(translations.addComponent),
components: formatMessage(translations.components),
done: formatMessage(translations.done)
}
}
});
});
},
[siteId, guestPath, hostToGuest$, config, formatMessage]
);
const onComponentDelete = useCallback(
(props) => {
const { zones, compPath } = props;
// compPath is the parent path
fetchContentDOM(siteId, compPath ? compPath : guestPath).subscribe((content) => {
let contentModel = legacyXmlModelToMap(content.documentElement);
let fieldId = Object.keys(zones)[0];
let index = null;
let indexByKey = {};
zones[fieldId].forEach((item, i) => {
indexByKey[item.key] = i;
});
contentModel[fieldId].forEach((item, i) => {
if (nou(indexByKey[item.key]) && index === null) {
index = i;
}
});
deleteItem(siteId, null, fieldId, index, compPath ? compPath : guestPath).subscribe(() => {
dispatch(
showSystemNotification({
message: formatMessage(guestMessages.deleteOperationComplete)
})
);
});
});
},
[dispatch, formatMessage, guestPath, siteId]
);
const onComponentDrop = useCallback(
(props) => {
const { type, path, isNew, trackingNumber, zones, compPath, datasource, isInsert } = props;
// path is the component path(shared component) and conComp is the parent path
// if isNew is false it means it is a sort, if it is new it means it a dnd for new component
// if is 'existing' it means it is a browse component
let zonesKeys = Object.keys(zones);
let fieldId = zonesKeys[0];
let zone = zones[fieldId];
const parentPath = compPath ?? guestPath;
const parentModelId = guest?.modelIdByPath[parentPath];
const model = guest?.models[parentModelId];
const parentContentTypeId = model?.craftercms.contentTypeId;
if (isNew) {
if (isNew === true) {
if (!path) {
// region embedded component
let index = 0;
zone.forEach((zone, i) => {
if (zone === trackingNumber) {
index = i;
}
});
const editDialogSuccess = 'editDialogSuccess';
const editDialogCancel = 'editDialogCancel';
dispatch(
showEditDialog({
site: siteId,
authoringBase,
path: compPath ? compPath : guestPath,
isHidden: true,
newEmbedded: {
contentType: type,
index,
fieldId,
datasource
},
onSaveSuccess: {
type: 'DISPATCH_DOM_EVENT',
payload: { id: editDialogSuccess }
},
onClosed: {
type: 'BATCH_ACTIONS',
payload: [
{
type: 'DISPATCH_DOM_EVENT',
payload: { id: editDialogCancel }
},
{
type: 'EDIT_DIALOG_CLOSED'
}
]
}
})
);
let unsubscribe, cancelUnsubscribe;
unsubscribe = createCustomDocumentEventListener(editDialogSuccess, (response) => {
dispatch(
showSystemNotification({
message: formatMessage(guestMessages.insertOperationComplete)
})
);
hostToGuest$.next({
type: 'REFRESH_PREVIEW'
});
cancelUnsubscribe();
});
cancelUnsubscribe = createCustomDocumentEventListener(editDialogCancel, () => {
hostToGuest$.next({
type: 'REFRESH_PREVIEW'
});
unsubscribe();
});
// endregion
} else {
// region shared component
let index = 0;
const editDialogSuccess = 'editDialogSuccess';
const editDialogCancel = 'editDialogCancel';
dispatch(
showEditDialog({
authoringBase,
path,
contentTypeId: type,
isNewContent: true,
onSaveSuccess: {
type: 'DISPATCH_DOM_EVENT',
payload: { id: editDialogSuccess }
},
onClosed: {
type: 'BATCH_ACTIONS',
payload: [
{
type: 'DISPATCH_DOM_EVENT',
payload: { id: editDialogCancel }
},
{
type: 'EDIT_DIALOG_CLOSED'
}
]
}
})
);
let unsubscribe, cancelUnsubscribe;
unsubscribe = createCustomDocumentEventListener(editDialogSuccess, (response) => {
zone.forEach((zone, i) => {
if (zone === trackingNumber) {
index = i;
}
});
fetchAndInsertContentInstance(
siteId,
parentPath,
response.item.uri,
fieldId,
index,
datasource,
contentTypesLookup,
parentModelId,
parentContentTypeId
).subscribe(() => {
dispatch(
showSystemNotification({
message: formatMessage(guestMessages.insertOperationComplete)
})
);
hostToGuest$.next({
type: 'REFRESH_PREVIEW'
});
});
cancelUnsubscribe();
});
cancelUnsubscribe = createCustomDocumentEventListener(editDialogCancel, () => {
hostToGuest$.next({
type: 'REFRESH_PREVIEW'
});
unsubscribe();
});
// endregion
}
} else {
// region browse component
let index = 0;
forEach(zone, (item, i) => {
if (item === trackingNumber) {
index = i;
return 'break';
}
});
fetchAndInsertContentInstance(
siteId,
parentPath,
path,
fieldId,
index,
datasource,
contentTypesLookup,
parentModelId,
parentContentTypeId
).subscribe(() => {
dispatch(
showSystemNotification({
message: formatMessage(guestMessages.insertOperationComplete)
})
);
hostToGuest$.next({
type: 'REFRESH_PREVIEW'
});
});
// endregion
}
} else {
// region sort/move components
fetchContentDOM(siteId, compPath ? compPath : guestPath).subscribe((content) => {
let contentModel = legacyXmlModelToMap(content.documentElement);
let contentModelZone = contentModel[fieldId];
if (isInsert) {
// region insert
let index = 0;
let indexByKey = {};
if (contentModelZone) {
contentModelZone.forEach((item, i) => {
indexByKey[item.key] = i;
});
forEach(zone, (item, i) => {
if (!indexByKey[item.key]) {
index = 1;
return 'break';
}
});
}
fetchAndInsertContentInstance(
siteId,
parentPath,
path,
fieldId,
index,
datasource,
contentTypesLookup,
parentModelId,
parentContentTypeId
).subscribe(() => {
dispatch(
showSystemNotification({
message: formatMessage(guestMessages.moveOperationComplete)
})
);
});
// endregion
} else {
if (zones[fieldId].length === contentModel[fieldId].length) {
// region sort
let currentIndex = null;
let targetIndex = null;
let indexByKey = {};
contentModelZone.forEach((item, i) => {
indexByKey[item.key] = i;
});
forEach(zone, (item, i) => {
if (indexByKey[item.key] !== i && currentIndex === null) {
currentIndex = indexByKey[item.key];
targetIndex = i;
return 'break';
}
});
sortItem(siteId, null, fieldId, currentIndex, targetIndex, compPath ? compPath : guestPath).subscribe(
() => {
dispatch(
showSystemNotification({
message: formatMessage(guestMessages.sortOperationComplete)
})
);
}
);
// endregion
} else {
// region delete
let index = null;
let indexByKey = {};
zone.forEach((item, i) => {
indexByKey[item.key] = i;
});
forEach(contentModelZone, (item, i) => {
if (indexByKey[item.key] === undefined && index === null) {
index = i;
return 'break';
}
});
deleteItem(siteId, null, fieldId, index, compPath ? compPath : guestPath).subscribe(() => {});
// endregion
}
}
});
// endregion
}
},
[
authoringBase,
contentTypesLookup,
dispatch,
formatMessage,
guestPath,
hostToGuest$,
siteId,
guest?.modelIdByPath,
guest?.models
]
);
useEffect(() => {
fetchConfigurationJSON(siteId, '/preview-tools/components-config.xml', 'studio', activeEnvironment).subscribe(
(dom) => {
let config = {
browse: [],
components: []
};
if (dom.config.browse) {
config.browse = Array.isArray(dom.config.browse) ? dom.config.browse : [dom.config.browse];
}
if (dom.config.category) {
config.components = Array.isArray(dom.config.category)
? dom.config.category.map(({ component, label }) => ({
components: component,
label
}))
: [
{
components: dom.config.category.component,
label: dom.config.category.label
}
];
}
setConfig(config);
}
);
}, [siteId, activeEnvironment]);
useEffect(() => {
if (!editMode && open) {
hostToGuest$.next({
type: 'DND_COMPONENTS_PANEL_OFF'
});
}
}, [editMode, hostToGuest$, open]);
useEffect(() => {
const guestToHostSubscription = guestToHost$
.pipe(filter((action) => action.type === 'GUEST_SITE_LOAD'))
.subscribe(({ payload }) => {
const { open } = getStoredLegacyComponentPanel(user.username) ?? { open: false };
const path = getPathFromPreviewURL(payload.url);
if (config !== null && open) {
startDnD(path);
}
});
return () => {
guestToHostSubscription.unsubscribe();
};
}, [config, guestPath, guestToHost$, startDnD, user.username]);
// region subscriptions
useEffect(() => {
const guestToHostSubscription = guestToHost$.subscribe((action) => {
const { type, payload } = action;
switch (type) {
case 'DRAG_AND_DROP_COMPONENTS_PANEL_CLOSED': {
if (editMode) {
getHostToGuestBus().next({
type: 'ICE_TOOLS_ON'
});
}
break;
}
case 'SET_SESSION_STORAGE_ITEM': {
if (payload['key'] === 'components-on') {
let value = payload['value'] === 'true';
setStoredLegacyComponentPanel({ open: value }, user.username);
setOpen(value);
}
break;
}
case 'REQUEST_SESSION_STORAGE_ITEM': {
if (payload.includes('pto-on') || payload.includes('ice-on')) {
hostToGuest$.next({
type: 'REQUEST_SESSION_STORAGE_ITEM_REPLY',
payload: Array.isArray(payload)
? { 'ice-on': editMode ? 'on' : null, ptoOn: open ? 'on' : null }
: { key: 'pto-on', value: open ? 'on' : null }
});
}
break;
}
case 'OPEN_BROWSE': {
setBrowsePath(payload.path);
browseFilesDialogState.onOpen();
break;
}
case 'START_DIALOG': {
const { messageKey, message } = payload;
dispatch(
showConfirmDialog({
body: messageKey ? formatMessage(dragAndDropMessages[messageKey]) : message
})
);
break;
}
case 'REQUEST_FORM_DEFINITION': {
const { contentType } = payload;
legacyLoadFormDefinition(siteId, contentType).subscribe((config) => {
hostToGuest$.next({
type: 'REQUEST_FORM_DEFINITION_RESPONSE',
payload: config
});
});
break;
}
case 'COMPONENT_DROPPED': {
onComponentDrop(payload);
break;
}
case 'SAVE_DRAG_AND_DROP': {
onComponentDelete(payload);
break;
}
case 'LOAD_MODEL_REQUEST': {
if (payload.aNotFound) {
fetchContentDOM(siteId, payload.aNotFound.path).subscribe((content) => {
let contentModel = legacyXmlModelToMap(content.documentElement);
hostToGuest$.next({
type: 'DND_COMPONENTS_MODEL_LOAD',
payload: contentModel
});
});
}
break;
}
default:
break;
}
});
return () => {
guestToHostSubscription.unsubscribe();
};
}, [
dispatch,
editMode,
formatMessage,
guestToHost$,
hostToGuest$,
onComponentDelete,
onComponentDrop,
open,
siteId,
user.username,
browseFilesDialogState
]);
// endregion
const onOpenComponentsMenu = () => {
if (!open) {
startDnD();
} else {
hostToGuest$.next({
type: 'DND_COMPONENTS_PANEL_OFF'
});
}
};
const onBrowseDialogClosed = () => {
setBrowsePath(null);
};
const onBrowseDialogItemSelected = (item) => {
browseFilesDialogState.onClose();
fetchLegacyItem(siteId, item.path).subscribe((legacyItem) => {
hostToGuest$.next({
type: 'DND_CREATE_BROWSE_COMP',
payload: {
component: legacyItem,
initialContentModel: legacyContentModel
}
});
});
};
return React.createElement(
React.Fragment,
null,
React.createElement(
ListItem,
{ ContainerComponent: 'div' },
React.createElement(
ListItemIcon,
null,
React.createElement(SystemIcon, { icon: icon, fontIconProps: { fontSize: 'small' } })
),
React.createElement(ListItemText, {
primary: usePossibleTranslation(title),
primaryTypographyProps: { noWrap: true },
secondaryTypographyProps: { noWrap: true }
}),
React.createElement(
ListItemSecondaryAction,
{ style: { right: '5px' } },
React.createElement(Switch, { color: 'primary', checked: open, onClick: onOpenComponentsMenu })
)
),
React.createElement(BrowseFilesDialog, {
open: browseFilesDialogState.open,
path: browsePath,
onClose: browseFilesDialogState.onClose,
onClosed: onBrowseDialogClosed,
onSuccess: onBrowseDialogItemSelected,
hasPendingChanges: browseFilesDialogState.hasPendingChanges,
isMinimized: browseFilesDialogState.isMinimized,
isSubmitting: browseFilesDialogState.isSubmitting
})
);
}
export default LegacyComponentsPanel;