Can attach file to movement
This commit is contained in:
parent
ee145dab4f
commit
211c81dd66
@ -42,6 +42,7 @@ pub struct ServerConstraints {
|
||||
pub token_max_inactivity: LenConstraints,
|
||||
pub account_name: LenConstraints,
|
||||
pub movement_label: LenConstraints,
|
||||
pub file_allowed_types: &'static [&'static str],
|
||||
}
|
||||
|
||||
impl Default for ServerConstraints {
|
||||
@ -52,6 +53,13 @@ impl Default for ServerConstraints {
|
||||
token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365),
|
||||
account_name: LenConstraints::not_empty(50),
|
||||
movement_label: LenConstraints::not_empty(200),
|
||||
file_allowed_types: &[
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/gif",
|
||||
"application/pdf",
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
28
moneymgr_web/src/api/FileApi.ts
Normal file
28
moneymgr_web/src/api/FileApi.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { APIClient } from "./ApiClient";
|
||||
|
||||
export interface UploadedFile {
|
||||
id: number;
|
||||
time_create: number;
|
||||
mime_type: string;
|
||||
sha512: string;
|
||||
file_size: number;
|
||||
file_name: string;
|
||||
user_id: number;
|
||||
}
|
||||
|
||||
export class FileApi {
|
||||
/**
|
||||
* Upload a new file
|
||||
*/
|
||||
static async UploadFile(file: File): Promise<UploadedFile> {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
return (
|
||||
await APIClient.exec({
|
||||
method: "POST",
|
||||
uri: "/file",
|
||||
formData: fd,
|
||||
})
|
||||
).data;
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ export interface ServerConstraints {
|
||||
token_max_inactivity: LenConstraint;
|
||||
account_name: LenConstraint;
|
||||
movement_label: LenConstraint;
|
||||
file_allowed_types: string[];
|
||||
}
|
||||
|
||||
export interface LenConstraint {
|
||||
|
@ -23,6 +23,8 @@ import { DateWidget } from "../widgets/DateWidget";
|
||||
import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer";
|
||||
import { NewMovementWidget } from "../widgets/NewMovementWidget";
|
||||
import { NotFoundRoute } from "./NotFound";
|
||||
import { UploadFileButton } from "../widgets/forms/UploadFileButton";
|
||||
import { UploadedFile } from "../api/FileApi";
|
||||
|
||||
export function AccountRoute(): React.ReactElement {
|
||||
const loadingMessage = useLoadingMessage();
|
||||
@ -105,6 +107,21 @@ function MovementsTable(p: {
|
||||
const [rowSelectionModel, setRowSelectionModel] =
|
||||
React.useState<GridRowSelectionModel>([]);
|
||||
|
||||
// Set uploaded file
|
||||
const setUploadedFile = async (
|
||||
m: Movement,
|
||||
file: UploadedFile | undefined
|
||||
) => {
|
||||
try {
|
||||
await MovementApi.Update({ ...m, file_id: file?.id ?? undefined });
|
||||
|
||||
p.needReload(false);
|
||||
} catch (e) {
|
||||
console.error("Failed to attach file to movement!", e);
|
||||
alert("Failed to attach uploaded file to movement!");
|
||||
}
|
||||
};
|
||||
|
||||
// Change account of movement
|
||||
const handleMoveClick = async (movement: Movement) => {
|
||||
const targetAccount = await chooseAccount(
|
||||
@ -283,7 +300,19 @@ function MovementsTable(p: {
|
||||
{
|
||||
field: "file",
|
||||
headerName: "File",
|
||||
// TODO
|
||||
editable: false,
|
||||
width: 150,
|
||||
renderCell: (params) => {
|
||||
if (!params.row.file_id)
|
||||
return (
|
||||
<UploadFileButton
|
||||
label="Attach file"
|
||||
tooltip="Attach a file to this movement"
|
||||
onUploaded={(f) => setUploadedFile(params.row, f)}
|
||||
/>
|
||||
);
|
||||
else return <>got file</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "actions",
|
||||
|
97
moneymgr_web/src/widgets/forms/UploadFileButton.tsx
Normal file
97
moneymgr_web/src/widgets/forms/UploadFileButton.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { Button, IconButton, Tooltip, Typography } from "@mui/material";
|
||||
import UploadFileIcon from "@mui/icons-material/UploadFile";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import React from "react";
|
||||
import { FileApi, UploadedFile } from "../../api/FileApi";
|
||||
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
||||
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
|
||||
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
|
||||
|
||||
// https://medium.com/@dprincecoder/creating-a-drag-and-drop-file-upload-component-in-react-a-step-by-step-guide-4d93b6cc21e0
|
||||
|
||||
export function UploadFileButton(p: {
|
||||
onUploaded: (file: UploadedFile) => void;
|
||||
label: string;
|
||||
tooltip: string;
|
||||
}): React.ReactElement {
|
||||
const alert = useAlert();
|
||||
const loadingMessage = useLoadingMessage();
|
||||
const snackbar = useSnackbar();
|
||||
|
||||
const fileInput = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const [dragActive, setDragActive] = React.useState(false);
|
||||
|
||||
const forceFileSelect = () => fileInput.current?.click();
|
||||
|
||||
const dragEnter = () => {
|
||||
setDragActive(true);
|
||||
};
|
||||
const dragLeave = () => {
|
||||
setDragActive(false);
|
||||
};
|
||||
|
||||
const handleUpload = async (file: File[]) => {
|
||||
if (file.length < 1) return;
|
||||
|
||||
try {
|
||||
loadingMessage.show("Uploading file...");
|
||||
|
||||
const result = await FileApi.UploadFile(file[0]);
|
||||
|
||||
snackbar("The file was successfully uploaded!");
|
||||
|
||||
p.onUploaded(result);
|
||||
} catch (e) {
|
||||
console.error("Failed to upload file!", e);
|
||||
alert(`Failed to upload file! ${e}`);
|
||||
} finally {
|
||||
loadingMessage.hide();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (ev: React.DragEvent) => {
|
||||
ev.preventDefault();
|
||||
handleUpload([...ev.dataTransfer.files]);
|
||||
};
|
||||
|
||||
const handlefileChange = (ev: React.ChangeEvent) => {
|
||||
ev.preventDefault();
|
||||
if ((fileInput.current?.files?.length ?? 0) > 0) {
|
||||
handleUpload([...fileInput.current?.files!]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={p.tooltip}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
onDragEnter={dragEnter}
|
||||
onDragLeave={dragLeave}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<UploadFileIcon fontSize="small" />}
|
||||
variant={dragActive ? "outlined" : "text"}
|
||||
onClick={forceFileSelect}
|
||||
>
|
||||
<input
|
||||
ref={fileInput}
|
||||
type="file"
|
||||
accept={ServerApi.Config.constraints.file_allowed_types.join(",")}
|
||||
style={{
|
||||
border: "0",
|
||||
height: "1px",
|
||||
width: "1px",
|
||||
padding: "0px",
|
||||
position: "absolute",
|
||||
clipPath: "inset(50%)",
|
||||
}}
|
||||
onChange={handlefileChange}
|
||||
/>
|
||||
<Typography variant="caption">{p.label}</Typography>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user