@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
292 lines (290 loc) • 11.4 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 { ofType } from 'redux-observable';
import { filter, ignoreElements, map, switchMap, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
import { NEVER, of } from 'rxjs';
import { camelize, dasherize } from '../../utils/string';
import {
closeCodeEditorDialog,
closeCompareVersionsDialog,
closeConfirmDialog,
closeDeleteDialog,
closeDependenciesDialog,
closeHistoryDialog,
closeNewContentDialog,
closePublishDialog,
closeSingleFileUploadDialog,
closeViewVersionDialog,
fetchContentVersion,
fetchContentVersionComplete,
fetchContentVersionFailed,
fetchDeleteDependencies,
fetchDeleteDependenciesComplete,
fetchDeleteDependenciesFailed,
fetchRenameAssetDependants,
fetchRenameAssetDependantsComplete,
fetchRenameAssetDependantsFailed,
newContentCreationComplete,
showCodeEditorDialog,
showConfirmDialog,
showEditDialog,
showPreviewDialog,
showPublishDialog,
updateCodeEditorDialog,
updateEditConfig,
updatePreviewDialog,
closeRenameAssetDialog
} from '../actions/dialogs';
import { fetchDeleteDependencies as fetchDeleteDependenciesService, fetchDependant } from '../../services/dependencies';
import { fetchContentXML, fetchItemVersion } from '../../services/content';
import { catchAjaxError } from '../../utils/ajax';
import { batchActions } from '../actions/misc';
import { asArray } from '../../utils/array';
import { changeCurrentUrl, requestWorkflowCancellationDialogOnResult } from '../actions/preview';
import { formEngineMessages } from '../../env/i18n-legacy';
import infoGraphic from '../../assets/information.svg';
import { nnou, nou } from '../../utils/object';
import { getHostToGuestBus } from '../../utils/subjects';
import { fetchDetailedItems, unlockItem } from '../actions/content';
import { parseLegacyItemToDetailedItem } from '../../utils/content';
function getDialogNameFromType(type) {
let name = getDialogActionNameFromType(type);
return camelize(dasherize(name.toLowerCase()));
}
function getDialogActionNameFromType(type) {
return type.replace(/(CLOSE_)|(_DIALOG)/g, '');
}
function getDialogState(type, state) {
const stateName = getDialogNameFromType(type);
const dialog = state.dialogs[stateName];
if (!dialog) {
console.error(`[epics/dialogs] Unable to retrieve dialog state from "${stateName}" action`);
}
return dialog;
}
const dialogEpics = [
// region onClose Actions
(action$, state$) =>
action$.pipe(
ofType(
closeConfirmDialog.type,
closePublishDialog.type,
closeDeleteDialog.type,
closeNewContentDialog.type,
closeHistoryDialog.type,
closeViewVersionDialog.type,
closeCompareVersionsDialog.type,
closeDependenciesDialog.type,
closeSingleFileUploadDialog.type
),
withLatestFrom(state$),
map(([{ type, payload }, state]) => {
var _a;
// Setting both onDismiss & onClose to the "CLOSE_*_DIALOG" action, allows escape
// and backdrop click to work. MUI dialogs will call onClose either when escape is
// pressed or the backdrop is clicked which is fine. When onDismiss is called, however
// the MUI dialog would later also call the onClose action and this causes a infinite
// "loop" of "CLOSE_*_DIALOG" actions. The filter insures the actions to be called
// don't include the "CLOSE_*_DIALOG" action to avoid said loop.
const onClose = (_a = getDialogState(type, state)) === null || _a === void 0 ? void 0 : _a.onClose;
return [
// In the case of batch actions, save the additional BATCH_ACTIONS action itself
// and jump straight to the actions to dispatch.
...asArray(
(payload === null || payload === void 0 ? void 0 : payload.type) === batchActions.type
? payload.payload
: payload
),
...asArray(
(onClose === null || onClose === void 0 ? void 0 : onClose.type) === batchActions.type
? onClose.payload
: onClose
)
].filter((action) => Boolean(action) && action.type && action.type !== type);
}),
filter((actions) => actions.length > 0),
switchMap((actions) => actions)
),
// endregion
// region fetchContentVersion
(action$, state$) =>
action$.pipe(
ofType(fetchContentVersion.type),
withLatestFrom(state$),
switchMap(([{ payload }, state]) =>
fetchItemVersion(state.sites.active, payload.path, payload.versionNumber).pipe(
map(fetchContentVersionComplete),
catchAjaxError(fetchContentVersionFailed)
)
)
),
// endregion
// region newContentCreationComplete
(action$, state$) =>
action$.pipe(
ofType(newContentCreationComplete.type),
filter(({ payload }) => {
var _a;
return ((_a = payload.item) === null || _a === void 0 ? void 0 : _a.isPage) && payload.item.isPreviewable;
}),
map(({ payload }) => changeCurrentUrl(payload.redirectUrl))
),
// endregion
// region fetchDeleteDependencies
(action$, state$) =>
action$.pipe(
ofType(fetchDeleteDependencies.type),
withLatestFrom(state$),
switchMap(
([
{
payload: { paths }
},
state
]) =>
fetchDeleteDependenciesService(state.sites.active, paths).pipe(
map(fetchDeleteDependenciesComplete),
catchAjaxError(fetchDeleteDependenciesFailed)
)
)
),
// endregion
// region showEditDialog, showCodeEditorDialog
(action$, state$, { getIntl }) =>
action$.pipe(
ofType(showEditDialog.type, showCodeEditorDialog.type),
withLatestFrom(state$),
switchMap(([{ type, payload }, state]) => {
// If state.path isn't null and the payload.path is different, it means another form is getting opened.
// To avoid losing state of the form, we disallow this and show a dialog indicating to close the current
// form before opening another.
let showValidation = false;
if (type === showEditDialog.type) {
showValidation =
payload.path !== state.dialogs.edit.path ||
payload.iceGroupId !== state.dialogs.edit.iceGroupId ||
payload.modelId !== state.dialogs.edit.modelId;
} else {
showValidation = payload.path !== state.dialogs.codeEditor.path;
}
if (nou(payload.path) || !showValidation) {
// If showEditDialog action is called while the dialog is already open & minimized, we maximize it.
// Differences in the showEditDialog payload — to what's on the state — are ignored, except for the path,
// which is used to check if it's the same form that's getting opened.
const { isMinimized, updateDialogAction } =
type === showEditDialog.type
? { isMinimized: state.dialogs.edit.isMinimized, updateDialogAction: updateEditConfig }
: { isMinimized: state.dialogs.codeEditor.isMinimized, updateDialogAction: updateCodeEditorDialog };
if (isMinimized === true) {
return of(updateDialogAction({ isMinimized: false }));
} else {
return NEVER;
}
} else {
return of(
showConfirmDialog({
body: getIntl().formatMessage(formEngineMessages.inProgressConfirmation),
imageUrl: infoGraphic
})
);
}
})
),
// endregion
// region showPreviewDialog
(action$, state$) =>
action$.pipe(
ofType(showPreviewDialog.type),
withLatestFrom(state$),
filter(
([{ payload }, state]) => payload.type === 'editor' && nnou(payload.url) && nou(state.dialogs.preview.content)
),
switchMap(([{ payload }, state]) =>
fetchContentXML(state.sites.active, payload.url).pipe(map((content) => updatePreviewDialog({ content })))
)
),
// endregion
// region requestWorkflowCancellationDialogOnResult
(action$) =>
action$.pipe(
ofType(requestWorkflowCancellationDialogOnResult.type),
tap((action) => {
const hostToGuest$ = getHostToGuestBus();
hostToGuest$.next(action);
}),
ignoreElements()
),
// endregion
// region Show Publish Dialog
(action$) =>
action$.pipe(
ofType(showPublishDialog.type),
filter(({ payload }) => {
var _a;
return Boolean((_a = payload.items) === null || _a === void 0 ? void 0 : _a.length);
}),
map(({ payload }) => fetchDetailedItems({ paths: payload.items.map((item) => item.path) }))
),
// endregion
// region closeCodeEditorDialog
// Moved unlock from dialog to epics since the container has no visibility of the backdrop click close and
// was hence unable to unlock the item in all cases.
(action$, state$) =>
action$.pipe(
ofType(closeCodeEditorDialog.type),
withLatestFrom(state$),
filter(([, state]) => {
const username = state.user.username;
const item = state.content.itemsByPath[state.dialogs.codeEditor.path];
return item.stateMap.locked && item.lockOwner === username;
}),
map(([, state]) => unlockItem({ path: state.dialogs.codeEditor.path }))
),
// endregion
// region renameAssetDialog
(action$, state$) =>
action$.pipe(
ofType(fetchRenameAssetDependants.type),
withLatestFrom(state$),
switchMap(([, state]) =>
fetchDependant(state.sites.active, state.dialogs.renameAsset.path).pipe(
takeUntil(action$.pipe(ofType(closeRenameAssetDialog.type))),
map((response) => {
const dependants = parseLegacyItemToDetailedItem(response);
return fetchRenameAssetDependantsComplete({ dependants });
}),
catchAjaxError(fetchRenameAssetDependantsFailed)
)
)
)
// endregion
];
export default dialogEpics;