From 54c39edafd4e6e3fbdf1819689c9a76ec0432571 Mon Sep 17 00:00:00 2001 From: Glen Huang Date: Fri, 26 Sep 2025 10:51:24 +0800 Subject: [PATCH] ui/HostFileInput: refactor - Contain api call within itself - Flatten input attributes - Fix directory validation type error --- .../src/components/Form/Fieldset.stories.tsx | 6 +- .../components/Form/HostFileInput.stories.tsx | 7 +- .../ui/src/components/Form/HostFileInput.tsx | 134 +++++++++++------- .../ui/src/routes/Onboarding/Onboarding.tsx | 55 +------ .../InstallMachine/steps/createInstaller.tsx | 11 +- 5 files changed, 90 insertions(+), 123 deletions(-) diff --git a/pkgs/clan-app/ui/src/components/Form/Fieldset.stories.tsx b/pkgs/clan-app/ui/src/components/Form/Fieldset.stories.tsx index 95c968a41..77b8a447b 100644 --- a/pkgs/clan-app/ui/src/components/Form/Fieldset.stories.tsx +++ b/pkgs/clan-app/ui/src/components/Form/Fieldset.stories.tsx @@ -64,11 +64,7 @@ export const Default: Story = { label="Bio" input={{ placeholder: "Tell us a bit about yourself", rows: 8 }} /> - "/home/foo/bar/baz/fizz/buzz/bla/bizz"} - /> + ), diff --git a/pkgs/clan-app/ui/src/components/Form/HostFileInput.stories.tsx b/pkgs/clan-app/ui/src/components/Form/HostFileInput.stories.tsx index 55f887c09..d62e6f864 100644 --- a/pkgs/clan-app/ui/src/components/Form/HostFileInput.stories.tsx +++ b/pkgs/clan-app/ui/src/components/Form/HostFileInput.stories.tsx @@ -57,12 +57,7 @@ export type Story = StoryObj; export const Bare: Story = { args: { - onSelectFile: async () => { - return "/home/github/clans/my-clan/foo/bar/baz/fizz/buzz"; - }, - input: { - placeholder: "e.g. 11/06/89", - }, + placeholder: "e.g. 11/06/89", }, }; diff --git a/pkgs/clan-app/ui/src/components/Form/HostFileInput.tsx b/pkgs/clan-app/ui/src/components/Form/HostFileInput.tsx index 080f148ab..29465fb48 100644 --- a/pkgs/clan-app/ui/src/components/Form/HostFileInput.tsx +++ b/pkgs/clan-app/ui/src/components/Form/HostFileInput.tsx @@ -1,92 +1,114 @@ -import { - TextField, - TextFieldInputProps, - TextFieldRootProps, -} from "@kobalte/core/text-field"; +import { TextField } from "@kobalte/core/text-field"; import cx from "classnames"; import { Label } from "./Label"; import { Button } from "../Button/Button"; import styles from "./HostFileInput.module.css"; -import { PolymorphicProps } from "@kobalte/core/polymorphic"; import { FieldProps } from "./Field"; import { Orienter } from "./Orienter"; -import { createSignal, mergeProps, splitProps } from "solid-js"; +import { createSignal, JSX, splitProps } from "solid-js"; import { Tooltip } from "@kobalte/core/tooltip"; import { Typography } from "@/src/components/Typography/Typography"; import { keepTruthy } from "@/src/util"; +import { callApi } from "@/src/hooks/api"; -export type HostFileInputProps = FieldProps & - TextFieldRootProps & { - onSelectFile: () => Promise; - input?: PolymorphicProps<"input", TextFieldInputProps<"input">>; - placeholder?: string; - }; +export type HostFileInputProps = FieldProps & { + name: string; + windowTitle?: string; + required?: boolean; + disabled?: boolean; + placeholder?: string; + initialFolder?: string; + readOnly?: boolean; + defaultValue?: string; + ref?: (element: HTMLInputElement) => void; + onInput?: JSX.EventHandler< + HTMLInputElement | HTMLTextAreaElement, + InputEvent + >; + onChange?: JSX.EventHandler; + onBlur?: JSX.EventHandler; +}; export const HostFileInput = (props: HostFileInputProps) => { - const withDefaults = mergeProps({ value: "" } as const, props); - const [local, other] = splitProps(withDefaults, [ - "size", - "orientation", - "inverted", - "ghost", - ]); - const [value, setValue] = createSignal(other.value); + const [rootProps, inputProps, local, labelProps] = splitProps( + props, + ["name", "defaultValue", "required", "disabled"], + ["onInput", "onChange", "onBlur"], + ["windowTitle", "initialFolder", "readOnly", "ref", "placeholder"], + ); - let actualInputElement: HTMLInputElement | undefined; + let inputElement!: HTMLInputElement; + const [value, setValue] = createSignal(rootProps.defaultValue || ""); - const selectFile = async () => { - try { - console.log("selecting file", props.onSelectFile); - setValue(await props.onSelectFile()); - actualInputElement?.dispatchEvent( - new Event("input", { bubbles: true, cancelable: true }), - ); - } catch (error) { - console.log("Error selecting file", error); - // todo work out how to display the error + const onSelectFile = async () => { + if (local.readOnly) { + return; } + const req = callApi("get_system_file", { + file_request: { + mode: "select_folder", + title: local.windowTitle || labelProps.label, + initial_folder: local.initialFolder, + }, + }); + + const resp = await req.result; + + // TOOD: When a user clicks cancel button in the file picker, an error will + // be return, the backend should provide more data so we can target the + // cancellation specifically and not swallow other errors + if (resp.status === "error") { + return; + } + + setValue(resp.data![0]); + inputElement.dispatchEvent( + new Event("input", { bubbles: true, cancelable: true }), + ); }; return ( - +