working fileSelect component

This commit is contained in:
Qubasa
2025-05-12 17:54:05 +02:00
parent 9544a3e522
commit 3ea01c60f6
7 changed files with 38 additions and 28 deletions

View File

@@ -35,6 +35,15 @@ class FileRequest:
@API.register_abstract @API.register_abstract
def cancel_task(task_id: str) -> None: def cancel_task(task_id: str) -> None:
"""Cancel a task by its op_key.""" """Cancel a task by its op_key."""
msg = "cancel_task() is not implemented"
raise NotImplementedError(msg)
@API.register_abstract
def list_tasks() -> list[str]:
"""List all tasks."""
msg = "list_tasks() is not implemented"
raise NotImplementedError(msg)
@API.register_abstract @API.register_abstract
@@ -43,6 +52,8 @@ def open_file(file_request: FileRequest) -> list[str] | None:
Abstract api method to open a file dialog window. Abstract api method to open a file dialog window.
It must return the name of the selected file or None if no file was selected. It must return the name of the selected file or None if no file was selected.
""" """
msg = "open_file() is not implemented"
raise NotImplementedError(msg)
@dataclass @dataclass

View File

@@ -56,8 +56,9 @@ const _callApi = <K extends OperationNames>(
) => Promise<OperationResponse<OperationNames>> ) => Promise<OperationResponse<OperationNames>>
> >
)[method](args) as Promise<OperationResponse<K>>; )[method](args) as Promise<OperationResponse<K>>;
const op_key = (promise as any)._webviewMessageId as string; // eslint-disable-next-line @typescript-eslint/no-explicit-any
debugger; const op_key = (promise as any)._webviewMessageId as string;
return { promise, op_key }; return { promise, op_key };
}; };

View File

@@ -3,11 +3,7 @@ import { Typography } from "@/src/components/Typography";
import Fieldset from "@/src/Form/fieldset"; import Fieldset from "@/src/Form/fieldset";
import Icon from "@/src/components/icon"; // For displaying file icons import Icon from "@/src/components/icon"; // For displaying file icons
import { callApi } from "@/src/api"; import { callApi } from "@/src/api";
import type { import type { FieldValues } from "@modular-forms/solid";
FieldComponent,
FieldValues,
FieldName,
} from "@modular-forms/solid";
import { Show, For, type Component, type JSX } from "solid-js"; import { Show, For, type Component, type JSX } from "solid-js";
// Types for the file dialog options passed to callApi // Types for the file dialog options passed to callApi
@@ -23,24 +19,23 @@ export interface FileDialogOptions {
} }
// Props for the CustomFileField component // Props for the CustomFileField component
interface FileSelectorOpts< interface FileSelectorOpts<TFieldName extends string> {
TForm extends FieldValues, // eslint-disable-next-line @typescript-eslint/no-explicit-any
TFieldName extends FieldName<TForm>, Field: any; // The Field component from createForm
> {
Field: FieldComponent<TForm>; // The Field component from createForm
name: TFieldName; // Name of the form field (e.g., "sshKeys", "profilePicture") name: TFieldName; // Name of the form field (e.g., "sshKeys", "profilePicture")
label: string; // Legend for Fieldset or main label for the input label: string; // Legend for Fieldset or main label for the input
description?: string | JSX.Element; // Optional description text description?: string | JSX.Element; // Optional description text
multiple?: boolean; // True if multiple files can be selected, false for single file multiple?: boolean; // True if multiple files can be selected, false for single file
fileDialogOptions: FileDialogOptions; // Configuration for the custom file dialog fileDialogOptions: FileDialogOptions; // Configuration for the custom file dialog
// eslint-disable-next-line @typescript-eslint/no-explicit-any
of: any;
// Optional props for styling // Optional props for styling
inputClass?: string; inputClass?: string;
fileListClass?: string; fileListClass?: string;
// You can add more specific props like `validate` if you want to pass them to Field // You can add more specific props like `validate` if you want to pass them to Field
} }
export const FileSelectorField: Component<FileSelectorOpts<any, any>> = ( export const FileSelectorField: Component<FileSelectorOpts<string>> = (
props, props,
) => { ) => {
const { const {
@@ -57,7 +52,7 @@ export const FileSelectorField: Component<FileSelectorOpts<any, any>> = (
// Ref to the underlying HTMLInputElement (assuming FileInput forwards refs or is simple) // Ref to the underlying HTMLInputElement (assuming FileInput forwards refs or is simple)
let actualInputElement: HTMLInputElement | undefined; let actualInputElement: HTMLInputElement | undefined;
const openAndSetFiles = async (event: MouseEvent) => { const openAndSetFiles = async (event: Event) => {
event.preventDefault(); event.preventDefault();
if (!actualInputElement) { if (!actualInputElement) {
console.error( console.error(
@@ -127,7 +122,10 @@ export const FileSelectorField: Component<FileSelectorOpts<any, any>> = (
))} ))}
<Field name={name} type={multiple ? "File[]" : "File"}> <Field name={name} type={multiple ? "File[]" : "File"}>
{(field, fieldProps) => ( {(
field: { value: File | File[]; error?: string },
fieldProps: Record<string, unknown>,
) => (
<> <>
{/* {/*
This FileInput component should be clickable. This FileInput component should be clickable.
@@ -135,8 +133,9 @@ export const FileSelectorField: Component<FileSelectorOpts<any, any>> = (
If FileInput is complex, it might need an 'inputRef' prop or similar. If FileInput is complex, it might need an 'inputRef' prop or similar.
*/} */}
<FileInput <FileInput
{...(fieldProps as FileInputProps)} // Spread modular-forms props {...(fieldProps as unknown as FileInputProps)} // Spread modular-forms props
ref={(el: HTMLInputElement) => { ref={(el: HTMLInputElement) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(fieldProps as any).ref(el); // Pass ref to modular-forms (fieldProps as any).ref(el); // Pass ref to modular-forms
actualInputElement = el; // Capture for local use actualInputElement = el; // Capture for local use
}} }}
@@ -150,7 +149,7 @@ export const FileSelectorField: Component<FileSelectorOpts<any, any>> = (
error={field.error} // Display error from modular-forms error={field.error} // Display error from modular-forms
/> />
{field.error && ( {field.error && (
<Typography color="error" hierarchy="body" size="xs" class="mt-1"> <Typography hierarchy="body" size="xs" class="mt-1">
{field.error} {field.error}
</Typography> </Typography>
)} )}
@@ -175,9 +174,8 @@ export const FileSelectorField: Component<FileSelectorOpts<any, any>> = (
} }
> >
{(file) => ( {(file) => (
<div class="flex items-center justify-between rounded border border-def-1 bg-bg-2 p-2 text-sm"> <div class="flex items-center justify-between rounded border p-2 text-sm border-def-1">
<span class="truncate" title={file.name}> <span class="truncate" title={file.name}>
<Icon icon="File" class="mr-2 inline-block" size={14} />
{file.name} {file.name}
</span> </span>
{/* A remove button per file is complex with FileList & modular-forms. {/* A remove button per file is complex with FileList & modular-forms.

View File

@@ -107,8 +107,7 @@ const closeButtonStyle: JSX.CSSProperties = {
// --- Toast Component Definitions --- // --- Toast Component Definitions ---
// Error Toast // Error Toast
export interface ErrorToastProps extends BaseToastProps {} export const ErrorToastComponent: Component<BaseToastProps> = (props) => {
export const ErrorToastComponent: Component<ErrorToastProps> = (props) => {
const handleCancelClick = () => { const handleCancelClick = () => {
if (props.onCancel) { if (props.onCancel) {
props.onCancel(); props.onCancel();
@@ -136,8 +135,7 @@ export const ErrorToastComponent: Component<ErrorToastProps> = (props) => {
}; };
// Info Toast // Info Toast
export interface InfoToastProps extends BaseToastProps {} export const InfoToastComponent: Component<BaseToastProps> = (props) => {
export const InfoToastComponent: Component<InfoToastProps> = (props) => {
const handleCancelClick = () => { const handleCancelClick = () => {
if (props.onCancel) { if (props.onCancel) {
props.onCancel(); props.onCancel();
@@ -165,8 +163,7 @@ export const InfoToastComponent: Component<InfoToastProps> = (props) => {
}; };
// Warning Toast // Warning Toast
export interface WarningToastProps extends BaseToastProps {} export const WarningToastComponent: Component<BaseToastProps> = (props) => {
export const WarningToastComponent: Component<WarningToastProps> = (props) => {
const handleCancelClick = () => { const handleCancelClick = () => {
if (props.onCancel) { if (props.onCancel) {
props.onCancel(); props.onCancel();

View File

@@ -243,6 +243,7 @@ export const Flash = () => {
description="Provide your SSH public key(s) for secure, passwordless connections. (.pub files)" description="Provide your SSH public key(s) for secure, passwordless connections. (.pub files)"
multiple={true} // Allow multiple SSH keys multiple={true} // Allow multiple SSH keys
fileDialogOptions={sshKeyDialogOptions} fileDialogOptions={sshKeyDialogOptions}
of={Array<File>}
// You could add custom validation via modular-forms 'validate' prop on CustomFileField if needed // You could add custom validation via modular-forms 'validate' prop on CustomFileField if needed
// e.g. validate={[required("At least one SSH key is required.")]} // e.g. validate={[required("At least one SSH key is required.")]}
// This would require CustomFileField to accept and pass `validate` to its internal `Field`. // This would require CustomFileField to accept and pass `validate` to its internal `Field`.

View File

@@ -623,10 +623,11 @@ const MachineForm = (props: MachineDetailsProps) => {
</Field> </Field>
<FileSelectorField <FileSelectorField
Field={Field} Field={Field}
of={Array<File>}
multiple={true}
name="sshKeys" // Corresponds to FlashFormValues.sshKeys name="sshKeys" // Corresponds to FlashFormValues.sshKeys
label="SSH Private Key" label="SSH Private Key"
description="Provide your SSH private key for secure, passwordless connections." description="Provide your SSH private key for secure, passwordless connections."
multiple={false}
fileDialogOptions={ fileDialogOptions={
{ {
title: "Select SSH Keys", title: "Select SSH Keys",

View File

@@ -127,10 +127,11 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
</Field> </Field>
<FileSelectorField <FileSelectorField
Field={Field} Field={Field}
of={File}
multiple={false}
name="sshKey" // Corresponds to FlashFormValues.sshKeys name="sshKey" // Corresponds to FlashFormValues.sshKeys
label="SSH Private Key" label="SSH Private Key"
description="Provide your SSH private key for secure, passwordless connections." description="Provide your SSH private key for secure, passwordless connections."
multiple={false}
fileDialogOptions={ fileDialogOptions={
{ {
title: "Select SSH Keys", title: "Select SSH Keys",