@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
531 lines (529 loc) • 19.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 { ofType } from 'redux-observable';
import { ignoreElements, map, mergeMap, switchMap, tap, throttleTime, withLatestFrom } from 'rxjs/operators';
import { catchAjaxError } from '../../utils/ajax';
import {
checkPathExistence,
fetchChildrenByPath,
fetchChildrenByPaths,
fetchItemsByPath,
fetchItemWithChildrenByPath
} from '../../services/content';
import { getIndividualPaths, getParentPath, getRootPath, withIndex, withoutIndex } from '../../utils/path';
import { forkJoin, NEVER } from 'rxjs';
import {
pathNavigatorBackgroundRefresh,
pathNavigatorBulkFetchPathComplete,
pathNavigatorBulkFetchPathFailed,
pathNavigatorBulkRefresh,
pathNavigatorChangeLimit,
pathNavigatorChangePage,
pathNavigatorConditionallySetPath,
pathNavigatorConditionallySetPathComplete,
pathNavigatorConditionallySetPathFailed,
pathNavigatorFetchParentItems,
pathNavigatorFetchParentItemsComplete,
pathNavigatorFetchPath,
pathNavigatorFetchPathComplete,
pathNavigatorFetchPathFailed,
pathNavigatorInit,
pathNavigatorRefresh,
pathNavigatorSetCollapsed,
pathNavigatorSetCurrentPath,
pathNavigatorSetKeyword,
pathNavRootPathMissing
} from '../actions/pathNavigator';
import { setStoredPathNavigator } from '../../utils/state';
import { showErrorDialog } from '../reducers/dialogs/error';
import {
contentEvent,
deleteContentEvent,
deleteContentEvents,
moveContentEvent,
pluginInstalled,
publishEvent,
workflowEvent
} from '../actions/system';
export default [
// region pathNavigatorInit
(action$, state$) =>
action$.pipe(
ofType(pathNavigatorInit.type),
withLatestFrom(state$),
mergeMap(
([
{
payload: { id, excludes, rootPath, limit, sortStrategy, order }
},
state
]) =>
checkPathExistence(state.sites.active, rootPath).pipe(
map((exists) =>
exists
? pathNavigatorFetchParentItems({
id,
path: state.pathNavigator[id].currentPath,
offset: state.pathNavigator[id].offset,
keyword: state.pathNavigator[id].keyword,
excludes,
limit,
sortStrategy,
order
})
: pathNavRootPathMissing({ id })
)
)
)
),
// endregion
// region pathNavigatorRefresh
(action$, state$) =>
action$.pipe(
ofType(pathNavigatorRefresh.type, pathNavigatorBackgroundRefresh.type),
withLatestFrom(state$),
mergeMap(
([
{
type,
payload: { id }
},
state
]) =>
fetchItemWithChildrenByPath(state.sites.active, state.pathNavigator[id].currentPath, {
keyword: state.pathNavigator[id].keyword,
limit: state.pathNavigator[id].limit,
offset: state.pathNavigator[id].offset,
excludes: state.pathNavigator[id].excludes,
sortStrategy: state.pathNavigator[id].sortStrategy,
order: state.pathNavigator[id].order
}).pipe(
map(({ item, children }) => pathNavigatorFetchPathComplete({ id, parent: item, children })),
catchAjaxError((error) => {
if (error.status === 404 && state.pathNavigator[id].rootPath !== state.pathNavigator[id].currentPath) {
return pathNavigatorConditionallySetPath({ id, path: state.pathNavigator[id].rootPath });
} else {
return pathNavigatorFetchPathFailed({ error, id });
}
})
)
)
),
// endregion
// region pathNavigatorBulkRefresh
(action$, state$) =>
action$.pipe(
ofType(pathNavigatorBulkRefresh.type),
withLatestFrom(state$),
mergeMap(([{ payload }, state]) => {
const { requests } = payload;
let paths = [];
let optionsByPath = {};
requests.forEach(({ id }) => {
const chunk = state.pathNavigator[id];
const { currentPath, keyword, limit, offset, excludes, sortStrategy, order } = chunk;
paths.push(currentPath);
optionsByPath[currentPath] = {
keyword,
limit,
offset,
excludes,
sortStrategy,
order
};
});
return requests.length
? forkJoin([
fetchItemsByPath(state.sites.active, paths, { castAsDetailedItem: true }),
fetchChildrenByPaths(state.sites.active, optionsByPath)
]).pipe(
map(([items, children]) =>
pathNavigatorBulkFetchPathComplete({
paths: requests.map(({ id }) => ({
id,
parent: items.find((item) =>
item.path.startsWith(withoutIndex(state.pathNavigator[id].currentPath))
),
children: children[state.pathNavigator[id].currentPath]
}))
})
),
catchAjaxError((error) => pathNavigatorBulkFetchPathFailed({ ids: requests.map(({ id }) => id), error }))
)
: NEVER;
})
),
// endregion
// region pathNavigatorFetchPath
(action$, state$) =>
action$.pipe(
ofType(pathNavigatorFetchPath.type),
withLatestFrom(state$),
mergeMap(
([
{
type,
payload: { id, path, keyword }
},
state
]) =>
fetchItemWithChildrenByPath(state.sites.active, path, {
excludes: state.pathNavigator[id].excludes,
limit: state.pathNavigator[id].limit,
sortStrategy: state.pathNavigator[id].sortStrategy,
order: state.pathNavigator[id].order,
...(keyword && { keyword })
}).pipe(
map(({ item, children }) => pathNavigatorFetchPathComplete({ id, parent: item, children })),
catchAjaxError(
(error) => pathNavigatorFetchPathFailed({ id, error }),
(error) => showErrorDialog({ error: error.response ?? error })
)
)
)
),
// endregion
// region pathNavigatorConditionallySetPath
(action$, state$) =>
action$.pipe(
ofType(pathNavigatorConditionallySetPath.type),
withLatestFrom(state$),
mergeMap(
([
{
payload: { id, path, keyword }
},
state
]) =>
fetchItemWithChildrenByPath(state.sites.active, path, {
excludes: state.pathNavigator[id].excludes,
limit: state.pathNavigator[id].limit,
sortStrategy: state.pathNavigator[id].sortStrategy,
order: state.pathNavigator[id].order,
...(keyword && { keyword })
}).pipe(
map(({ item, children }) =>
pathNavigatorConditionallySetPathComplete({ id, path, parent: item, children })
),
catchAjaxError(
(error) => pathNavigatorConditionallySetPathFailed({ id, error }),
(error) => showErrorDialog({ error: error.response ?? error })
)
)
)
),
// endregion
// region pathNavigatorSetCurrentPath
(action$, state$) =>
action$.pipe(
ofType(pathNavigatorSetCurrentPath.type),
withLatestFrom(state$),
mergeMap(
([
{
payload: { id, path }
},
state
]) =>
fetchItemWithChildrenByPath(state.sites.active, path, {
sortStrategy: state.pathNavigator[id].sortStrategy,
order: state.pathNavigator[id].order
}).pipe(
map(({ item, children }) => pathNavigatorFetchPathComplete({ id, parent: item, children })),
catchAjaxError((error) => pathNavigatorFetchPathFailed({ error, id }))
)
)
),
// endregion
// region pathNavigatorSetKeyword
(action$, state$) =>
action$.pipe(
ofType(pathNavigatorSetKeyword.type),
withLatestFrom(state$),
mergeMap(
([
{
type,
payload: { id, keyword }
},
state
]) =>
fetchChildrenByPath(state.sites.active, state.pathNavigator[id].currentPath, {
keyword,
limit: state.pathNavigator[id].limit,
sortStrategy: state.pathNavigator[id].sortStrategy,
order: state.pathNavigator[id].order,
excludes: state.pathNavigator[id].excludes
}).pipe(
map((children) =>
pathNavigatorFetchPathComplete({
id,
parent: state.content.itemsByPath[state.pathNavigator[id].currentPath],
children
})
),
catchAjaxError((error) => pathNavigatorFetchPathFailed({ error, id }))
)
)
),
// endregion
// region pathNavigatorChangePage
(action$, state$) =>
action$.pipe(
ofType(pathNavigatorChangePage.type, pathNavigatorChangeLimit.type),
withLatestFrom(state$),
mergeMap(
([
{
type,
payload: { id, offset }
},
state
]) =>
fetchChildrenByPath(state.sites.active, state.pathNavigator[id].currentPath, {
limit: state.pathNavigator[id].limit,
sortStrategy: state.pathNavigator[id].sortStrategy,
order: state.pathNavigator[id].order,
excludes: state.pathNavigator[id].excludes,
...(Boolean(state.pathNavigator[id].keyword) && { keyword: state.pathNavigator[id].keyword }),
offset
}).pipe(
map((children) => pathNavigatorFetchPathComplete({ id, children })),
catchAjaxError((error) => pathNavigatorFetchPathFailed({ error, id }))
)
)
),
// endregion
// region pathNavigatorFetchParentItems
(action$, state$) =>
action$.pipe(
ofType(pathNavigatorFetchParentItems.type),
withLatestFrom(state$),
mergeMap(
([
{
type,
payload: { id, path, excludes, limit, offset, keyword, sortStrategy, order }
},
state
]) => {
const site = state.sites.active;
const parentsPath = getIndividualPaths(path, state.pathNavigator[id].rootPath);
if (parentsPath.length > 1) {
return forkJoin([
fetchItemsByPath(site, parentsPath, { castAsDetailedItem: true }),
fetchChildrenByPath(site, path, {
excludes,
limit,
offset,
keyword,
sortStrategy,
order
})
]).pipe(
map(([items, children]) => pathNavigatorFetchParentItemsComplete({ id, items, children })),
catchAjaxError((error) => {
if (error.status === 404) {
return pathNavigatorConditionallySetPath({ id, path: getRootPath(path) });
} else {
return pathNavigatorFetchPathFailed({ error, id });
}
})
);
} else {
return fetchItemWithChildrenByPath(site, path, {
excludes,
limit,
offset,
keyword,
sortStrategy: state.pathNavigator[id].sortStrategy,
order: state.pathNavigator[id].order
}).pipe(
map(({ item, children }) => pathNavigatorFetchPathComplete({ id, parent: item, children })),
catchAjaxError((error) => pathNavigatorFetchPathFailed({ error, id }))
);
}
}
)
),
// endregion
// region pathNavigatorFetchPathComplete, pathNavigatorConditionallySetPathComplete, pathNavigatorSetCollapsed
(action$, state$) =>
action$.pipe(
ofType(
pathNavigatorFetchPathComplete.type,
pathNavigatorConditionallySetPathComplete.type,
pathNavigatorSetCollapsed.type,
pathNavigatorChangeLimit.type
),
withLatestFrom(state$),
tap(
([
{
type,
payload: { id, parent }
},
state
]) => {
if (type !== pathNavigatorConditionallySetPathComplete.type || parent?.childrenCount > 0) {
const uuid = state.sites.byId[state.sites.active].uuid;
setStoredPathNavigator(uuid, state.user.username, id, {
currentPath: state.pathNavigator[id].currentPath,
collapsed: state.pathNavigator[id].collapsed,
keyword: state.pathNavigator[id].keyword,
offset: state.pathNavigator[id].offset,
limit: state.pathNavigator[id].limit
});
}
}
),
ignoreElements()
),
// endregion
// region contentEvent
(action$, state$) =>
action$.pipe(
ofType(contentEvent.type),
withLatestFrom(state$),
mergeMap(([action, state]) => {
// Cases:
// a. Item is the current path in the navigator: refresh navigator
// b. Item is a direct child of the current path: refresh navigator
// b. Item is a direct child of the current path: refresh navigator
// c. Item is a child of an item on the current path: refresh item's child count
const {
payload: { targetPath }
} = action;
const parentPathOfTargetPath = getParentPath(targetPath);
const parentOfTargetWithIndex = withIndex(parentPathOfTargetPath);
const refreshRequests = [];
Object.values(state.pathNavigator).forEach((navigator) => {
if (
// Case (a)
navigator.currentPath === targetPath ||
// Case (b)
navigator.currentPath === parentPathOfTargetPath ||
navigator.currentPath === parentOfTargetWithIndex
) {
refreshRequests.push({ id: navigator.id, backgroundRefresh: true });
} /* else if (
// Case (c) - Content epics load any item that's on the state already
navigator.currentPath === getParentPath(parentPathOfTargetPath)
) {
actions.push(fetchSandboxItem({ path: parentPathOfTargetPath }));
} */
});
return refreshRequests.length ? [pathNavigatorBulkRefresh({ requests: refreshRequests })] : NEVER;
})
),
// endregion
// region deleteContentEvent, deleteContentEvents
(action$, state$) =>
action$.pipe(
ofType(deleteContentEvent.type, deleteContentEvents.type),
withLatestFrom(state$),
mergeMap(([action, state]) => {
const targetPaths =
deleteContentEvents.type === action.type ? action.payload.targetPaths : [action.payload.targetPath];
const actions = [];
const navigators = Object.values(state.pathNavigator);
targetPaths.forEach((targetPath) => {
navigators.forEach((navigator) => {
if (!navigator.isRootPathMissing && navigator.currentPath.startsWith(targetPath)) {
actions.push(pathNavigatorSetCurrentPath({ id: navigator.id, path: navigator.rootPath }));
}
});
});
return actions;
})
),
// endregion
// region moveContentEvent
(action$, state$) =>
action$.pipe(
ofType(moveContentEvent.type),
withLatestFrom(state$),
mergeMap(([action, state]) => {
const actions = [];
const {
payload: { targetPath, sourcePath }
} = action;
const parentOfTargetPath = getParentPath(targetPath);
const parentOfSourcePath = getParentPath(sourcePath);
const refreshRequests = [];
// const idsToBgRefresh = [];
Object.values(state.pathNavigator).forEach((navigator) => {
if (navigator.isRootPathMissing && targetPath === navigator.rootPath) {
refreshRequests.push({ id: navigator.id });
} else if (!navigator.isRootPathMissing && navigator.currentPath.startsWith(sourcePath)) {
actions.push(pathNavigatorSetCurrentPath({ id: navigator.id, path: navigator.rootPath }));
} else if (
withoutIndex(navigator.currentPath) === parentOfTargetPath ||
withoutIndex(navigator.currentPath) === parentOfSourcePath
) {
refreshRequests.push({ id: navigator.id, backgroundRefresh: true });
}
});
refreshRequests.length && actions.push(pathNavigatorBulkRefresh({ requests: refreshRequests }));
return actions.length ? actions : NEVER;
})
),
// endregion
// region pluginInstalled
(action$, state$) =>
action$.pipe(
ofType(pluginInstalled.type),
throttleTime(500),
withLatestFrom(state$),
switchMap(([, state]) => {
const requests = [];
Object.values(state.pathNavigator).forEach((tree) => {
if (['/templates', '/scripts', '/static-assets'].includes(getRootPath(tree.rootPath))) {
requests.push({ id: tree.id, backgroundRefresh: true });
}
});
return requests.length ? [pathNavigatorBulkRefresh({ requests })] : NEVER;
})
),
// endregion
// region publishEvent, workflowEvent
(action$, state$) =>
action$.pipe(
ofType(publishEvent.type, workflowEvent.type),
throttleTime(500),
withLatestFrom(state$),
switchMap(([, state]) => {
const requests = Object.keys(state.pathNavigator).map((id) => ({ id, backgroundRefresh: true }));
return requests.length ? [pathNavigatorBulkRefresh({ requests })] : NEVER;
})
)
// endregion
];