@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
364 lines (362 loc) • 13.6 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 React, { useCallback, useRef, useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { useActiveSiteId } from '../../hooks/useActiveSiteId';
import { useSpreadState } from '../../hooks/useSpreadState';
import { menuInitialState, menuOptions } from './utils';
import { useLogicResource } from '../../hooks/useLogicResource';
import ContextMenu from '../ContextMenu';
import {
closeConfirmDialog,
fetchContentVersion,
historyDialogUpdate,
showCompareVersionsDialog,
showConfirmDialog,
showHistoryDialog,
showPreviewDialog,
showViewVersionDialog
} from '../../state/actions/dialogs';
import translations from './translations';
import { batchActions } from '../../state/actions/misc';
import { fetchContentTypes } from '../../state/actions/preview';
import { fetchContentByCommitId } from '../../services/content';
import { getEditorMode, isImage, isPreviewable, isVideo, isPdfDocument } from '../PathNavigator/utils';
import {
compareBothVersions,
compareToPreviousVersion,
compareVersion,
revertContent,
revertToPreviousVersion,
versionsChangeItem,
versionsChangeLimit,
versionsChangePage
} from '../../state/actions/versions';
import { asDayMonthDateTime } from '../../utils/datetime';
import DialogBody from '../DialogBody/DialogBody';
import SingleItemSelector from '../SingleItemSelector';
import { SuspenseWithEmptyState } from '../Suspencified/Suspencified';
import VersionList from '../VersionList';
import DialogFooter from '../DialogFooter/DialogFooter';
import { HistoryDialogPagination } from './HistoryDialogPagination';
import { historyStyles } from './HistoryDialog';
import useSelection from '../../hooks/useSelection';
import useFetchSandboxItems from '../../hooks/useFetchSandboxItems';
import { UNDEFINED } from '../../utils/constants';
export function HistoryDialogContainer(props) {
var _a;
const { versionsBranch } = props;
const { count, page, limit, current, rootPath, isConfig } = versionsBranch;
useFetchSandboxItems([versionsBranch.item.path]);
// TODO: It'd be best for the dialog to directly receive a live item. Must change versions branch to only hold the path.
const item = useSelection((state) => state.content.itemsByPath[versionsBranch.item.path]);
const path = (_a = item === null || item === void 0 ? void 0 : item.path) !== null && _a !== void 0 ? _a : '';
const [openSelector, setOpenSelector] = useState(false);
const { formatMessage } = useIntl();
const { classes } = historyStyles();
const dispatch = useDispatch();
const site = useActiveSiteId();
const timeoutRef = useRef(null);
const isItemPreviewable = isPreviewable(item);
const [menu, setMenu] = useSpreadState(menuInitialState);
const versionsResource = useLogicResource(versionsBranch, {
shouldResolve: (versionsBranch) => Boolean(versionsBranch.versions) && !versionsBranch.isFetching,
shouldReject: (versionsBranch) => Boolean(versionsBranch.error),
shouldRenew: (versionsBranch, resource) => resource.complete,
resultSelector: (versionsBranch) => versionsBranch.versions,
errorSelector: (versionsBranch) => versionsBranch.error
});
const handleOpenMenu = useCallback(
(anchorEl, version, isCurrent = false, initialCommit) => {
const hasOptions = ['page', 'component', 'taxonomy'].includes(item.systemType);
const contextMenuOptions = {};
Object.entries(menuOptions).forEach(([key, value]) => {
contextMenuOptions[key] = {
id: value.id,
label: formatMessage(value.label, value.values)
};
});
const sections = [];
if (isItemPreviewable) {
sections.push([contextMenuOptions.view]);
}
if (count > 1) {
if (hasOptions) {
if (initialCommit) {
sections.push([contextMenuOptions.compareTo, contextMenuOptions.compareToCurrent]);
} else if (isCurrent) {
sections.push([contextMenuOptions.compareTo, contextMenuOptions.compareToPrevious]);
} else {
sections.push([
contextMenuOptions.compareTo,
contextMenuOptions.compareToCurrent,
contextMenuOptions.compareToPrevious
]);
}
}
if (!item.stateMap.locked && (item.availableActionsMap.revert || isConfig)) {
sections.push([isCurrent ? contextMenuOptions.revertToPrevious : contextMenuOptions.revertToThisVersion]);
}
}
setMenu({
sections,
anchorEl,
activeItem: version
});
},
[
item === null || item === void 0 ? void 0 : item.systemType,
item === null || item === void 0 ? void 0 : item.availableActionsMap.revert,
count,
setMenu,
formatMessage,
isConfig,
item === null || item === void 0 ? void 0 : item.stateMap.locked,
isItemPreviewable
]
);
const hasMenuOptions = isItemPreviewable || count > 1;
const compareVersionDialogWithActions = () =>
showCompareVersionsDialog({
disableItemSwitching: true,
rightActions: [
{
icon: { id: '@mui/icons-material/HistoryRounded' },
onClick: showHistoryDialog({}),
'aria-label': formatMessage(translations.backToHistoryList)
}
]
});
const handleViewItem = (version) => {
const supportsDiff = ['page', 'component', 'taxonomy'].includes(item.systemType);
if (supportsDiff) {
dispatch(
batchActions([
fetchContentTypes(),
fetchContentVersion({ path, versionNumber: version.versionNumber }),
showViewVersionDialog({
rightActions: [
{
icon: { id: '@mui/icons-material/HistoryRounded' },
onClick: showHistoryDialog({}),
'aria-label': formatMessage(translations.backToHistoryList)
}
]
})
])
);
} else if (isItemPreviewable) {
fetchContentByCommitId(site, item.path, version.versionNumber).subscribe((content) => {
const image = isImage(item);
const video = isVideo(item);
const pdf = isPdfDocument(item.mimeType);
dispatch(
showPreviewDialog(
Object.assign(
{
type: image ? 'image' : video ? 'video' : pdf ? 'pdf' : 'editor',
title: item.label,
[image || video || pdf ? 'url' : 'content']: content,
mode: image || video || pdf ? UNDEFINED : getEditorMode(item),
subtitle: `v.${version.versionNumber}`
},
video ? { mimeType: item.mimeType } : {}
)
)
);
});
}
};
const compareTo = (versionNumber) => {
dispatch(
batchActions([fetchContentTypes(), compareVersion({ id: versionNumber }), compareVersionDialogWithActions()])
);
};
const compareBoth = (selected) => {
dispatch(
batchActions([
fetchContentTypes(),
compareBothVersions({ versions: selected }),
compareVersionDialogWithActions()
])
);
};
const compareToPrevious = (versionNumber) => {
dispatch(
batchActions([
fetchContentTypes(),
compareToPreviousVersion({ id: versionNumber }),
compareVersionDialogWithActions()
])
);
};
const revertToPrevious = (activeItem) => {
const previousBranch = getPreviousBranch(activeItem);
dispatch(
showConfirmDialog({
title: formatMessage(translations.confirmRevertTitle),
body: formatMessage(translations.confirmRevertBody, {
versionTitle: asDayMonthDateTime(previousBranch.lastModifiedDate)
}),
onCancel: closeConfirmDialog(),
onOk: batchActions([closeConfirmDialog(), revertToPreviousVersion({ id: activeItem.versionNumber })])
})
);
};
const getPreviousBranch = (currentBranch) => {
var _a;
const versions = versionsBranch.versions;
const currentIndex = versions.findIndex((branch) => branch.versionNumber === currentBranch.versionNumber);
return (_a = versions[currentIndex + 1]) !== null && _a !== void 0 ? _a : null;
};
const revertTo = (activeItem) => {
dispatch(
showConfirmDialog({
title: formatMessage(translations.confirmRevertTitle),
body: formatMessage(translations.confirmRevertBody, {
versionTitle: asDayMonthDateTime(activeItem.lastModifiedDate)
}),
onCancel: closeConfirmDialog(),
onOk: batchActions([closeConfirmDialog(), revertContent({ path, versionNumber: activeItem.versionNumber })])
})
);
};
const handleContextMenuClose = () => {
setMenu({
anchorEl: null,
activeItem: null
});
};
const handleContextMenuItemClicked = (option) => {
const activeItem = menu.activeItem;
setMenu(menuInitialState);
switch (option) {
case 'view': {
handleViewItem(activeItem);
break;
}
case 'compareTo': {
compareTo(activeItem.versionNumber);
break;
}
case 'compareToCurrent': {
compareBoth([activeItem.versionNumber, current]);
break;
}
case 'compareToPrevious': {
compareToPrevious(activeItem.versionNumber);
break;
}
case 'revertToPrevious': {
revertToPrevious(activeItem);
break;
}
case 'revertToThisVersion': {
revertTo(activeItem);
break;
}
default:
break;
}
};
// A user clicking too eagerly to jump to a much later page may end up clicking
// the backdrop by mistake when the number of versions goes from pushing the height
// of the dialog away from its minimum and then on the next page back to the minimum.
// The timeout gives the user a chance to catch up with the change in the position of
// the pagination button and/or realise he's reached the last page without closing the
// dialog unintentionally.
const temporaryBackdropClickDisable = () => {
clearTimeout(timeoutRef.current);
dispatch(historyDialogUpdate({ hasPendingChanges: true }));
timeoutRef.current = setTimeout(() => dispatch(historyDialogUpdate({ hasPendingChanges: false })), 700);
};
const onPageChanged = (nextPage) => {
temporaryBackdropClickDisable();
dispatch(versionsChangePage({ page: nextPage }));
};
const onRowsPerPageChange = (limit) => {
temporaryBackdropClickDisable();
dispatch(versionsChangeLimit({ limit }));
};
return React.createElement(
React.Fragment,
null,
React.createElement(
DialogBody,
{ className: classes.dialogBody, minHeight: true },
React.createElement(SingleItemSelector, {
classes: { root: classes.singleItemSelector },
label: React.createElement(FormattedMessage, { id: 'words.item', defaultMessage: 'Item' }),
open: openSelector,
disabled: isConfig,
onClose: () => setOpenSelector(false),
onDropdownClick: () => setOpenSelector(!openSelector),
rootPath: rootPath,
selectedItem: item,
onItemClicked: (item) => {
setOpenSelector(false);
dispatch(versionsChangeItem({ item }));
}
}),
React.createElement(
SuspenseWithEmptyState,
{ resource: versionsResource },
React.createElement(VersionList, {
versions: versionsResource,
onOpenMenu: hasMenuOptions ? handleOpenMenu : null,
onItemClick: handleViewItem,
current: current
})
)
),
React.createElement(
DialogFooter,
{ classes: { root: classes.dialogFooter } },
count > 0 &&
React.createElement(HistoryDialogPagination, {
count: count,
page: page,
rowsPerPage: limit,
onPageChanged: onPageChanged,
onRowsPerPageChange: onRowsPerPageChange
})
),
Boolean(menu.anchorEl) &&
React.createElement(ContextMenu, {
open: true,
anchorEl: menu.anchorEl,
onClose: handleContextMenuClose,
options: menu.sections,
onMenuItemClicked: handleContextMenuItemClicked
})
);
}