@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
434 lines (432 loc) • 15.3 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, useReducer, useRef } from 'react';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import { makeStyles } from 'tss-react/mui';
import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded';
import Popover from '@mui/material/Popover';
import Paper from '@mui/material/Paper';
import { createAction } from '@reduxjs/toolkit';
import Breadcrumbs from '../PathNavigator/PathNavigatorBreadcrumbs';
import PathNavigatorList from '../PathNavigator/PathNavigatorList';
import { fetchChildrenByPath, fetchItemsByPath, fetchItemWithChildrenByPath } from '../../services/content';
import { getIndividualPaths, getParentPath, withIndex, withoutIndex } from '../../utils/path';
import { createLookupTable, nou } from '../../utils/object';
import { forkJoin } from 'rxjs';
import { isFolder } from '../PathNavigator/utils';
import { lookupItemByPath, parseSandBoxItemToDetailedItem } from '../../utils/content';
import Pagination from '../Pagination';
import NavItem from '../PathNavigator/PathNavigatorItem';
import { useActiveSiteId } from '../../hooks/useActiveSiteId';
import ItemDisplay from '../ItemDisplay';
import PathNavigatorSkeleton from '../PathNavigator/PathNavigatorSkeleton';
const useStyles = makeStyles()((theme) => ({
popoverRoot: {
minWidth: '200px',
maxWidth: '400px',
marginTop: '5px',
padding: '0 5px 5px 5px'
},
root: {
backgroundColor: theme.palette.background.paper,
display: 'flex',
paddingLeft: '15px',
alignItems: 'center',
justifyContent: 'space-between',
marginRight: 'auto',
maxWidth: '100%',
minWidth: '200px',
'&.disable': {
minWidth: 'auto',
backgroundColor: 'inherit'
}
},
selectedItem: {
marginLeft: 'auto',
display: 'flex',
minWidth: 0
},
title: {
fontWeight: 600,
marginRight: '30px'
},
changeBtn: {},
itemName: {},
selectIcon: {}
}));
const init = (props) => {
var _a, _b;
return {
byId: null,
isFetching: null,
error: null,
items: [],
keywords: '',
pageNumber: 0,
breadcrumb: [],
offset: 0,
limit: 10,
total: 0,
rootPath: props.rootPath,
currentPath:
(_b = (_a = props.selectedItem) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0
? _b
: props.rootPath
};
};
const reducer = (state, { type, payload }) => {
switch (type) {
case changeCurrentPath.type: {
return Object.assign(Object.assign({}, state), { currentPath: payload.path });
}
case setKeyword.type: {
return Object.assign(Object.assign({}, state), { keywords: payload, isFetching: true });
}
case changePage.type: {
return Object.assign(Object.assign({}, state), { isFetching: true });
}
case fetchParentsItems.type:
case fetchChildrenByPathAction.type: {
return Object.assign(Object.assign({}, state), { currentPath: payload, isFetching: true });
}
case fetchChildrenByPathComplete.type: {
const { currentPath, rootPath, byId } = state;
const { children, parent } = payload;
if (children.length === 0 && withoutIndex(currentPath) !== withoutIndex(rootPath)) {
return Object.assign(Object.assign({}, state), {
currentPath: getNextPath(currentPath, byId),
total: children.total,
isFetching: false
});
} else {
const nextItems = Object.assign(
Object.assign({}, Object.assign(Object.assign({}, state.byId), createLookupTable(children, 'path'))),
parent && { [parent.path]: parent }
);
return Object.assign(Object.assign({}, state), {
byId: nextItems,
items: children.map((item) => item.path),
isFetching: false,
total: children.total,
offset: children.offset,
limit: children.limit,
breadcrumb: getIndividualPaths(withoutIndex(currentPath), withoutIndex(rootPath))
});
}
}
case fetchParentsItemsComplete.type: {
const { currentPath, rootPath, byId } = state;
const { children, items } = payload;
return Object.assign(Object.assign({}, state), {
byId: Object.assign(
Object.assign(
Object.assign({}, byId),
createLookupTable(children.map(parseSandBoxItemToDetailedItem), 'path')
),
createLookupTable(items, 'path')
),
items: children.map((item) => item.path),
isFetching: false,
limit: children.limit,
total: children.total,
offset: children.offset,
breadcrumb: getIndividualPaths(withoutIndex(currentPath), withoutIndex(rootPath))
});
}
default:
throw new Error(`Unknown action "${type}"`);
}
};
function getNextPath(currentPath, byId) {
let pieces = currentPath.split('/').slice(0);
pieces.pop();
if (currentPath.includes('index.xml')) {
pieces.pop();
}
let nextPath = pieces.join('/');
if (nou(byId[nextPath])) {
nextPath = withIndex(nextPath);
}
return nextPath;
}
const changeCurrentPath = /*#__PURE__*/ createAction('CHANGE_SELECTED_ITEM');
const setKeyword = /*#__PURE__*/ createAction('SET_KEYWORD');
const changePage = /*#__PURE__*/ createAction('CHANGE_PAGE');
const changeRowsPerPage = /*#__PURE__*/ createAction('CHANGE_PAGE');
const fetchChildrenByPathAction = /*#__PURE__*/ createAction('FETCH_CHILDREN_BY_PATH');
const fetchParentsItems = /*#__PURE__*/ createAction('FETCH_PARENTS_ITEMS');
const fetchParentsItemsComplete = /*#__PURE__*/ createAction('FETCH_PARENTS_ITEMS_COMPLETE');
const fetchChildrenByPathComplete = /*#__PURE__*/ createAction('FETCH_CHILDREN_BY_PATH_COMPLETE');
const fetchChildrenByPathFailed = /*#__PURE__*/ createAction('FETCH_CHILDREN_BY_PATH_FAILED');
export function SingleItemSelector(props) {
const {
selectIcon: SelectIcon = ExpandMoreRoundedIcon,
classes: propClasses,
titleVariant = 'body1',
hideUI = false,
disabled = false,
onItemClicked,
onDropdownClick,
onClose,
label,
open,
selectedItem,
rootPath,
canSelectFolders = false,
filterChildren = () => true
} = props;
const { classes, cx } = useStyles();
const anchorEl = useRef();
const [state, _dispatch] = useReducer(reducer, props, init);
const site = useActiveSiteId();
const exec = useCallback(
(action) => {
var _a, _b;
_dispatch(action);
const { type, payload } = action;
switch (type) {
case setKeyword.type: {
fetchChildrenByPath(site, state.currentPath, { limit: state.limit, keyword: payload }).subscribe(
(children) => exec(fetchChildrenByPathComplete({ children })),
(response) => exec(fetchChildrenByPathFailed(response))
);
break;
}
case changeRowsPerPage.type:
case changePage.type: {
fetchChildrenByPath(site, state.currentPath, {
limit: (_a = payload.limit) !== null && _a !== void 0 ? _a : state.limit,
offset: (_b = payload.offset) !== null && _b !== void 0 ? _b : state.offset
}).subscribe(
(children) => exec(fetchChildrenByPathComplete({ children })),
(response) => exec(fetchChildrenByPathFailed(response))
);
break;
}
case fetchChildrenByPathAction.type:
fetchItemWithChildrenByPath(site, payload, { limit: state.limit }).subscribe(
({ item, children }) => exec(fetchChildrenByPathComplete({ parent: item, children })),
(response) => exec(fetchChildrenByPathFailed(response))
);
break;
case fetchParentsItems.type:
const parentsPath = getIndividualPaths(payload, state.rootPath);
if (parentsPath.length > 1) {
forkJoin([
fetchItemsByPath(site, parentsPath, { castAsDetailedItem: true }),
fetchChildrenByPath(site, payload, {
limit: state.limit
})
]).subscribe(
([items, children]) => {
const { levelDescriptor, total, offset, limit } = children;
return exec(
fetchParentsItemsComplete({
items,
children: Object.assign(children.filter(filterChildren), { levelDescriptor, total, offset, limit })
})
);
},
(response) => exec(fetchChildrenByPathFailed(response))
);
} else {
fetchItemWithChildrenByPath(site, payload, { limit: state.limit }).subscribe(
({ item, children }) => {
const { levelDescriptor, total, offset, limit } = children;
return exec(
fetchChildrenByPathComplete({
parent: item,
children: Object.assign(children.filter(filterChildren), { levelDescriptor, total, offset, limit })
})
);
},
(response) => exec(fetchChildrenByPathFailed(response))
);
}
break;
}
},
[state, site, filterChildren]
);
const handleDropdownClick = (item) => {
onDropdownClick();
let nextPath = item
? withoutIndex(item.path) === withoutIndex(rootPath)
? item.path
: getParentPath(item.path)
: rootPath;
exec(fetchParentsItems(nextPath));
};
const onPathSelected = (item) => {
exec(fetchChildrenByPathAction(item.path));
};
const onSearch = (keyword) => {
exec(setKeyword(keyword));
};
const onCrumbSelected = (item) => {
if (state.breadcrumb.length === 1) {
handleItemClicked(item);
} else {
exec(fetchChildrenByPathAction(item.path));
}
};
const handleItemClicked = (item) => {
const folder = isFolder(item);
if (folder && canSelectFolders === false) {
onPathSelected(item);
} else {
exec(changeCurrentPath(item));
onItemClicked(item);
}
};
const onPageChanged = (e, page) => {
const offset = page * state.limit;
exec(changePage({ offset }));
};
const onChangeRowsPerPage = (e) => {
exec(changeRowsPerPage({ offset: 0, limit: e.target.value }));
};
const Wrapper = hideUI ? React.Fragment : Paper;
const wrapperProps = hideUI
? {}
: {
className: cx(
classes.root,
!onDropdownClick && 'disable',
propClasses === null || propClasses === void 0 ? void 0 : propClasses.root
),
elevation: 0
};
return React.createElement(
Wrapper,
Object.assign({}, wrapperProps),
!hideUI &&
React.createElement(
React.Fragment,
null,
label &&
React.createElement(
Typography,
{
variant: titleVariant,
className: cx(classes.title, propClasses === null || propClasses === void 0 ? void 0 : propClasses.title)
},
label
),
selectedItem &&
React.createElement(
'div',
{ className: classes.selectedItem },
React.createElement(ItemDisplay, { item: selectedItem, showNavigableAsLinks: false })
)
),
onDropdownClick &&
React.createElement(
IconButton,
{
className: classes.changeBtn,
ref: anchorEl,
disabled: disabled,
onClick: disabled ? null : () => handleDropdownClick(selectedItem),
size: 'large'
},
React.createElement(SelectIcon, {
className: cx(
classes.selectIcon,
propClasses === null || propClasses === void 0 ? void 0 : propClasses.selectIcon
)
})
),
React.createElement(
Popover,
{
anchorEl: anchorEl.current,
open: open,
classes: {
paper: cx(
classes.popoverRoot,
propClasses === null || propClasses === void 0 ? void 0 : propClasses.popoverRoot
)
},
onClose: onClose,
anchorOrigin: {
vertical: 'bottom',
horizontal: 'right'
},
transformOrigin: {
vertical: 'top',
horizontal: 'right'
}
},
React.createElement(Breadcrumbs, {
keyword: state === null || state === void 0 ? void 0 : state.keywords,
breadcrumb: state.breadcrumb.map((path) => {
var _a;
return (_a = state.byId[path]) !== null && _a !== void 0 ? _a : state.byId[withIndex(path)];
}),
onSearch: onSearch,
onCrumbSelected: onCrumbSelected
}),
state.byId &&
lookupItemByPath(state.currentPath, state.byId) &&
React.createElement(NavItem, {
item: lookupItemByPath(state.currentPath, state.byId),
locale: 'en_US',
isLevelDescriptor: false,
isCurrentPath: true,
onItemClicked: handleItemClicked,
showItemNavigateToButton: false
}),
state.isFetching
? React.createElement(PathNavigatorSkeleton, null)
: React.createElement(
React.Fragment,
null,
React.createElement(PathNavigatorList, {
locale: 'en_US',
items: state.items.map((p) => state.byId[p]),
onPathSelected: onPathSelected,
onItemClicked: handleItemClicked
}),
React.createElement(Pagination, {
count: state.total,
rowsPerPageOptions: [5, 10, 20],
rowsPerPage: state.limit,
page: state && Math.ceil(state.offset / state.limit),
onRowsPerPageChange: onChangeRowsPerPage,
onPageChange: onPageChanged
})
)
)
);
}
export default SingleItemSelector;