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
def cancel_task(task_id: str) -> None:
"""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
@@ -43,6 +52,8 @@ def open_file(file_request: FileRequest) -> list[str] | None:
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.
"""
msg = "open_file() is not implemented"
raise NotImplementedError(msg)
@dataclass

View File

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

View File

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

View File

@@ -107,8 +107,7 @@ const closeButtonStyle: JSX.CSSProperties = {
// --- Toast Component Definitions ---
// Error Toast
export interface ErrorToastProps extends BaseToastProps {}
export const ErrorToastComponent: Component<ErrorToastProps> = (props) => {
export const ErrorToastComponent: Component<BaseToastProps> = (props) => {
const handleCancelClick = () => {
if (props.onCancel) {
props.onCancel();
@@ -136,8 +135,7 @@ export const ErrorToastComponent: Component<ErrorToastProps> = (props) => {
};
// Info Toast
export interface InfoToastProps extends BaseToastProps {}
export const InfoToastComponent: Component<InfoToastProps> = (props) => {
export const InfoToastComponent: Component<BaseToastProps> = (props) => {
const handleCancelClick = () => {
if (props.onCancel) {
props.onCancel();
@@ -165,8 +163,7 @@ export const InfoToastComponent: Component<InfoToastProps> = (props) => {
};
// Warning Toast
export interface WarningToastProps extends BaseToastProps {}
export const WarningToastComponent: Component<WarningToastProps> = (props) => {
export const WarningToastComponent: Component<BaseToastProps> = (props) => {
const handleCancelClick = () => {
if (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)"
multiple={true} // Allow multiple SSH keys
fileDialogOptions={sshKeyDialogOptions}
of={Array<File>}
// 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.")]}
// This would require CustomFileField to accept and pass `validate` to its internal `Field`.

View File

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

View File

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