@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
327 lines (325 loc) • 11.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, { useEffect, useRef, useState } from 'react';
import PathNavigatorTreeUI from './PathNavigatorTreeUI';
import { useDispatch } from 'react-redux';
import {
pathNavigatorTreeBackgroundRefresh,
pathNavigatorTreeCollapsePath,
pathNavigatorTreeExpandPath,
pathNavigatorTreeFetchPathChildren,
pathNavigatorTreeFetchPathPage,
pathNavigatorTreeInit,
pathNavigatorTreeRefresh,
pathNavigatorTreeSetKeyword,
pathNavigatorTreeToggleCollapsed
} from '../../state/actions/pathNavigatorTree';
import {
getEditorMode,
isEditableViaFormEditor,
isImage,
isNavigable,
isPreviewable,
isVideo,
isPdfDocument
} from '../PathNavigator/utils';
import ContextMenu from '../ContextMenu/ContextMenu';
import { getNumOfMenuOptionsForItem, lookupItemByPath } from '../../utils/content';
import { previewItem } from '../../state/actions/preview';
import { getOffsetLeft, getOffsetTop } from '@mui/material/Popover';
import { showEditDialog, showItemMegaMenu, showPreviewDialog } from '../../state/actions/dialogs';
import { getStoredPathNavigatorTree } from '../../utils/state';
import PathNavigatorSkeleton from '../PathNavigator/PathNavigatorSkeleton';
import { useSelection } from '../../hooks/useSelection';
import { useEnv } from '../../hooks/useEnv';
import { useActiveUser } from '../../hooks/useActiveUser';
import { useItemsByPath } from '../../hooks/useItemsByPath';
import { useSubject } from '../../hooks/useSubject';
import { debounceTime } from 'rxjs/operators';
import { useActiveSite } from '../../hooks/useActiveSite';
import { batchActions } from '../../state/actions/misc';
import { UNDEFINED } from '../../utils/constants';
// @see https://github.com/craftercms/craftercms/issues/5360
// const translations = defineMessages({
// refresh: {
// id: 'words.refresh',
// defaultMessage: 'Refresh'
// }
// });
//
// const menuOptions: LookupTable<ContextMenuOptionDescriptor> = {
// refresh: {
// id: 'refresh',
// label: translations.refresh
// }
// };
export function PathNavigatorTree(props) {
// region const { ... } = props;
const {
label,
id = props.label.replace(/\s/g, ''),
excludes,
limit = 10,
icon,
expandedIcon,
collapsedIcon,
container,
rootPath,
initialExpanded,
initialCollapsed = true,
collapsible = true,
initialSystemTypes,
onNodeClick,
active,
classes,
showNavigableAsLinks,
showPublishingTarget,
showWorkflowState,
showItemMenu
} = props;
// endregion
const state = useSelection((state) => state.pathNavigatorTree[id]);
const { id: siteId, uuid } = useActiveSite();
const user = useActiveUser();
const onSearch$ = useSubject();
const uiConfig = useSelection((state) => state.uiConfig);
const [widgetMenu, setWidgetMenu] = useState({ anchorEl: null, sections: [] });
const { authoringBase } = useEnv();
const dispatch = useDispatch();
const itemsByPath = useItemsByPath();
const initialRefs = useRef({ initialCollapsed, initialSystemTypes, limit, excludes, initialExpanded });
const keywordByPath = state === null || state === void 0 ? void 0 : state.keywordByPath;
const totalByPath = state === null || state === void 0 ? void 0 : state.totalByPath;
const childrenByParentPath = state === null || state === void 0 ? void 0 : state.childrenByParentPath;
const rootItem = lookupItemByPath(rootPath, itemsByPath);
useEffect(() => {
// Adding uiConfig as means to stop navigator from trying to
// initialize with previous state information when switching sites
if (
rootPath !== (state === null || state === void 0 ? void 0 : state.rootPath) &&
uiConfig.currentSite === siteId
) {
const storedState = getStoredPathNavigatorTree(uuid, user.username, id);
const { initialSystemTypes, initialCollapsed, limit, excludes, initialExpanded } = initialRefs.current;
dispatch(
pathNavigatorTreeInit(
Object.assign(
{
id,
rootPath,
excludes,
limit,
collapsed: initialCollapsed,
systemTypes: initialSystemTypes,
expanded: initialExpanded
},
storedState
)
)
);
}
}, [
dispatch,
id,
rootPath,
siteId,
state === null || state === void 0 ? void 0 : state.rootPath,
uiConfig.currentSite,
user.username,
uuid
]);
useEffect(() => {
const subscription = onSearch$.pipe(debounceTime(400)).subscribe(({ keyword, path }) => {
dispatch(
batchActions([
pathNavigatorTreeSetKeyword({
id,
path,
keyword
}),
pathNavigatorTreeBackgroundRefresh({ id })
])
);
});
return () => {
subscription.unsubscribe();
};
}, [dispatch, id, onSearch$, rootPath]);
if (!rootItem || !state) {
const storedState = getStoredPathNavigatorTree(uuid, user.username, id);
return React.createElement(PathNavigatorSkeleton, {
renderBody: storedState ? !storedState.collapsed : !initialCollapsed
});
}
// region Handlers
const onChangeCollapsed = (collapsed) => {
collapsible && dispatch(pathNavigatorTreeToggleCollapsed({ id, collapsed }));
};
const onNodeLabelClick =
onNodeClick !== null && onNodeClick !== void 0
? onNodeClick
: (event, path) => {
if (isNavigable(itemsByPath[path])) {
dispatch(
previewItem({
item: itemsByPath[path],
newTab: event.ctrlKey || event.metaKey
})
);
} else if (isPreviewable(itemsByPath[path])) {
onPreview(itemsByPath[path]);
} else {
onToggleNodeClick(path);
}
};
const onToggleNodeClick = (path) => {
// If the path is already expanded, should be collapsed
if (state.expanded.includes(path)) {
dispatch(pathNavigatorTreeCollapsePath({ id, path }));
} else {
// If the item's children have been loaded, should simply be expanded
if (childrenByParentPath[path]) {
dispatch(pathNavigatorTreeExpandPath({ id, path }));
} else {
// Children not fetched yet, should be fetched
dispatch(pathNavigatorTreeFetchPathChildren({ id, path }));
}
}
};
const onHeaderButtonClick = (element) => {
// @see https://github.com/craftercms/craftercms/issues/5360
onWidgetOptionsClick('refresh');
// setWidgetMenu({
// sections: [[toContextMenuOptionsLookup(menuOptions, formatMessage).refresh]],
// anchorEl: element
// });
};
const onOpenItemMenu = (element, path) => {
const anchorRect = element.getBoundingClientRect();
const top = anchorRect.top + getOffsetTop(anchorRect, 'top');
const left = anchorRect.left + getOffsetLeft(anchorRect, 'left');
dispatch(
showItemMegaMenu({
path,
anchorReference: 'anchorPosition',
anchorPosition: { top, left },
loaderItems: getNumOfMenuOptionsForItem(itemsByPath[path])
})
);
};
const onCloseWidgetOptions = () => setWidgetMenu(Object.assign(Object.assign({}, widgetMenu), { anchorEl: null }));
const onWidgetOptionsClick = (option) => {
onCloseWidgetOptions();
if (option === 'refresh') {
dispatch(
pathNavigatorTreeRefresh({
id
})
);
}
};
const onFilterChange = (keyword, path) => {
if (!state.expanded.includes(path)) {
dispatch(
pathNavigatorTreeExpandPath({
id,
path
})
);
}
onSearch$.next({ keyword, path });
};
const onMoreClick = (path) => {
dispatch(pathNavigatorTreeFetchPathPage({ id, path }));
};
const onPreview = (item) => {
if (isEditableViaFormEditor(item)) {
dispatch(showEditDialog({ path: item.path, authoringBase, site: siteId, readonly: true }));
} else if (isImage(item) || isVideo(item) || isPdfDocument(item.mimeType)) {
dispatch(
showPreviewDialog({
type: isImage(item) ? 'image' : isVideo(item) ? 'video' : 'pdf',
title: item.label,
url: item.path
})
);
} else {
const mode = getEditorMode(item);
dispatch(
showPreviewDialog({
type: 'editor',
title: item.label,
url: item.path,
mode
})
);
}
};
// endregion
return React.createElement(
React.Fragment,
null,
React.createElement(PathNavigatorTreeUI, {
classes: { header: classes === null || classes === void 0 ? void 0 : classes.header },
title: label,
active: active,
icon: expandedIcon && collapsedIcon ? (state.collapsed ? collapsedIcon : expandedIcon) : icon,
container: container,
isCollapsed: state.collapsed,
rootPath: rootPath,
isRootPathMissing: state.isRootPathMissing,
itemsByPath: itemsByPath,
keywordByPath: keywordByPath,
totalByPath: totalByPath,
childrenByParentPath: childrenByParentPath,
expandedNodes: state === null || state === void 0 ? void 0 : state.expanded,
onIconClick: onToggleNodeClick,
onLabelClick: onNodeLabelClick,
onChangeCollapsed: onChangeCollapsed,
onOpenItemMenu: onOpenItemMenu,
onHeaderButtonClick: state.collapsed ? UNDEFINED : onHeaderButtonClick,
onFilterChange: onFilterChange,
onMoreClick: onMoreClick,
showNavigableAsLinks: showNavigableAsLinks,
showPublishingTarget: showPublishingTarget,
showWorkflowState: showWorkflowState,
showItemMenu: showItemMenu
}),
React.createElement(ContextMenu, {
anchorEl: widgetMenu.anchorEl,
options: widgetMenu.sections,
open: Boolean(widgetMenu.anchorEl),
onClose: onCloseWidgetOptions,
onMenuItemClicked: onWidgetOptionsClick
})
);
}
export default PathNavigatorTree;