Skip to content

Commit

Permalink
feat(app-file-manager): add bulk actions (#3608)
Browse files Browse the repository at this point in the history
  • Loading branch information
leopuleo committed Oct 25, 2023
1 parent 6d1492b commit ce35610
Show file tree
Hide file tree
Showing 19 changed files with 380 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Icon } from "@webiny/ui/Icon";
import { List, ListItemText, ListItemTextPrimary, ListItemTextSecondary } from "@webiny/ui/List";
import { ShowResultsDialogParams } from "./index";

import { ListItem, ListItemGraphic, MessageContainer } from "./useDialogWithReport.styles";
import { ListItem, ListItemGraphic, MessageContainer } from "./useDialogWithReport.styled";

type ResultDialogMessageProps = Pick<ShowResultsDialogParams, "results" | "message">;

Expand Down
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: "Finished deleting files! See full report below:"
});
}
});

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: "Finished moving files! See full report below:"
});
}
});
},
[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

0 comments on commit ce35610

Please sign in to comment.