@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
367 lines (365 loc) • 11.5 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, useState } from 'react';
import ContextMenu from '../ContextMenu/ContextMenu';
import { useDispatch } from 'react-redux';
import { withIndex, withoutIndex } from '../../utils/path';
import {
pathNavigatorBackgroundRefresh,
pathNavigatorChangeLimit,
pathNavigatorChangePage,
pathNavigatorConditionallySetPath,
pathNavigatorFetchPath,
pathNavigatorInit,
pathNavigatorItemChecked,
pathNavigatorItemUnchecked,
pathNavigatorRefresh,
pathNavigatorSetCollapsed,
pathNavigatorSetKeyword,
pathNavigatorSetLocaleCode
} from '../../state/actions/pathNavigator';
import { showEditDialog, showItemMegaMenu, showPreviewDialog } from '../../state/actions/dialogs';
import {
getEditorMode,
isEditableViaFormEditor,
isFolder,
isImage,
isNavigable,
isPreviewable,
isVideo,
isPdfDocument
} from './utils';
import { debounceTime } from 'rxjs/operators';
import PathNavigatorUI from './PathNavigatorUI';
import PathNavigatorSkeleton from './PathNavigatorSkeleton';
import { getOffsetLeft, getOffsetTop } from '@mui/material/Popover';
import { getNumOfMenuOptionsForItem, lookupItemByPath } from '../../utils/content';
import { useSelection } from '../../hooks/useSelection';
import { useEnv } from '../../hooks/useEnv';
import { useItemsByPath } from '../../hooks/useItemsByPath';
import { useSubject } from '../../hooks/useSubject';
import { useSiteLocales } from '../../hooks/useSiteLocales';
import { useMount } from '../../hooks/useMount';
import { getSystemLink } from '../../utils/system';
import { getStoredPathNavigator } from '../../utils/state';
import { useActiveSite } from '../../hooks/useActiveSite';
import { useActiveUser } from '../../hooks/useActiveUser';
// @see https://github.com/craftercms/craftercms/issues/5360
// const menuOptions: Record<'refresh', ContextMenuOptionDescriptor> = {
// refresh: {
// id: 'refresh',
// label: translations.refresh
// }
// };
export function PathNavigator(props) {
// region const { ... } = props;
const {
label = '(No name)',
icon,
expandedIcon,
collapsedIcon,
container,
rootPath: path,
id = label.replace(/\s/g, ''),
limit = 10,
locale,
excludes,
initialCollapsed = true,
onItemClicked: onItemClickedProp,
createItemClickedHandler = (defaultHandler) => defaultHandler,
computeActiveItems,
sortStrategy,
order
} = props;
// endregion
const state = useSelection((state) => state.pathNavigator)[id];
const itemsByPath = useItemsByPath();
const { id: siteId, uuid } = useActiveSite();
const user = useActiveUser();
const { authoringBase } = useEnv();
const dispatch = useDispatch();
const [widgetMenu, setWidgetMenu] = useState({
anchorEl: null,
sections: [],
emptyState: null
});
const [keyword, setKeyword] = useState('');
const onSearch$ = useSubject();
const uiConfig = useSelection((state) => state.uiConfig);
const siteLocales = useSiteLocales();
useEffect(() => {
// Adding uiConfig as means to stop navigator from trying to
// initialize with previous state information when switching sites
if (!state && uiConfig.currentSite === siteId) {
const storedState = getStoredPathNavigator(uuid, user.username, id);
if (storedState?.keyword) {
setKeyword(storedState.keyword);
}
dispatch(
pathNavigatorInit({
id,
rootPath: path,
locale,
excludes,
limit,
collapsed: initialCollapsed,
sortStrategy,
order,
...storedState
})
);
}
}, [
dispatch,
excludes,
id,
limit,
locale,
path,
siteId,
state,
initialCollapsed,
uiConfig.currentSite,
user.username,
uuid,
sortStrategy,
order
]);
useMount(() => {
if (state) {
dispatch(pathNavigatorBackgroundRefresh({ id }));
}
});
useEffect(() => {
const subscription = onSearch$.pipe(debounceTime(400)).subscribe((keyword) => {
dispatch(pathNavigatorSetKeyword({ id, keyword }));
});
return () => {
subscription.unsubscribe();
};
}, [dispatch, id, onSearch$]);
useEffect(() => {
if (siteLocales.defaultLocaleCode && state?.localeCode !== siteLocales.defaultLocaleCode) {
dispatch(
pathNavigatorSetLocaleCode({
id,
locale: siteLocales.defaultLocaleCode
})
);
}
}, [dispatch, id, siteLocales.defaultLocaleCode, state?.localeCode]);
if (!state) {
const storedState = getStoredPathNavigator(uuid, user.username, id);
return React.createElement(PathNavigatorSkeleton, {
renderBody: storedState ? !storedState.collapsed : !initialCollapsed
});
}
const onPathSelected = (item) => {
dispatch(
pathNavigatorFetchPath({
id,
path: item.path,
keyword
})
);
};
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,
path: item.path,
mode
})
);
}
};
const onPageChanged = (page) => {
const offset = page * state.limit;
dispatch(
pathNavigatorChangePage({
id,
offset
})
);
};
const onRowsPerPageChange = (e) => {
const limit = Number(e.target.value);
dispatch(
pathNavigatorChangeLimit({
id,
limit,
offset: 0
})
);
};
const onSelectItem = (item, checked) => {
dispatch(
checked
? pathNavigatorItemChecked({ id, item })
: pathNavigatorItemUnchecked({
id,
item
})
);
};
const onCurrentParentMenu = (element) => {
const anchorRect = element.getBoundingClientRect();
const top = anchorRect.top + getOffsetTop(anchorRect, 'top');
const left = anchorRect.left + getOffsetLeft(anchorRect, 'left');
let path = state.currentPath;
if (path === '/site/website') {
path = withIndex(state.currentPath);
}
dispatch(
showItemMegaMenu({
path: path,
anchorReference: 'anchorPosition',
anchorPosition: { top, left },
loaderItems: getNumOfMenuOptionsForItem(lookupItemByPath(path, itemsByPath))
})
);
};
const onOpenItemMenu = (element, item) => {
const anchorRect = element.getBoundingClientRect();
const top = anchorRect.top + getOffsetTop(anchorRect, 'top');
const left = anchorRect.left + getOffsetLeft(anchorRect, 'left');
dispatch(
showItemMegaMenu({
path: item.path,
anchorReference: 'anchorPosition',
anchorPosition: { top, left },
loaderItems: getNumOfMenuOptionsForItem(item)
})
);
};
const onHeaderButtonClick = (anchorEl, type) => {
// @see https://github.com/craftercms/craftercms/issues/5360
onSimpleMenuClick('refresh');
// setWidgetMenu({
// sections: [[toContextMenuOptionsLookup(menuOptions, formatMessage).refresh]],
// anchorEl
// });
};
const onCloseWidgetMenu = () => setWidgetMenu({ ...widgetMenu, anchorEl: null });
const onItemClicked = onItemClickedProp
? onItemClickedProp
: createItemClickedHandler((item, e) => {
if (isNavigable(item)) {
const url = getSystemLink({
site: siteId,
systemLinkId: 'preview',
authoringBase,
page: item.previewUrl
});
if (e.ctrlKey || e.metaKey) {
window.open(url);
} else {
window.location.href = url;
}
} else if (isFolder(item)) {
onPathSelected(item);
} else if (isPreviewable(item)) {
onPreview?.(item);
}
});
const onBreadcrumbSelected = (item) => {
if (withoutIndex(item.path) !== withoutIndex(state.currentPath)) {
dispatch(pathNavigatorConditionallySetPath({ id, path: item.path, keyword }));
}
};
const onSimpleMenuClick = (option) => {
onCloseWidgetMenu();
if (option === 'refresh') {
dispatch(
pathNavigatorRefresh({
id
})
);
}
};
const onChangeCollapsed = (collapsed) => {
dispatch(pathNavigatorSetCollapsed({ id, collapsed }));
};
const onSearch = (keyword) => {
setKeyword(keyword);
onSearch$.next(keyword);
};
return React.createElement(
React.Fragment,
null,
React.createElement(PathNavigatorUI, {
state: state,
classes: props.classes,
itemsByPath: itemsByPath,
icon: expandedIcon && collapsedIcon ? (state.collapsed ? collapsedIcon : expandedIcon) : icon,
container: container,
title: label,
onChangeCollapsed: onChangeCollapsed,
onHeaderButtonClick: state.collapsed ? void 0 : onHeaderButtonClick,
onCurrentParentMenu: onCurrentParentMenu,
siteLocales: siteLocales,
keyword: keyword,
onSearch: onSearch,
onBreadcrumbSelected: onBreadcrumbSelected,
onSelectItem: onSelectItem,
onPathSelected: onPathSelected,
onPreview: onPreview,
onOpenItemMenu: onOpenItemMenu,
onItemClicked: onItemClicked,
onPageChanged: onPageChanged,
onRowsPerPageChange: onRowsPerPageChange,
computeActiveItems: computeActiveItems
}),
React.createElement(ContextMenu, {
anchorEl: widgetMenu.anchorEl,
options: widgetMenu.sections,
emptyState: widgetMenu.emptyState,
open: Boolean(widgetMenu.anchorEl),
onClose: onCloseWidgetMenu,
onMenuItemClicked: onSimpleMenuClick
})
);
}
export default PathNavigator;