From 8418231c43938b43d3046607f6609c4b73649e65 Mon Sep 17 00:00:00 2001 From: dswbx Date: Wed, 5 Feb 2025 16:11:53 +0100 Subject: [PATCH] improved media settings implementation --- app/package.json | 2 +- app/src/core/utils/strings.ts | 2 +- app/src/media/media-schema.ts | 4 +- .../adapters/StorageCloudinaryAdapter.ts | 2 +- .../StorageLocalAdapter.ts | 2 +- .../storage/adapters/StorageS3Adapter.ts | 3 +- app/src/ui/components/buttons/Button.tsx | 9 +- .../ui/components/form/Formy/components.tsx | 37 ++++++ .../form/json-schema-form/AnyOfField.tsx | 31 +++-- .../form/json-schema-form/Field.tsx | 22 ++-- .../form/json-schema-form/FieldWrapper.tsx | 8 +- .../components/form/json-schema-form/Form.tsx | 68 ++++++---- .../form/json-schema-form/ObjectField.tsx | 7 +- .../components/form/json-schema-form/utils.ts | 29 ++++- app/src/ui/components/modal/Modal2.tsx | 2 +- app/src/ui/components/radix/ScrollArea.tsx | 22 ---- app/src/ui/components/radix/extend.tsx | 86 ------------- app/src/ui/layouts/AppShell/AppShell.tsx | 16 ++- app/src/ui/routes/media/_media.root.tsx | 2 +- app/src/ui/routes/media/media.settings.tsx | 118 ++++++++++++------ app/src/ui/routes/test/index.tsx | 4 +- app/src/ui/routes/test/tests/formy-test.tsx | 17 +++ .../routes/test/tests/json-schema-form3.tsx | 2 +- app/src/ui/styles.css | 6 +- bun.lockb | Bin 1076264 -> 1110728 bytes 25 files changed, 291 insertions(+), 210 deletions(-) delete mode 100644 app/src/ui/components/radix/ScrollArea.tsx delete mode 100644 app/src/ui/components/radix/extend.tsx create mode 100644 app/src/ui/routes/test/tests/formy-test.tsx diff --git a/app/package.json b/app/package.json index 4373a2a..1e9f061 100644 --- a/app/package.json +++ b/app/package.json @@ -54,6 +54,7 @@ "lodash-es": "^4.17.21", "oauth4webapi": "^2.11.1", "object-path-immutable": "^4.1.2", + "radix-ui": "^1.1.2", "swr": "^2.2.5" }, "devDependencies": { @@ -64,7 +65,6 @@ "@hono/zod-validator": "^0.4.1", "@hookform/resolvers": "^3.9.1", "@libsql/kysely-libsql": "^0.4.1", - "@radix-ui/react-scroll-area": "^1.2.0", "@rjsf/core": "^5.22.2", "@tabler/icons-react": "3.18.0", "@types/node": "^22.10.0", diff --git a/app/src/core/utils/strings.ts b/app/src/core/utils/strings.ts index fc268e4..a28d7ea 100644 --- a/app/src/core/utils/strings.ts +++ b/app/src/core/utils/strings.ts @@ -81,7 +81,7 @@ export function identifierToHumanReadable(str: string) { case "SCREAMING_SNAKE_CASE": return snakeToPascalWithSpaces(str.toLowerCase()); case "unknown": - return str; + return ucFirst(str); } } export function autoFormatString(str: string) { diff --git a/app/src/media/media-schema.ts b/app/src/media/media-schema.ts index 64a52ba..f1d793f 100644 --- a/app/src/media/media-schema.ts +++ b/app/src/media/media-schema.ts @@ -16,7 +16,8 @@ export function buildMediaSchema() { config: adapter.schema }, { - title: name, + title: adapter.schema.title ?? name, + description: adapter.schema.description, additionalProperties: false } ); @@ -32,6 +33,7 @@ export function buildMediaSchema() { { body_max_size: Type.Optional( Type.Number({ + minimum: 0, description: "Max size of the body in bytes. Leave blank for unlimited." }) ) diff --git a/app/src/media/storage/adapters/StorageCloudinaryAdapter.ts b/app/src/media/storage/adapters/StorageCloudinaryAdapter.ts index 771b389..cfb4100 100644 --- a/app/src/media/storage/adapters/StorageCloudinaryAdapter.ts +++ b/app/src/media/storage/adapters/StorageCloudinaryAdapter.ts @@ -9,7 +9,7 @@ export const cloudinaryAdapterConfig = Type.Object( api_secret: Type.String(), upload_preset: Type.Optional(Type.String()) }, - { title: "Cloudinary" } + { title: "Cloudinary", description: "Cloudinary media storage" } ); export type CloudinaryConfig = Static; diff --git a/app/src/media/storage/adapters/StorageLocalAdapter/StorageLocalAdapter.ts b/app/src/media/storage/adapters/StorageLocalAdapter/StorageLocalAdapter.ts index b6c2650..2c142ff 100644 --- a/app/src/media/storage/adapters/StorageLocalAdapter/StorageLocalAdapter.ts +++ b/app/src/media/storage/adapters/StorageLocalAdapter/StorageLocalAdapter.ts @@ -7,7 +7,7 @@ export const localAdapterConfig = Type.Object( { path: Type.String({ default: "./" }) }, - { title: "Local" } + { title: "Local", description: "Local file system storage" } ); export type LocalAdapterConfig = Static; diff --git a/app/src/media/storage/adapters/StorageS3Adapter.ts b/app/src/media/storage/adapters/StorageS3Adapter.ts index 90c3cb2..b330d64 100644 --- a/app/src/media/storage/adapters/StorageS3Adapter.ts +++ b/app/src/media/storage/adapters/StorageS3Adapter.ts @@ -25,7 +25,8 @@ export const s3AdapterConfig = Type.Object( }) }, { - title: "S3" + title: "AWS S3", + description: "AWS S3 or compatible storage" } ); diff --git a/app/src/ui/components/buttons/Button.tsx b/app/src/ui/components/buttons/Button.tsx index a9a55e2..eb5ad35 100644 --- a/app/src/ui/components/buttons/Button.tsx +++ b/app/src/ui/components/buttons/Button.tsx @@ -1,4 +1,5 @@ import type React from "react"; +import { Children } from "react"; import { forwardRef } from "react"; import { twMerge } from "tailwind-merge"; import { Link } from "ui/components/wouter/Link"; @@ -19,7 +20,7 @@ const styles = { default: "bg-primary/5 hover:bg-primary/10 link text-primary/70", primary: "bg-primary hover:bg-primary/80 link text-background", ghost: "bg-transparent hover:bg-primary/5 link text-primary/70", - outline: "border border-primary/70 bg-transparent hover:bg-primary/5 link text-primary/70", + outline: "border border-primary/50 bg-transparent hover:bg-primary/5 link text-primary/80", red: "dark:bg-red-950 dark:hover:bg-red-900 bg-red-100 hover:bg-red-200 link text-primary/70", subtlered: "dark:text-red-950 text-red-700 dark:hover:bg-red-900 bg-transparent hover:bg-red-50 link" @@ -58,7 +59,11 @@ const Base = ({ children: ( <> {IconLeft && } - {children && {children}} + {children && Children.count(children) === 1 ? ( + {children} + ) : ( + children + )} {IconRight && } ) diff --git a/app/src/ui/components/form/Formy/components.tsx b/app/src/ui/components/form/Formy/components.tsx index 7bf691f..74d4717 100644 --- a/app/src/ui/components/form/Formy/components.tsx +++ b/app/src/ui/components/form/Formy/components.tsx @@ -1,6 +1,9 @@ import { getBrowser } from "core/utils"; import type { Field } from "data"; +import { Switch as RadixSwitch } from "radix-ui"; import { + type ChangeEventHandler, + type ComponentPropsWithoutRef, type ElementType, forwardRef, useEffect, @@ -26,6 +29,7 @@ export const Group = ({ className={twMerge( "flex flex-col gap-1.5", as === "fieldset" && "border border-primary/10 p-3 rounded-md", + as === "fieldset" && error && "border-red-500", error && "text-red-500", props.className )} @@ -171,6 +175,39 @@ export const BooleanInput = forwardRef, + "name" | "required" | "disabled" | "checked" | "defaultChecked" | "id" | "type" + > & { + value?: SwitchValue; + onChange?: (e: { target: { value: boolean } }) => void; + onCheckedChange?: (checked: boolean) => void; + } +>(({ type, ...props }, ref) => { + return ( + { + props.onChange?.({ target: { value: bool } }); + }} + {...(props as any)} + checked={ + typeof props.checked !== "undefined" + ? props.checked + : typeof props.value !== "undefined" + ? Boolean(props.value) + : undefined + } + ref={ref} + > + + + ); +}); + export const Select = forwardRef< HTMLSelectElement, React.ComponentProps<"select"> & { diff --git a/app/src/ui/components/form/json-schema-form/AnyOfField.tsx b/app/src/ui/components/form/json-schema-form/AnyOfField.tsx index 87670cf..2a526bd 100644 --- a/app/src/ui/components/form/json-schema-form/AnyOfField.tsx +++ b/app/src/ui/components/form/json-schema-form/AnyOfField.tsx @@ -1,5 +1,7 @@ +import type { JsonError } from "json-schema-library"; import type { JSONSchema } from "json-schema-to-ts"; import { type ChangeEvent, type ReactNode, createContext, useContext, useState } from "react"; +import { twMerge } from "tailwind-merge"; import * as Formy from "ui/components/form/Formy"; import { FieldComponent, Field as FormField, type FieldProps as FormFieldProps } from "./Field"; import { FormContextOverride, useFieldContext } from "./Form"; @@ -19,19 +21,18 @@ export type AnyOfFieldContext = { selected: number | null; select: (index: number | null) => void; options: string[]; + errors: JsonError[]; selectSchema: any; }; const AnyOfContext = createContext(undefined!); export const useAnyOfContext = () => { - const ctx = useContext(AnyOfContext); - if (!ctx) throw new Error("useAnyOfContext: no context"); - return ctx; + return useContext(AnyOfContext); }; const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) => { - const { setValue, pointer, lib, value, ...ctx } = useFieldContext(path); + const { setValue, pointer, lib, value, errors, ...ctx } = useFieldContext(path); const schema = _schema ?? ctx.schema; if (!schema) return `AnyOfField(${path}): no schema ${pointer}`; const [matchedIndex, schemas = []] = getMultiSchemaMatched(schema, value); @@ -40,6 +41,7 @@ const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) => const selectSchema = { enum: options }; + //console.log("AnyOf:root", { value, matchedIndex, selected, schema }); const selectedSchema = selected !== null ? (schemas[selected] as Exclude) : undefined; @@ -51,8 +53,19 @@ const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) => return ( + {/*
{JSON.stringify({ value, selected, errors: errors.length }, null, 2)}
*/} {children}
); @@ -62,7 +75,7 @@ const Select = () => { const { selected, select, path, schema, selectSchema } = useAnyOfContext(); function handleSelect(e: ChangeEvent) { - console.log("selected", e.target.value); + //console.log("selected", e.target.value); const i = e.target.value ? Number(e.target.value) : null; select(i); } @@ -81,11 +94,13 @@ const Select = () => { }; const Field = ({ name, label, ...props }: Partial) => { - const { selected, selectedSchema, path } = useAnyOfContext(); + const { selected, selectedSchema, path, errors } = useAnyOfContext(); if (selected === null) return null; return ( - +
0 && "bg-red-500/10")}> + +
); }; diff --git a/app/src/ui/components/form/json-schema-form/Field.tsx b/app/src/ui/components/form/json-schema-form/Field.tsx index 755f93a..904a1a5 100644 --- a/app/src/ui/components/form/json-schema-form/Field.tsx +++ b/app/src/ui/components/form/json-schema-form/Field.tsx @@ -5,7 +5,7 @@ import { ArrayField } from "./ArrayField"; import { FieldWrapper } from "./FieldWrapper"; import { useFieldContext } from "./Form"; import { ObjectField } from "./ObjectField"; -import { coerce, isType } from "./utils"; +import { coerce, isType, isTypeSchema } from "./utils"; export type FieldProps = { name: string; @@ -18,7 +18,8 @@ export type FieldProps = { export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }: FieldProps) => { const { pointer, value, errors, setValue, required, ...ctx } = useFieldContext(name); const schema = _schema ?? ctx.schema; - if (!schema) return `"${name}" (${pointer}) has no schema`; + if (!isTypeSchema(schema)) return
{pointer} has no schema
; + //console.log("field", name, schema); if (isType(schema.type, "object")) { return ; @@ -38,7 +39,7 @@ export const Field = ({ name, schema: _schema, onChange, label: _label, hidden } const value = coerce(e.target.value, schema as any, { required }); //console.log("handleChange", pointer, e.target.value, { value }); - if (!value && !required) { + if (typeof value === "undefined" && !required) { ctx.deleteValue(pointer); } else { setValue(pointer, value); @@ -58,7 +59,6 @@ export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }