GenericFields

This commit is contained in:
Felix Kaspar 2024-02-25 20:55:48 +01:00
parent 20f027bb5a
commit 13bfa0b0d0
13 changed files with 173 additions and 28 deletions

View File

@ -5,6 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/stirling-pdf-logo.svg" /> <link rel="icon" type="image/svg+xml" href="/stirling-pdf-logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + React + TS</title> <title>Tauri + React + TS</title>
<script src="browserfs.min.js"></script>
</head> </head>
<body> <body>

View File

@ -1,15 +1,31 @@
import Joi from "@stirling-tools/joi"; import Joi from "@stirling-tools/joi";
import { GenericField } from "./GenericField";
import React from "react";
interface BuildFieldsProps { interface BuildFieldsProps {
/** The text to display inside the button */ /** The text to display inside the button */
schemaDescription: Joi.Description | undefined; schemaDescription: Joi.Description | undefined;
onSubmit: React.FormEventHandler<HTMLFormElement>;
} }
export function BuildFields({ schemaDescription }: BuildFieldsProps) { export function BuildFields({ schemaDescription, onSubmit }: BuildFieldsProps) {
console.log("Render Build Fields", schemaDescription); console.log("Render Build Fields", schemaDescription);
const label = (schemaDescription?.flags as any)?.label
const description = (schemaDescription?.flags as any)?.description;
const values = (schemaDescription?.keys as any)?.values.keys as { [key: string]: Joi.Description};
return ( return (
<div>Description: {(schemaDescription?.flags as any)?.description}</div> <div>
<h3>{label}</h3>
{description}
<hr />
<form onSubmit={(e) => { onSubmit(e); e.preventDefault(); }}>
{
values ? Object.keys(values).map((key, i) => {
return (<GenericField key={key} fieldName={key} joiDefinition={values[key]} />)
}) : undefined
}
<input type="submit" value="Submit" />
</form>
</div>
); );
} }

View File

@ -0,0 +1,57 @@
import Joi from "@stirling-tools/joi";
import { Fragment } from "react";
interface GenericFieldProps {
fieldName: string
joiDefinition: Joi.Description;
}
export function GenericField({ fieldName, joiDefinition }: GenericFieldProps) {
switch (joiDefinition.type) {
case "number":
var validValues = joiDefinition.allow;
if(validValues) { // Restrained text input
return (
<Fragment>
<label htmlFor={fieldName}>{fieldName}:</label>
<input type="number" list={fieldName} name={fieldName}/>
<datalist id={fieldName}>
{joiDefinition.allow.map((e: string) => {
return (<option key={e} value={e}/>)
})}
</datalist>
<br/>
</Fragment>
);
}
else {
// TODO: Implement unrestrained text input
return (<pre>{JSON.stringify(joiDefinition, null, 2)}</pre>)
}
break;
case "string":
var validValues = joiDefinition.allow;
if(validValues) { // Restrained text input
return (
<Fragment>
<label htmlFor={fieldName}>{fieldName}:</label>
<input type="text" list={fieldName} name={fieldName}/>
<datalist id={fieldName}>
{joiDefinition.allow.map((e: string) => {
return (<option key={e} value={e}/>)
})}
</datalist>
<br/>
</Fragment>
);
}
else {
// TODO: Implement unrestrained text input
return (<pre>{JSON.stringify(joiDefinition, null, 2)}</pre>)
}
break;
default:
return (<div>Field "{fieldName}": <br /> requested type "{joiDefinition.type}" not found</div>)
}
}

View File

@ -1,16 +1,20 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { BaseSyntheticEvent, createContext, useState } from "react"; import { BaseSyntheticEvent, createContext, useRef, useState } from "react";
import { Operator } from "@stirling-pdf/shared-operations/src/functions"; import { Operator } from "@stirling-pdf/shared-operations/src/functions";
import i18next from "i18next"; import i18next from "i18next";
import Joi from "@stirling-tools/joi"; import Joi from "@stirling-tools/joi";
import { BuildFields } from "../components/fields/BuildFields"; import { BuildFields } from "../components/fields/BuildFields";
import { listOperatorNames } from "@stirling-pdf/shared-operations/src/workflow/operatorAccessor";
import { PdfFile, RepresentationType } from "@stirling-pdf/shared-operations/src/wrappers/PdfFile";
import { Action } from "@stirling-pdf/shared-operations/declarations/Action";
import { JoiPDFFileSchema } from "@stirling-pdf/shared-operations/src/wrappers/PdfFileJoi";
function Dynamic() { function Dynamic() {
const [schemaDescription, setSchemaDescription] = useState<Joi.Description>(); const [schemaDescription, setSchemaDescription] = useState<Joi.Description>();
const operators = listOperatorNames();
const operators = ["impose"]; // TODO: Make this dynamic const activeOperator = useRef<typeof Operator>();
function selectionChanged(s: BaseSyntheticEvent) { function selectionChanged(s: BaseSyntheticEvent) {
const selectedValue = s.target.value; const selectedValue = s.target.value;
@ -19,7 +23,7 @@ function Dynamic() {
return; return;
} }
i18next.loadNamespaces("impose", (err, t) => { i18next.loadNamespaces(selectedValue, (err, t) => {
if (err) throw err; if (err) throw err;
const LoadingModule = import(`@stirling-pdf/shared-operations/src/functions/${selectedValue}`) as Promise<{ [key: string]: typeof Operator }>; const LoadingModule = import(`@stirling-pdf/shared-operations/src/functions/${selectedValue}`) as Promise<{ [key: string]: typeof Operator }>;
@ -27,12 +31,77 @@ function Dynamic() {
const Operator = Module[capitalizeFirstLetter(selectedValue)]; const Operator = Module[capitalizeFirstLetter(selectedValue)];
const description = Operator.schema.describe(); const description = Operator.schema.describe();
setSchemaDescription(description); // This will update children activeOperator.current = Operator;
console.log(description); // This will update children
setSchemaDescription(description);
}); });
}); });
} }
function formDataToObject(formData: FormData): Record<string, string> {
const result: Record<string, string> = {};
formData.forEach((value, key) => {
result[key] = value.toString();
});
return result;
}
async function handleSubmit(e: BaseSyntheticEvent) {
if(!activeOperator.current) {
throw new Error("Please select an Operator in the Dropdown");
}
const formData = new FormData(e.target);
const action: Action = {type: activeOperator.current.constructor.name, values: formDataToObject(formData)};
// Validate PDF File
// Createing the pdffile before validation because joi cant handle it for some reason and I can't fix the underlying issue / I want to make progress, wasted like 3 hours on this already. TODO: The casting should be done in JoiPDFFileSchema.ts if done correctly...
const files = (document.getElementById("pdfFile") as HTMLInputElement).files;
const inputs: PdfFile[] = [];
if(files) {
const filesArray: File[] = Array.from(files as any);
for (let i = 0; i < files.length; i++) {
const file = filesArray[i];
if(file) {
console.log(new Uint8Array(await file.arrayBuffer()));
inputs.push(new PdfFile(
file.name.replace(/\.[^/.]+$/, ""), // Strip Extension
new Uint8Array(await file.arrayBuffer()),
RepresentationType.Uint8Array
));
}
else
throw new Error("This should not happen. Contact maintainers.");
}
}
const pdfValidationResults = await JoiPDFFileSchema.validate(inputs);
if(pdfValidationResults.error) {
console.log({error: "PDF validation failed", details: pdfValidationResults.error.message});
}
const pdfFiles: PdfFile[] = pdfValidationResults.value;
// Validate Action Values
const actionValidationResults = activeOperator.current.schema.validate({input: pdfFiles, values: action.values});
if(actionValidationResults.error) {
console.log({error: "Value validation failed", details: actionValidationResults.error.message});
return;
}
action.values = pdfValidationResults.value.values;
const operation = new activeOperator.current(action);
operation.run(pdfValidationResults.value, (progress) => {}).then(pdfFiles => {
console.log("Done");
});
};
function capitalizeFirstLetter(string: String) { function capitalizeFirstLetter(string: String) {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
} }
@ -46,17 +115,14 @@ function Dynamic() {
<select id="pdfOptions" onChange={selectionChanged}> <select id="pdfOptions" onChange={selectionChanged}>
<option value="none">none</option> <option value="none">none</option>
{ operators.map((operator, i) => { { operators.map((operator, i) => {
return (<option value={operator}>{operator}</option>) return (<option key={operator} value={operator}>{operator}</option>)
}) } }) }
</select> </select>
<div id="values"> <div id="values">
<BuildFields schemaDescription={schemaDescription}></BuildFields> <BuildFields schemaDescription={schemaDescription} onSubmit={handleSubmit}></BuildFields>
</div> </div>
<br />
<button id="processButton">Process process file with current settings</button>
<p> <p>
<Link to="/">Go back home...</Link> <Link to="/">Go back home...</Link>
</p> </p>

View File

@ -18,7 +18,7 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
}, },
"include": [ "include": [
"src", "src",

View File

@ -3,6 +3,7 @@ import react from "@vitejs/plugin-react";
import topLevelAwait from "vite-plugin-top-level-await"; import topLevelAwait from "vite-plugin-top-level-await";
import dynamicImport from 'vite-plugin-dynamic-import' import dynamicImport from 'vite-plugin-dynamic-import'
import compileTime from "vite-plugin-compile-time" import compileTime from "vite-plugin-compile-time"
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
@ -18,7 +19,11 @@ export default defineConfig(async () => ({
compileTime(), compileTime(),
dynamicImport(), dynamicImport(),
], ],
resolve: {
alias: {
'#pdfcpu': fileURLToPath(new URL("../shared-operations/src/wasm/pdfcpu/pdfcpu-wrapper.client", import.meta.url))
}
},
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// //
// 1. prevent vite from obscuring rust errors // 1. prevent vite from obscuring rust errors

View File

@ -3,7 +3,7 @@ const app = express();
const PORT = 80; const PORT = 80;
// Server Frontend TODO: Make this typescript compatible // Server Frontend TODO: Make this typescript compatible
app.use(express.static('../client-vanilla/public')); app.use(express.static('./public'));
app.use(express.static('../shared-operations')); app.use(express.static('../shared-operations'));
// serve // serve

View File

@ -10,7 +10,7 @@
<script src="/dep/pdf.min.js"></script> <script src="/dep/pdf.min.js"></script>
<script src="/dep/jsQR.js"></script> <script src="/dep/jsQR.js"></script>
<script src="/wasm/browserfs.min.js"></script> <script src="/browserfs.min.js"></script>
<script src="/wasm/opencv/opencv_3_4_custom_O3.js"></script> <script src="/wasm/opencv/opencv_3_4_custom_O3.js"></script>
<script src="index.js" type="module"></script> <script src="index.js" type="module"></script>

View File

@ -24,7 +24,7 @@ async function handleEndpoint(req: Request, res: Response) {
return; return;
} }
const validationResults = JoiPDFFileSchema.validate(req.files); const validationResults = await JoiPDFFileSchema.validateAsync(req.files);
if(validationResults.error) { if(validationResults.error) {
res.status(400).json({error: "PDF validation failed", details: validationResults.error.message}); res.status(400).json({error: "PDF validation failed", details: validationResults.error.message});
return; return;

View File

@ -43,7 +43,7 @@ router.post("/:workflowUuid?", [
} }
} }
const validationResults = JoiPDFFileSchema.validate(req.files); const validationResults = await JoiPDFFileSchema.validateAsync(req.files);
if(validationResults.error) { if(validationResults.error) {
res.status(400).json({error: "PDF validation failed", details: validationResults.error.message}); res.status(400).json({error: "PDF validation failed", details: validationResults.error.message});
return; return;

View File

@ -1,6 +1,6 @@
export interface Action { export interface Action {
values: any; values: any;
type: string; type: "wait" | "done" | "impose" | string;
actions?: Action[]; actions?: Action[];
} }

View File

@ -1,3 +1,4 @@
import { PdfFile } from "wrappers/PdfFile";
import { Action } from "../../declarations/Action"; import { Action } from "../../declarations/Action";
import Joi from "@stirling-tools/joi"; import Joi from "@stirling-tools/joi";
@ -33,7 +34,7 @@ export class Operator {
this.actionValues = action.values; this.actionValues = action.values;
} }
async run(input: any[], progressCallback: (progress: Progress) => void): Promise<any[]> { async run(input: PdfFile[] | any[], progressCallback: (progress: Progress) => void): Promise<PdfFile[] | any[]> {
return []; return [];
} }
} }

View File

@ -1,4 +1,5 @@
// imports browserfs via index.html script-tag // imports browserfs via index.html script-tag
import wasmUrl from '../../../public/wasm/pdfcpu/pdfcpu.wasm?url'
let wasmLocation = "/wasm/pdfcpu/"; let wasmLocation = "/wasm/pdfcpu/";
@ -29,15 +30,12 @@ function configureFs() {
} }
function loadWasm() { function loadWasm() {
const script = document.createElement("script"); import("./wasm_exec.js");
script.src = wasmLocation + "/wasm_exec.js";
script.async = true;
document.body.appendChild(script);
} }
const runWasm = async (param) => { const runWasm = async (param) => {
if (window.cachedWasmResponse === undefined) { if (window.cachedWasmResponse === undefined) {
const response = await fetch(wasmLocation + "/pdfcpu.wasm"); const response = await fetch(wasmUrl);
const buffer = await response.arrayBuffer(); const buffer = await response.arrayBuffer();
window.cachedWasmResponse = buffer; window.cachedWasmResponse = buffer;
window.go = new Go(); window.go = new Go();