Skip to content

Commit

Permalink
feat(app-file-manager): add support for shift-selecting files (#3675)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pavel910 committed Nov 6, 2023
1 parent 29c295d commit 654c156
Show file tree
Hide file tree
Showing 22 changed files with 416 additions and 61 deletions.
4 changes: 3 additions & 1 deletion packages/app-admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@webiny/telemetry": "0.0.0",
"@webiny/ui": "0.0.0",
"@webiny/ui-composer": "0.0.0",
"@webiny/utils": "0.0.0",
"@webiny/validation": "0.0.0",
"apollo-cache": "^1.3.5",
"apollo-client": "^2.6.10",
Expand All @@ -43,9 +44,9 @@
"emotion": "^10.0.17",
"graphlib": "^2.1.7",
"graphql": "^15.7.2",
"is-hotkey": "^0.1.3",
"lodash": "^4.17.11",
"mobx": "^6.9.0",
"nanoid": "^3.1.23",
"prop-types": "^15.7.2",
"react": "17.0.2",
"react-dom": "17.0.2",
Expand All @@ -62,6 +63,7 @@
"@babel/preset-typescript": "^7.22.5",
"@types/bytes": "^3.1.1",
"@types/graphlib": "^2.1.8",
"@types/is-hotkey": "^0.1.7",
"@types/store": "^2.0.2",
"@webiny/cli": "0.0.0",
"@webiny/project-utils": "0.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { plugins } from "@webiny/plugins";
import { nanoid } from "nanoid";
import { generateId } from "@webiny/utils";
import { AddQuerySelectionPlugin } from "@webiny/app/plugins/AddQuerySelectionPlugin";
import { DocumentNode } from "graphql";

Expand All @@ -11,7 +11,7 @@ interface Props {
}

export const AddGraphQLQuerySelection: React.FC<Props> = props => {
const [name] = useState(`AddGraphQLQuerySelection:${props.operationName}:${nanoid()}`);
const [name] = useState(`AddGraphQLQuerySelection:${props.operationName}:${generateId()}`);

useEffect(() => {
const plugin = new AddQuerySelectionPlugin(props);
Expand Down
8 changes: 6 additions & 2 deletions packages/app-admin/src/base/ui/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import React, {
useState,
useContext
} from "react";
import { nanoid } from "nanoid";
import { generateId } from "@webiny/utils";
import { makeComposable, Plugins } from "@webiny/app";
import { MenuData, MenuProps, AddMenu as Menu, Tags, MenuUpdater, createEmptyMenu } from "~/index";
import { plugins } from "@webiny/plugins";
Expand Down Expand Up @@ -40,7 +40,11 @@ export function useNavigation() {
// scaffolded plugins in users' projects, as well as our own applications (Page Builder and Form Builder).
const LegacyMenu: React.FC<MenuProps | SectionProps | ItemProps> = props => {
return (
<Menu {...props} name={(props as MenuProps).name || nanoid()} label={props.label as string}>
<Menu
{...props}
name={(props as MenuProps).name || generateId()}
label={props.label as string}
>
{props.children}
</Menu>
);
Expand Down
3 changes: 3 additions & 0 deletions packages/app-admin/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export * from "./useConfirmationDialog";
export * from "./useDialog";
export * from "./useSnackbar";
export * from "./useKeyHandler";
export * from "./useShiftKey";
export * from "./useModKey";
91 changes: 91 additions & 0 deletions packages/app-admin/src/hooks/useKeyHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { SyntheticEvent } from "react";
import isHotkey from "is-hotkey";
import { generateId } from "@webiny/utils";

interface KeyHandler {
id: string;
handler: any;
}
const keyStack: Record<string, KeyHandler[]> = {};

let listener = false;
const filter = ["TEXTAREA", "INPUT"];

const isContentEditable = (value: any) => {
return ["true", true].includes(value);
};

const setupListener = (): void => {
if (listener || !document.body) {
return;
}

const eventListener = (ev: KeyboardEvent) => {
if (!ev.target) {
return;
}
const target = ev.target as HTMLElement;
// We ignore all keyboard events coming from within contentEditable element and inputs.
if (filter.includes(target.nodeName) || isContentEditable(target.contentEditable)) {
return;
}

const matchedKey = Object.keys(keyStack).find(key => isHotkey(key, ev));

if (matchedKey && keyStack[matchedKey].length > 0) {
const item = keyStack[matchedKey][0];
item.handler(ev);
ev.stopPropagation();
}
};

document.body.addEventListener("keydown", eventListener);

listener = true;
};

const addKeyHandler = (
id: string,
key: string,
handler: (e: SyntheticEvent<HTMLElement>) => void
) => {
setupListener();
keyStack[key] = keyStack[key] || [];
if (!keyStack[key].find(item => item.id === id)) {
keyStack[key].unshift({ id, handler });
}
};

const removeKeyHandler = (id: string, key: string): void => {
if (!keyStack[key]) {
return;
}

const index = keyStack[key].findIndex(item => item.id === id);
if (index >= 0) {
keyStack[key].splice(index, 1);
}
};

type AddKeyHandlerType = (key: string, handler: (e: SyntheticEvent<HTMLElement>) => void) => void;

type RemoveKeyHandlerType = (key: string) => void;

export function useKeyHandler(): {
addKeyHandler: AddKeyHandlerType;
removeKeyHandler: RemoveKeyHandlerType;
} {
const [id] = React.useState(generateId());

return React.useMemo(
() => ({
addKeyHandler(key, handler) {
addKeyHandler(id, key, handler);
},
removeKeyHandler(key) {
removeKeyHandler(id, key);
}
}),
[]
);
}
36 changes: 36 additions & 0 deletions packages/app-admin/src/hooks/useModKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useState, useEffect, useCallback } from "react";
import isHotkey from "is-hotkey";

export function useModKey() {
const [pressed, setPressed] = useState(false);

const onKeyDown = useCallback((event: KeyboardEvent) => {
if (isHotkey("Control", event) || isHotkey("Meta", event)) {
setPressed(true);
}
}, []);

const onKeyUp = useCallback((event: KeyboardEvent) => {
if (event.key === "Control" && event.ctrlKey === false) {
setPressed(false);
}

if (event.key === "Meta" && event.metaKey === false) {
setPressed(false);
}
}, []);

useEffect(() => {
document.addEventListener("keydown", onKeyDown);
document.addEventListener("keyup", onKeyUp);

return () => {
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("keyup", onKeyUp);
};
}, []);

console.log("pressed", pressed);

return pressed;
}
40 changes: 40 additions & 0 deletions packages/app-admin/src/hooks/useShiftKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useState, useEffect, useCallback, useRef } from "react";
import isHotkey from "is-hotkey";

export function useShiftKey() {
const [pressed, setPressed] = useState(false);
const onselectstartRef = useRef(document.onselectstart);

const onKeyDown = useCallback((event: KeyboardEvent) => {
if (isHotkey("shift", event)) {
setPressed(true);
}
}, []);

const onKeyUp = useCallback((event: KeyboardEvent) => {
if (event.key === "Shift" && event.shiftKey === false) {
setPressed(false);
}
}, []);

useEffect(() => {
// @ts-ignore
document.onselectstart = () => !pressed;

return () => {
document.onselectstart = onselectstartRef.current;
};
}, [pressed]);

useEffect(() => {
document.addEventListener("keydown", onKeyDown);
document.addEventListener("keyup", onKeyUp);

return () => {
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("keyup", onKeyUp);
};
}, []);

return pressed;
}
1 change: 1 addition & 0 deletions packages/app-admin/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
{ "path": "../react-router/tsconfig.build.json" },
{ "path": "../ui/tsconfig.build.json" },
{ "path": "../ui-composer/tsconfig.build.json" },
{ "path": "../utils/tsconfig.build.json" },
{ "path": "../validation/tsconfig.build.json" }
],
"compilerOptions": {
Expand Down
3 changes: 3 additions & 0 deletions packages/app-admin/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
{ "path": "../react-router" },
{ "path": "../ui" },
{ "path": "../ui-composer" },
{ "path": "../utils" },
{ "path": "../validation" }
],
"compilerOptions": {
Expand Down Expand Up @@ -44,6 +45,8 @@
"@webiny/ui": ["../ui/src"],
"@webiny/ui-composer/*": ["../ui-composer/src/*"],
"@webiny/ui-composer": ["../ui-composer/src"],
"@webiny/utils/*": ["../utils/src/*"],
"@webiny/utils": ["../utils/src"],
"@webiny/validation/*": ["../validation/src/*"],
"@webiny/validation": ["../validation/src"]
},
Expand Down
10 changes: 8 additions & 2 deletions packages/app-file-manager/src/components/Grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface GridProps {
selected: FileItem[];
multiple?: boolean;
toggleSelected: (file: FileItem) => void;
deselectAll: () => void;
onChange?: Function;
onClose?: Function;
hasOnSelectCallback: boolean;
Expand All @@ -35,6 +36,7 @@ export const Grid: React.FC<GridProps> = ({
onChange,
onClose,
toggleSelected,
deselectAll,
multiple,
hasOnSelectCallback
}) => {
Expand All @@ -47,7 +49,11 @@ export const Grid: React.FC<GridProps> = ({
return undefined;
}

return (record: FileItem) => () => {
return (record: FileItem) => (event?: React.MouseEvent) => {
if (event) {
event.stopPropagation();
}

if (!hasOnSelectCallback || multiple) {
toggleSelected(record);
return;
Expand All @@ -63,7 +69,7 @@ export const Grid: React.FC<GridProps> = ({
<FolderList>
<FolderGrid folders={folders} onFolderClick={onFolderClick} />
</FolderList>
<FileList>
<FileList onClick={deselectAll}>
{records.map(record => (
<FileProvider file={record} key={record.id}>
<FileThumbnail
Expand Down
3 changes: 3 additions & 0 deletions packages/app-file-manager/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface TableProps {
onRecordClick: (id: string) => void;
onFolderClick: (id: string) => void;
onSelectRow: ((rows: Entry[] | []) => void) | undefined;
onToggleRow: ((row: Entry) => void) | undefined;
sorting: Sorting;
onSortingChange: OnSortingChange;
settings?: Settings;
Expand Down Expand Up @@ -107,6 +108,7 @@ export const Table = forwardRef<HTMLDivElement, TableProps>((props, ref) => {
records,
selectedRecords,
onSelectRow,
onToggleRow,
loading,
onRecordClick,
onFolderClick,
Expand Down Expand Up @@ -247,6 +249,7 @@ export const Table = forwardRef<HTMLDivElement, TableProps>((props, ref) => {
loadingInitial={loading}
stickyRows={1}
onSelectRow={onSelectRow}
onToggleRow={onToggleRow}
isRowSelectable={row => row.original.$selectable}
sorting={sorting}
initialSorting={[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,21 @@ const FileManagerView = () => {
view.setSelected(files);
};

const onToggleRow: TableProps["onToggleRow"] = view.hasOnSelectCallback
? row => {
const files = getSelectableRow([row]);

if (view.multiple) {
view.toggleSelected(files[0]);
} else {
view.onChange(files[0]);
}
}
: row => {
const files = getSelectableRow([row]);
view.toggleSelected(files[0]);
};

return (
<Table
folders={view.folders}
Expand All @@ -217,6 +232,7 @@ const FileManagerView = () => {
onRecordClick={view.showFileDetails}
onFolderClick={view.setFolderId}
onSelectRow={onSelectRow}
onToggleRow={onToggleRow}
sorting={tableSorting}
onSortingChange={setTableSorting}
settings={view.settings}
Expand All @@ -234,6 +250,7 @@ const FileManagerView = () => {
selected={view.selected}
multiple={view.multiple}
toggleSelected={view.toggleSelected}
deselectAll={view.deselectAll}
onChange={view.onChange}
onClose={view.onClose}
hasOnSelectCallback={view.hasOnSelectCallback}
Expand Down Expand Up @@ -264,7 +281,6 @@ const FileManagerView = () => {
uploadFiles(filesToUpload);
}}
onError={errors => {
console.log("onError", errors);
const message = outputFileSelectionError(errors);
showSnackbar(message);
}}
Expand Down Expand Up @@ -324,7 +340,7 @@ const FileManagerView = () => {
{...getDropZoneProps({
onDragOver: () => view.setDragging(true),
onDragLeave: () => view.setDragging(false),
onDrop
onDrop: () => view.setDragging(false)
})}
data-testid={"fm-list-wrapper"}
>
Expand Down
Loading

0 comments on commit 654c156

Please sign in to comment.