Skip to content
Draft
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
50 changes: 29 additions & 21 deletions frontend/src/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,36 +49,44 @@ interface ContextMenuOption {
onClick?: (contextData: { posX: number; posY: number }) => void;
}


interface ContextMenuProps {
target: React.RefObject<HTMLElement>;
options: () => ContextMenuOption[];
options: (contextData?: any) => ContextMenuOption[];
}

const ContextMenu: React.FC<ContextMenuProps> = ({ target, options }) => {
const [contextData, setContextData] = useState({
const [menuState, setMenuState] = useState({
visible: false,
posX: 0,
posY: 0,
contextData: undefined,
});
const contextRef = useRef(null);

useEffect(() => {
const contextMenuEventHandler = (event: MouseEvent) => {
const targetElement = target.current;
let contextData = undefined;
// Try to get context data from a custom property on the event
if ((event as any).contextData) {
contextData = (event as any).contextData;
}
if (targetElement && targetElement.contains(event.target as Node)) {
event.preventDefault();
setTimeout(() => {
setContextData({
setMenuState({
visible: true,
posX: event.clientX,
posY: event.clientY,
contextData,
});
}, 0);
} else if (
contextRef.current &&
(contextRef.current as HTMLElement).contains(event.target as Node)
) {
setContextData({ ...contextData, visible: false });
setMenuState({ ...menuState, visible: false });
}
};

Expand All @@ -87,7 +95,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ target, options }) => {
contextRef.current &&
!(contextRef.current as HTMLElement).contains(event.target as Node)
) {
setContextData({ ...contextData, visible: false });
setMenuState({ ...menuState, visible: false });
}
};

Expand All @@ -97,45 +105,45 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ target, options }) => {
document.removeEventListener('contextmenu', contextMenuEventHandler);
document.removeEventListener('click', offClickHandler);
};
}, [contextData, target]);
}, [menuState, target]);

useLayoutEffect(() => {
if (!contextRef?.current) return;
const element = contextRef.current as HTMLElement;

if (contextData.posX + element.offsetWidth > window.innerWidth) {
setContextData({
...contextData,
posX: contextData.posX - element.offsetWidth,
if (menuState.posX + element.offsetWidth > window.innerWidth) {
setMenuState({
...menuState,
posX: menuState.posX - element.offsetWidth,
});
}
if (contextData.posY + element.offsetHeight > window.innerHeight) {
setContextData({
...contextData,
posY: contextData.posY - element.offsetHeight,
if (menuState.posY + element.offsetHeight > window.innerHeight) {
setMenuState({
...menuState,
posY: menuState.posY - element.offsetHeight,
});
}
}, [contextData]);
}, [menuState]);

return (
<ContextMenuWrapper
ref={contextRef}
style={{
display: `${contextData.visible ? 'block' : 'none'}`,
left: contextData.posX,
top: contextData.posY,
display: `${menuState.visible ? 'block' : 'none'}`,
left: menuState.posX,
top: menuState.posY,
}}
>
{options().map((option, idx) => (
{options(menuState.contextData).map((option, idx) => (
<span key={idx}>
{option.separator && <hr />}
<Button
label={option.label}
icon={option.icon}
iconStyle={option.hlColor ? { color: option.hlColor } : {}}
onClick={() => {
setContextData({ ...contextData, visible: false });
if (option.onClick) option.onClick(contextData);
setMenuState({ ...menuState, visible: false });
if (option.onClick) option.onClick(menuState.contextData);
}}
/>
</span>
Expand Down
16 changes: 14 additions & 2 deletions frontend/src/components/table/BodyCell.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
const BodyCell = ({ rowData, column, cellFormatter }) => {

const BodyCell = ({ rowData, column, cellFormatter, onCellContextMenu }) => {
const handleContextMenu = (e) => {
if (onCellContextMenu) {
e.preventDefault();
onCellContextMenu({
column: column.name,
value: rowData[column.name],
rowData,
event: e,
});
}
};
if (cellFormatter) return cellFormatter(rowData, column.name);
return <td>{rowData[column.name]}</td>;
return <td onContextMenu={handleContextMenu}>{rowData[column.name]}</td>;
};
export default BodyCell;
10 changes: 10 additions & 0 deletions frontend/src/components/table/DataRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ const DataRow = ({
// Reder the row
//


// Handler to forward cell context menu events up to parent (Table)
const handleCellContextMenu = (cellInfo) => {
// Attach contextData to the native event and trigger contextmenu
if (cellInfo && cellInfo.event) {
cellInfo.event.contextData = { column: cellInfo.column, value: cellInfo.value, rowData, rowIndex: index };
}
};

const rowContent = useMemo(() => {
return (
<>
Expand All @@ -77,6 +86,7 @@ const DataRow = ({
column={column}
rowData={rowData}
cellFormatter={column.formatter}
onCellContextMenu={handleCellContextMenu}
/>
))}
</>
Expand Down
65 changes: 38 additions & 27 deletions frontend/src/containers/Browser/Browser.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,33 +237,44 @@ const BrowserTable = ({ isDragging }) => {
.catch(() => {});
};

const contextMenu = () => [
{
label: 'Send to...',
icon: 'send',
onClick: () => sendTo(),
},
{
label: 'Reset',
icon: 'undo',
onClick: () =>
setSelectionStatus(5, 'Do you want to reload selected assets metadata?'),
},
{
label: 'Archive',
separator: true,
icon: 'archive',
onClick: () =>
setSelectionStatus(4, 'Do you want to move selected assets to archive?'),
},
{
label: 'Trash',
icon: 'delete',
hlColor: 'var(--color-red)',
onClick: () =>
setSelectionStatus(3, 'Do you want to move selected assets to trash?'),
},
];
const contextMenu = (contextData) => {
const menu = [
{
label: 'Send to...',
icon: 'send',
onClick: () => sendTo(),
},
{
label: 'Reset',
icon: 'undo',
onClick: () =>
setSelectionStatus(5, 'Do you want to reload selected assets metadata?'),
},
{
label: 'Archive',
separator: true,
icon: 'archive',
onClick: () =>
setSelectionStatus(4, 'Do you want to move selected assets to archive?'),
},
{
label: 'Trash',
icon: 'delete',
hlColor: 'var(--color-red)',
onClick: () =>
setSelectionStatus(3, 'Do you want to move selected assets to trash?'),
},
];
if (contextData && contextData.column && contextData.value !== undefined) {
menu.unshift({
label: `Filter by ${contextData.column}`,
icon: 'filter_list',
separator: true,
onClick: () => alert(`filtering by ${contextData.value}`),
});
}
return menu;
};

const tableClass = clsx('contained', isDragging && 'no-scroll');

Expand Down