Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { useSpreadState } from '../../hooks/useSpreadState';
import { useDispatch } from 'react-redux';
import LookupTable from '../../models/LookupTable';
import { BrowseFilesDialogUI, viewModes } from '.';
import { BrowseFilesDialogContainerProps, initialParameters } from './utils';
import { BrowseFilesDialogContainerProps, contentItemToMediaItem, initialParameters } from './utils';
import { checkPathExistence } from '../../services/content';
import { FormattedMessage } from 'react-intl';
import EmptyState from '../EmptyState';
Expand All @@ -40,6 +40,8 @@ import { popDialog, pushDialog } from '../../state/actions/dialogStack';
import { nanoid } from 'nanoid';

import { createComponentId } from '../../utils/system';
import { useItemsByPath } from '../../hooks/useItemsByPath';
import { lookupItemByPath } from '../../utils/content';

const defaultPreselectedPaths = [];

Expand Down Expand Up @@ -82,6 +84,7 @@ export function BrowseFilesDialogContainer(props: BrowseFilesDialogContainerProp
(items?.length > 0 && selectedInCurrentPage.length > 0 && selectedInCurrentPage.length < items?.length) ?? false;
const browsePath = path.replace(/\/+$/, '');
const [currentPath, setCurrentPath] = useState(browsePath);
const [treeSelectedPath, setTreeSelectedPath] = useState<string>();
const [fetchingBrowsePathExists, setFetchingBrowsePathExists] = useState(false);
const [browsePathExists, setBrowsePathExists] = useState(false);
const [sortKeys, setSortKeys] = useState([]);
Expand All @@ -90,6 +93,8 @@ export function BrowseFilesDialogContainer(props: BrowseFilesDialogContainerProp
const [fetchingPreselectedItems, setFetchingPreselectedItems] = useState(false);
const disableSubmission = fetchingPreselectedItems || (!selectedArray.length && !selectedCard);
const preselectedLookup = createPresenceTable(preselectedPaths);
const itemsByPath = useItemsByPath();
const isCurrentPathLeaf = Boolean(treeSelectedPath); // treeSelectedPath is set when a leaf page is selected in the tree view
Comment thread
jvega190 marked this conversation as resolved.

const fetchItems = useCallback(() => {
// Since lookahead regex is not supported by opensearch, we are excluding the current path from the search using a
Expand Down Expand Up @@ -195,7 +200,28 @@ export function BrowseFilesDialogContainer(props: BrowseFilesDialogContainerProp
};

const onPathSelected = (path: string) => {
setCurrentPath(withoutIndex(path));
const item = lookupItemByPath(path, itemsByPath);
const nextPath = withoutIndex(path);
setCurrentPath(nextPath);

// If the selected path is a page and has no children, select the page itself.
if (item?.systemType === 'page' && item.childrenCount === 0) {
const mediaItem = items?.find((searchItem) => searchItem.path === item.path) ?? contentItemToMediaItem(item);
multiSelect ? replaceSelectedLookup({ [mediaItem.path]: mediaItem }) : setSelectedCard(mediaItem);
setTreeSelectedPath(withoutIndex(mediaItem.path));
} else if (treeSelectedPath) {
multiSelect ? replaceSelectedLookup() : setSelectedCard(null);
setTreeSelectedPath(null);
}
};

const replaceSelectedLookup = (lookup?: LookupTable<MediaItem>) => {
const cleared = Object.fromEntries(Object.keys(selectedLookup).map((key) => [key, null]));
if (!lookup || Object.keys(lookup).length === 0) {
setSelectedLookup(cleared);
} else {
setSelectedLookup({ ...cleared, ...lookup });
}
};
Comment thread
jvega190 marked this conversation as resolved.

const onCloseButtonClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => onClose(e, null);
Expand Down Expand Up @@ -266,6 +292,7 @@ export function BrowseFilesDialogContainer(props: BrowseFilesDialogContainerProp
handleSearchKeyword={handleSearchKeyword}
onCloseButtonClick={onCloseButtonClick}
onPathSelected={onPathSelected}
treeSelectedPath={treeSelectedPath}
onSelectButtonClick={onSelectButtonClick}
numOfLoaderItems={numOfLoaderItems}
onRefresh={onRefresh}
Expand All @@ -277,6 +304,7 @@ export function BrowseFilesDialogContainer(props: BrowseFilesDialogContainerProp
onSelectAll={onSelectAll}
allSelected={allSelectedInCurrentPage}
someSelected={someSelectedInCurrentPage}
isCurrentPathLeaf={isCurrentPathLeaf}
/>
) : (
<EmptyState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ import GridViewIcon from '@mui/icons-material/GridOnRounded';
import ReorderRoundedIcon from '@mui/icons-material/ReorderRounded';
import { SORT_AUTO } from '../Search/utils';
import Checkbox from '@mui/material/Checkbox';
import ResizeableDrawer from '../ResizeableDrawer/ResizeableDrawer';

const TREE_PANEL_DEFAULT_WIDTH = 270;
const TREE_PANEL_MIN_WIDTH = 240;
const TREE_PANEL_MAX_WIDTH = 480;

export function BrowseFilesDialogUI(props: BrowseFilesDialogUIProps) {
// region const { ... } = props;
Expand All @@ -75,6 +80,7 @@ export function BrowseFilesDialogUI(props: BrowseFilesDialogUIProps) {
onCheckboxChecked,
handleSearchKeyword,
onPathSelected,
treeSelectedPath,
onSelectButtonClick,
onChangePage,
onChangeRowsPerPage,
Expand All @@ -89,32 +95,56 @@ export function BrowseFilesDialogUI(props: BrowseFilesDialogUIProps) {
disableSubmission,
allSelected,
someSelected,
onSelectAll
onSelectAll,
isCurrentPathLeaf
} = props;
// endregion
const { formatMessage } = useIntl();
const [sortMenuOpen, setSortMenuOpen] = useState(false);
const buttonRef = useRef(undefined);
const [treePanelWidth, setTreePanelWidth] = useState(TREE_PANEL_DEFAULT_WIDTH);

return (
<>
<DialogBody sx={{ minHeight: '60vh', padding: 0 }}>
<Box display="flex" sx={{ overflow: 'hidden' }}>
<Box
sx={{
width: '270px',
minWidth: '270px',
<DialogBody sx={{ minHeight: '60vh', padding: 0, position: 'relative' }}>
<ResizeableDrawer
open
width={treePanelWidth}
minWidth={TREE_PANEL_MIN_WIDTH}
maxWidth={TREE_PANEL_MAX_WIDTH}
onWidthChange={setTreePanelWidth}
sxs={{
drawerPaper: {
position: 'absolute',
top: 0,
bottom: 0,
height: 'auto'
},
resizeHandle: { backgroundColor: 'transparent' },
drawerBody: {
padding: '16px',
overflow: 'auto',
rowGap: (theme) => theme.spacing(1)
}}
display="flex"
flexDirection="column"
rowGap="20px"
>
<FolderBrowserTreeView rootPath={path} onPathSelected={onPathSelected} selectedPath={currentPath} />
</Box>
<Box component="section" sx={{ flexGrow: 1, padding: '16px', overflow: 'auto' }}>
overflowY: 'auto',
overflowX: 'hidden'
}
}}
>
<FolderBrowserTreeView
rootPath={path}
onPathSelected={onPathSelected}
selectedPath={currentPath}
highlightedPath={treeSelectedPath}
/>
</ResizeableDrawer>
<Box
component="section"
sx={{
marginLeft: `${treePanelWidth}px`,
minHeight: '60vh',
minWidth: 0,
padding: '16px',
overflow: 'auto'
}}
>
<Paper
sx={{
paddingLeft: (theme) => theme.spacing(1),
Expand Down Expand Up @@ -355,14 +385,20 @@ export function BrowseFilesDialogUI(props: BrowseFilesDialogUIProps) {
})
: new Array(numOfLoaderItems).fill(null).map((x, i) => <MediaSkeletonCard key={i} />)}
</Box>
{items && items.length === 0 && (
<EmptyState
sxs={{ root: { flexGrow: 1 } }}
title={<FormattedMessage id="browseFilesDialog.noResults" defaultMessage="No items found." />}
/>
)}
{items &&
items.length === 0 &&
(isCurrentPathLeaf ? (
<EmptyState
sxs={{ root: { flexGrow: 1 } }}
title={<FormattedMessage defaultMessage="This item has no children." />}
/>
) : (
<EmptyState
sxs={{ root: { flexGrow: 1 } }}
title={<FormattedMessage id="browseFilesDialog.noResults" defaultMessage="No items found." />}
/>
))}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</Box>
</Box>
</DialogBody>
<DialogFooter>
<SecondaryButton onClick={onCloseButtonClick}>
Expand Down
19 changes: 18 additions & 1 deletion studio-ui/ui/app/src/components/BrowseFilesDialog/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { ElasticParams, MediaItem, SearchItem } from '../../models/Search';
import { ContentItem } from '../../models/Item';
import StandardAction from '../../models/StandardAction';
import { EnhancedDialogProps } from '../EnhancedDialog';
import React from 'react';
Expand Down Expand Up @@ -71,11 +72,13 @@ export interface BrowseFilesDialogUIProps {
disableSubmission?: boolean;
allSelected: boolean;
someSelected: boolean;
isCurrentPathLeaf: boolean;
onCardSelected(item: MediaItem): void;
onPreviewImage?(item: MediaItem): void;
onCheckboxChecked(path: string, selected: boolean): void;
handleSearchKeyword(keyword: string): void;
onPathSelected(path: string): void;
onPathSelected(path: string, item?: ContentItem): void;
treeSelectedPath: string;
onSelectButtonClick(): void;
onChangePage(page: number): void;
onChangeRowsPerPage(event): void;
Expand All @@ -97,3 +100,17 @@ export const initialParameters: ElasticParams = {
};

export const viewModes: MediaCardViewModes[] = ['card', 'compact', 'row'];

export function contentItemToMediaItem(item: ContentItem): MediaItem {
return {
path: item.path,
name: item.label,
type: item.contentTypeId,
mimeType: item.mimeType ?? '',
previewUrl: item.previewUrl ?? '',
lastModifier: item.modifier?.username ?? '',
lastModified: item.dateModified ?? '',
size: 0,
snippets: ''
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@
*/

// @ts-ignore - React typings haven't been updated to include react 18 hooks
import React, { useEffect, useId } from 'react';
import React, { useCallback, useEffect, useId, useRef } from 'react';
import useActiveSite from '../../hooks/useActiveSite';
import { PathNavigatorTree } from '../PathNavigatorTree';
import { removeStoredPathNavigatorTree } from '../../utils/state';
import useActiveUser from '../../hooks/useActiveUser';
import { useDispatch } from 'react-redux';
import { pathNavigatorTreeExpandPath, pathNavigatorTreeFetchPathChildren } from '../../state/actions/pathNavigatorTree';
import { getIndividualPaths, withIndex } from '../../utils/path';
import { forkJoin, of } from 'rxjs';
import { batchActions } from '../../state/actions/misc';
import useSelection from '../../hooks/useSelection';
import useUpdateRefs from '../../hooks/useUpdateRefs';
Expand All @@ -32,59 +31,90 @@ import { useIntl } from 'react-intl';
export interface FolderBrowserTreeViewProps {
rootPath: string;
selectedPath: string;
highlightedPath?: string;
onPathSelected(path: string): void;
}

export function FolderBrowserTreeView(props: FolderBrowserTreeViewProps) {
const { rootPath, selectedPath, onPathSelected } = props;
const { rootPath, selectedPath, highlightedPath, onPathSelected } = props;
const { formatMessage } = useIntl();
const id = useId();
const tree = useSelection((state) => state.pathNavigatorTree[id]);
const { uuid, id: siteId } = useActiveSite();
const { username } = useActiveUser();
const dispatch = useDispatch();
const selectedPathWithIndex = withIndex(selectedPath);
const pendingChildFetchPathsRef = useRef<Set<string>>(new Set());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const refs = useUpdateRefs({ tree });
useEffect(() => {
if (
// Simply checking that the tree has been initialized. Not using the very root object to
// avoid changes on its state to trigger this effect unnecessarily.
tree?.id === id
) {
const chunk = refs.current.tree;
const path = selectedPath || rootPath;
// If it's `/site/website/*`, there's possibility of `index.xml` behaviours
if (path.startsWith('/site/website')) {
const paths = getIndividualPaths(path, rootPath);
forkJoin(
paths.map((p) => {
const actions = path.startsWith('/site/website')
? getIndividualPaths(path, rootPath).map((p) => {
const withIndexXml = withIndex(p);
return withIndexXml in refs.current.tree.childrenByParentPath || p in refs.current.tree.childrenByParentPath
? of(
pathNavigatorTreeExpandPath({
id,
path: withIndexXml in refs.current.tree.childrenByParentPath ? withIndexXml : p
})
)
: of(pathNavigatorTreeFetchPathChildren({ id, path: p, expand: true }));
return withIndexXml in chunk.childrenByParentPath || p in chunk.childrenByParentPath
? pathNavigatorTreeExpandPath({
id,
path: withIndexXml in chunk.childrenByParentPath ? withIndexXml : p
})
: pathNavigatorTreeFetchPathChildren({ id, path: p, expand: true });
})
).subscribe((actions) => {
dispatch(actions.length === 1 ? actions[0] : batchActions(actions));
});
} else {
const actions = getIndividualPaths(path, rootPath).map((p) =>
p in refs.current.tree.childrenByParentPath
? pathNavigatorTreeExpandPath({ id, path: p })
: pathNavigatorTreeFetchPathChildren({ id, path: p, expand: true })
);
actions.length && dispatch(actions.length === 1 ? actions[0] : batchActions(actions));
}
: getIndividualPaths(path, rootPath).map((p) =>
p in chunk.childrenByParentPath
? pathNavigatorTreeExpandPath({ id, path: p })
: pathNavigatorTreeFetchPathChildren({ id, path: p, expand: true })
);
actions.length && dispatch(actions.length === 1 ? actions[0] : batchActions(actions));
}
}, [refs, dispatch, id, rootPath, selectedPath, siteId, tree?.id]);
useEffect(() => {
return () => {
removeStoredPathNavigatorTree(uuid, username, id);
};
}, [id, uuid, username]);

const handleNodeClick = useCallback(
(event: React.MouseEvent, path: string) => {
onPathSelected?.(path);
if (tree?.id !== id) {
return;
}
const withIndexXml = withIndex(path);
const isExpanded = tree.expanded.includes(path) || tree.expanded.includes(withIndexXml);
const childCount = tree.totalByPath[path] ?? tree.totalByPath[withIndexXml] ?? 0;
if (childCount <= 0) {
return;
}
const childrenLoaded = path in tree.childrenByParentPath || withIndexXml in tree.childrenByParentPath;
if (childrenLoaded) {
if (!isExpanded) {
dispatch(
pathNavigatorTreeExpandPath({
id,
path: withIndexXml in tree.childrenByParentPath ? withIndexXml : path
})
);
}
return;
}
const fetchError = tree.errorByPath[path];
if (fetchError) {
pendingChildFetchPathsRef.current.delete(path);
}
if ((!fetchError && isExpanded) || pendingChildFetchPathsRef.current.has(path)) {
return;
}
pendingChildFetchPathsRef.current.add(path);
dispatch(pathNavigatorTreeFetchPathChildren({ id, path, expand: true }));
},
[dispatch, id, onPathSelected, tree]
);
Comment thread
jvega190 marked this conversation as resolved.

return (
<PathNavigatorTree
id={id}
Expand All @@ -94,9 +124,13 @@ export function FolderBrowserTreeView(props: FolderBrowserTreeViewProps) {
initialCollapsed={false}
initialSystemTypes={['folder', 'page']}
active={{ [selectedPathWithIndex in (tree?.totalByPath ?? {}) ? selectedPathWithIndex : selectedPath]: true }}
onNodeClick={(e, path) => onPathSelected?.(path)}
onNodeClick={handleNodeClick}
sxs={{
header: { '.MuiTypography-root': { fontWeight: 'bold' } }
header: { '.MuiTypography-root': { fontWeight: 'bold' } },
activeItem:
selectedPath === highlightedPath
? { boxShadow: (theme) => `0px 0px 2px 2px ${theme.palette.primary.main}`, borderRadius: '2px' }
: {}
}}
showNavigableAsLinks={false}
showPublishingTarget={false}
Expand Down
Loading