@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
518 lines (516 loc) • 24 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, { lazy, Suspense, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { isPlainObject } from '../../utils/object';
import { useSnackbar } from 'notistack';
import { getHostToHostBus } from '../../utils/subjects';
import { blockUI, newProjectReady, showSystemNotification, unblockUI } from '../../state/actions/system';
import Launcher from '../Launcher/Launcher';
import useSelection from '../../hooks/useSelection';
import { useWithPendingChangesCloseRequest } from '../../hooks/useWithPendingChangesCloseRequest';
import MinimizedBar from '../MinimizedBar';
import { RenameAssetDialog } from '../RenameAssetDialog';
import { FormattedMessage, useIntl } from 'react-intl';
import Button from '@mui/material/Button';
import { getSystemLink } from '../../utils/system';
import useEnv from '../../hooks/useEnv';
import { filter, map, switchMap } from 'rxjs/operators';
import { fetchAll as fetchSitesService } from '../../services/sites';
import IconButton from '@mui/material/IconButton';
import CloseRounded from '@mui/icons-material/CloseRounded';
import useAuth from '../../hooks/useAuth';
import useActiveSiteId from '../../hooks/useActiveSiteId';
// region const ... = lazy(() => import('...'));
const ViewVersionDialog = lazy(() => import('../ViewVersionDialog'));
const CompareVersionsDialog = lazy(() => import('../CompareVersionsDialog'));
const RejectDialog = lazy(() => import('../RejectDialog'));
const EditSiteDialog = lazy(() => import('../EditSiteDialog'));
const ConfirmDialog = lazy(() => import('../ConfirmDialog'));
const ErrorDialog = lazy(() => import('../ErrorDialog'));
const NewContentDialog = lazy(() => import('../NewContentDialog'));
const ChangeContentTypeDialog = lazy(() => import('../ChangeContentTypeDialog'));
const HistoryDialog = lazy(() => import('../HistoryDialog'));
const PublishDialog = lazy(() => import('../PublishDialog'));
const DependenciesDialog = lazy(() => import('../DependenciesDialog/DependenciesDialog'));
const DeleteDialog = lazy(() => import('../DeleteDialog'));
const WorkflowCancellationDialog = lazy(() => import('../WorkflowCancellationDialog'));
const LegacyFormDialog = lazy(() => import('../LegacyFormDialog'));
const CreateFolderDialog = lazy(() => import('../CreateFolderDialog'));
const CopyItemsDialog = lazy(() => import('../CopyDialog'));
const CreateFileDialog = lazy(() => import('../CreateFileDialog'));
const BulkUploadDialog = lazy(() => import('../UploadDialog'));
const SingleFileUploadDialog = lazy(() => import('../SingleFileUploadDialog'));
const PreviewDialog = lazy(() => import('../PreviewDialog'));
const ItemMenu = lazy(() => import('../ItemActionsMenu'));
const ItemMegaMenu = lazy(() => import('../ItemMegaMenu'));
const AuthMonitor = lazy(() => import('../AuthMonitor'));
const PublishingStatusDialog = lazy(() => import('../PublishingStatusDialog'));
const UIBlocker = lazy(() => import('../UIBlocker'));
const PathSelectionDialog = lazy(() => import('../PathSelectionDialog'));
const UnlockPublisherDialog = lazy(() => import('../UnlockPublisherDialog'));
const WidgetDialog = lazy(() => import('../WidgetDialog'));
const CodeEditorDialog = lazy(() => import('../CodeEditorDialog'));
const BrokenReferencesDialog = lazy(() => import('../BrokenReferencesDialog'));
const FolderMoveAlertDialog = lazy(() => import('../FolderMoveAlert/FolderMoveAlertDialog'));
// endregion
// @formatter:off
function createCallback(action, dispatch) {
// prettier-ignore
return action ? (output) => {
const hasPayload = Boolean(action.payload);
const hasOutput = Boolean(output) && isPlainObject(output);
const payload = (hasPayload && !hasOutput)
// If there's a payload in the original action and there
// is no output from the resulting callback, simply use the
// original payload
? action.payload
// Otherwise, if there's no payload but there is an output sent
// to the resulting callback, use the output as the payload
: (!hasPayload && hasOutput)
? output
: ((hasPayload && hasOutput)
// If there's an output and a payload, merge them both into a single object.
// We're supposed to be using objects for all our payloads, otherwise this
// could fail with literal native values such as strings or numbers.
? Array.isArray(action.payload)
// If it's an array, assume is a BATCH_ACTIONS action payload; each item
// of the array should be an action, so merge each item with output.
? action.payload.map((a) => ({ ...a, payload: { ...a.payload, ...output } }))
// If it's not an array, it's a single action. Merge with output.
: { ...action.payload, ...output }
// Later, we check if there's a payload to add it
: false);
dispatch({
type: action.type,
...(payload ? { payload } : {})
});
} : null;
}
// @formatter:on
function GlobalDialogManager() {
const state = useSelection((state) => state.dialogs);
const contentTypesBranch = useSelection((state) => state.contentTypes);
const versionsBranch = useSelection((state) => state.versions);
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const dispatch = useDispatch();
const { authoringBase, socketConnected } = useEnv();
const { active: authActive } = useAuth();
const activeSiteId = useActiveSiteId();
const { formatMessage } = useIntl();
useEffect(() => {
const hostToHost$ = getHostToHostBus();
const subscription = hostToHost$.subscribe(({ type, payload }) => {
switch (type) {
case showSystemNotification.type:
enqueueSnackbar(payload.message, payload.options);
break;
}
});
return () => {
subscription.unsubscribe();
};
}, [enqueueSnackbar]);
useEffect(() => {
const subscription = getHostToHostBus()
.pipe(
filter((e) => e.type === newProjectReady.type),
switchMap((e) =>
// Not the most efficient approach to (re)fetch all sites (which already occurs when a new site is created), but it's not possible to
// look site by uuid or to sync this even with the completion of the background fetch of the sites.
fetchSitesService().pipe(
map((sites) => sites.find((site) => site.uuid === e.payload.siteUuid)),
filter((site) => Boolean(site))
)
)
)
.subscribe((site) => {
if (!Boolean(document.querySelector('[data-dialog-id="create-site-dialog"]'))) {
const siteId = site.id;
enqueueSnackbar(
React.createElement(FormattedMessage, {
defaultMessage: `Project "{siteId}" has been created.`,
values: { siteId }
}),
{
action: React.createElement(
Button,
{
size: 'small',
onClick: () => {
window.location.href = getSystemLink({
systemLinkId: 'preview',
authoringBase,
site: siteId,
page: '/'
});
}
},
React.createElement(FormattedMessage, { id: 'words.view', defaultMessage: 'View' })
)
}
);
}
});
return () => subscription.unsubscribe();
}, [authoringBase, enqueueSnackbar]);
useEffect(() => {
const isIframe = window.location !== window.parent.location;
if (!isIframe && authActive && !socketConnected && activeSiteId !== null) {
let timeout, key;
timeout = setTimeout(() => {
fetch(`${authoringBase}/help/socket-connection-error`)
.then((r) => {
if (r.ok) {
return r.text();
} else {
throw new Error('socket-connection-error fetch failed');
}
})
.then(() => {
key = enqueueSnackbar(
React.createElement(FormattedMessage, {
defaultMessage: 'Studio will continue to retry the connection.'
}),
{
variant: 'warning',
persist: true,
anchorOrigin: { vertical: 'bottom', horizontal: 'center' },
alertTitle: React.createElement(FormattedMessage, {
defaultMessage: 'Connection with the server interrupted'
}),
action: (key) =>
React.createElement(
React.Fragment,
null,
React.createElement(
Button,
{
href: `${authoringBase}/help/socket-connection-error`,
target: '_blank',
size: 'small',
color: 'inherit'
},
React.createElement(FormattedMessage, { defaultMessage: 'Learn more' })
),
React.createElement(
IconButton,
{ size: 'small', color: 'inherit', onClick: () => closeSnackbar(key) },
React.createElement(CloseRounded, null)
)
)
}
);
})
.catch(() => {
dispatch(
blockUI({
title: formatMessage({ defaultMessage: 'Connection with the server interrupted' }),
message: formatMessage({
defaultMessage:
'Studio servers might be down, being restarted or your network connection dropped. Check your connection or ask the administrator to validate server status.'
})
})
);
});
}, 5000);
return () => {
clearTimeout(timeout);
if (key) {
closeSnackbar(key);
} else {
dispatch(unblockUI());
}
};
}
}, [
authoringBase,
authActive,
closeSnackbar,
enqueueSnackbar,
socketConnected,
dispatch,
formatMessage,
activeSiteId
]);
return React.createElement(
Suspense,
{ fallback: '' },
React.createElement(ConfirmDialog, {
...state.confirm,
onOk: createCallback(state.confirm.onOk, dispatch),
onCancel: createCallback(state.confirm.onCancel, dispatch),
onClose: createCallback(state.confirm.onClose, dispatch),
onClosed: createCallback(state.confirm.onClosed, dispatch)
}),
React.createElement(ErrorDialog, {
...state.error,
onClose: createCallback(state.error.onClose, dispatch),
onClosed: createCallback(state.error.onClosed, dispatch),
onDismiss: createCallback(state.error.onDismiss, dispatch)
}),
React.createElement(LegacyFormDialog, {
...state.edit,
onClose: createCallback(state.edit.onClose, dispatch),
onMinimize: createCallback(state.edit.onMinimize, dispatch),
onMaximize: createCallback(state.edit.onMaximize, dispatch),
onClosed: createCallback(state.edit.onClosed, dispatch),
onSaveSuccess: createCallback(state.edit.onSaveSuccess, dispatch)
}),
React.createElement(CodeEditorDialog, {
...state.codeEditor,
onClose: createCallback(state.codeEditor.onClose, dispatch),
onMinimize: createCallback(state.codeEditor.onMinimize, dispatch),
onMaximize: createCallback(state.codeEditor.onMaximize, dispatch),
onClosed: createCallback(state.codeEditor.onClosed, dispatch),
onSuccess: createCallback(state.codeEditor.onSuccess, dispatch),
onFullScreen: createCallback(state.codeEditor.onFullScreen, dispatch),
onCancelFullScreen: createCallback(state.codeEditor.onCancelFullScreen, dispatch),
onWithPendingChangesCloseRequest: useWithPendingChangesCloseRequest(
createCallback(state.codeEditor.onClose, dispatch)
)
}),
React.createElement(PublishDialog, {
...state.publish,
onClose: createCallback(state.publish.onClose, dispatch),
onClosed: createCallback(state.publish.onClosed, dispatch),
onSuccess: createCallback(state.publish.onSuccess, dispatch),
onWithPendingChangesCloseRequest: useWithPendingChangesCloseRequest(
createCallback(state.publish.onClose, dispatch)
)
}),
React.createElement(NewContentDialog, {
...state.newContent,
onContentTypeSelected: createCallback(state.newContent.onContentTypeSelected, dispatch),
onClose: createCallback(state.newContent.onClose, dispatch),
onClosed: createCallback(state.newContent.onClosed, dispatch)
}),
React.createElement(ChangeContentTypeDialog, {
...state.changeContentType,
onContentTypeSelected: createCallback(state.changeContentType.onContentTypeSelected, dispatch),
onClose: createCallback(state.changeContentType.onClose, dispatch),
onClosed: createCallback(state.changeContentType.onClosed, dispatch)
}),
React.createElement(DependenciesDialog, {
...state.dependencies,
onClose: createCallback(state.dependencies.onClose, dispatch),
onClosed: createCallback(state.dependencies.onClosed, dispatch)
}),
React.createElement(DeleteDialog, {
...state.delete,
onClose: createCallback(state.delete.onClose, dispatch),
onClosed: createCallback(state.delete.onClosed, dispatch),
onSuccess: createCallback(state.delete.onSuccess, dispatch),
onWithPendingChangesCloseRequest: useWithPendingChangesCloseRequest(
createCallback(state.delete.onClose, dispatch)
)
}),
React.createElement(HistoryDialog, {
...state.history,
versionsBranch: versionsBranch,
onClose: createCallback(state.history.onClose, dispatch),
onClosed: createCallback(state.history.onClosed, dispatch)
}),
React.createElement(ViewVersionDialog, {
...state.viewVersion,
rightActions: state.viewVersion.rightActions?.map((action) => ({
...action,
onClick: createCallback(action.onClick, dispatch)
})),
leftActions: state.viewVersion.leftActions?.map((action) => ({
...action,
onClick: createCallback(action.onClick, dispatch)
})),
contentTypesBranch: contentTypesBranch,
onClose: createCallback(state.viewVersion.onClose, dispatch),
onClosed: createCallback(state.viewVersion.onClosed, dispatch)
}),
React.createElement(CompareVersionsDialog, {
...state.compareVersions,
leftActions: state.compareVersions.leftActions?.map((action) => ({
...action,
onClick: createCallback(action.onClick, dispatch)
})),
rightActions: state.compareVersions.rightActions?.map((action) => ({
...action,
onClick: createCallback(action.onClick, dispatch)
})),
contentTypesBranch: contentTypesBranch,
selectedA: versionsBranch?.selected[0] ? versionsBranch.byId[versionsBranch.selected[0]] : null,
selectedB: versionsBranch?.selected[1] ? versionsBranch.byId[versionsBranch.selected[1]] : null,
versionsBranch: versionsBranch,
onClose: createCallback(state.compareVersions.onClose, dispatch),
onClosed: createCallback(state.compareVersions.onClosed, dispatch)
}),
React.createElement(AuthMonitor, null),
React.createElement(WorkflowCancellationDialog, {
...state.workflowCancellation,
onClose: createCallback(state.workflowCancellation.onClose, dispatch),
onClosed: createCallback(state.workflowCancellation.onClosed, dispatch),
onContinue: createCallback(state.workflowCancellation.onContinue, dispatch)
}),
React.createElement(BrokenReferencesDialog, {
...state.brokenReferences,
onClose: createCallback(state.brokenReferences.onClose, dispatch),
onClosed: createCallback(state.brokenReferences.onClosed, dispatch),
onContinue: createCallback(state.brokenReferences.onContinue, dispatch)
}),
React.createElement(RejectDialog, {
...state.reject,
onClose: createCallback(state.reject.onClose, dispatch),
onClosed: createCallback(state.reject.onClosed, dispatch),
onRejectSuccess: createCallback(state.reject.onRejectSuccess, dispatch),
onWithPendingChangesCloseRequest: useWithPendingChangesCloseRequest(
createCallback(state.reject.onClose, dispatch)
)
}),
React.createElement(CreateFolderDialog, {
...state.createFolder,
onClose: createCallback(state.createFolder.onClose, dispatch),
onClosed: createCallback(state.createFolder.onClosed, dispatch),
onCreated: createCallback(state.createFolder.onCreated, dispatch),
onRenamed: createCallback(state.createFolder.onRenamed, dispatch),
onWithPendingChangesCloseRequest: useWithPendingChangesCloseRequest(
createCallback(state.createFolder.onClose, dispatch)
)
}),
React.createElement(CreateFileDialog, {
...state.createFile,
onClose: createCallback(state.createFile.onClose, dispatch),
onClosed: createCallback(state.createFile.onClosed, dispatch),
onCreated: createCallback(state.createFile.onCreated, dispatch),
onWithPendingChangesCloseRequest: useWithPendingChangesCloseRequest(
createCallback(state.createFile.onClose, dispatch)
)
}),
React.createElement(RenameAssetDialog, {
...state.renameAsset,
onClose: createCallback(state.renameAsset.onClose, dispatch),
onClosed: createCallback(state.renameAsset.onClosed, dispatch),
onRenamed: createCallback(state.renameAsset.onRenamed, dispatch),
onWithPendingChangesCloseRequest: useWithPendingChangesCloseRequest(
createCallback(state.renameAsset.onClose, dispatch)
)
}),
React.createElement(CopyItemsDialog, {
...state.copy,
onClose: createCallback(state.copy.onClose, dispatch),
onClosed: createCallback(state.copy.onClosed, dispatch),
onOk: createCallback(state.copy.onOk, dispatch)
}),
React.createElement(BulkUploadDialog, {
...state.upload,
onClose: createCallback(state.upload.onClose, dispatch),
onClosed: createCallback(state.upload.onClosed, dispatch),
onFileAdded: createCallback(state.upload.onFileAdded, dispatch),
onUploadSuccess: createCallback(state.upload.onUploadSuccess, dispatch)
}),
React.createElement(SingleFileUploadDialog, {
...state.singleFileUpload,
onClose: createCallback(state.singleFileUpload.onClose, dispatch),
onClosed: createCallback(state.singleFileUpload.onClosed, dispatch),
onUploadStart: createCallback(state.singleFileUpload.onUploadStart, dispatch),
onUploadComplete: createCallback(state.singleFileUpload.onUploadComplete, dispatch),
onUploadError: createCallback(state.singleFileUpload.onUploadError, dispatch)
}),
React.createElement(PreviewDialog, {
...state.preview,
onMinimize: createCallback(state.preview.onMinimize, dispatch),
onMaximize: createCallback(state.preview.onMaximize, dispatch),
onClose: createCallback(state.preview.onClose, dispatch),
onClosed: createCallback(state.preview.onClosed, dispatch),
onFullScreen: createCallback(state.preview.onFullScreen, dispatch),
onCancelFullScreen: createCallback(state.preview.onCancelFullScreen, dispatch)
}),
React.createElement(EditSiteDialog, {
...state.editSite,
onClose: createCallback(state.editSite.onClose, dispatch),
onClosed: createCallback(state.editSite.onClosed, dispatch),
onSaveSuccess: createCallback(state.editSite.onSaveSuccess, dispatch),
onSiteImageChange: createCallback(state.editSite.onSiteImageChange, dispatch),
onWithPendingChangesCloseRequest: useWithPendingChangesCloseRequest(
createCallback(state.editSite.onClose, dispatch)
)
}),
React.createElement(PathSelectionDialog, {
...state.pathSelection,
onClose: createCallback(state.pathSelection.onClose, dispatch),
onClosed: createCallback(state.pathSelection.onClosed, dispatch),
onOk: createCallback(state.pathSelection.onOk, dispatch)
}),
React.createElement(ItemMenu, { ...state.itemMenu, onClose: createCallback(state.itemMenu.onClose, dispatch) }),
React.createElement(ItemMegaMenu, {
...state.itemMegaMenu,
onClose: createCallback(state.itemMegaMenu.onClose, dispatch),
onClosed: createCallback(state.itemMegaMenu.onClosed, dispatch)
}),
React.createElement(Launcher, { ...state.launcher }),
React.createElement(PublishingStatusDialog, {
...state.publishingStatus,
onClose: createCallback(state.publishingStatus.onClose, dispatch),
onRefresh: createCallback(state.publishingStatus.onRefresh, dispatch),
onUnlock: createCallback(state.publishingStatus.onUnlock, dispatch)
}),
React.createElement(UnlockPublisherDialog, {
open: state.unlockPublisher.open,
onError: createCallback(state.unlockPublisher.onError, dispatch),
onCancel: createCallback(state.unlockPublisher.onCancel, dispatch),
onComplete: createCallback(state.unlockPublisher.onComplete, dispatch)
}),
React.createElement(WidgetDialog, {
...state.widget,
onClose: createCallback(state.widget.onClose, dispatch),
onMinimize: createCallback(state.widget.onMinimize, dispatch),
onMaximize: createCallback(state.widget.onMaximize, dispatch),
onClosed: createCallback(state.widget.onClosed, dispatch),
onWithPendingChangesCloseRequest: useWithPendingChangesCloseRequest(
createCallback(state.widget.onClose, dispatch)
)
}),
Object.values(state.minimizedTabs).map((tab) =>
React.createElement(MinimizedBar, {
key: tab.id,
open: tab.minimized,
title: tab.title,
subtitle: tab.subtitle,
status: tab.status,
onMaximize: createCallback(tab.onMaximized, dispatch)
})
),
React.createElement(UIBlocker, { ...state.uiBlocker }),
React.createElement(FolderMoveAlertDialog, {
...state.folderMoveAlert,
onClose: createCallback(state.folderMoveAlert.onClose, dispatch),
onClosed: createCallback(state.folderMoveAlert.onClosed, dispatch)
})
);
}
export default React.memo(GlobalDialogManager);