1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-10-21 19:00:12 +00:00

card view support toolbar menu and right click menu (#8278)

This commit is contained in:
Michael An
2025-10-09 13:00:44 +08:00
committed by GitHub
parent 94138dfd29
commit fa2c16035f
7 changed files with 433 additions and 56 deletions

View File

@@ -0,0 +1,229 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import ItemDropdownMenu from '../dropdown-menu/item-dropdown-menu';
import { gettext } from '../../utils/constants';
import { EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY } from '../../metadata/constants';
import TextTranslation from '../../utils/text-translation';
import RowUtils from '../../metadata/views/table/utils/row-utils';
import { checkIsDir } from '../../metadata/utils/row';
import { Utils } from '../../utils/utils';
import { getFileNameFromRecord, getParentDirFromRecord } from '../../metadata/utils/cell';
import { openInNewTab, openParentFolder } from '../../metadata/utils/file';
import { buildCardToolbarMenuOptions } from '../../metadata/utils/menu-builder';
import { useMetadataStatus } from '../../hooks';
import { getColumnByKey } from '../sf-table/utils/column';
const CardFilesToolbar = ({ repoID, updateCurrentDirent }) => {
const [selectedRecordIds, setSelectedRecordIds] = useState([]);
const [metadata, setMetadata] = useState({});
const metadataRef = useRef([]);
const menuRef = useRef(null);
const { enableFaceRecognition, enableTags } = useMetadataStatus();
const eventBus = window.sfMetadataContext && window.sfMetadataContext.eventBus;
const records = useMemo(() => selectedRecordIds.map(id => RowUtils.getRecordById(id, metadataRef.current)).filter(Boolean) || [], [selectedRecordIds]);
const areRecordsInSameFolder = useMemo(() => {
if (records.length <= 1) return true;
const firstPath = records[0] ? getParentDirFromRecord(records[0]) : null;
return firstPath && records.every(record => getParentDirFromRecord(record) === firstPath);
}, [records]);
const readOnly = !window.sfMetadataContext.canModify();
const isMultiple = selectedRecordIds.length > 1;
const toolbarMenuOptions = useMemo(() => {
if (!records.length || !metadata.columns) return [];
const metadataStatus = {
enableFaceRecognition,
enableGenerateDescription: getColumnByKey(metadataRef.current.columns, PRIVATE_COLUMN_KEY.FILE_DESCRIPTION) !== null,
enableTags
};
return buildCardToolbarMenuOptions(
records,
readOnly,
metadataStatus,
isMultiple,
areRecordsInSameFolder,
false
);
}, [records, metadata.columns, enableFaceRecognition, enableTags, readOnly, isMultiple, areRecordsInSameFolder]);
const unSelect = useCallback(() => {
setSelectedRecordIds([]);
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SELECTED_RECORD_IDS, []);
updateCurrentDirent();
}, [eventBus, updateCurrentDirent]);
const deleteRecords = useCallback(() => {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.DELETE_RECORDS, selectedRecordIds, {
success_callback: () => {
updateCurrentDirent();
}
});
}, [eventBus, selectedRecordIds, updateCurrentDirent]);
const toggleMoveDialog = useCallback(() => {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MOVE_DIALOG, records);
}, [eventBus, records]);
const toggleCopyDialog = useCallback(() => {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_COPY_DIALOG, records);
}, [eventBus, records]);
const downloadRecords = useCallback(() => {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.DOWNLOAD_RECORDS, selectedRecordIds);
}, [eventBus, selectedRecordIds]);
const getMenuList = useCallback(() => {
return toolbarMenuOptions;
}, [toolbarMenuOptions]);
const onMenuItemClick = useCallback((operation) => {
switch (operation) {
case TextTranslation.MOVE.key:
case TextTranslation.MOVE_FILE.key:
case TextTranslation.MOVE_FOLDER.key: {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MOVE_DIALOG, records);
break;
}
case TextTranslation.COPY.key: {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_COPY_DIALOG, records);
break;
}
case TextTranslation.DOWNLOAD.key: {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.DOWNLOAD_RECORDS, selectedRecordIds);
break;
}
case TextTranslation.EXTRACT_FILE_DETAIL.key:
case TextTranslation.EXTRACT_FILE_DETAILS.key: {
const imageOrVideoRecords = records.filter(record => {
const isFolder = checkIsDir(record);
if (isFolder || readOnly) return false;
const fileName = getFileNameFromRecord(record);
return Utils.imageCheck(fileName) || Utils.videoCheck(fileName);
});
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_RECORD_DETAILS, imageOrVideoRecords);
break;
}
case TextTranslation.DETECT_FACES.key: {
const imageRecords = records.filter(record => {
const isFolder = checkIsDir(record);
if (isFolder || readOnly) return false;
const fileName = getFileNameFromRecord(record);
return Utils.imageCheck(fileName);
});
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_FACE_RECOGNITION, imageRecords);
break;
}
case TextTranslation.GENERATE_DESCRIPTION.key: {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.GENERATE_DESCRIPTION, records[0]);
break;
}
case TextTranslation.GENERATE_TAGS.key: {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.GENERATE_FILE_TAGS, records[0]);
break;
}
case TextTranslation.EXTRACT_TEXT.key: {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.EXTRACT_TEXT, records[0], menuRef.current.dropdownRef.current);
break;
}
case TextTranslation.OPEN_FILE_IN_NEW_TAB.key:
case TextTranslation.OPEN_FOLDER_IN_NEW_TAB.key: {
openInNewTab(repoID, records[0]);
break;
}
case TextTranslation.OPEN_PARENT_FOLDER.key: {
openParentFolder(records[0]);
break;
}
case TextTranslation.RENAME.key: {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_CARD_RENAME_DIALOG);
break;
}
default:
break;
}
}, [eventBus, records, selectedRecordIds, readOnly, repoID]);
useEffect(() => {
const unsubscribeSelectedFileIds = eventBus && eventBus.subscribe(EVENT_BUS_TYPE.SELECT_RECORDS, (ids, metadataObj) => {
metadataRef.current = metadataObj || [];
setMetadata(metadataObj || {});
setSelectedRecordIds(ids);
});
return () => {
unsubscribeSelectedFileIds && unsubscribeSelectedFileIds();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const length = selectedRecordIds.length;
return (
<div className="selected-dirents-toolbar">
<span className="cur-view-path-btn px-2" onClick={unSelect}>
<span className="sf3-font-x-01 sf3-font mr-2" aria-label={gettext('Unselect')} title={gettext('Unselect')}></span>
<span>{length}{' '}{gettext('selected')}</span>
</span>
{!isMultiple && !readOnly && (
<>
<span
className="cur-view-path-btn"
onClick={toggleMoveDialog}
title={gettext('Move')}
>
<span className="sf3-font-move1 sf3-font" aria-label={gettext('Move')}></span>
</span>
<span
className="cur-view-path-btn"
onClick={toggleCopyDialog}
title={gettext('Copy')}
>
<span className="sf3-font-copy1 sf3-font" aria-label={gettext('Copy')}></span>
</span>
</>
)}
<span
className="cur-view-path-btn"
onClick={downloadRecords}
title={gettext('Download')}
>
<span className="sf3-font-download1 sf3-font" aria-label={gettext('Download')}></span>
</span>
{!readOnly && (
<span
className="cur-view-path-btn"
onClick={deleteRecords}
title={gettext('Delete')}
>
<span className="sf3-font-delete1 sf3-font" aria-label={gettext('Delete')}></span>
</span>
)}
{length > 0 && (
<ItemDropdownMenu
ref={menuRef}
item={{}}
toggleClass="cur-view-path-btn sf3-font-more sf3-font"
onMenuItemClick={onMenuItemClick}
getMenuList={getMenuList}
/>
)}
</div>
);
};
CardFilesToolbar.propTypes = {
repoID: PropTypes.string.isRequired,
};
export default CardFilesToolbar;

View File

@@ -10,6 +10,7 @@ import TableFilesToolbar from './table-files-toolbar';
import GalleryFilesToolbar from './gallery-files-toolbar';
import FaceRecognitionFilesToolbar from './face-recognition-files-toolbar';
import KanbanFilesToolbar from './kanban-files-toolbar';
import CardFilesToolbar from './card-files-toolbar';
const ViewToolbar = ({ repoID, repoInfo, mode, path, viewId, updateCurrentDirent }) => {
const { idViewMap } = useMetadata();
@@ -36,6 +37,10 @@ const ViewToolbar = ({ repoID, repoInfo, mode, path, viewId, updateCurrentDirent
return <KanbanFilesToolbar repoID={repoID} updateCurrentDirent={updateCurrentDirent} />;
}
if (type === VIEW_TYPE.CARD) {
return <CardFilesToolbar repoID={repoID} updateCurrentDirent={updateCurrentDirent} />;
}
if (mode === TAGS_MODE) {
const isAllTagsView = path.split('/').pop() === ALL_TAGS_ID;
if (isAllTagsView) {

View File

@@ -50,13 +50,13 @@ const SortSetter = ({ target = 'sf-metadata-sort-popover', type, sorts: propsSor
}, [modifySorts]);
if (!columns) return null;
const className = classnames(wrapperClass, { 'active': sorts.length > 0 });
return (
<>
<IconBtn
symbol="sort"
size={24}
className={className}
className={classnames(wrapperClass, { 'active': sorts.length > 0 })}
onClick={onSetterToggle}
role="button"
onKeyDown={onKeyDown}
@@ -78,7 +78,6 @@ const SortSetter = ({ target = 'sf-metadata-sort-popover', type, sorts: propsSor
)}
</>
);
};

View File

@@ -96,6 +96,8 @@ export const EVENT_BUS_TYPE = {
TOGGLE_CARD_SETTINGS: 'toggle_card_settings',
OPEN_CARD_SETTINGS: 'open_card_settings',
CLOSE_CARD_SETTINGS: 'close_card_settings',
DISPLAY_SORTS: 'display_sorts',
TOGGLE_CARD_RENAME_DIALOG: 'toggle_card_rename_dialog',
// kanban
TOGGLE_KANBAN_SETTINGS: 'toggle_kanban_settings',

View File

@@ -572,3 +572,102 @@ export const buildKanbanToolbarMenuOptions = (records, readOnly, metadataStatus)
}
return menuOptions;
};
export const buildCardToolbarMenuOptions = (records, readOnly, metadataStatus) => {
if (!records || records.length === 0) {
return [];
}
const menuOptions = [
{
key: TextTranslation.OPEN_FILE_IN_NEW_TAB.key,
value: TextTranslation.OPEN_FILE_IN_NEW_TAB.value
},
{
key: TextTranslation.OPEN_PARENT_FOLDER.key,
value: TextTranslation.OPEN_PARENT_FOLDER.value
}
];
if (!readOnly) {
menuOptions.push('Divider');
menuOptions.push(
{
key: TextTranslation.RENAME.key,
value: TextTranslation.RENAME.value
}
);
}
const aiOptions = buildAISubmenuOptions(records, readOnly, metadataStatus);
if (aiOptions.length > 0) {
if (menuOptions.length > 0) {
menuOptions.push('Divider');
}
menuOptions.push(
{
key: 'AI',
value: gettext('AI'),
subOpList: aiOptions
}
);
}
return menuOptions;
};
export const buildCardMenuOptions = (records, readOnly, metadataStatus) => {
if (!records || records.length === 0) {
return [];
}
const menuOptions = [];
menuOptions.push({
key: TextTranslation.OPEN_FILE_IN_NEW_TAB.key,
value: TextTranslation.OPEN_FILE_IN_NEW_TAB.value
});
menuOptions.push({
key: TextTranslation.OPEN_PARENT_FOLDER.key,
value: TextTranslation.OPEN_PARENT_FOLDER.value
});
menuOptions.push('Divider');
if (!readOnly) {
menuOptions.push({
key: TextTranslation.RENAME.key,
value: TextTranslation.RENAME.value
});
menuOptions.push({
key: TextTranslation.MOVE.key,
value: TextTranslation.MOVE.value
});
menuOptions.push({
key: TextTranslation.COPY.key,
value: TextTranslation.COPY.value
});
}
menuOptions.push({
key: TextTranslation.DOWNLOAD.key,
value: TextTranslation.DOWNLOAD.value
});
if (!readOnly) {
menuOptions.push({
key: TextTranslation.DELETE.key,
value: TextTranslation.DELETE.value
});
}
const aiOptions = buildAISubmenuOptions(records, readOnly, metadataStatus);
if (aiOptions.length > 0) {
menuOptions.push('Divider');
menuOptions.push({
key: 'AI',
value: gettext('AI'),
subOpList: aiOptions
});
}
return menuOptions;
};

View File

@@ -2,7 +2,7 @@ import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react'
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useMetadataView } from '../../../hooks/metadata-view';
import { CARD_SETTINGS_KEYS, PRIVATE_COLUMN_KEY } from '../../../constants';
import { CARD_SETTINGS_KEYS, PRIVATE_COLUMN_KEY, EVENT_BUS_TYPE } from '../../../constants';
import { gettext } from '../../../../utils/constants';
import { getRecordIdFromRecord, getFileNameFromRecord, getParentDirFromRecord } from '../../../utils/cell';
import { openFile } from '../../../utils/file';
@@ -23,7 +23,19 @@ const CardItems = ({ modifyRecord, deleteRecords, modifyColumnData, onCloseSetti
const currentImageRef = useRef(null);
const containerRef = useRef(null);
const { isDirentDetailShow, metadata, updateCurrentDirent, showDirentDetail } = useMetadataView();
const eventBus = window.sfMetadataContext && window.sfMetadataContext.eventBus;
useEffect(() => {
const unsubscribe = eventBus && eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SELECTED_RECORD_IDS, (ids) => {
setSelectedCard(Array.isArray(ids) ? ids[0] : null);
});
return () => {
unsubscribe && unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { isDirentDetailShow, metadata, updateCurrentDirent, showDirentDetail, updateSelectedRecordIds } = useMetadataView();
const { tagsData } = useTags();
const repoID = window.sfMetadataContext.getSetting('repoID');
@@ -88,7 +100,8 @@ const CardItems = ({ modifyRecord, deleteRecords, modifyColumnData, onCloseSetti
file_tags: []
});
setSelectedCard(recordId);
}, [updateCurrentDirent]);
updateSelectedRecordIds([recordId]);
}, [updateCurrentDirent, updateSelectedRecordIds]);
const onSelectCard = useCallback((record) => {
const recordId = getRecordIdFromRecord(record);
@@ -100,8 +113,9 @@ const CardItems = ({ modifyRecord, deleteRecords, modifyColumnData, onCloseSetti
const handleClickOutside = useCallback((event) => {
setSelectedCard(null);
updateSelectedRecordIds([]);
updateCurrentDirent();
}, [updateCurrentDirent]);
}, [updateCurrentDirent, updateSelectedRecordIds]);
const onContextMenu = useCallback((event, recordId) => {
event.preventDefault();
@@ -114,10 +128,11 @@ const CardItems = ({ modifyRecord, deleteRecords, modifyColumnData, onCloseSetti
deleteRecords(recordIds, {
success_callback: () => {
setSelectedCard(null);
updateSelectedRecordIds([]);
updateCurrentDirent();
},
});
}, [deleteRecords, updateCurrentDirent]);
}, [deleteRecords, updateCurrentDirent, updateSelectedRecordIds]);
const onRename = useCallback((rowId, updates, oldRowData, originalUpdates, originalOldRowData, { success_callback }) => {
modifyRecord(rowId, updates, oldRowData, originalUpdates, originalOldRowData, {
@@ -132,8 +147,9 @@ const CardItems = ({ modifyRecord, deleteRecords, modifyColumnData, onCloseSetti
useEffect(() => {
if (!isDirentDetailShow) {
setSelectedCard(null);
updateSelectedRecordIds([]);
}
}, [isDirentDetailShow]);
}, [isDirentDetailShow, updateSelectedRecordIds]);
if (records.length == 0) {
return <EmptyTip text={gettext('No items')} />;

View File

@@ -1,54 +1,52 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import ContextMenu from '../../../components/context-menu';
import RenameDialog from '../../../components/dialog/rename-dialog';
import { getRowById } from '../../../../components/sf-table/utils/table';
import { checkIsDir } from '../../../utils/row';
import { getFileNameFromRecord, getParentDirFromRecord } from '../../../utils/cell';
import { gettext } from '../../../../utils/constants';
import { openInNewTab, openParentFolder } from '../../../utils/file';
import { useMetadataView } from '../../../hooks/metadata-view';
import { PRIVATE_COLUMN_KEY } from '../../../constants';
import { PRIVATE_COLUMN_KEY, EVENT_BUS_TYPE } from '../../../constants';
import { useFileOperations } from '../../../../hooks/file-operations';
import { buildCardMenuOptions } from '../../../utils/menu-builder';
import { useMetadataStatus } from '../../../../hooks/metadata-status';
import { getColumnByKey } from '../../../utils/column';
import TextTranslation from '../../../../utils/text-translation';
const CONTEXT_MENU_KEY = {
OPEN_IN_NEW_TAB: 'open_in_new_tab',
OPEN_PARENT_FOLDER: 'open_parent_folder',
DOWNLOAD: 'download',
DELETE: 'delete',
RENAME: 'rename',
};
const KanbanContextMenu = ({ selectedCard, onDelete, onRename }) => {
const CardContextMenu = ({ selectedCard, onDelete, onRename }) => {
const [isRenameDialogShow, setIsRenameDialogShow] = useState(false);
const { metadata } = useMetadataView();
const { enableFaceRecognition, enableTags } = useMetadataStatus();
const { handleDownload: handleDownloadAPI } = useFileOperations();
const {
metadata,
updateRecordDetails,
updateFaceRecognition,
updateRecordDescription,
onOCR,
generateFileTags
} = useMetadataView();
const selectedRecord = useMemo(() => getRowById(metadata, selectedCard), [metadata, selectedCard]);
const isDir = useMemo(() => checkIsDir(selectedRecord), [selectedRecord]);
const oldName = useMemo(() => getFileNameFromRecord(selectedRecord), [selectedRecord]);
const parentDir = useMemo(() => getParentDirFromRecord(selectedRecord), [selectedRecord]);
const repoID = window.sfMetadataContext.getSetting('repoID');
const checkCanDeleteRow = window.sfMetadataContext.checkCanDeleteRow();
const canModifyRow = window.sfMetadataContext.canModifyRow();
const readOnly = !window.sfMetadataContext.canModify();
const record = useMemo(() => getRowById(metadata, selectedCard), [metadata, selectedCard]);
const options = useMemo(() => {
let validOptions = [
{ value: CONTEXT_MENU_KEY.OPEN_IN_NEW_TAB, label: isDir ? gettext('Open folder in new tab') : gettext('Open file in new tab') },
{ value: CONTEXT_MENU_KEY.OPEN_PARENT_FOLDER, label: gettext('Open parent folder') },
{ value: CONTEXT_MENU_KEY.DOWNLOAD, label: gettext('Download') },
];
if (checkCanDeleteRow) {
validOptions.push({ value: CONTEXT_MENU_KEY.DELETE, label: isDir ? gettext('Delete folder') : gettext('Delete file') });
}
if (canModifyRow) {
validOptions.push({ value: CONTEXT_MENU_KEY.RENAME, label: isDir ? gettext('Rename folder') : gettext('Rename file') });
}
return validOptions;
}, [isDir, checkCanDeleteRow, canModifyRow]);
const metadataStatus = {
enableFaceRecognition,
enableGenerateDescription: getColumnByKey(metadata.columns, PRIVATE_COLUMN_KEY.FILE_DESCRIPTION) !== null,
enableTags
};
return buildCardMenuOptions(
[record],
readOnly,
metadataStatus,
);
}, [enableFaceRecognition, metadata.columns, enableTags, record, readOnly]);
const openRenameDialog = useCallback(() => {
setIsRenameDialogShow(true);
@@ -72,43 +70,72 @@ const KanbanContextMenu = ({ selectedCard, onDelete, onRename }) => {
}, [handleDownloadAPI, parentDir, oldName, isDir]);
const handleOptionClick = useCallback((option) => {
if (!selectedCard) return;
const record = getRowById(metadata, selectedCard);
if (!record) return;
switch (option.value) {
case CONTEXT_MENU_KEY.OPEN_IN_NEW_TAB: {
switch (option.key) {
case TextTranslation.OPEN_FILE_IN_NEW_TAB.key: {
openInNewTab(repoID, record);
break;
}
case CONTEXT_MENU_KEY.OPEN_PARENT_FOLDER: {
case TextTranslation.OPEN_PARENT_FOLDER.key: {
openParentFolder(record);
break;
}
case CONTEXT_MENU_KEY.DOWNLOAD: {
handleDownload(record);
case TextTranslation.RENAME.key: {
openRenameDialog();
break;
}
case CONTEXT_MENU_KEY.DELETE: {
case TextTranslation.MOVE.key:
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MOVE_DIALOG, [record]);
break;
case TextTranslation.COPY.key:
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_COPY_DIALOG, [record]);
break;
case TextTranslation.DOWNLOAD.key: {
handleDownload();
break;
}
case TextTranslation.DELETE.key: {
onDelete([selectedCard]);
break;
}
case CONTEXT_MENU_KEY.RENAME: {
openRenameDialog();
case TextTranslation.DETECT_FACES.key: {
updateFaceRecognition([record]);
break;
}
case TextTranslation.EXTRACT_FILE_DETAIL.key: {
updateRecordDetails([record]);
break;
}
case TextTranslation.GENERATE_DESCRIPTION.key: {
updateRecordDescription(record);
break;
}
case TextTranslation.GENERATE_TAGS.key: {
generateFileTags(record);
break;
}
case TextTranslation.EXTRACT_TEXT.key: {
onOCR(record, '.sf-metadata-card-item-image-container');
break;
}
default: {
break;
}
}
}, [metadata, repoID, selectedCard, onDelete, openRenameDialog, handleDownload]);
}, [record, updateFaceRecognition, repoID, openRenameDialog, handleDownload, onDelete, selectedCard, updateRecordDetails, updateRecordDescription, generateFileTags, onOCR]);
useEffect(() => {
const unsubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_CARD_RENAME_DIALOG, openRenameDialog);
return () => {
unsubscribe();
};
}, [openRenameDialog]);
return (
<>
<ContextMenu
options={options}
onOptionClick={handleOptionClick}
allowedTriggerElements={['.sf-metadata-kanban-card']}
allowedTriggerElements={['.sf-metadata-view-card']}
/>
{isRenameDialogShow && (
<RenameDialog
@@ -122,10 +149,10 @@ const KanbanContextMenu = ({ selectedCard, onDelete, onRename }) => {
);
};
KanbanContextMenu.propTypes = {
CardContextMenu.propTypes = {
selectedCard: PropTypes.string,
onDelete: PropTypes.func,
onRename: PropTypes.func,
};
export default KanbanContextMenu;
export default CardContextMenu;