Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add bulk actions to file manager #3608

Merged
merged 16 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from 14 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 @@ -201,7 +201,7 @@ Fix the width of input components when inside grids

// dialog
.mdc-dialog {
z-index: 20;
z-index: 22;
.mdc-dialog__container {
width: 100%;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { useMemo } from "react";
import { ReactComponent as DeleteIcon } from "@material-design-icons/svg/outlined/delete.svg";
import { observer } from "mobx-react-lite";

import { FileManagerViewConfig } from "~/modules/FileManagerRenderer/FileManagerView/FileManagerViewConfig";
import { useFileManagerApi } from "~/modules/FileManagerApiProvider/FileManagerApiContext";
import { useFileManagerView } from "~/modules/FileManagerRenderer/FileManagerViewProvider";
import { getFilesLabel } from "~/components/BulkActions/BulkActions";

export const ActionDelete = observer(() => {
const { deleteFile } = useFileManagerView();
const { canDelete } = useFileManagerApi();

const { useWorker, useButtons, useDialog } = FileManagerViewConfig.Browser.BulkAction;
const { IconButton } = useButtons();
const worker = useWorker();
const { showConfirmationDialog, showResultsDialog } = useDialog();

const filesLabel = useMemo(() => {
return getFilesLabel(worker.items.length);
}, [worker.items.length]);

const canDeleteAll = useMemo(() => {
return worker.items.every(item => canDelete(item));
}, [worker.items]);

const openDeleteDialog = () =>
showConfirmationDialog({
title: "Delete files",
message: `You are about to delete ${filesLabel}. Are you sure you want to continue?`,
loadingLabel: `Processing ${filesLabel}`,
execute: async () => {
await worker.processInSeries(async ({ item, report }) => {
try {
await deleteFile(item.id);

report.success({
title: `${item.name}`,
message: "File successfully deleted."
});
} catch (e) {
report.error({
title: `${item.name}`,
message: e.message
});
}
});

worker.resetItems();

showResultsDialog({
results: worker.results,
title: "Delete files",
message: "Operation completed, here below you find the complete report:"
});
}
});

if (!canDeleteAll) {
console.log("You don't have permissions to delete files.");
return null;
}

return (
<IconButton
icon={<DeleteIcon />}
onAction={openDeleteDialog}
label={`Delete ${filesLabel}`}
tooltipPlacement={"bottom"}
/>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { useCallback, useMemo } from "react";
import { ReactComponent as MoveIcon } from "@material-design-icons/svg/outlined/drive_file_move.svg";
import { observer } from "mobx-react-lite";
import { useMoveToFolderDialog, useNavigateFolder } from "@webiny/app-aco";
import { FolderItem } from "@webiny/app-aco/types";

import { useFileManagerView } from "~/modules/FileManagerRenderer/FileManagerViewProvider";
import { FileManagerViewConfig } from "~/modules/FileManagerRenderer/FileManagerView/FileManagerViewConfig";
import { getFilesLabel } from "~/components/BulkActions/BulkActions";
import { ROOT_FOLDER } from "~/constants";

export const ActionMove = observer(() => {
const { moveFileToFolder } = useFileManagerView();
const { currentFolderId } = useNavigateFolder();

const { useWorker, useButtons, useDialog } = FileManagerViewConfig.Browser.BulkAction;
const { IconButton } = useButtons();
const worker = useWorker();
const { showConfirmationDialog, showResultsDialog } = useDialog();
const { showDialog: showMoveDialog } = useMoveToFolderDialog();

const filesLabel = useMemo(() => {
return getFilesLabel(worker.items.length);
}, [worker.items.length]);

const openWorkerDialog = useCallback(
(folder: FolderItem) => {
showConfirmationDialog({
title: "Move files",
message: `You are about to move ${filesLabel} to ${folder.title}. Are you sure you want to continue?`,
loadingLabel: `Processing ${filesLabel}`,
execute: async () => {
await worker.processInSeries(async ({ item, report }) => {
try {
await moveFileToFolder(item.id, folder.id);

report.success({
title: `${item.name}`,
message: "File successfully moved."
});
} catch (e) {
report.error({
title: `${item.name}`,
message: e.message
});
}
});

worker.resetItems();

showResultsDialog({
results: worker.results,
title: "Move files",
message: "Operation completed, here below you find the complete report:"
});
}
});
},
[filesLabel]
);

const openMoveDialog = () =>
showMoveDialog({
title: "Select folder",
message: "Select a new location for selected files:",
loadingLabel: `Processing ${filesLabel}`,
acceptLabel: `Move`,
focusedFolderId: currentFolderId || ROOT_FOLDER,
async onAccept({ folder }) {
openWorkerDialog(folder);
}
});

return (
<IconButton
icon={<MoveIcon />}
onAction={openMoveDialog}
label={`Move ${filesLabel}`}
tooltipPlacement={"bottom"}
/>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import styled from "@emotion/styled";
import { ButtonContainer } from "@webiny/app-admin";

export const BulkActionsContainer = styled.div`
width: 100%;
height: 64px;
background-color: var(--mdc-theme-surface);
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
`;

export const BulkActionsInner = styled.div`
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px 0 16px;
`;

export const ButtonsContainer = styled.div`
display: flex;
align-items: center;

${ButtonContainer} {
margin: 0;
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { useMemo } from "react";
import { ReactComponent as Close } from "@material-design-icons/svg/outlined/close.svg";
import { i18n } from "@webiny/app/i18n";
import { Buttons } from "@webiny/app-admin";
import { IconButton } from "@webiny/ui/Button";
import { Typography } from "@webiny/ui/Typography";

import { useFileManagerViewConfig } from "~/modules/FileManagerRenderer/FileManagerView/FileManagerViewConfig";
import { useFileManagerView } from "~/modules/FileManagerRenderer/FileManagerViewProvider";

import { BulkActionsContainer, BulkActionsInner, ButtonsContainer } from "./BulkActions.styled";

const t = i18n.ns("app-file-manager/components/bulk-actions");

export const getFilesLabel = (count = 0): string => {
return `${count} ${count === 1 ? "file" : "files"}`;
};

export const BulkActions = () => {
const { browser } = useFileManagerViewConfig();
const view = useFileManagerView();

const headline = useMemo((): string => {
return t`{label} selected:`({
label: getFilesLabel(view.selected.length)
});
}, [view.selected]);

if (view.hasOnSelectCallback || !view.selected.length) {
return null;
}

return (
<BulkActionsContainer>
<BulkActionsInner>
<ButtonsContainer>
<Typography use={"headline6"}>{headline}</Typography>
<Buttons actions={browser.bulkActions} />
</ButtonsContainer>
<IconButton icon={<Close />} onClick={() => view.setSelected([])} />
</BulkActionsInner>
</BulkActionsContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { ActionDelete } from "./ActionDelete";
export { ActionMove } from "./ActionMove";
export * from "./BulkActions";
6 changes: 4 additions & 2 deletions packages/app-file-manager/src/components/Grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface GridProps {
toggleSelected: (file: FileItem) => void;
onChange?: Function;
onClose?: Function;
hasOnSelectCallback: boolean;
}

export const Grid: React.FC<GridProps> = ({
Expand All @@ -34,7 +35,8 @@ export const Grid: React.FC<GridProps> = ({
onChange,
onClose,
toggleSelected,
multiple
multiple,
hasOnSelectCallback
}) => {
if (loading) {
return <CircularProgress label={t`Loading Files...`} style={{ opacity: 1 }} />;
Expand All @@ -46,7 +48,7 @@ export const Grid: React.FC<GridProps> = ({
}

return (record: FileItem) => () => {
if (multiple) {
if (!hasOnSelectCallback || multiple) {
toggleSelected(record);
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react";

import { ReactComponent as GridIcon } from "@material-design-icons/svg/outlined/view_module.svg";
import { ReactComponent as TableIcon } from "@material-design-icons/svg/outlined/view_list.svg";
import { i18n } from "@webiny/app/i18n";
import { IconButton } from "@webiny/ui/Button";
import { Tooltip } from "@webiny/ui/Tooltip";

import { useFileManagerView } from "~/modules/FileManagerRenderer/FileManagerViewProvider";

const t = i18n.ns("app-file-manager/components/layout-switch");

export const LayoutSwitch = () => {
const view = useFileManagerView();

return (
<Tooltip
content={t`{mode} layout`({
mode: view.listTable ? "Grid" : "Table"
})}
placement={"bottom"}
>
<IconButton
icon={view.listTable ? <GridIcon /> : <TableIcon />}
onClick={() => view.setListTable(!view.listTable)}
>
{t`Switch`}
</IconButton>
</Tooltip>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./LayoutSwitch";
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ interface RecordActionDeleteProps {
}

export const RecordActionDelete: React.VFC<RecordActionDeleteProps> = ({ record }) => {
const { canEdit } = useFileManagerApi();
const { canDelete } = useFileManagerApi();
const { openDialogDeleteFile } = useDeleteFile({
file: record
});

if (!canEdit(record)) {
if (!canDelete(record)) {
return null;
}

Expand Down
18 changes: 6 additions & 12 deletions packages/app-file-manager/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ export interface TableProps {
sorting: Sorting;
onSortingChange: OnSortingChange;
settings?: Settings;
selectableItems: boolean;
canSelectAllRows: boolean;
}

interface BaseEntry {
Expand All @@ -62,18 +60,17 @@ interface FileEntry extends BaseEntry {

interface FolderEntry extends BaseEntry {
$type: "FOLDER";
$selectable: boolean;
title: string;
original: FolderItem;
}

type Entry = FolderEntry | FileEntry;
export type Entry = FolderEntry | FileEntry;

const createRecordsData = (items: FileItem[], selectable: boolean): FileEntry[] => {
const createRecordsData = (items: FileItem[]): FileEntry[] => {
return items.map(data => {
return {
$type: "RECORD",
$selectable: selectable,
$selectable: true, // Files a.k.a. records are always selectable to perform bulk actions
id: data.id,
name: data.name,
createdBy: data.createdBy?.displayName || "-",
Expand Down Expand Up @@ -114,9 +111,7 @@ export const Table = forwardRef<HTMLDivElement, TableProps>((props, ref) => {
onRecordClick,
onFolderClick,
sorting,
onSortingChange,
selectableItems,
canSelectAllRows
onSortingChange
} = props;

const [selectedFolder, setSelectedFolder] = useState<FolderItem>();
Expand All @@ -125,7 +120,7 @@ export const Table = forwardRef<HTMLDivElement, TableProps>((props, ref) => {
const [managePermissionsDialogOpen, setManagePermissionsDialogOpen] = useState<boolean>(false);

const data = useMemo<Entry[]>(() => {
return [...createFoldersData(folders), ...createRecordsData(records, selectableItems)];
return [...createFoldersData(folders), ...createRecordsData(records)];
}, [folders, records]);

const columns: Columns<Entry> = useMemo(() => {
Expand Down Expand Up @@ -252,7 +247,6 @@ export const Table = forwardRef<HTMLDivElement, TableProps>((props, ref) => {
return (
<div ref={ref}>
<DataTable<Entry>
canSelectAllRows={canSelectAllRows}
columns={columns}
data={data}
loadingInitial={loading}
Expand All @@ -267,7 +261,7 @@ export const Table = forwardRef<HTMLDivElement, TableProps>((props, ref) => {
}
]}
onSortingChange={onSortingChange}
selectedRows={createRecordsData(selectedRecords, true)}
selectedRows={createRecordsData(selectedRecords)}
/>
{selectedFolder && (
<>
Expand Down
Loading
Loading