@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
328 lines (326 loc) • 13.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 { createReducer } from '@reduxjs/toolkit';
import {
pathNavigatorTreeBulkFetchPathChildren,
pathNavigatorTreeBulkFetchPathChildrenComplete,
pathNavigatorTreeBulkRestoreComplete,
pathNavigatorTreeCollapsePath,
pathNavigatorTreeExpandPath,
pathNavigatorTreeFetchPathChildren,
pathNavigatorTreeFetchPathChildrenComplete,
pathNavigatorTreeFetchPathChildrenFailed,
pathNavigatorTreeFetchPathPage,
pathNavigatorTreeFetchPathPageComplete,
pathNavigatorTreeInit,
pathNavigatorTreeRestore,
pathNavigatorTreeRestoreComplete,
pathNavigatorTreeRootMissing,
pathNavigatorTreeSetKeyword,
pathNavigatorTreeToggleCollapsed,
pathNavigatorTreeUpdate
} from '../actions/pathNavigatorTree';
import { changeSiteComplete } from '../actions/sites';
import { fetchSiteUiConfig } from '../actions/configuration';
import { reversePluckProps } from '../../utils/object';
import { fetchSandboxItemComplete } from '../actions/content';
import { getIndividualPaths, getParentPath, withIndex, withoutIndex } from '../../utils/path';
import { deleteContentEvent, deleteContentEvents, moveContentEvent } from '../actions/system';
import { createPresenceTable } from '../../utils/array';
export function contentAndDeleteEventForEachApplicableTree(state, targetPath, callbackFn) {
const parentPathOfTargetPath = getParentPath(targetPath);
Object.values(state).forEach((tree) => {
if (
tree.rootPath === targetPath ||
tree.rootPath === withIndex(targetPath) ||
targetPath in tree.totalByPath ||
withIndex(targetPath) in tree.totalByPath ||
parentPathOfTargetPath in tree.totalByPath ||
withIndex(parentPathOfTargetPath) in tree.totalByPath
) {
callbackFn(tree, targetPath, parentPathOfTargetPath);
}
});
}
const expandPath = (state, { payload: { id, path } }) => {
const chunk = state[id];
if (path.startsWith(withoutIndex(chunk.rootPath)) && !chunk.expanded.includes(path)) {
const paths = getIndividualPaths(path, chunk.rootPath);
const expandedPathLookup = createPresenceTable(chunk.expanded);
paths.forEach((path) => {
!expandedPathLookup[path] && !expandedPathLookup[`${path}/index.xml`] && chunk.expanded.push(path);
});
}
};
export function deleteItemFromState(tree, targetPath) {
let parentPath = getParentPath(targetPath);
let totalByPath = tree.totalByPath;
// path in totalByPath may be a page, and its path has index.xml
parentPath = totalByPath[parentPath] ? parentPath : withIndex(parentPath);
let childrenByParentPath = tree.childrenByParentPath;
// Remove deleted item from the parent path's children
if (childrenByParentPath[parentPath]) {
childrenByParentPath[parentPath] = childrenByParentPath[parentPath]?.filter(
(childPath) => targetPath !== childPath
);
}
// Discount deleted item from parent path child count
if (totalByPath[parentPath]) {
totalByPath[parentPath] = totalByPath[parentPath] - 1;
}
// Remove item
delete totalByPath[targetPath];
delete tree.keywordByPath[targetPath];
delete tree.offsetByPath[targetPath];
// Remove children of the item
delete childrenByParentPath[targetPath];
// Remove item from expanded. Parent too if pertinent.
tree.expanded = tree.expanded.filter(
// If the parent is left without children, remove from expanded too.
totalByPath[parentPath] === 0
? (expandedPath) => expandedPath !== targetPath && expandedPath !== parentPath
: (expandedPath) => expandedPath !== targetPath
);
}
const updatePath = (state, payload) => {
const { id, parentPath, children, options } = payload;
const chunk = state[id];
chunk.totalByPath[parentPath] = children.total;
chunk.childrenByParentPath[parentPath] = [];
if (children.levelDescriptor) {
chunk.childrenByParentPath[parentPath].push(children.levelDescriptor.path);
chunk.totalByPath[children.levelDescriptor.path] = 0;
}
children.forEach((item) => {
chunk.childrenByParentPath[parentPath].push(item.path);
chunk.totalByPath[item.path] = item.childrenCount;
});
// If the expanded node has no children, no level descriptor and is not filtered, it's a
// leaf node and there's no point keeping it in `expanded`
if (children.length === 0 && !children.levelDescriptor && !options?.keyword) {
chunk.expanded = chunk.expanded.filter((path) => path !== parentPath);
}
};
const restoreTree = (state, payload) => {
const { id, children, items, expanded } = payload;
const chunk = state[id];
chunk.childrenByParentPath = {};
chunk.totalByPath = {};
chunk.expanded = expanded;
const childrenByParentPath = chunk.childrenByParentPath;
const totalByPath = chunk.totalByPath;
const offsetByPath = chunk.offsetByPath;
// Set totalByPath of items for the tree to know which items have children (in case they are not expanded).
items.forEach((item) => {
totalByPath[item.path] = item.childrenCount;
});
Object.keys(children).forEach((parentPath) => {
const childrenOfPath = children[parentPath];
if (childrenOfPath.length || childrenOfPath.levelDescriptor) {
childrenByParentPath[parentPath] = [];
if (childrenOfPath.levelDescriptor) {
childrenByParentPath[parentPath].push(childrenOfPath.levelDescriptor.path);
totalByPath[childrenOfPath.levelDescriptor.path] = 0;
}
childrenOfPath.forEach((child) => {
childrenByParentPath[parentPath].push(child.path);
// If we have the total in the children object, use it (since that object has the total considering filters),
// otherwise use the childrenCount.
totalByPath[child.path] = children[child.path]?.total ?? child.childrenCount;
});
}
// Should we account here for the level descriptor (LD)? if there's a LD, add 1 to the total?
totalByPath[parentPath] = childrenOfPath.total;
offsetByPath[parentPath] = offsetByPath[parentPath] ?? 0;
// If the expanded node is filtered or has children it means, it's not a leaf,
// and we should keep it in 'expanded'.
// if (chunk.keywordByPath[parentPath] || childrenByParentPath[parentPath].length) {
// chunk.expanded.push(parentPath);
// }
});
};
const deleteContentEventHandler = (state, { payload: { targetPath } }) => {
contentAndDeleteEventForEachApplicableTree(state, targetPath, (tree, targetPath, parentPathOfTargetPath) => {
if (targetPath === tree.rootPath) {
tree.isRootPathMissing = true;
} else if (parentPathOfTargetPath in tree.totalByPath) {
deleteItemFromState(tree, targetPath);
}
});
};
const reducer = createReducer({}, (builder) => {
builder
// region pathNavigatorTreeInit
.addCase(pathNavigatorTreeInit, (state, action) => {
const {
payload: {
id,
rootPath,
collapsed = true,
limit,
expanded = [],
keywordByPath = {},
excludes = null,
systemTypes = null,
sortStrategy = null,
order = null
}
} = action;
state[id] = {
id,
rootPath,
collapsed,
limit,
expanded,
childrenByParentPath: {},
errorByPath: {},
offsetByPath: {},
keywordByPath,
totalByPath: {},
excludes,
error: null,
isRootPathMissing: false,
systemTypes,
sortStrategy,
order
};
})
// endregion
.addCase(pathNavigatorTreeExpandPath, expandPath)
.addCase(pathNavigatorTreeCollapsePath, (state, { payload: { id, path } }) => {
state[id].expanded = state[id].expanded.filter((expanded) => !expanded.startsWith(path));
})
.addCase(pathNavigatorTreeToggleCollapsed, (state, { payload: { id, collapsed } }) => {
state[id].collapsed = collapsed;
})
.addCase(pathNavigatorTreeSetKeyword, (state, { payload: { id, path, keyword } }) => {
state[id].keywordByPath[path] = keyword;
})
.addCase(pathNavigatorTreeFetchPathChildren, (state, action) => {
const { expand = true } = action.payload;
delete state[action.payload.id].errorByPath[action.payload.path];
expand && expandPath(state, action);
})
.addCase(pathNavigatorTreeFetchPathChildrenComplete, (state, { payload }) => {
updatePath(state, payload);
})
.addCase(pathNavigatorTreeFetchPathChildrenFailed, (state, action) => {
state[action.payload.id].errorByPath[action.payload.path] = action.payload.error;
})
.addCase(pathNavigatorTreeBulkFetchPathChildren, (state, action) => {
const { requests } = action.payload;
requests.forEach((request) => {
const { expand = true } = request;
expand && expandPath(state, { payload: request });
});
})
.addCase(pathNavigatorTreeBulkFetchPathChildrenComplete, (state, { payload: { paths } }) => {
paths.forEach((path) => {
updatePath(state, path);
});
})
.addCase(pathNavigatorTreeFetchPathPage, (state, { payload: { id, path } }) => {
state[id].offsetByPath[path] = state[id].offsetByPath[path]
? state[id].offsetByPath[path] + state[id].limit
: state[id].limit;
})
.addCase(pathNavigatorTreeFetchPathPageComplete, (state, { payload: { id, parentPath, children, options } }) => {
const chunk = state[id];
chunk.totalByPath[parentPath] = children.total;
if (children.levelDescriptor) {
chunk.totalByPath[children.levelDescriptor.path] = 0;
}
children.forEach((item) => {
chunk.childrenByParentPath[parentPath].push(item.path);
chunk.totalByPath[item.path] = item.childrenCount;
});
})
.addCase(pathNavigatorTreeUpdate, (state, { payload }) => {
return {
...state,
[payload.id]: {
...state[payload.id],
...reversePluckProps(payload, 'id')
}
};
})
.addCase(pathNavigatorTreeRestore, (state, { payload: { id } }) => {
state[id].isRootPathMissing = false;
})
// region pathNavigatorTreeRestoreComplete
// Assumption: this reducer is a reset. Not suitable for partial updates.
.addCase(pathNavigatorTreeRestoreComplete, (state, { payload }) => {
restoreTree(state, payload);
})
// endregion
// region pathNavigatorTreeBulkRestoreComplete
.addCase(pathNavigatorTreeBulkRestoreComplete, (state, { payload: { trees } }) => {
trees.forEach((tree) => {
restoreTree(state, tree);
});
})
//
.addCase(changeSiteComplete, () => ({}))
.addCase(fetchSiteUiConfig, () => ({}))
// region fetchSandboxItemComplete
.addCase(fetchSandboxItemComplete, (state, { payload: { item } }) => {
const path = item.path;
Object.values(state).forEach((tree) => {
if (path in tree.totalByPath) {
tree.totalByPath[path] = item.childrenCount;
}
});
})
// endregion
.addCase(pathNavigatorTreeRootMissing, (state, { payload: { id } }) => {
state[id].isRootPathMissing = true;
})
// region deleteContentEvent
.addCase(deleteContentEvent, deleteContentEventHandler)
.addCase(deleteContentEvents, (state, action) => {
const auxAction = deleteContentEvent({ ...action.payload, targetPath: '' });
action.payload.targetPaths.forEach((targetPath) => {
auxAction.payload.targetPath = targetPath;
deleteContentEventHandler(state, auxAction);
});
})
// endregion
.addCase(moveContentEvent, (state, { payload: { sourcePath } }) => {
Object.values(state).forEach((tree) => {
if (tree.rootPath === sourcePath) {
tree.isRootPathMissing = true;
} else if (sourcePath in tree.totalByPath) {
deleteItemFromState(tree, sourcePath);
}
});
});
});
export default reducer;