Generalize the expert-panel boilerplate (container + title + widgets + upload button) and surface real upload errors via hey-api. - SettingsPanel: consistent padded Paper + Typography title (one place to size it) + optional Upload button + result snackbar; PanelTitle and UploadSnackbarView exported for panels with bespoke layouts (DetectorSettings grid, ROI) - useUpload(mutation): wraps a generated hey-api mutation, shows the server's error message on failure (errorMessage handles 400 string / 500 error_message) - "unsaved changes" dot on panel titles, derived from the existing last-downloaded snapshot (dirty = !isEqual(value, lastDownloaded)) - fix PixelMask: TIFF upload was swallowing errors; now shows them - config panels (FileWriter, ImageFormat, Detector, AzInt, DarkMask, Indexing, Instrument, ZeroMQ, ROI, DetectorSelection) upload through the typed SDK instead of ButtonWithSnackbar's raw fetch; status/display panels share the same shell for consistent spacing - ButtonWithSnackbar now only powers bodyless action buttons (pedestal/cancel/ initialize, start/trigger, deactivate) Build (tsc + vite) passes; dev server transforms all modules. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
42 lines
1.4 KiB
TypeScript
42 lines
1.4 KiB
TypeScript
import { useState } from "react";
|
|
import { useMutation, UseMutationOptions } from "@tanstack/react-query";
|
|
|
|
/** Pull a human-readable message out of a hey-api error (400 = string, 500 = error_message). */
|
|
export function errorMessage(error: unknown): string {
|
|
if (typeof error === "string") return error;
|
|
if (error && typeof error === "object" && "msg" in error)
|
|
return String((error as { msg?: unknown }).msg ?? "Unknown error");
|
|
return "Upload failed";
|
|
}
|
|
|
|
export type UploadSnackbar = {
|
|
open: boolean;
|
|
success: boolean;
|
|
message: string;
|
|
onClose: () => void;
|
|
};
|
|
|
|
/**
|
|
* Wraps a generated hey-api mutation (e.g. putConfigFileWriterMutation()) and
|
|
* surfaces the result as a snackbar. submit(body) sends the typed body; on
|
|
* failure the server's error message is shown.
|
|
*/
|
|
export function useUpload<TError, TVars extends { body?: unknown }>(
|
|
mutationOptions: UseMutationOptions<unknown, TError, TVars>,
|
|
) {
|
|
const mutation = useMutation(mutationOptions);
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const submit = (body: TVars["body"]) =>
|
|
mutation.mutate({ body } as TVars, { onSettled: () => setOpen(true) });
|
|
|
|
const snackbar: UploadSnackbar = {
|
|
open,
|
|
success: mutation.isSuccess,
|
|
message: mutation.error ? errorMessage(mutation.error) : "",
|
|
onClose: () => setOpen(false),
|
|
};
|
|
|
|
return { submit, pending: mutation.isPending, snackbar };
|
|
}
|