Skip to content

Commit

Permalink
Merge pull request #924 from betagouv/feat/file-epxlorer
Browse files Browse the repository at this point in the history
Browsable file table
  • Loading branch information
wiwski authored May 1, 2024
2 parents 32c101b + 814f4f9 commit 06b848b
Show file tree
Hide file tree
Showing 19 changed files with 421 additions and 324 deletions.
39 changes: 39 additions & 0 deletions lab/assets/js/components/BaseDirectoryActionCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { css } from "@emotion/react";
import { useContext } from "react";
import { FileContext } from "./FileContext";
import { fileTableRowActionCellBtnStyle } from "./style";

interface BaseTableActionCellProps {
onOpen: (name: string) => void;
}

const cellStyle = css({
width: "150px",
});

export default function BaseTableActionCell({
onOpen,
}: BaseTableActionCellProps) {
const t = {
Open: window.gettext("Open"),
};
const file = useContext(FileContext);

return (
<td css={cellStyle}>
{file && (
<ul className="fr-btns-group fr-btns-group--inline fr-btns-group--sm">
<li>
<button
className="fr-btn fr-icon-arrow-right-line fr-btn--secondary"
css={fileTableRowActionCellBtnStyle}
onClick={() => onOpen(file.name)}
>
{t["Open"]}
</button>
</li>
</ul>
)}
</td>
);
}
93 changes: 93 additions & 0 deletions lab/assets/js/components/BaseFileActionCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { displayMessage } from "../utils";
import { FileService } from "../file-service";
import { css } from "@emotion/react";
import { useContext } from "react";
import { FileContext } from "./FileContext";
import { fileTableRowActionCellBtnStyle } from "./style";

interface BaseFileActionCellProps {
fileService: FileService;
canDelete: boolean;
onDeleteSuccess?: (fileName: string) => void;
setIsLoading?: (isLoading: boolean) => void;
}

const cellStyle = css({
width: "150px",
});

export default function BaseFileActionCell({
fileService,
canDelete,
onDeleteSuccess,
setIsLoading,
}: BaseFileActionCellProps) {
const t = {
"Delete the document %s ?": window.gettext("Delete the document %s ?"),
"File %s could not be removed.": window.gettext(
"File %s could not be removed.",
),
"File %s has been removed.": window.gettext("File %s has been removed."),
"Download file": window.gettext("Download file"),
"Delete file": window.gettext("Delete file"),
};
const file = useContext(FileContext);

const downloadFile = async (path: string) => {
const url = await fileService.fetchPresignedURL(path);
window.open(url, "_blank");
};

const deleteFile = async (name: string, path: string) => {
if (
!window.confirm(window.interpolate(t["Delete the document %s ?"], [name]))
) {
return;
}
setIsLoading && setIsLoading(true);
try {
await fileService.deleteFile(path);
} catch (error) {
displayMessage(
window.interpolate(t["File %s could not be removed."], [name]),
"error",
);
setIsLoading && setIsLoading(false);
}
if (onDeleteSuccess) onDeleteSuccess(name);
setIsLoading && setIsLoading(false);
displayMessage(
window.interpolate(t["File %s has been removed."], [name]),
"success",
);
};

return (
<td css={cellStyle}>
{file && (
<ul className="fr-btns-group fr-btns-group--inline fr-btns-group--sm">
<li>
<button
className="fr-btn fr-icon-download-line fr-btn--secondary"
css={fileTableRowActionCellBtnStyle}
onClick={() => downloadFile(file.path)}
>
{t["Download file"]}
</button>
</li>
{canDelete && (
<li>
<button
className="fr-btn fr-icon-delete-line fr-btn--secondary"
css={fileTableRowActionCellBtnStyle}
onClick={() => deleteFile(file.name, file.path)}
>
{t["Delete file"]}
</button>
</li>
)}
</ul>
)}
</td>
);
}
86 changes: 17 additions & 69 deletions lab/assets/js/components/BaseTableActionCell.tsx
Original file line number Diff line number Diff line change
@@ -1,90 +1,38 @@
import { displayMessage } from "../utils";
import { FileService } from "../file-service";
import { css } from "@emotion/react";
import { useContext } from "react";
import { FileContext } from "./FileContext";
import BaseFileActionCell from "./BaseFileActionCell";
import BaseDirectoryActionCell from "./BaseDirectoryActionCell";

interface BaseTableActionCellProps {
fileService: FileService;
canDelete: boolean;
onDeleteSuccess?: (fileName: string) => void;
setIsLoading?: (isLoading: boolean) => void;
onFolderOpen: (name: string) => void;
}

const cellStyle = css({
width: "150px",
});

export default function BaseTableActionCell({
fileService,
canDelete,
onDeleteSuccess,
setIsLoading,
onFolderOpen,
}: BaseTableActionCellProps) {
const t = {
"Delete the document %s ?": window.gettext("Delete the document %s ?"),
"File %s could not be removed.": window.gettext(
"File %s could not be removed.",
),
"File %s has been removed.": window.gettext("File %s has been removed."),
"Download file": window.gettext("Download file"),
"Delete file": window.gettext("Delete file"),
};
const file = useContext(FileContext);

const downloadFile = async (path: string) => {
const url = await fileService.fetchPresignedURL(path);
window.open(url, "_blank");
};

const deleteFile = async (name: string, path: string) => {
if (
!window.confirm(window.interpolate(t["Delete the document %s ?"], [name]))
) {
return;
}
setIsLoading && setIsLoading(true);
try {
await fileService.deleteFile(path);
} catch (error) {
displayMessage(
window.interpolate(t["File %s could not be removed."], [name]),
"error",
);
setIsLoading && setIsLoading(false);
}
if (onDeleteSuccess) onDeleteSuccess(name);
setIsLoading && setIsLoading(false);
displayMessage(
window.interpolate(t["File %s has been removed."], [name]),
"success",
);
};

return (
<td css={cellStyle}>
{file && (
<ul className="fr-btns-group fr-btns-group--inline fr-btns-group--sm">
<li>
<button
className="download-btn fr-btn fr-icon-download-line fr-btn--secondary"
onClick={() => downloadFile(file.path)}
>
{t["Download file"]}
</button>
</li>
{canDelete && (
<li>
<button
className="delete-btn fr-btn fr-icon-delete-line fr-btn--secondary"
onClick={() => deleteFile(file.name, file.path)}
>
{t["Delete file"]}
</button>
</li>
)}
</ul>
)}
</td>
<>
{file &&
(file.isDir ? (
<BaseDirectoryActionCell onOpen={onFolderOpen} />
) : (
<BaseFileActionCell
fileService={fileService}
canDelete={canDelete}
onDeleteSuccess={onDeleteSuccess}
setIsLoading={setIsLoading}
/>
))}
</>
);
}
47 changes: 38 additions & 9 deletions lab/assets/js/components/FileTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,34 @@ import { useState } from "react";
import { FileProvider } from "./FileContext";
import { EuphrosyneFile } from "../file-service";
import { css } from "@emotion/react";
import { loadingDivStyle } from "./style";

export interface Col<T> {
label: string;
key: keyof T;
formatter?: (value: string) => string;
formatter?: (value: string | null) => string;
}

interface FileTableProps {
rows: EuphrosyneFile[];
cols: Col<EuphrosyneFile>[];
folder?: string[];
isLoading?: boolean;
isSearchable?: boolean;
actionCell?: React.ReactElement<"td">;
onPreviousFolderClick?: () => void;
}

const COLLAPSED_ROW_NUM = 25;

const fileTableStyle = css({
width: "100%",
});

const cellStyle = css({
verticalAlign: "middle",
});

const noDataCellStyle = css({
"&&": {
textAlign: "center",
Expand All @@ -32,9 +43,11 @@ const theadCellStyle = css({
export default function FileTable({
cols,
rows,
folder,
isLoading,
isSearchable,
actionCell,
onPreviousFolderClick,
}: FileTableProps) {
const t = {
Filter: window.gettext("Filter"),
Expand Down Expand Up @@ -92,7 +105,23 @@ export default function FileTable({
</p>
</div>
)}
<table className="file-table fr-table">
{folder && folder.length > 0 && (
<div
css={css({
display: "flex",
justifyItems: "center",
alignItems: "center",
})}
className="fr-mb-1w"
>
<button
className="fr-btn fr-icon-arrow-left-line fr-btn--sm fr-btn--secondary fr-mr-1w"
onClick={onPreviousFolderClick}
/>
<div>/{folder.join("/")}</div>
</div>
)}
<table className="fr-table" css={fileTableStyle}>
<thead>
<tr>
{cols.map(({ label }) => (
Expand All @@ -111,8 +140,8 @@ export default function FileTable({
{isLoading ? (
<tr className="loading">
{[...Array(numCols)].map((_, i) => (
<td key={`loading-cell-${i}`}>
<div>&nbsp;</div>
<td key={`loading-cell-${i}`} css={cellStyle}>
<div css={loadingDivStyle}>&nbsp;</div>
</td>
))}
</tr>
Expand All @@ -123,9 +152,9 @@ export default function FileTable({
{displayedRows.map((row, i) => (
<tr key={`file-table-row-${i}`}>
{cols.map((col) => (
<td key={`file-table-cell-${col.key}`}>
<td key={`file-table-cell-${col.key}`} css={cellStyle}>
{col.formatter
? col.formatter(row[col.key].toString())
? col.formatter((row[col.key] || "").toString())
: String(row[col.key])}
</td>
))}
Expand All @@ -137,7 +166,7 @@ export default function FileTable({
</>
) : (
<tr className="no_data">
<td colSpan={numCols} css={noDataCellStyle}>
<td colSpan={numCols} css={[cellStyle, noDataCellStyle]}>
{t["No file yet"]}
</td>
</tr>
Expand All @@ -148,7 +177,7 @@ export default function FileTable({
{filteredRows.length > COLLAPSED_ROW_NUM && (
<tfoot>
<tr>
<td colSpan={numCols} style={{ width: "100%" }}>
<td colSpan={numCols} style={{ width: "100%" }} css={cellStyle}>
<button
className={`fr-btn fr-btn--tertiary-no-outline fr-btn--sm fr-btn--icon-left ${
isExpanded
Expand All @@ -162,7 +191,7 @@ export default function FileTable({
{isExpanded
? t["Show less"]
: t["Show more"] +
` (${filteredRows.length - COLLAPSED_ROW_NUM})`}
` (${isLoading ? 0 : filteredRows.length - COLLAPSED_ROW_NUM})`}
</button>
</td>
</tr>
Expand Down
3 changes: 2 additions & 1 deletion lab/assets/js/components/FileTableCols.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const workplaceTableCols: Col<EuphrosyneFile>[] = [
{
label: window.gettext("Size"),
key: "size",
formatter: (value: string) => formatBytes(parseInt(value)),
formatter: (value: string | null) =>
value ? formatBytes(parseInt(value)) : "",
},
];
Loading

0 comments on commit 06b848b

Please sign in to comment.