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 token_max_inactivity: LenConstraints,
|
||||||
pub account_name: LenConstraints,
|
pub account_name: LenConstraints,
|
||||||
pub movement_label: LenConstraints,
|
pub movement_label: LenConstraints,
|
||||||
|
pub file_allowed_types: &'static [&'static str],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ServerConstraints {
|
impl Default for ServerConstraints {
|
||||||
@ -52,6 +53,13 @@ impl Default for ServerConstraints {
|
|||||||
token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365),
|
token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365),
|
||||||
account_name: LenConstraints::not_empty(50),
|
account_name: LenConstraints::not_empty(50),
|
||||||
movement_label: LenConstraints::not_empty(200),
|
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;
|
token_max_inactivity: LenConstraint;
|
||||||
account_name: LenConstraint;
|
account_name: LenConstraint;
|
||||||
movement_label: LenConstraint;
|
movement_label: LenConstraint;
|
||||||
|
file_allowed_types: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LenConstraint {
|
export interface LenConstraint {
|
||||||
|
@ -23,6 +23,8 @@ import { DateWidget } from "../widgets/DateWidget";
|
|||||||
import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer";
|
import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer";
|
||||||
import { NewMovementWidget } from "../widgets/NewMovementWidget";
|
import { NewMovementWidget } from "../widgets/NewMovementWidget";
|
||||||
import { NotFoundRoute } from "./NotFound";
|
import { NotFoundRoute } from "./NotFound";
|
||||||
|
import { UploadFileButton } from "../widgets/forms/UploadFileButton";
|
||||||
|
import { UploadedFile } from "../api/FileApi";
|
||||||
|
|
||||||
export function AccountRoute(): React.ReactElement {
|
export function AccountRoute(): React.ReactElement {
|
||||||
const loadingMessage = useLoadingMessage();
|
const loadingMessage = useLoadingMessage();
|
||||||
@ -105,6 +107,21 @@ function MovementsTable(p: {
|
|||||||
const [rowSelectionModel, setRowSelectionModel] =
|
const [rowSelectionModel, setRowSelectionModel] =
|
||||||
React.useState<GridRowSelectionModel>([]);
|
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
|
// Change account of movement
|
||||||
const handleMoveClick = async (movement: Movement) => {
|
const handleMoveClick = async (movement: Movement) => {
|
||||||
const targetAccount = await chooseAccount(
|
const targetAccount = await chooseAccount(
|
||||||
@ -283,7 +300,19 @@ function MovementsTable(p: {
|
|||||||
{
|
{
|
||||||
field: "file",
|
field: "file",
|
||||||
headerName: "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",
|
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