UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

434 lines (432 loc) 15.3 kB
/* * 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;