diff --git a/app/__test__/api/MediaApi.spec.ts b/app/__test__/api/MediaApi.spec.ts index 785f1ab..a479d19 100644 --- a/app/__test__/api/MediaApi.spec.ts +++ b/app/__test__/api/MediaApi.spec.ts @@ -32,7 +32,7 @@ describe("MediaApi", () => { host, basepath, }); - expect(api.getFileUploadUrl({ path: "path" })).toBe(`${host}${basepath}/upload/path`); + expect(api.getFileUploadUrl({ path: "path" } as any)).toBe(`${host}${basepath}/upload/path`); }); it("should have correct upload headers", () => { diff --git a/app/__test__/auth/strategies/OAuthStrategy.spec.ts b/app/__test__/auth/strategies/OAuthStrategy.spec.ts index eb1a397..93ceae0 100644 --- a/app/__test__/auth/strategies/OAuthStrategy.spec.ts +++ b/app/__test__/auth/strategies/OAuthStrategy.spec.ts @@ -7,8 +7,8 @@ describe("OAuthStrategy", async () => { const strategy = new OAuthStrategy({ type: "oidc", client: { - client_id: process.env.OAUTH_CLIENT_ID, - client_secret: process.env.OAUTH_CLIENT_SECRET, + client_id: process.env.OAUTH_CLIENT_ID!, + client_secret: process.env.OAUTH_CLIENT_SECRET!, }, name: "google", }); @@ -19,11 +19,6 @@ describe("OAuthStrategy", async () => { const config = await strategy.getConfig(); console.log("config", JSON.stringify(config, null, 2)); - const request = await strategy.request({ - redirect_uri, - state, - }); - const server = Bun.serve({ fetch: async (req) => { const url = new URL(req.url); @@ -39,6 +34,11 @@ describe("OAuthStrategy", async () => { return new Response("Bun!"); }, }); + + const request = await strategy.request({ + redirect_uri, + state, + }); console.log("request", request); await new Promise((resolve) => setTimeout(resolve, 100000)); diff --git a/app/__test__/helper.ts b/app/__test__/helper.ts index ae6454f..405e46f 100644 --- a/app/__test__/helper.ts +++ b/app/__test__/helper.ts @@ -4,6 +4,9 @@ import Database from "libsql"; import { format as sqlFormat } from "sql-formatter"; import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data"; import type { em as protoEm } from "../src/data/prototype"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { slugify } from "core/utils/strings"; export function getDummyDatabase(memory: boolean = true): { dummyDb: SqliteDatabase; @@ -71,3 +74,46 @@ export function schemaToEm(s: ReturnType, conn?: Connection): En export const assetsPath = `${import.meta.dir}/_assets`; export const assetsTmpPath = `${import.meta.dir}/_assets/tmp`; + +export async function enableFetchLogging() { + const originalFetch = global.fetch; + + global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const response = await originalFetch(input, init); + const url = input instanceof URL || typeof input === "string" ? input : input.url; + + // Only clone if it's a supported content type + const contentType = response.headers.get("content-type") || ""; + const isSupported = + contentType.includes("json") || + contentType.includes("text") || + contentType.includes("xml"); + + if (isSupported) { + const clonedResponse = response.clone(); + let extension = "txt"; + let body: string; + + if (contentType.includes("json")) { + body = JSON.stringify(await clonedResponse.json(), null, 2); + extension = "json"; + } else if (contentType.includes("xml")) { + body = await clonedResponse.text(); + extension = "xml"; + } else { + body = await clonedResponse.text(); + } + + const fileName = `${new Date().getTime()}_${init?.method ?? "GET"}_${slugify(String(url))}.${extension}`; + const filePath = join(assetsTmpPath, fileName); + + await writeFile(filePath, body); + } + + return response; + }; + + return () => { + global.fetch = originalFetch; + }; +} diff --git a/app/__test__/media/MediaController.spec.ts b/app/__test__/media/MediaController.spec.ts index 71b9cbb..3584317 100644 --- a/app/__test__/media/MediaController.spec.ts +++ b/app/__test__/media/MediaController.spec.ts @@ -39,8 +39,8 @@ function makeName(ext: string) { return randomString(10) + "." + ext; } -/*beforeAll(disableConsoleLog); -afterAll(enableConsoleLog);*/ +beforeAll(disableConsoleLog); +afterAll(enableConsoleLog); describe("MediaController", () => { test.only("accepts direct", async () => { @@ -56,9 +56,9 @@ describe("MediaController", () => { console.log(result); expect(result.name).toBe(name); - /*const destFile = Bun.file(assetsTmpPath + "/" + name); + const destFile = Bun.file(assetsTmpPath + "/" + name); expect(destFile.exists()).resolves.toBe(true); - await destFile.delete();*/ + await destFile.delete(); }); test("accepts form data", async () => { diff --git a/app/__test__/media/adapters/StorageS3Adapter.spec.ts b/app/__test__/media/adapters/StorageS3Adapter.spec.ts index ad777e7..7b4a0a4 100644 --- a/app/__test__/media/adapters/StorageS3Adapter.spec.ts +++ b/app/__test__/media/adapters/StorageS3Adapter.spec.ts @@ -1,8 +1,9 @@ -import { describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { randomString } from "../../../src/core/utils"; import { StorageS3Adapter } from "../../../src/media"; import { config } from "dotenv"; +//import { enableFetchLogging } from "../../helper"; const dotenvOutput = config({ path: `${import.meta.dir}/../../../.env` }); const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_S3_URL } = dotenvOutput.parsed!; @@ -11,7 +12,17 @@ const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_ const ALL_TESTS = !!process.env.ALL_TESTS; console.log("ALL_TESTS?", ALL_TESTS); -describe.skipIf(true)("StorageS3Adapter", async () => { +/* +// @todo: preparation to mock s3 calls + replace fast-xml-parser +let cleanup: () => void; +beforeAll(async () => { + cleanup = await enableFetchLogging(); +}); +afterAll(() => { + cleanup(); +}); */ + +describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => { if (ALL_TESTS) return; const versions = [ @@ -66,7 +77,7 @@ describe.skipIf(true)("StorageS3Adapter", async () => { test.skipIf(disabled("putObject"))("puts an object", async () => { objects = (await adapter.listObjects()).length; - expect(await adapter.putObject(filename, file)).toBeString(); + expect(await adapter.putObject(filename, file as any)).toBeString(); }); test.skipIf(disabled("listObjects"))("lists objects", async () => { diff --git a/app/__test__/modules/AppAuth.spec.ts b/app/__test__/modules/AppAuth.spec.ts index 332b999..ca861d5 100644 --- a/app/__test__/modules/AppAuth.spec.ts +++ b/app/__test__/modules/AppAuth.spec.ts @@ -157,7 +157,7 @@ describe("AppAuth", () => { const authField = make(name, _authFieldProto as any); const field = users.field(name)!; for (const prop of props) { - expect(field.config[prop]).toBe(authField.config[prop]); + expect(field.config[prop]).toEqual(authField.config[prop]); } } }); diff --git a/app/bknd.config.js.ignore b/app/bknd.config.js.ignore deleted file mode 100644 index 5f279d3..0000000 --- a/app/bknd.config.js.ignore +++ /dev/null @@ -1,13 +0,0 @@ -//import type { BkndConfig } from "./src"; - -export default { - app: { - connection: { - type: "libsql", - config: { - //url: "http://localhost:8080" - url: ":memory:" - } - } - } -}; diff --git a/app/build.esbuild.ts b/app/build.esbuild.ts deleted file mode 100644 index d9fa6f4..0000000 --- a/app/build.esbuild.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { $, type Subprocess } from "bun"; -import * as esbuild from "esbuild"; -import postcss from "esbuild-postcss"; -import { entryOutputMeta } from "./internal/esbuild.entry-output-meta.plugin"; -import { guessMimeType } from "./src/media/storage/mime-types"; - -const args = process.argv.slice(2); -const watch = args.includes("--watch"); -const minify = args.includes("--minify"); -const types = args.includes("--types"); -const sourcemap = args.includes("--sourcemap"); - -type BuildOptions = esbuild.BuildOptions & { name: string }; - -const baseOptions: Partial> & { plugins?: any[] } = { - minify, - sourcemap, - metafile: true, - format: "esm", - drop: ["console", "debugger"], - loader: { - ".svg": "dataurl", - }, - define: { - __isDev: "0", - }, -}; - -// @ts-ignore -type BuildFn = (format?: "esm" | "cjs") => BuildOptions; - -// build BE -const builds: Record = { - backend: (format = "esm") => ({ - ...baseOptions, - name: `backend ${format}`, - entryPoints: [ - "src/index.ts", - "src/data/index.ts", - "src/core/index.ts", - "src/core/utils/index.ts", - "src/ui/index.ts", - "src/ui/main.css", - ], - outdir: "dist", - outExtension: { ".js": format === "esm" ? ".js" : ".cjs" }, - platform: "browser", - splitting: false, - bundle: true, - plugins: [postcss()], - //target: "es2022", - format, - }), - /*components: (format = "esm") => ({ - ...baseOptions, - name: `components ${format}`, - entryPoints: ["src/ui/index.ts", "src/ui/main.css"], - outdir: "dist/ui", - outExtension: { ".js": format === "esm" ? ".js" : ".cjs" }, - format, - platform: "browser", - splitting: false, - //target: "es2022", - bundle: true, - //external: ["react", "react-dom", "@tanstack/react-query-devtools"], - plugins: [postcss()], - loader: { - ".svg": "dataurl", - ".js": "jsx" - } - }),*/ - static: (format = "esm") => ({ - ...baseOptions, - name: `static ${format}`, - entryPoints: ["src/ui/main.tsx", "src/ui/main.css"], - entryNames: "[dir]/[name]-[hash]", - outdir: "dist/static", - outExtension: { ".js": format === "esm" ? ".js" : ".cjs" }, - platform: "browser", - bundle: true, - splitting: true, - inject: ["src/ui/inject.js"], - target: "es2022", - format, - loader: { - ".svg": "dataurl", - ".js": "jsx", - }, - define: { - __isDev: "0", - "process.env.NODE_ENV": '"production"', - }, - chunkNames: "chunks/[name]-[hash]", - plugins: [ - postcss(), - entryOutputMeta(async (info) => { - const manifest: Record = {}; - const toAsset = (output: string) => { - const name = output.split("/").pop()!; - return { - name, - path: output, - mime: guessMimeType(name), - }; - }; - for (const { output, meta } of info) { - manifest[meta.entryPoint as string] = toAsset(output); - if (meta.cssBundle) { - manifest["src/ui/main.css"] = toAsset(meta.cssBundle); - } - } - - const manifest_file = "dist/static/manifest.json"; - await Bun.write(manifest_file, JSON.stringify(manifest, null, 2)); - console.log(`Manifest written to ${manifest_file}`, manifest); - }), - ], - }), -}; - -function adapter(adapter: string, overrides: Partial = {}): BuildOptions { - return { - ...baseOptions, - name: `adapter ${adapter} ${overrides?.format === "cjs" ? "cjs" : "esm"}`, - entryPoints: [`src/adapter/${adapter}`], - platform: "neutral", - outfile: `dist/adapter/${adapter}/index.${overrides?.format === "cjs" ? "cjs" : "js"}`, - external: [ - "cloudflare:workers", - "@hono*", - "hono*", - "bknd*", - "*.html", - "node*", - "react*", - "next*", - "libsql", - "@libsql*", - ], - splitting: false, - treeShaking: true, - bundle: true, - ...overrides, - }; -} -const adapters = [ - adapter("vite", { platform: "node" }), - adapter("cloudflare"), - adapter("nextjs", { platform: "node", format: "esm" }), - adapter("nextjs", { platform: "node", format: "cjs" }), - adapter("remix", { format: "esm" }), - adapter("remix", { format: "cjs" }), - adapter("bun"), - adapter("node", { platform: "node", format: "esm" }), - adapter("node", { platform: "node", format: "cjs" }), -]; - -const collect = [ - builds.static(), - builds.backend(), - //builds.components(), - builds.backend("cjs"), - //builds.components("cjs"), - ...adapters, -]; - -if (watch) { - const _state: { - timeout: Timer | undefined; - cleanup: Subprocess | undefined; - building: Subprocess | undefined; - } = { - timeout: undefined, - cleanup: undefined, - building: undefined, - }; - - async function rebuildTypes() { - if (!types) return; - if (_state.timeout) { - clearTimeout(_state.timeout); - if (_state.cleanup) _state.cleanup.kill(); - if (_state.building) _state.building.kill(); - } - _state.timeout = setTimeout(async () => { - _state.cleanup = Bun.spawn(["bun", "clean:types"], { - onExit: () => { - _state.cleanup = undefined; - _state.building = Bun.spawn(["bun", "build:types"], { - onExit: () => { - _state.building = undefined; - console.log("Types rebuilt"); - }, - }); - }, - }); - }, 1000); - } - - for (const { name, ...build } of collect) { - const ctx = await esbuild.context({ - ...build, - plugins: [ - ...(build.plugins ?? []), - { - name: "rebuild-notify", - setup(build) { - build.onEnd((result) => { - console.log(`rebuilt ${name} with ${result.errors.length} errors`); - rebuildTypes(); - }); - }, - }, - ], - }); - ctx.watch(); - } -} else { - await $`rm -rf dist`; - - async function _build() { - let i = 0; - const count = collect.length; - for await (const { name, ...build } of collect) { - await esbuild.build({ - ...build, - plugins: [ - ...(build.plugins || []), - { - name: "progress", - setup(build) { - i++; - build.onEnd((result) => { - const errors = result.errors.length; - const from = String(i).padStart(String(count).length); - console.log(`[${from}/${count}] built ${name} with ${errors} errors`); - }); - }, - }, - ], - }); - } - - console.log("All builds complete"); - } - - async function _buildtypes() { - if (!types) return; - Bun.spawn(["bun", "build:types"], { - onExit: () => { - console.log("Types rebuilt"); - }, - }); - } - - await Promise.all([_build(), _buildtypes()]); -} diff --git a/app/build.ts b/app/build.ts index 6f7872a..0022a80 100644 --- a/app/build.ts +++ b/app/build.ts @@ -46,10 +46,18 @@ if (types && !watch) { buildTypes(); } +function banner(title: string) { + console.log(""); + console.log("=".repeat(40)); + console.log(title.toUpperCase()); + console.log("-".repeat(40)); +} + /** * Building backend and general API */ async function buildApi() { + banner("Building API"); await tsup.build({ minify, sourcemap, @@ -109,6 +117,7 @@ async function buildUi() { }, } satisfies tsup.Options; + banner("Building UI"); await tsup.build({ ...base, entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"], @@ -119,6 +128,7 @@ async function buildUi() { }, }); + banner("Building Client"); await tsup.build({ ...base, entry: ["src/ui/client/index.ts"], @@ -136,6 +146,7 @@ async function buildUi() { * - ui/client is external, and after built replaced with "bknd/client" */ async function buildUiElements() { + banner("Building UI Elements"); await tsup.build({ minify, sourcemap, @@ -205,6 +216,7 @@ function baseConfig(adapter: string, overrides: Partial = {}): tsu } async function buildAdapters() { + banner("Building Adapters"); // base adapter handles await tsup.build({ ...baseConfig(""), diff --git a/app/package.json b/app/package.json index 513b778..442c9a0 100644 --- a/app/package.json +++ b/app/package.json @@ -32,81 +32,83 @@ }, "license": "FSL-1.1-MIT", "dependencies": { - "@cfworker/json-schema": "^2.0.1", + "@cfworker/json-schema": "^4.1.1", "@codemirror/lang-html": "^6.4.9", "@codemirror/lang-json": "^6.0.1", - "@codemirror/lang-liquid": "^6.2.1", - "@hello-pangea/dnd": "^17.0.0", + "@codemirror/lang-liquid": "^6.2.2", + "@hello-pangea/dnd": "^18.0.1", "@libsql/client": "^0.14.0", - "@mantine/core": "^7.13.4", - "@sinclair/typebox": "^0.32.34", - "@tanstack/react-form": "0.19.2", - "@uiw/react-codemirror": "^4.23.6", - "@xyflow/react": "^12.3.2", - "aws4fetch": "^1.0.18", + "@mantine/core": "^7.17.1", + "@mantine/hooks": "^7.17.1", + "@sinclair/typebox": "^0.34.30", + "@tanstack/react-form": "^1.0.5", + "@uiw/react-codemirror": "^4.23.10", + "@xyflow/react": "^12.4.4", + "aws4fetch": "^1.0.20", "dayjs": "^1.11.13", - "fast-xml-parser": "^4.4.0", - "hono": "^4.6.12", + "fast-xml-parser": "^5.0.8", + "hono": "^4.7.4", "json-schema-form-react": "^0.0.2", "json-schema-library": "^10.0.0-rc7", "json-schema-to-ts": "^3.1.1", - "kysely": "^0.27.4", - "liquidjs": "^10.15.0", + "kysely": "^0.27.6", + "liquidjs": "^10.21.0", "lodash-es": "^4.17.21", "oauth4webapi": "^2.11.1", "object-path-immutable": "^4.1.2", "picocolors": "^1.1.1", - "radix-ui": "^1.1.2", - "swr": "^2.2.5" + "radix-ui": "^1.1.3", + "swr": "^2.3.3" }, "devDependencies": { - "@aws-sdk/client-s3": "^3.613.0", + "@aws-sdk/client-s3": "^3.758.0", "@bluwy/giget-core": "^0.1.2", "@dagrejs/dagre": "^1.1.4", - "@mantine/modals": "^7.13.4", - "@mantine/notifications": "^7.13.4", - "@hono/typebox-validator": "^0.2.6", - "@hono/vite-dev-server": "^0.17.0", - "@hono/zod-validator": "^0.4.1", - "@hookform/resolvers": "^3.9.1", + "@hono/typebox-validator": "^0.3.2", + "@hono/vite-dev-server": "^0.19.0", + "@hookform/resolvers": "^4.1.3", "@libsql/kysely-libsql": "^0.4.1", + "@mantine/modals": "^7.17.1", + "@mantine/notifications": "^7.17.1", "@rjsf/core": "5.22.2", "@tabler/icons-react": "3.18.0", - "@types/node": "^22.10.0", - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.3.3", - "autoprefixer": "^10.4.20", + "@tailwindcss/postcss": "^4.0.12", + "@tailwindcss/vite": "^4.0.12", + "@types/node": "^22.13.10", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.21", "clsx": "^2.1.1", "dotenv": "^16.4.7", - "esbuild-postcss": "^0.0.4", - "jotai": "^2.10.1", + "jotai": "^2.12.2", "kysely-d1": "^0.3.0", "open": "^10.1.0", "openapi-types": "^12.1.3", - "postcss": "^8.4.47", + "postcss": "^8.5.3", "postcss-preset-mantine": "^1.17.0", "postcss-simple-vars": "^7.0.1", - "react-hook-form": "^7.53.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", "react-icons": "5.2.1", - "react-json-view-lite": "^2.0.1", - "sql-formatter": "^15.4.9", - "tailwind-merge": "^2.5.4", - "tailwindcss": "^3.4.14", + "react-json-view-lite": "^2.4.1", + "sql-formatter": "^15.4.11", + "tailwind-merge": "^3.0.2", + "tailwindcss": "^4.0.12", "tailwindcss-animate": "^1.0.7", - "tsc-alias": "^1.8.10", - "tsup": "^8.3.5", - "vite": "^5.4.10", - "vite-plugin-static-copy": "^2.0.0", - "vite-tsconfig-paths": "^5.0.1", - "wouter": "^3.3.5" + "tsc-alias": "^1.8.11", + "tsup": "^8.4.0", + "vite": "^6.2.1", + "vite-tsconfig-paths": "^5.1.4", + "wouter": "^3.6.0" }, "optionalDependencies": { - "@hono/node-server": "^1.13.7" + "@hono/node-server": "^1.13.8" }, "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" + "react": "^19.x", + "react-dom": "^19.x" }, "main": "./dist/index.js", "module": "./dist/index.js", @@ -228,4 +230,4 @@ "bun", "node" ] -} \ No newline at end of file +} diff --git a/app/postcss.config.js b/app/postcss.config.js index 5633993..61ce526 100644 --- a/app/postcss.config.js +++ b/app/postcss.config.js @@ -1,9 +1,6 @@ export default { plugins: { - "postcss-import": {}, - "tailwindcss/nesting": {}, - tailwindcss: {}, - autoprefixer: {}, + "@tailwindcss/postcss": {}, "postcss-preset-mantine": {}, "postcss-simple-vars": { variables: { diff --git a/app/src/core/utils/strings.ts b/app/src/core/utils/strings.ts index a28d7ea..be115f5 100644 --- a/app/src/core/utils/strings.ts +++ b/app/src/core/utils/strings.ts @@ -118,3 +118,17 @@ export function patternMatch(target: string, pattern: RegExp | string): boolean } return false; } + +export function slugify(str: string): string { + return ( + String(str) + .normalize("NFKD") // split accented characters into their base characters and diacritical marks + // biome-ignore lint/suspicious/noMisleadingCharacterClass: + .replace(/[\u0300-\u036f]/g, "") // remove all the accents, which happen to be all in the \u03xx UNICODE block. + .trim() // trim leading or trailing whitespace + .toLowerCase() // convert to lowercase + .replace(/[^a-z0-9 -]/g, "") // remove non-alphanumeric characters + .replace(/\s+/g, "-") // replace spaces with hyphens + .replace(/-+/g, "-") // remove consecutive hyphens + ); +} diff --git a/app/src/modules/server/AppServer.ts b/app/src/modules/server/AppServer.ts index 07445ff..a1de99a 100644 --- a/app/src/modules/server/AppServer.ts +++ b/app/src/modules/server/AppServer.ts @@ -1,4 +1,4 @@ -import { Exception } from "core"; +import { Exception, isDebug } from "core"; import { type Static, StringEnum, Type } from "core/utils"; import { cors } from "hono/cors"; import { Module } from "modules/Module"; @@ -102,6 +102,12 @@ export class AppServer extends Module { return c.json(err.toJSON(), err.code as any); } + if (err instanceof Error) { + if (isDebug()) { + return c.json({ error: err.message, stack: err.stack }, 500); + } + } + return c.json({ error: err.message }, 500); }); this.setBuilt(); diff --git a/app/src/ui/client/BkndProvider.tsx b/app/src/ui/client/BkndProvider.tsx index 6cb517a..737ba24 100644 --- a/app/src/ui/client/BkndProvider.tsx +++ b/app/src/ui/client/BkndProvider.tsx @@ -41,7 +41,7 @@ export function BkndProvider({ useState>(); const [fetched, setFetched] = useState(false); const [error, setError] = useState(); - const errorShown = useRef(); + const errorShown = useRef(false); const fetching = useRef(Fetching.None); const [local_version, set_local_version] = useState(0); const api = useApi(); @@ -101,11 +101,13 @@ export function BkndProvider({ } startTransition(() => { - setSchema(newSchema); - setWithSecrets(_includeSecrets); - setFetched(true); - set_local_version((v) => v + 1); - fetching.current = Fetching.None; + document.startViewTransition(() => { + setSchema(newSchema); + setWithSecrets(_includeSecrets); + setFetched(true); + set_local_version((v) => v + 1); + fetching.current = Fetching.None; + }); }); } diff --git a/app/src/ui/client/schema/system/use-bknd-system.ts b/app/src/ui/client/schema/system/use-bknd-system.ts index 891913b..81fecda 100644 --- a/app/src/ui/client/schema/system/use-bknd-system.ts +++ b/app/src/ui/client/schema/system/use-bknd-system.ts @@ -36,6 +36,10 @@ export function useBkndSystemTheme() { return { theme: $sys.theme, set: $sys.actions.theme.set, - toggle: () => $sys.actions.theme.toggle(), + toggle: async () => { + document.startViewTransition(async () => { + await $sys.actions.theme.toggle(); + }); + }, }; } diff --git a/app/src/ui/components/buttons/Button.tsx b/app/src/ui/components/buttons/Button.tsx index f05483f..0cafa13 100644 --- a/app/src/ui/components/buttons/Button.tsx +++ b/app/src/ui/components/buttons/Button.tsx @@ -23,7 +23,7 @@ const styles = { outline: "border border-primary/20 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", + "dark:text-red-700 text-red-700 dark:hover:bg-red-900 dark:hover:text-red-200 bg-transparent hover:bg-red-50 link", }; export type BaseProps = { @@ -51,7 +51,7 @@ const Base = ({ }: BaseProps) => ({ ...props, className: twMerge( - "flex flex-row flex-nowrap items-center font-semibold disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed transition-[opacity,background-color,color,border-color]", + "flex flex-row flex-nowrap items-center !font-semibold disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed transition-[opacity,background-color,color,border-color]", sizes[size ?? "default"], styles[variant ?? "default"], props.className, diff --git a/app/src/ui/components/canvas/Canvas.tsx b/app/src/ui/components/canvas/Canvas.tsx index 29d1f00..974e3d2 100644 --- a/app/src/ui/components/canvas/Canvas.tsx +++ b/app/src/ui/components/canvas/Canvas.tsx @@ -19,7 +19,7 @@ type CanvasProps = ReactFlowProps & { externalProvider?: boolean; backgroundStyle?: "lines" | "dots"; minimap?: boolean | MiniMapProps; - children?: JSX.Element | ReactNode; + children?: Element | ReactNode; onDropNewNode?: (base: any) => any; onDropNewEdge?: (base: any) => any; }; diff --git a/app/src/ui/components/code/CodeEditor.tsx b/app/src/ui/components/code/CodeEditor.tsx index 095656d..480cb79 100644 --- a/app/src/ui/components/code/CodeEditor.tsx +++ b/app/src/ui/components/code/CodeEditor.tsx @@ -1,8 +1,7 @@ import { default as CodeMirror, type ReactCodeMirrorProps } from "@uiw/react-codemirror"; -import { useBknd } from "ui/client/bknd"; - import { json } from "@codemirror/lang-json"; import { type LiquidCompletionConfig, liquid } from "@codemirror/lang-liquid"; +import { useTheme } from "ui/client/use-theme"; export type CodeEditorProps = ReactCodeMirrorProps & { _extensions?: Partial<{ @@ -17,8 +16,7 @@ export default function CodeEditor({ _extensions = {}, ...props }: CodeEditorProps) { - const b = useBknd(); - const theme = b.app.getAdminConfig().color_scheme; + const { theme } = useTheme(); const _basicSetup: Partial = !editable ? { ...(typeof basicSetup === "object" ? basicSetup : {}), diff --git a/app/src/ui/components/form/json-schema/templates/ArrayFieldTemplate.tsx b/app/src/ui/components/form/json-schema/templates/ArrayFieldTemplate.tsx index 2622af0..5a55c9f 100644 --- a/app/src/ui/components/form/json-schema/templates/ArrayFieldTemplate.tsx +++ b/app/src/ui/components/form/json-schema/templates/ArrayFieldTemplate.tsx @@ -74,6 +74,7 @@ export default function ArrayFieldTemplate< {items.map( ({ key, children, ...itemProps }: ArrayFieldTemplateItemType) => { const newChildren = cloneElement(children, { + // @ts-ignore ...children.props, name: undefined, title: undefined, diff --git a/app/src/ui/components/overlay/Dropdown.tsx b/app/src/ui/components/overlay/Dropdown.tsx index 0bc1228..5d2ff4f 100644 --- a/app/src/ui/components/overlay/Dropdown.tsx +++ b/app/src/ui/components/overlay/Dropdown.tsx @@ -4,6 +4,7 @@ import { type ComponentPropsWithoutRef, Fragment, type ReactElement, + type ReactNode, cloneElement, useState, } from "react"; @@ -11,7 +12,7 @@ import { twMerge } from "tailwind-merge"; import { useEvent } from "ui/hooks/use-event"; export type DropdownItem = - | (() => JSX.Element) + | (() => ReactNode) | { label: string | ReactElement; icon?: any; diff --git a/app/src/ui/elements/media/Dropzone.tsx b/app/src/ui/elements/media/Dropzone.tsx index e1e97b7..952bca6 100644 --- a/app/src/ui/elements/media/Dropzone.tsx +++ b/app/src/ui/elements/media/Dropzone.tsx @@ -2,6 +2,7 @@ import type { DB } from "core"; import { type ComponentPropsWithRef, type ComponentPropsWithoutRef, + type ReactNode, type RefObject, memo, useEffect, @@ -27,7 +28,7 @@ export type FileState = { export type FileStateWithData = FileState & { data: DB["media"] }; export type DropzoneRenderProps = { - wrapperRef: RefObject; + wrapperRef: RefObject; inputProps: ComponentPropsWithRef<"input">; state: { files: FileState[]; @@ -59,7 +60,7 @@ export type DropzoneProps = { show?: boolean; text?: string; }; - children?: (props: DropzoneRenderProps) => JSX.Element; + children?: (props: DropzoneRenderProps) => ReactNode; }; function handleUploadError(e: unknown) { @@ -459,7 +460,7 @@ const UploadPlaceholder = ({ onClick, text = "Upload files" }) => { export type PreviewComponentProps = { file: FileState; - fallback?: (props: { file: FileState }) => JSX.Element; + fallback?: (props: { file: FileState }) => ReactNode; className?: string; onClick?: () => void; onTouchStart?: () => void; @@ -486,7 +487,7 @@ type PreviewProps = { handleUpload: (file: FileState) => Promise; handleDelete: (file: FileState) => Promise; }; -const Preview: React.FC = ({ file, handleUpload, handleDelete }) => { +const Preview = ({ file, handleUpload, handleDelete }: PreviewProps) => { const dropdownItems = [ ["initial", "uploaded"].includes(file.state) && { label: "Delete", diff --git a/app/src/ui/layouts/AppShell/AppShell.tsx b/app/src/ui/layouts/AppShell/AppShell.tsx index 1853180..25110d7 100644 --- a/app/src/ui/layouts/AppShell/AppShell.tsx +++ b/app/src/ui/layouts/AppShell/AppShell.tsx @@ -48,7 +48,7 @@ export const NavLink = ({ {children} diff --git a/app/src/ui/layouts/AppShell/Breadcrumbs2.tsx b/app/src/ui/layouts/AppShell/Breadcrumbs2.tsx index f98a092..b495b43 100644 --- a/app/src/ui/layouts/AppShell/Breadcrumbs2.tsx +++ b/app/src/ui/layouts/AppShell/Breadcrumbs2.tsx @@ -1,4 +1,3 @@ -import { ucFirstAllSnakeToPascalWithSpaces } from "core/utils"; import { useMemo } from "react"; import { TbArrowLeft, TbDots } from "react-icons/tb"; import { Link, useLocation } from "wouter"; @@ -7,7 +6,7 @@ import { Dropdown } from "../../components/overlay/Dropdown"; import { useEvent } from "../../hooks/use-event"; type Breadcrumb = { - label: string | JSX.Element; + label: string | Element; onClick?: () => void; href?: string; }; diff --git a/app/src/ui/lib/mantine/theme.ts b/app/src/ui/lib/mantine/theme.ts index c625bcb..0f6bfce 100644 --- a/app/src/ui/lib/mantine/theme.ts +++ b/app/src/ui/lib/mantine/theme.ts @@ -107,8 +107,11 @@ export function createMantineTheme(scheme: "light" | "dark"): { }), }), Tabs: Tabs.extend({ - classNames: (theme, props) => ({ - tab: "data-[active=true]:border-primary", + vars: (theme, props) => ({ + // https://mantine.dev/styles/styles-api/ + root: { + "--tabs-color": "border-primary", + }, }), }), Menu: Menu.extend({ diff --git a/app/src/ui/main.css b/app/src/ui/main.css index e79e101..ff59812 100644 --- a/app/src/ui/main.css +++ b/app/src/ui/main.css @@ -1,24 +1,14 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; -#bknd-admin.dark, -.dark .bknd-admin, -.bknd-admin.dark { - --color-primary: 250 250 250; /* zinc-50 */ - --color-background: 30 31 34; - --color-muted: 47 47 52; - --color-darkest: 255 255 255; /* white */ - --color-lightest: 24 24 27; /* black */ -} +@custom-variant dark (&:where(.dark, .dark *)); #bknd-admin, .bknd-admin { - --color-primary: 9 9 11; /* zinc-950 */ - --color-background: 250 250 250; /* zinc-50 */ - --color-muted: 228 228 231; /* ? */ - --color-darkest: 0 0 0; /* black */ - --color-lightest: 255 255 255; /* white */ + --color-primary: rgb(9 9 11); /* zinc-950 */ + --color-background: rgb(250 250 250); /* zinc-50 */ + --color-muted: rgb(228 228 231); /* ? */ + --color-darkest: rgb(0 0 0); /* black */ + --color-lightest: rgb(255 255 255); /* white */ @mixin light { --mantine-color-body: rgb(250 250 250); @@ -32,6 +22,24 @@ } } +.dark, +#bknd-admin.dark, +.bknd-admin.dark { + --color-primary: rgb(250 250 250); /* zinc-50 */ + --color-background: rgb(30 31 34); + --color-muted: rgb(47 47 52); + --color-darkest: rgb(255 255 255); /* white */ + --color-lightest: rgb(24 24 27); /* black */ +} + +@theme { + --color-primary: var(--color-primary); + --color-background: var(--color-background); + --color-muted: var(--color-muted); + --color-darkest: var(--color-darkest); + --color-lightest: var(--color-lightest); +} + #bknd-admin { @apply bg-background text-primary overflow-hidden h-dvh w-dvw; @@ -51,37 +59,12 @@ body, @apply flex flex-1 flex-col h-dvh w-dvw; } -@layer components { - .link { - @apply transition-colors active:translate-y-px; - } +.link { + @apply active:translate-y-px; +} - .img-responsive { - @apply max-h-full w-auto; - } - - /** - * debug classes - */ - .bordered-red { - @apply border-2 border-red-500; - } - - .bordered-green { - @apply border-2 border-green-500; - } - - .bordered-blue { - @apply border-2 border-blue-500; - } - - .bordered-violet { - @apply border-2 border-violet-500; - } - - .bordered-yellow { - @apply border-2 border-yellow-500; - } +.img-responsive { + @apply max-h-full w-auto; } #bknd-admin, diff --git a/app/src/ui/main.tsx b/app/src/ui/main.tsx index f7e5b3d..07e6540 100644 --- a/app/src/ui/main.tsx +++ b/app/src/ui/main.tsx @@ -4,11 +4,19 @@ import Admin from "./Admin"; import "./main.css"; import "./styles.css"; -ReactDOM.createRoot(document.getElementById("root")!).render( - - - , -); +function render() { + ReactDOM.createRoot(document.getElementById("root")!).render( + + + , + ); +} + +if ("startViewTransition" in document) { + document.startViewTransition(render); +} else { + render(); +} // REGISTER ERROR OVERLAY const showOverlay = true; diff --git a/app/src/ui/modules/data/components/EntityForm.tsx b/app/src/ui/modules/data/components/EntityForm.tsx index 7fb74dc..1551551 100644 --- a/app/src/ui/modules/data/components/EntityForm.tsx +++ b/app/src/ui/modules/data/components/EntityForm.tsx @@ -1,4 +1,5 @@ -import type { FieldApi, FormApi } from "@tanstack/react-form"; +import type { FieldApi, ReactFormExtendedApi } from "@tanstack/react-form"; +import type { JSX } from "react"; import { type Entity, type EntityData, @@ -8,6 +9,7 @@ import { JsonSchemaField, RelationField, } from "data"; +import { useStore } from "@tanstack/react-store"; import { MediaField } from "media/MediaField"; import { type ComponentProps, Suspense } from "react"; import { JsonEditor } from "ui/components/code/JsonEditor"; @@ -20,13 +22,18 @@ import { EntityRelationalFormField } from "./fields/EntityRelationalFormField"; import ErrorBoundary from "ui/components/display/ErrorBoundary"; import { Alert } from "ui/components/display/Alert"; +// simplify react form types 🤦 +export type FormApi = ReactFormExtendedApi; +// biome-ignore format: ... +export type TFieldApi = FieldApi; + type EntityFormProps = { entity: Entity; entityId?: number; data?: EntityData; handleSubmit: (e: React.FormEvent) => void; fieldsDisabled: boolean; - Form: FormApi; + Form: FormApi; className?: string; action: "create" | "update"; }; @@ -42,7 +49,6 @@ export function EntityForm({ action, }: EntityFormProps) { const fields = entity.getFillableFields(action, true); - console.log("data", { data, fields }); return (
@@ -132,7 +138,7 @@ type EntityFormFieldProps< T extends keyof JSX.IntrinsicElements = "input", F extends Field = Field, > = ComponentProps & { - fieldApi: FieldApi; + fieldApi: TFieldApi; field: F; action: "create" | "update"; data?: EntityData; @@ -215,7 +221,7 @@ function EntityMediaFormField({ entityId, disabled, }: { - formApi: FormApi; + formApi: FormApi; field: MediaField; entity: Entity; entityId?: number; @@ -223,7 +229,7 @@ function EntityMediaFormField({ }) { if (!entityId) return; - const value = formApi.useStore((state) => { + const value = useStore(formApi.store, (state) => { const val = state.values[field.name]; if (!val || typeof val === "undefined") return []; if (Array.isArray(val)) return val; @@ -253,7 +259,7 @@ function EntityJsonFormField({ fieldApi, field, ...props -}: { fieldApi: FieldApi; field: JsonField }) { +}: { fieldApi: TFieldApi; field: JsonField }) { const handleUpdate = useEvent((value: any) => { fieldApi.handleChange(value); }); @@ -289,7 +295,7 @@ function EntityEnumFormField({ fieldApi, field, ...props -}: { fieldApi: FieldApi; field: EnumField }) { +}: { fieldApi: TFieldApi; field: EnumField }) { const handleUpdate = useEvent((e: React.ChangeEvent) => { fieldApi.handleChange(e.target.value); }); diff --git a/app/src/ui/modules/data/components/fields/EntityJsonSchemaFormField.tsx b/app/src/ui/modules/data/components/fields/EntityJsonSchemaFormField.tsx index af09948..6fb3ee3 100644 --- a/app/src/ui/modules/data/components/fields/EntityJsonSchemaFormField.tsx +++ b/app/src/ui/modules/data/components/fields/EntityJsonSchemaFormField.tsx @@ -1,8 +1,8 @@ -import type { FieldApi } from "@tanstack/react-form"; import type { EntityData, JsonSchemaField } from "data"; import * as Formy from "ui/components/form/Formy"; import { FieldLabel } from "ui/components/form/Formy"; import { JsonSchemaForm } from "ui/components/form/json-schema"; +import type { TFieldApi } from "ui/modules/data/components/EntityForm"; export function EntityJsonSchemaFormField({ fieldApi, @@ -11,7 +11,7 @@ export function EntityJsonSchemaFormField({ disabled, ...props }: { - fieldApi: FieldApi; + fieldApi: TFieldApi; field: JsonSchemaField; data?: EntityData; disabled?: boolean; diff --git a/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx b/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx index f7c3439..5de71bd 100644 --- a/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx +++ b/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx @@ -1,5 +1,4 @@ import { getHotkeyHandler, useHotkeys } from "@mantine/hooks"; -import type { FieldApi } from "@tanstack/react-form"; import { ucFirst } from "core/utils"; import type { EntityData, RelationField } from "data"; import { useEffect, useRef, useState } from "react"; @@ -12,10 +11,11 @@ import { Popover } from "ui/components/overlay/Popover"; import { Link } from "ui/components/wouter/Link"; import { routes } from "ui/lib/routes"; import { useLocation } from "wouter"; -import { EntityTable, type EntityTableProps } from "../EntityTable"; +import type { EntityTableProps } from "../EntityTable"; import type { ResponseObject } from "modules/ModuleApi"; import ErrorBoundary from "ui/components/display/ErrorBoundary"; import { EntityTable2 } from "ui/modules/data/components/EntityTable2"; +import type { TFieldApi } from "ui/modules/data/components/EntityForm"; // @todo: allow clear if not required export function EntityRelationalFormField({ @@ -25,7 +25,7 @@ export function EntityRelationalFormField({ disabled, tabIndex, }: { - fieldApi: FieldApi; + fieldApi: TFieldApi; field: RelationField; data?: EntityData; disabled?: boolean; diff --git a/app/src/ui/modules/data/components/schema/create-modal/templates/register.ts b/app/src/ui/modules/data/components/schema/create-modal/templates/register.ts index 79ab6e3..a5d42a0 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/templates/register.ts +++ b/app/src/ui/modules/data/components/schema/create-modal/templates/register.ts @@ -1,5 +1,6 @@ import type { IconType } from "react-icons"; import { TemplateMediaComponent, TemplateMediaMeta } from "./media"; +import type { ReactNode } from "react"; export type StepTemplate = { id: string; @@ -8,8 +9,6 @@ export type StepTemplate = { Icon: IconType; }; -const Templates: [() => JSX.Element, StepTemplate][] = [ - [TemplateMediaComponent, TemplateMediaMeta], -]; +const Templates: [() => ReactNode, StepTemplate][] = [[TemplateMediaComponent, TemplateMediaMeta]]; export default Templates; diff --git a/app/src/ui/routes/data/data.$entity.$id.tsx b/app/src/ui/routes/data/data.$entity.$id.tsx index 29d2ac2..e042934 100644 --- a/app/src/ui/routes/data/data.$entity.$id.tsx +++ b/app/src/ui/routes/data/data.$entity.$id.tsx @@ -1,5 +1,5 @@ import { ucFirst } from "core/utils"; -import type { Entity, EntityData, EntityRelation, RepoQuery } from "data"; +import type { Entity, EntityData, EntityRelation } from "data"; import { Fragment, useState } from "react"; import { TbDots } from "react-icons/tb"; import { useApiQuery, useEntityQuery } from "ui/client"; diff --git a/app/src/ui/routes/flows/components/FlowCreateModal.tsx b/app/src/ui/routes/flows/components/FlowCreateModal.tsx index 4ac3ad4..1159436 100644 --- a/app/src/ui/routes/flows/components/FlowCreateModal.tsx +++ b/app/src/ui/routes/flows/components/FlowCreateModal.tsx @@ -70,7 +70,7 @@ export function StepCreate() { name: "", trigger: "manual", mode: "async", - }, + } as Static, mode: "onSubmit", }); diff --git a/app/src/ui/routes/test/index.tsx b/app/src/ui/routes/test/index.tsx index 6246a60..c70a6fe 100644 --- a/app/src/ui/routes/test/index.tsx +++ b/app/src/ui/routes/test/index.tsx @@ -16,7 +16,6 @@ import ModalTest from "../../routes/test/tests/modal-test"; import QueryJsonFormTest from "../../routes/test/tests/query-jsonform"; import DropdownTest from "./tests/dropdown-test"; import DropzoneElementTest from "./tests/dropzone-element-test"; -import EntityFieldsForm from "./tests/entity-fields-form"; import FlowsTest from "./tests/flows-test"; import JsonSchemaForm3 from "./tests/json-schema-form3"; import JsonFormTest from "./tests/jsonform-test"; @@ -27,9 +26,11 @@ import ReactFlowTest from "./tests/reactflow-test"; import SchemaTest from "./tests/schema-test"; import SortableTest from "./tests/sortable-test"; import { SqlAiTest } from "./tests/sql-ai-test"; +import Themes from "./tests/themes"; const tests = { DropdownTest, + Themes, ModalTest, JsonFormTest, FlowFormTest, @@ -42,7 +43,6 @@ const tests = { SqlAiTest, SortableTest, ReactHookErrors, - EntityFieldsForm, FlowsTest, AppShellAccordionsTest, SwaggerTest, diff --git a/app/src/ui/routes/test/tests/entity-fields-form.tsx b/app/src/ui/routes/test/tests/entity-fields-form.tsx deleted file mode 100644 index 12677ec..0000000 --- a/app/src/ui/routes/test/tests/entity-fields-form.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import { typeboxResolver } from "@hookform/resolvers/typebox"; -import { Select, Switch, Tabs, TextInput, Textarea, Tooltip } from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; -import { Type } from "@sinclair/typebox"; -import { StringEnum, StringIdentifier, transformObject } from "core/utils"; -import { FieldClassMap } from "data"; -import { omit } from "lodash-es"; -import { - type FieldArrayWithId, - type FieldValues, - type UseControllerProps, - type UseFormReturn, - useController, - useFieldArray, - useForm, -} from "react-hook-form"; -import { TbChevronDown, TbChevronUp, TbGripVertical, TbTrash } from "react-icons/tb"; -import { Button } from "../../../components/buttons/Button"; -import { IconButton } from "../../../components/buttons/IconButton"; -import { MantineSelect } from "../../../components/form/hook-form-mantine/MantineSelect"; - -const fieldSchemas = transformObject(omit(FieldClassMap, ["primary"]), (value) => value.schema); -const fieldSchema = Type.Union( - Object.entries(fieldSchemas).map(([type, schema]) => - Type.Object( - { - type: Type.Const(type), - name: StringIdentifier, - config: Type.Optional(schema), - }, - { - additionalProperties: false, - }, - ), - ), -); -const schema = Type.Object({ - fields: Type.Array(fieldSchema), -}); - -const fieldSchema2 = Type.Object({ - type: StringEnum(Object.keys(fieldSchemas)), - name: StringIdentifier, -}); - -function specificFieldSchema(type: keyof typeof fieldSchemas) { - return Type.Omit(fieldSchemas[type], [ - "label", - "description", - "required", - "fillable", - "hidden", - "virtual", - ]); -} - -export default function EntityFieldsForm() { - const { - control, - formState: { isValid, errors }, - getValues, - handleSubmit, - watch, - register, - setValue, - } = useForm({ - mode: "onTouched", - resolver: typeboxResolver(schema), - defaultValues: { - fields: [{ type: "text", name: "", config: {} }], - sort: { by: "-1", dir: "asc" }, - }, - }); - const defaultType = Object.keys(fieldSchemas)[0]; - const { fields, append, prepend, remove, swap, move, insert, update } = useFieldArray({ - control, // control props comes from useForm (optional: if you are using FormProvider) - name: "fields", // unique name for your Field Array - }); - - function handleAppend() { - append({ type: "text", name: "", config: {} }); - } - - return ( -
- {/*{fields.map((field, index) => ( - - ))}*/} - {fields.map((field, index) => ( - - ))} - - - -
-
{JSON.stringify(watch(), null, 2)}
-
-
- ); -} - -function EntityFieldForm({ update, index, value }) { - const { - register, - handleSubmit, - control, - formState: { errors }, - } = useForm({ - mode: "onBlur", - resolver: typeboxResolver( - Type.Object({ - type: StringEnum(Object.keys(fieldSchemas)), - name: Type.String({ minLength: 1, maxLength: 3 }), - }), - ), - defaultValues: value, - }); - - function handleUpdate({ id, ...data }) { - console.log("data", data); - update(index, data); - } - - return ( - - - - - ); -} - -function EntityFieldController({ - name, - control, - defaultValue, - rules, - shouldUnregister, -}: UseControllerProps & { - index: number; -}) { - const { - field: { value, onChange: fieldOnChange, ...field }, - fieldState, - } = useController({ - name, - control, - defaultValue, - rules, - shouldUnregister, - }); - - return
field
; -} - -function EntityField({ - field, - index, - form: { watch, register, setValue, getValues, control }, - remove, - defaultType, -}: { - field: FieldArrayWithId; - index: number; - form: Pick, "watch" | "register" | "setValue" | "getValues" | "control">; - remove: (index: number) => void; - defaultType: string; -}) { - const [opened, handlers] = useDisclosure(false); - const prefix = `fields.${index}` as const; - const name = watch(`${prefix}.name`); - const enabled = name?.length > 0; - const type = watch(`${prefix}.type`); - //const config = watch(`${prefix}.config`); - const selectFieldRegister = register(`${prefix}.type`); - //console.log("type", type, specificFieldSchema(type as any)); - - function handleDelete(index: number) { - return () => { - if (name.length === 0) { - remove(index); - return; - } - window.confirm(`Sure to delete "${name}"?`) && remove(index); - }; - } - - return ( -
-
-
- -
-
-
-