UI: replace TextInput with simple Layout of InputBase, InputLabel, ErrorMessage

This commit is contained in:
Johannes Kirschbauer
2024-12-17 19:37:07 +01:00
parent 0a31552661
commit f492601d7b
11 changed files with 85 additions and 184 deletions

View File

@@ -1,6 +1,8 @@
import { createEffect, Show, type JSX } from "solid-js"; import { createEffect, Show, type JSX } from "solid-js";
import cx from "classnames"; import cx from "classnames";
import { Label } from "../base/label"; import { Label } from "../base/label";
import { InputBase, InputLabel } from "@/src/components/inputBase";
import { Typography } from "@/src/components/Typography";
interface TextInputProps { interface TextInputProps {
value: string; value: string;
@@ -22,49 +24,39 @@ interface TextInputProps {
} }
export function TextInput(props: TextInputProps) { export function TextInput(props: TextInputProps) {
const value = () => props.value; // createEffect(() => {
// console.log("TextInput", props.error, props.value);
// });
return ( return (
<label <div
class={cx("form-control w-full", props.class)} class="grid grid-cols-12"
aria-disabled={props.disabled} classList={{
"mb-[14.5px]": !props.error,
}}
> >
<div class="label"> <InputLabel
<Label label={props.label} required={props.required} /> class="col-span-2"
<span class="label-text-alt block">{props.altLabel}</span> required={props.required}
</div> error={!!props.error}
>
<div class="input input-bordered flex items-center gap-2"> {props.label}
<Show when={props.adornment && props.adornment.position === "start"}> </InputLabel>
{props.adornment?.content} <InputBase
</Show> error={!!props.error}
{props.inlineLabel} class="col-span-10"
<input {...props.inputProps}
{...props.inputProps} value={props.value}
value={value()} />
type={props.type ? props.type : "text"} {props.error && (
class="grow" <Typography
classList={{ hierarchy="body"
"input-disabled": props.disabled, size="xxs"
}} weight="medium"
placeholder={`${props.placeholder || props.label}`} class="col-span-full px-1 !fg-semantic-4"
required >
disabled={props.disabled} {props.error}
/> </Typography>
<Show when={props.adornment && props.adornment.position === "end"}> )}
{props.adornment?.content} </div>
</Show>
</div>
<div class="label">
{props.helperText && (
<span class="label-text text-neutral">{props.helperText}</span>
)}
{props.error && (
<span class="label-text-alt font-bold text-error-700">
{props.error}
</span>
)}
</div>
</label>
); );
} }

View File

@@ -4,7 +4,7 @@ import {
getValues, getValues,
SubmitHandler, SubmitHandler,
} from "@modular-forms/solid"; } from "@modular-forms/solid";
import { TextInput } from "./components/TextInput"; import { TextInput } from "@/src/Form/fields/TextInput";
import { Button } from "./components/button"; import { Button } from "./components/button";
import { callApi } from "./api"; import { callApi } from "./api";
import { API } from "@/api/API"; import { API } from "@/api/API";
@@ -68,7 +68,6 @@ export const ApiTester = () => {
label={"endpoint"} label={"endpoint"}
value={field.value || ""} value={field.value || ""}
inputProps={fieldProps} inputProps={fieldProps}
formStore={formStore}
/> />
)} )}
</Field> </Field>
@@ -78,7 +77,6 @@ export const ApiTester = () => {
label={"payload"} label={"payload"}
value={field.value || ""} value={field.value || ""}
inputProps={fieldProps} inputProps={fieldProps}
formStore={formStore}
/> />
)} )}
</Field> </Field>

View File

@@ -1,72 +0,0 @@
import { FieldValues, FormStore, ResponseData } from "@modular-forms/solid";
import { Show, type JSX } from "solid-js";
import cx from "classnames";
interface TextInputProps<T extends FieldValues, R extends ResponseData> {
formStore: FormStore<T, R>;
value: string;
inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
label: JSX.Element;
error?: string;
required?: boolean;
type?: string;
inlineLabel?: JSX.Element;
class?: string;
adornment?: {
position: "start" | "end";
content: JSX.Element;
};
placeholder?: string;
}
export function TextInput<T extends FieldValues, R extends ResponseData>(
props: TextInputProps<T, R>,
) {
const value = () => props.value;
return (
<label
class={cx("form-control w-full", props.class)}
aria-disabled={props.formStore.submitting}
>
<div class="label">
<span
class="label-text block"
classList={{
"after:ml-0.5 after:text-primary after:content-['*']":
props.required,
}}
>
{props.label}
</span>
</div>
<div class="input input-bordered flex items-center gap-2">
<Show when={props.adornment && props.adornment.position === "start"}>
{props.adornment?.content}
</Show>
{props.inlineLabel}
<input
{...props.inputProps}
value={value()}
type={props.type ? props.type : "text"}
class="grow"
classList={{
"input-disabled": props.formStore.submitting,
}}
placeholder={`${props.placeholder || props.label}`}
required
disabled={props.formStore.submitting}
/>
<Show when={props.adornment && props.adornment.position === "end"}>
{props.adornment?.content}
</Show>
</div>
{props.error && (
<span class="label-text-alt font-bold text-error-700">
{props.error}
</span>
)}
</label>
);
}

View File

@@ -56,7 +56,7 @@ export const InputBase = (props: InputBaseProps) => {
// Cursor // Cursor
"aria-readonly:cursor-no-drop", "aria-readonly:cursor-no-drop",
props.class props.class,
)} )}
classList={{ classList={{
[cx("!border !border-semantic-1 !outline-semantic-1")]: !!props.error, [cx("!border !border-semantic-1 !outline-semantic-1")]: !!props.error,

View File

@@ -8,7 +8,7 @@ import {
} from "@modular-forms/solid"; } from "@modular-forms/solid";
import toast from "solid-toast"; import toast from "solid-toast";
import { setActiveURI, setClanList } from "@/src/App"; import { setActiveURI, setClanList } from "@/src/App";
import { TextInput } from "@/src/components/TextInput"; import { TextInput } from "@/src/Form/fields/TextInput";
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
import { Button } from "@/src/components/button"; import { Button } from "@/src/components/button";
import Icon from "@/src/components/icon"; import Icon from "@/src/components/icon";
@@ -165,7 +165,6 @@ export const CreateClan = () => {
), ),
position: "start", position: "start",
}} }}
formStore={formStore}
inputProps={props} inputProps={props}
label="Template to use" label="Template to use"
value={field.value ?? ""} value={field.value ?? ""}

View File

@@ -11,7 +11,7 @@ import {
setValue, setValue,
SubmitHandler, SubmitHandler,
} from "@modular-forms/solid"; } from "@modular-forms/solid";
import { TextInput } from "@/src/components/TextInput"; import { TextInput } from "@/src/Form/fields/TextInput";
import toast from "solid-toast"; import toast from "solid-toast";
import { set_single_service } from "@/src/api/inventory"; import { set_single_service } from "@/src/api/inventory";
import { Button } from "@/src/components/button"; import { Button } from "@/src/components/button";
@@ -215,7 +215,6 @@ const AdminModuleForm = (props: AdminModuleFormProps) => {
<Field name={`allowedKeys.${idx()}.name`}> <Field name={`allowedKeys.${idx()}.name`}>
{(field, props) => ( {(field, props) => (
<TextInput <TextInput
formStore={formStore}
inputProps={props} inputProps={props}
label={"Name"} label={"Name"}
adornment={{ adornment={{
@@ -235,7 +234,6 @@ const AdminModuleForm = (props: AdminModuleFormProps) => {
{(field, props) => ( {(field, props) => (
<> <>
<TextInput <TextInput
formStore={formStore}
inputProps={props} inputProps={props}
label={"Value"} label={"Value"}
value={field.value ?? ""} value={field.value ?? ""}
@@ -243,7 +241,10 @@ const AdminModuleForm = (props: AdminModuleFormProps) => {
class="col-span-6" class="col-span-6"
required required
/> />
<span class="tooltip mt-auto" data-tip="Select file"> <span
class="tooltip col-span-12 mt-auto"
data-tip="Select file"
>
<label <label
class={"form-control w-full"} class={"form-control w-full"}
aria-disabled={formStore.submitting} aria-disabled={formStore.submitting}

View File

@@ -41,8 +41,8 @@ export const Components = () => {
readonly={readOnly} readonly={readOnly}
/> />
</> </>
)) )),
) ),
)} )}
<span class="col-span-2">Input Ghost</span> <span class="col-span-2">Input Ghost</span>
{disabled.map((disabled) => {disabled.map((disabled) =>
@@ -66,8 +66,8 @@ export const Components = () => {
readonly={readOnly} readonly={readOnly}
/> />
</> </>
)) )),
) ),
)} )}
<span class="col-span-2">Input Label</span> <span class="col-span-2">Input Label</span>
<span>Default</span> <span>Default</span>
@@ -96,7 +96,7 @@ export const Components = () => {
<Field <Field
name="ef" name="ef"
validate={required( validate={required(
"This field is required very long descriptive error message" "This field is required very long descriptive error message",
)} )}
> >
{(field, inputProps) => ( {(field, inputProps) => (

View File

@@ -3,7 +3,7 @@ import { Button } from "@/src/components/button";
import { FileInput } from "@/src/components/FileInput"; import { FileInput } from "@/src/components/FileInput";
import Icon from "@/src/components/icon"; import Icon from "@/src/components/icon";
import { SelectInput } from "@/src/components/SelectInput"; import { SelectInput } from "@/src/components/SelectInput";
import { TextInput } from "@/src/components/TextInput"; import { TextInput } from "@/src/Form/fields/TextInput";
import { Typography } from "@/src/components/Typography"; import { Typography } from "@/src/components/Typography";
import { Header } from "@/src/layout/header"; import { Header } from "@/src/layout/header";
import { import {
@@ -269,7 +269,6 @@ export const Flash = () => {
> >
{(field, props) => ( {(field, props) => (
<TextInput <TextInput
formStore={formStore}
inputProps={props} inputProps={props}
label="SSID" label="SSID"
value={field.value ?? ""} value={field.value ?? ""}
@@ -286,7 +285,6 @@ export const Flash = () => {
{(field, props) => ( {(field, props) => (
<div class="relative col-span-3 w-full"> <div class="relative col-span-3 w-full">
<TextInput <TextInput
formStore={formStore}
inputProps={props} inputProps={props}
type={ type={
passwordVisibility()[index()] ? "text" : "password" passwordVisibility()[index()] ? "text" : "password"
@@ -357,7 +355,6 @@ export const Flash = () => {
{(field, props) => ( {(field, props) => (
<> <>
<TextInput <TextInput
formStore={formStore}
inputProps={props} inputProps={props}
label="Source (flake URL)" label="Source (flake URL)"
value={String(field.value)} value={String(field.value)}
@@ -377,7 +374,6 @@ export const Flash = () => {
{(field, props) => ( {(field, props) => (
<> <>
<TextInput <TextInput
formStore={formStore}
inputProps={props} inputProps={props}
label="Image Name (attribute name)" label="Image Name (attribute name)"
value={String(field.value)} value={String(field.value)}

View File

@@ -2,7 +2,7 @@ import { callApi, OperationArgs } from "@/src/api";
import { activeURI } from "@/src/App"; import { activeURI } from "@/src/App";
import { Button } from "@/src/components/button"; import { Button } from "@/src/components/button";
import Icon from "@/src/components/icon"; import Icon from "@/src/components/icon";
import { TextInput } from "@/src/components/TextInput"; import { TextInput } from "@/src/Form/fields/TextInput";
import { Header } from "@/src/layout/header"; import { Header } from "@/src/layout/header";
import { createForm, required, reset } from "@modular-forms/solid"; import { createForm, required, reset } from "@modular-forms/solid";
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
@@ -73,7 +73,7 @@ export function CreateMachine() {
<Header title="Create Machine" /> <Header title="Create Machine" />
<div class="flex w-full p-4"> <div class="flex w-full p-4">
<div class="mt-4 w-full self-stretch px-2"> <div class="mt-4 w-full self-stretch px-2">
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit} class="">
<Field <Field
name="opts.machine.name" name="opts.machine.name"
validate={[required("This field is required")]} validate={[required("This field is required")]}
@@ -85,7 +85,6 @@ export function CreateMachine() {
</div> </div>
<TextInput <TextInput
inputProps={props} inputProps={props}
formStore={formStore}
value={`${field.value}`} value={`${field.value}`}
label={"name"} label={"name"}
error={field.error} error={field.error}
@@ -99,7 +98,6 @@ export function CreateMachine() {
{(field, props) => ( {(field, props) => (
<TextInput <TextInput
inputProps={props} inputProps={props}
formStore={formStore}
value={`${field.value}`} value={`${field.value}`}
label={"description"} label={"description"}
error={field.error} error={field.error}
@@ -143,7 +141,6 @@ export function CreateMachine() {
<> <>
<TextInput <TextInput
inputProps={props} inputProps={props}
formStore={formStore}
value={`${field.value}`} value={`${field.value}`}
label={"Target"} label={"Target"}
error={field.error} error={field.error}

View File

@@ -3,7 +3,7 @@ import { activeURI } from "@/src/App";
import { Button } from "@/src/components/button"; import { Button } from "@/src/components/button";
import { FileInput } from "@/src/components/FileInput"; import { FileInput } from "@/src/components/FileInput";
import Icon from "@/src/components/icon"; import Icon from "@/src/components/icon";
import { TextInput } from "@/src/components/TextInput"; import { TextInput } from "@/src/Form/fields/TextInput";
import { selectSshKeys } from "@/src/hooks"; import { selectSshKeys } from "@/src/hooks";
import { import {
createForm, createForm,
@@ -17,6 +17,7 @@ import { createSignal, For, Show } from "solid-js";
import toast from "solid-toast"; import toast from "solid-toast";
import { MachineAvatar } from "./avatar"; import { MachineAvatar } from "./avatar";
import { Header } from "@/src/layout/header"; import { Header } from "@/src/layout/header";
import { InputLabel } from "@/src/components/inputBase";
type MachineFormInterface = MachineData & { type MachineFormInterface = MachineData & {
sshKey?: File; sshKey?: File;
@@ -302,14 +303,13 @@ const MachineForm = (props: MachineDetailsProps) => {
}; };
return ( return (
<> <>
<Form onSubmit={handleSubmit}> <div class="card-body">
<div class="card-body"> <span class="text-xl text-primary-800">General</span>
<span class="text-xl text-primary-800">General</span> <MachineAvatar name={machineName()} />
<MachineAvatar name={machineName()} /> <Form onSubmit={handleSubmit} class="grid grid-cols-12 gap-y-4">
<Field name="machine.name"> <Field name="machine.name">
{(field, props) => ( {(field, props) => (
<TextInput <TextInput
formStore={formStore}
inputProps={props} inputProps={props}
label="Name" label="Name"
value={field.value ?? ""} value={field.value ?? ""}
@@ -321,22 +321,23 @@ const MachineForm = (props: MachineDetailsProps) => {
</Field> </Field>
<Field name="machine.tags" type="string[]"> <Field name="machine.tags" type="string[]">
{(field, props) => ( {(field, props) => (
<For each={field.value}> <>
{(tag) => ( <InputLabel class="col-span-2">Tags</InputLabel>
<label class="p-1"> <span class="col-span-10">
Tags <For each={field.value}>
<span class="mx-2 w-fit rounded-full px-3 py-1 bg-inv-4 fg-inv-1"> {(tag) => (
{tag} <span class="mx-2 w-fit rounded-full px-3 py-1 bg-inv-4 fg-inv-1">
</span> {tag}
</label> </span>
)} )}
</For> </For>
</span>
</>
)} )}
</Field> </Field>
<Field name="machine.description"> <Field name="machine.description">
{(field, props) => ( {(field, props) => (
<TextInput <TextInput
formStore={formStore}
inputProps={props} inputProps={props}
label="Description" label="Description"
value={field.value ?? ""} value={field.value ?? ""}
@@ -348,16 +349,24 @@ const MachineForm = (props: MachineDetailsProps) => {
</Field> </Field>
<Field name="hw_config"> <Field name="hw_config">
{(field, props) => ( {(field, props) => (
<label>Hardware report: {field.value || "None"}</label> <>
<InputLabel class="col-span-2">
Hardware Configuration
</InputLabel>
<span class="col-span-10">{field.value || "None"}</span>
</>
)} )}
</Field> </Field>
<Field name="disk_schema"> <Field name="disk_schema">
{(field, props) => ( {(field, props) => (
<span>Disk schema: {field.value || "None"}</span> <>
<InputLabel class="col-span-2">Disk schema</InputLabel>
<span class="col-span-10">{field.value || "None"}</span>
</>
)} )}
</Field> </Field>
<div class="collapse collapse-arrow" tabindex="0"> <div class="collapse collapse-arrow col-span-full" tabindex="0">
<input type="checkbox" /> <input type="checkbox" />
<div class="collapse-title link px-0 text-xl "> <div class="collapse-title link px-0 text-xl ">
Connection Settings Connection Settings
@@ -366,7 +375,6 @@ const MachineForm = (props: MachineDetailsProps) => {
<Field name="machine.deploy.targetHost"> <Field name="machine.deploy.targetHost">
{(field, props) => ( {(field, props) => (
<TextInput <TextInput
formStore={formStore}
inputProps={props} inputProps={props}
label="Target Host" label="Target Host"
value={field.value ?? ""} value={field.value ?? ""}
@@ -411,7 +419,7 @@ const MachineForm = (props: MachineDetailsProps) => {
</div> </div>
{ {
<div class="card-actions justify-end"> <div class="card-actions col-span-full justify-end">
<Button <Button
type="submit" type="submit"
disabled={formStore.submitting || !formStore.dirty} disabled={formStore.submitting || !formStore.dirty}
@@ -420,8 +428,8 @@ const MachineForm = (props: MachineDetailsProps) => {
</Button> </Button>
</div> </div>
} }
</div> </Form>
</Form> </div>
<div class="card-body"> <div class="card-body">
<div class="divider"></div> <div class="divider"></div>
@@ -579,11 +587,10 @@ function WifiModule(props: MachineWifiProps) {
<span class="text-neutral">Preconfigure wireless networks</span> <span class="text-neutral">Preconfigure wireless networks</span>
<For each={nets()}> <For each={nets()}>
{(_, idx) => ( {(_, idx) => (
<div class="flex gap-4"> <div class="grid grid-cols-2">
<Field name={`networks.${idx()}.ssid`}> <Field name={`networks.${idx()}.ssid`}>
{(field, props) => ( {(field, props) => (
<TextInput <TextInput
formStore={formStore}
inputProps={props} inputProps={props}
label="Name" label="Name"
value={field.value ?? ""} value={field.value ?? ""}
@@ -595,7 +602,6 @@ function WifiModule(props: MachineWifiProps) {
<Field name={`networks.${idx()}.password`}> <Field name={`networks.${idx()}.password`}>
{(field, props) => ( {(field, props) => (
<TextInput <TextInput
formStore={formStore}
inputProps={props} inputProps={props}
label="Password" label="Password"
value={field.value ?? ""} value={field.value ?? ""}

View File

@@ -1,29 +1,13 @@
import { callApi } from "@/src/api";
import { activeURI } from "@/src/App"; import { activeURI } from "@/src/App";
import { BackButton } from "@/src/components/BackButton"; import { BackButton } from "@/src/components/BackButton";
import { createModulesQuery } from "@/src/queries"; import { createModulesQuery } from "@/src/queries";
import { useParams, useNavigate } from "@solidjs/router"; import { useParams, useNavigate } from "@solidjs/router";
import { import { createEffect, For, Match, Switch } from "solid-js";
createEffect,
createSignal,
For,
JSX,
Match,
Show,
Switch,
} from "solid-js";
import { SolidMarkdown } from "solid-markdown"; import { SolidMarkdown } from "solid-markdown";
import toast from "solid-toast";
import { ModuleInfo } from "./list"; import { ModuleInfo } from "./list";
import { createQuery } from "@tanstack/solid-query"; import { createQuery } from "@tanstack/solid-query";
import { JSONSchema7 } from "json-schema"; import { JSONSchema7 } from "json-schema";
import { TextInput } from "@/src/components/TextInput"; import { SubmitHandler } from "@modular-forms/solid";
import {
createForm,
getValue,
setValue,
SubmitHandler,
} from "@modular-forms/solid";
import { DynForm } from "@/src/Form/form"; import { DynForm } from "@/src/Form/form";
import { Button } from "@/src/components/button"; import { Button } from "@/src/components/button";
import Icon from "@/src/components/icon"; import Icon from "@/src/components/icon";