一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
535 行
19 KiB
TypeScript
535 行
19 KiB
TypeScript
import * as React from 'react';
|
|
import { DragEventHandler, MouseEventHandler, useCallback, useMemo, useRef } from 'react';
|
|
import { ItemClickListener, ItemDragListener, ListItem, ListItemType } from '../types';
|
|
import TagItem from '../listItemComponents/TagItem';
|
|
import { Dispatch } from 'redux';
|
|
import { clipboard } from 'electron';
|
|
import type { MenuItem as MenuItemType } from 'electron';
|
|
import { getTrashFolderId } from '@joplin/lib/services/trash';
|
|
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
|
import Tag from '@joplin/lib/models/Tag';
|
|
import { _ } from '@joplin/lib/locale';
|
|
import { substrWithEllipsis } from '@joplin/lib/string-utils';
|
|
import Folder from '@joplin/lib/models/Folder';
|
|
import bridge from '../../../services/bridge';
|
|
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
|
import CommandService from '@joplin/lib/services/CommandService';
|
|
import { FolderEntity } from '@joplin/lib/services/database/types';
|
|
import InteropService from '@joplin/lib/services/interop/InteropService';
|
|
import InteropServiceHelper from '../../../InteropServiceHelper';
|
|
import Setting from '@joplin/lib/models/Setting';
|
|
import PerFolderSortOrderService from '@joplin/lib/services/sortOrder/PerFolderSortOrderService';
|
|
import { getFolderCallbackUrl, getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
|
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
|
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
|
import FolderItem from '../listItemComponents/FolderItem';
|
|
import Logger from '@joplin/utils/Logger';
|
|
import onFolderDrop from '@joplin/lib/models/utils/onFolderDrop';
|
|
import HeaderItem from '../listItemComponents/HeaderItem';
|
|
import AllNotesItem from '../listItemComponents/AllNotesItem';
|
|
import ListItemWrapper, { ItemSelectionState } from '../listItemComponents/ListItemWrapper';
|
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
|
import shim from '@joplin/lib/shim';
|
|
import useOnItemClick from './useOnItemClick';
|
|
import { ShareType, StateShare } from '@joplin/lib/services/share/reducer';
|
|
|
|
const Menu = bridge().Menu;
|
|
const MenuItem: typeof MenuItemType = bridge().MenuItem;
|
|
|
|
const logger = Logger.create('useOnRenderItem');
|
|
|
|
interface Props {
|
|
dispatch: Dispatch;
|
|
themeId: number;
|
|
plugins: PluginStates;
|
|
shares: StateShare[];
|
|
folders: FolderEntity[];
|
|
collapsedFolderIds: string[];
|
|
containerRef: React.RefObject<HTMLDivElement>;
|
|
|
|
selectedIndex: number;
|
|
selectedIndexes: number[];
|
|
listItems: ListItem[];
|
|
}
|
|
|
|
type ItemContextMenuListener = MouseEventHandler<HTMLElement>;
|
|
|
|
const menuUtils = new MenuUtils(CommandService.instance());
|
|
|
|
// Checks whether an element is at least partially visible within a scrollable
|
|
// container by comparing their bounding rectangles.
|
|
const isElementVisibleInContainer = (element: HTMLElement, container: HTMLElement) => {
|
|
const elementRect = element.getBoundingClientRect();
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
return elementRect.bottom > containerRect.top && elementRect.top < containerRect.bottom;
|
|
};
|
|
|
|
const focusListItem = (item: HTMLElement|null) => {
|
|
if (item) {
|
|
const activeElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
const itemList = item.closest('.item-list');
|
|
const activeTreeItem = activeElement?.closest('[role="treeitem"]');
|
|
const focusWasLost = activeElement === document.body;
|
|
|
|
// If the currently focused element is a tree item inside the same list,
|
|
// the user is navigating with the keyboard — always allow focus to move
|
|
// to the newly selected item so arrow-key scrolling is not interrupted.
|
|
const isKeyboardNavigating = !!activeTreeItem && itemList?.contains(activeTreeItem);
|
|
|
|
// Avoid disturbing scroll while user is manually scrolling through the list.
|
|
// However, if focus was lost (activeElement -> <body>), or the user is
|
|
// navigating with the keyboard, allow re-focusing even when the selected
|
|
// item is currently out of view.
|
|
if (itemList instanceof HTMLElement && !isElementVisibleInContainer(item, itemList) && !focusWasLost && !isKeyboardNavigating) {
|
|
return;
|
|
}
|
|
|
|
// Move focus only if needed: either focus was lost, or selection changed
|
|
// to a different tree item.
|
|
if (focusWasLost || activeTreeItem !== item) {
|
|
// preventScroll: true avoids a secondary scroll caused by the focus() call
|
|
// itself when the item is near the edge of the visible area.
|
|
focus('useOnRenderItem', item, { preventScroll: true });
|
|
}
|
|
}
|
|
};
|
|
|
|
const noFocusListItem = () => {};
|
|
|
|
const folderCommandToMenuItem = (commandId: string, folderIds: string|string[]) => {
|
|
const options = Array.isArray(folderIds) ? { commandFolderIds: folderIds } : { commandFolderId: folderIds };
|
|
return new MenuItem(menuUtils.commandToStatefulMenuItem(commandId, folderIds, options));
|
|
};
|
|
|
|
const useOnRenderItem = (props: Props) => {
|
|
|
|
const pluginsRef = useRef<PluginStates>(null);
|
|
pluginsRef.current = props.plugins;
|
|
const foldersRef = useRef<FolderEntity[]>(null);
|
|
foldersRef.current = props.folders;
|
|
|
|
const onTagDrop_: DragEventHandler<HTMLElement> = useCallback(async event => {
|
|
const tagId = event.currentTarget.getAttribute('data-tag-id');
|
|
const dt = event.dataTransfer;
|
|
if (!dt) return;
|
|
|
|
if (dt.types.indexOf('text/x-jop-note-ids') >= 0) {
|
|
event.preventDefault();
|
|
|
|
const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids'));
|
|
for (let i = 0; i < noteIds.length; i++) {
|
|
await Tag.addNote(tagId, noteIds[i]);
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
const selectedIndexesRef = useRef(props.selectedIndexes);
|
|
selectedIndexesRef.current = props.selectedIndexes;
|
|
const itemsRef = useRef(props.listItems);
|
|
itemsRef.current = props.listItems;
|
|
const getSelectedIds = useCallback(() => {
|
|
return selectedIndexesRef.current.map(index => {
|
|
const item = itemsRef.current[index];
|
|
if (item.kind === ListItemType.Folder) {
|
|
return item.folder.id;
|
|
} else if (item.kind === ListItemType.Tag) {
|
|
return item.tag.id;
|
|
}
|
|
return null;
|
|
}).filter(id => !!id);
|
|
}, []);
|
|
|
|
const onItemClick = useOnItemClick({ dispatch: props.dispatch, selectedIndexesRef, itemsRef });
|
|
|
|
const onItemContextMenu: ItemContextMenuListener = useCallback(async event => {
|
|
const itemId = event.currentTarget.getAttribute('data-id');
|
|
if (itemId === Folder.conflictFolderId()) return;
|
|
|
|
const itemType = Number(event.currentTarget.getAttribute('data-type'));
|
|
if (!itemId || !itemType) throw new Error('No data on element');
|
|
|
|
let itemIds = [itemId];
|
|
const itemIndex = Number(event.currentTarget.getAttribute('data-index'));
|
|
if (selectedIndexesRef.current.includes(itemIndex)) {
|
|
itemIds = getSelectedIds();
|
|
}
|
|
|
|
let deleteMessage = '';
|
|
const deleteButtonLabel = _('Remove');
|
|
|
|
if (itemType === BaseModel.TYPE_TAG) {
|
|
if (itemIds.length === 1) {
|
|
const tag = await Tag.load(itemId);
|
|
deleteMessage = _('Remove tag "%s" from all notes?', substrWithEllipsis(tag.title, 0, 32));
|
|
} else {
|
|
deleteMessage = _('Remove %d tags from all notes? This cannot be undone.', itemIds.length);
|
|
}
|
|
} else if (itemType === BaseModel.TYPE_SEARCH) {
|
|
deleteMessage = _('Remove this search from the sidebar?');
|
|
}
|
|
|
|
const menu = new Menu();
|
|
|
|
if (itemId === getTrashFolderId()) {
|
|
menu.append(
|
|
new MenuItem(menuUtils.commandToStatefulMenuItem('emptyTrash')),
|
|
);
|
|
menu.popup({ window: bridge().activeWindow() });
|
|
return;
|
|
}
|
|
|
|
let item = null;
|
|
if (itemType === BaseModel.TYPE_FOLDER) {
|
|
item = BaseModel.byId(foldersRef.current, itemId);
|
|
}
|
|
|
|
const isDeleted = item ? !!item.deleted_time : false;
|
|
|
|
if (!isDeleted) {
|
|
const isDecryptedFolder = itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied;
|
|
if (isDecryptedFolder && itemIds.length === 1) {
|
|
menu.append(folderCommandToMenuItem('newFolder', itemId));
|
|
}
|
|
|
|
if (itemType === BaseModel.TYPE_FOLDER) {
|
|
menu.append(folderCommandToMenuItem('deleteFolder', itemIds));
|
|
} else {
|
|
menu.append(
|
|
new MenuItem({
|
|
label: deleteButtonLabel,
|
|
click: async () => {
|
|
const ok = bridge().showConfirmMessageBox(deleteMessage, {
|
|
buttons: [deleteButtonLabel, _('Cancel')],
|
|
defaultId: 1,
|
|
});
|
|
if (!ok) return;
|
|
|
|
if (itemType === BaseModel.TYPE_TAG) {
|
|
for (const itemId of itemIds) {
|
|
await Tag.untagAll(itemId);
|
|
}
|
|
} else if (itemType === BaseModel.TYPE_SEARCH) {
|
|
props.dispatch({
|
|
type: 'SEARCH_DELETE',
|
|
id: itemId,
|
|
});
|
|
}
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
if (isDecryptedFolder) {
|
|
const whenClause = CommandService.instance().currentWhenClauseContext({ commandFolderIds: itemIds });
|
|
menu.append(new MenuItem({
|
|
...menuUtils.commandToStatefulMenuItem('moveToFolder', itemIds),
|
|
// By default, moveToFolder's enabled condition is based on the selected notes. However, the right-click
|
|
// menu item applies to folders. For now, use a custom condition:
|
|
enabled: !whenClause.foldersIncludeReadOnly,
|
|
}));
|
|
}
|
|
|
|
if (isDecryptedFolder && itemIds.length === 1) {
|
|
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('openFolderDialog', { folderId: itemId }, { commandFolderId: itemId })));
|
|
|
|
menu.append(new MenuItem({ type: 'separator' }));
|
|
|
|
const exportMenu = new Menu();
|
|
const ioService = InteropService.instance();
|
|
const ioModules = ioService.modules();
|
|
for (let i = 0; i < ioModules.length; i++) {
|
|
const module = ioModules[i];
|
|
if (module.type !== 'exporter') continue;
|
|
|
|
exportMenu.append(
|
|
new MenuItem({
|
|
label: module.fullLabel(),
|
|
click: async () => {
|
|
await InteropServiceHelper.export(props.dispatch, module, { sourceFolderIds: itemIds, plugins: pluginsRef.current });
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Only show the share/leave share actions for top-level folders
|
|
const shareFolderItem = folderCommandToMenuItem('showShareFolderDialog', itemId);
|
|
if (shareFolderItem.enabled) menu.append(shareFolderItem);
|
|
const leaveSharedFolderItem = folderCommandToMenuItem('leaveSharedFolder', itemId);
|
|
if (leaveSharedFolderItem.enabled) menu.append(leaveSharedFolderItem);
|
|
const publishFolderItem = folderCommandToMenuItem('showPublishFolderDialog', itemId);
|
|
if (publishFolderItem.enabled) menu.append(publishFolderItem);
|
|
|
|
menu.append(
|
|
new MenuItem({
|
|
label: _('Export'),
|
|
submenu: exportMenu,
|
|
}),
|
|
);
|
|
if (Setting.value('notes.perFolderSortOrderEnabled')) {
|
|
menu.append(new MenuItem({
|
|
...menuUtils.commandToStatefulMenuItem('togglePerFolderSortOrder', itemId, { commandFolderId: itemId }),
|
|
type: 'checkbox',
|
|
checked: PerFolderSortOrderService.isSet(itemId),
|
|
}));
|
|
}
|
|
}
|
|
|
|
if (itemType === BaseModel.TYPE_FOLDER && itemIds.length === 1) {
|
|
menu.append(
|
|
new MenuItem({
|
|
label: _('Copy external link'),
|
|
click: () => {
|
|
clipboard.writeText(getFolderCallbackUrl(itemId));
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
if (itemType === BaseModel.TYPE_TAG && itemIds.length === 1) {
|
|
menu.append(new MenuItem(
|
|
menuUtils.commandToStatefulMenuItem('renameTag', itemId),
|
|
));
|
|
menu.append(
|
|
new MenuItem({
|
|
label: _('Copy external link'),
|
|
click: () => {
|
|
clipboard.writeText(getTagCallbackUrl(itemId));
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
const pluginViews = pluginUtils.viewsByType(pluginsRef.current, 'menuItem');
|
|
|
|
for (const view of pluginViews) {
|
|
const location = view.location;
|
|
|
|
if (itemType === ModelType.Tag && location === MenuItemLocation.TagContextMenu) {
|
|
menu.append(
|
|
new MenuItem(menuUtils.commandToStatefulMenuItem(view.commandName, itemId)),
|
|
);
|
|
} else if (itemType === ModelType.Folder && location === MenuItemLocation.FolderContextMenu) {
|
|
menu.append(folderCommandToMenuItem(view.commandName, itemId));
|
|
}
|
|
}
|
|
} else {
|
|
if (itemType === BaseModel.TYPE_FOLDER) {
|
|
menu.append(folderCommandToMenuItem('restoreFolder', itemIds));
|
|
}
|
|
}
|
|
|
|
menu.popup({ window: bridge().activeWindow() });
|
|
}, [props.dispatch, pluginsRef, getSelectedIds]);
|
|
|
|
|
|
|
|
const onFolderDragStart_: ItemDragListener = useCallback(event => {
|
|
const folderId = event.currentTarget.getAttribute('data-folder-id');
|
|
if (!folderId) return;
|
|
|
|
let itemIds = [folderId];
|
|
const itemIndex = Number(event.currentTarget.getAttribute('data-index'));
|
|
if (selectedIndexesRef.current.includes(itemIndex)) {
|
|
itemIds = getSelectedIds();
|
|
}
|
|
|
|
event.dataTransfer.setDragImage(new Image(), 1, 1);
|
|
event.dataTransfer.clearData();
|
|
event.dataTransfer.setData('text/x-jop-folder-ids', JSON.stringify(itemIds));
|
|
}, [getSelectedIds]);
|
|
|
|
const onFolderDragOver_: ItemDragListener = useCallback(event => {
|
|
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') >= 0) event.preventDefault();
|
|
if (event.dataTransfer.types.indexOf('text/x-jop-folder-ids') >= 0) event.preventDefault();
|
|
}, []);
|
|
|
|
const onFolderDrop_: ItemDragListener = useCallback(async event => {
|
|
const folderId = event.currentTarget.getAttribute('data-folder-id');
|
|
const dt = event.dataTransfer;
|
|
if (!dt) return;
|
|
|
|
// folderId can be NULL when dropping on the sidebar Notebook header. In that case, it's used
|
|
// to put the dropped folder at the root. But for notes, folderId needs to always be defined
|
|
// since there's no such thing as a root note.
|
|
|
|
try {
|
|
if (dt.types.indexOf('text/x-jop-note-ids') >= 0) {
|
|
event.preventDefault();
|
|
if (!folderId) return;
|
|
const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids'));
|
|
await onFolderDrop(noteIds, [], folderId);
|
|
} else if (dt.types.indexOf('text/x-jop-folder-ids') >= 0) {
|
|
event.preventDefault();
|
|
const folderIds = JSON.parse(dt.getData('text/x-jop-folder-ids'));
|
|
await onFolderDrop([], folderIds, folderId);
|
|
}
|
|
} catch (error) {
|
|
logger.error(error);
|
|
await shim.showErrorDialog(error.message);
|
|
}
|
|
}, []);
|
|
|
|
const onFolderToggleClick_: ItemClickListener = useCallback(event => {
|
|
const folderId = event.currentTarget.getAttribute('data-folder-id');
|
|
|
|
props.dispatch({
|
|
type: 'FOLDER_TOGGLE',
|
|
id: folderId,
|
|
});
|
|
}, [props.dispatch]);
|
|
|
|
// If at least one of the folder has an icon, then we display icons for all
|
|
// folders (those without one will get the default icon). This is so that
|
|
// visual alignment is correct for all folders, otherwise the folder tree
|
|
// looks messy.
|
|
const showFolderIcons = useMemo(() => {
|
|
return Folder.shouldShowFolderIcons(props.folders);
|
|
}, [props.folders]);
|
|
const publishedFolderIds = useMemo(() => {
|
|
const output = new Set(props.shares.filter(share => share.type === ShareType.PublishedFolder).map(share => share.folder_id));
|
|
for (const folder of props.folders) {
|
|
if (folder.is_shared && !folder.share_id) output.add(folder.id);
|
|
}
|
|
let size = 0;
|
|
while (size !== output.size) {
|
|
size = output.size;
|
|
for (const folder of props.folders) {
|
|
if (output.has(folder.parent_id)) output.add(folder.id);
|
|
}
|
|
}
|
|
return output;
|
|
}, [props.folders, props.shares]);
|
|
|
|
const itemCount = props.listItems.length;
|
|
return useCallback((item: ListItem, index: number) => {
|
|
const primarySelected = props.selectedIndex === index;
|
|
const selected = primarySelected || props.selectedIndexes.includes(index);
|
|
const selectionState: ItemSelectionState = {
|
|
primarySelected,
|
|
selected,
|
|
multipleItemsSelected: props.selectedIndexes.length > 1,
|
|
};
|
|
|
|
const sidebarContainsFocus = props.containerRef.current?.contains(document.activeElement);
|
|
// Focus moves to <body> when the previously-focused element is removed
|
|
// from the DOM (e.g. scrolled out of the virtualized list). We still
|
|
// want to restore focus to the newly-selected item in that case.
|
|
const focusLostFromDom = document.activeElement === document.body;
|
|
const focusInList = document.hasFocus() && (sidebarContainsFocus || focusLostFromDom);
|
|
const anchorRef = (focusInList && primarySelected) ? focusListItem : noFocusListItem;
|
|
|
|
if (item.kind === ListItemType.Tag) {
|
|
const tag = item.tag;
|
|
return <TagItem
|
|
key={item.key}
|
|
anchorRef={anchorRef}
|
|
selectionState={selectionState}
|
|
onClick={onItemClick}
|
|
onTagDrop={onTagDrop_}
|
|
onContextMenu={onItemContextMenu}
|
|
label={item.label}
|
|
tag={tag}
|
|
itemCount={itemCount}
|
|
index={index}
|
|
/>;
|
|
} else if (item.kind === ListItemType.Folder) {
|
|
const folder = item.folder;
|
|
const isExpanded = props.collapsedFolderIds.indexOf(folder.id) < 0;
|
|
let noteCount = (folder as FolderEntity & { note_count?: number }).note_count;
|
|
|
|
// For now hide the count for folders in the trash because it doesn't work and getting it to
|
|
// work would be tricky.
|
|
if (folder.deleted_time || folder.id === getTrashFolderId()) noteCount = 0;
|
|
|
|
// Thunderbird count: Subtract children note_count from parent folder if it expanded.
|
|
if (isExpanded) {
|
|
for (let i = 0; i < props.folders.length; i++) {
|
|
if (props.folders[i].parent_id === folder.id) {
|
|
noteCount -= (props.folders[i] as FolderEntity & { note_count?: number }).note_count;
|
|
}
|
|
}
|
|
}
|
|
return <FolderItem
|
|
key={item.key}
|
|
anchorRef={anchorRef}
|
|
selectionState={selectionState}
|
|
folderId={folder.id}
|
|
folderTitle={item.label}
|
|
folderIcon={Folder.unserializeIcon(folder.icon)}
|
|
depth={item.depth}
|
|
isExpanded={isExpanded}
|
|
hasChildren={item.hasChildren}
|
|
noteCount={noteCount}
|
|
onFolderDragStart_={onFolderDragStart_}
|
|
onFolderDragOver_={onFolderDragOver_}
|
|
onFolderDrop_={onFolderDrop_}
|
|
itemContextMenu={onItemContextMenu}
|
|
folderItem_click={onItemClick}
|
|
onFolderToggleClick_={onFolderToggleClick_}
|
|
shareId={folder.share_id}
|
|
isPublished={publishedFolderIds.has(folder.id)}
|
|
parentId={folder.parent_id}
|
|
showFolderIcon={showFolderIcons}
|
|
index={index}
|
|
itemCount={itemCount}
|
|
/>;
|
|
} else if (item.kind === ListItemType.Header) {
|
|
return <HeaderItem
|
|
key={item.id}
|
|
anchorRef={anchorRef}
|
|
item={item}
|
|
selectionState={selectionState}
|
|
onDrop={item.supportsFolderDrop ? onFolderDrop_ : null}
|
|
index={index}
|
|
itemCount={itemCount}
|
|
/>;
|
|
} else if (item.kind === ListItemType.AllNotes) {
|
|
return <AllNotesItem
|
|
key={item.key}
|
|
anchorRef={anchorRef}
|
|
selectionState={selectionState}
|
|
item={item}
|
|
index={index}
|
|
itemCount={itemCount}
|
|
/>;
|
|
} else if (item.kind === ListItemType.Spacer) {
|
|
return (
|
|
<ListItemWrapper
|
|
key={item.key}
|
|
containerRef={anchorRef}
|
|
depth={1}
|
|
selectionState={selectionState}
|
|
itemIndex={index}
|
|
itemCount={itemCount}
|
|
highlightOnHover={false}
|
|
className='sidebar-spacer-item'
|
|
>
|
|
<div aria-label={_('Spacer')}></div>
|
|
</ListItemWrapper>
|
|
);
|
|
} else {
|
|
const exhaustivenessCheck: never = item;
|
|
return exhaustivenessCheck;
|
|
}
|
|
}, [
|
|
onItemClick,
|
|
onFolderDragOver_,
|
|
onFolderDragStart_,
|
|
onFolderDrop_,
|
|
onFolderToggleClick_,
|
|
onItemContextMenu,
|
|
onTagDrop_,
|
|
props.collapsedFolderIds,
|
|
props.folders,
|
|
publishedFolderIds,
|
|
showFolderIcons,
|
|
props.selectedIndex,
|
|
props.selectedIndexes,
|
|
props.containerRef,
|
|
itemCount,
|
|
]);
|
|
};
|
|
|
|
export default useOnRenderItem;
|