mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 12:37:20 +00:00
using vite for static bundling to solve external package's react resolution
This commit is contained in:
@@ -74,9 +74,6 @@ export class App {
|
||||
this.trigger_first_boot = true;
|
||||
},
|
||||
onServerInit: async (server) => {
|
||||
server.get("/favicon.ico", (c) =>
|
||||
c.redirect(config.server.assets_path + "/favicon.ico")
|
||||
);
|
||||
server.use(async (c, next) => {
|
||||
c.set("app", this);
|
||||
await next();
|
||||
|
||||
@@ -11,7 +11,8 @@ export interface DB {}
|
||||
export const config = {
|
||||
server: {
|
||||
default_port: 1337,
|
||||
assets_path: "/assets/"
|
||||
// resetted to root for now, bc bundling with vite
|
||||
assets_path: "/"
|
||||
},
|
||||
data: {
|
||||
default_primary_field: "id"
|
||||
|
||||
@@ -50,7 +50,7 @@ export class AdminController extends Controller {
|
||||
const { auth: authMiddleware, permission } = this.middlewares;
|
||||
const hono = this.create().use(
|
||||
authMiddleware({
|
||||
skip: [/favicon\.ico$/]
|
||||
//skip: [/favicon\.ico$/]
|
||||
})
|
||||
);
|
||||
|
||||
@@ -102,6 +102,7 @@ export class AdminController extends Controller {
|
||||
});
|
||||
}
|
||||
|
||||
// @todo: only load known paths
|
||||
hono.get(
|
||||
"/*",
|
||||
permission(SystemPermissions.accessAdmin, {
|
||||
@@ -160,8 +161,9 @@ export class AdminController extends Controller {
|
||||
const manifest = await import("bknd/dist/manifest.json", {
|
||||
assert: { type: "json" }
|
||||
}).then((m) => m.default);
|
||||
assets.js = manifest["src/ui/main.tsx"].name;
|
||||
assets.css = manifest["src/ui/main.css"].name;
|
||||
// @todo: load all marked as entry (incl. css)
|
||||
assets.js = manifest["src/ui/main.tsx"].file;
|
||||
assets.css = manifest["src/ui/main.tsx"].css[0] as any;
|
||||
} catch (e) {
|
||||
console.error("Error loading manifest", e);
|
||||
}
|
||||
|
||||
@@ -1,350 +0,0 @@
|
||||
/**
|
||||
* @todo: currently just hard importing this library due to building and react issues with static assets
|
||||
* man I hate bundling for react.
|
||||
*/
|
||||
|
||||
import {
|
||||
type ComponentPropsWithoutRef,
|
||||
type ForwardedRef,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
|
||||
export type JSONSchemaTypeName =
|
||||
| "string"
|
||||
| "number"
|
||||
| "integer"
|
||||
| "boolean"
|
||||
| "object"
|
||||
| "array"
|
||||
| "null"
|
||||
| string;
|
||||
|
||||
export type JSONSchemaDefinition = JSONSchema | boolean;
|
||||
|
||||
export interface JSONSchema {
|
||||
$id?: string;
|
||||
$ref?: string;
|
||||
$schema?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
default?: any;
|
||||
|
||||
// Data types
|
||||
type?: JSONSchemaTypeName | JSONSchemaTypeName[];
|
||||
enum?: any[];
|
||||
const?: any;
|
||||
|
||||
// Numbers
|
||||
multipleOf?: number;
|
||||
maximum?: number;
|
||||
exclusiveMaximum?: number;
|
||||
minimum?: number;
|
||||
exclusiveMinimum?: number;
|
||||
|
||||
// Strings
|
||||
maxLength?: number;
|
||||
minLength?: number;
|
||||
pattern?: string;
|
||||
format?: string;
|
||||
|
||||
// Arrays
|
||||
items?: JSONSchemaDefinition | JSONSchemaDefinition[];
|
||||
additionalItems?: JSONSchemaDefinition;
|
||||
uniqueItems?: boolean;
|
||||
maxItems?: number;
|
||||
minItems?: number;
|
||||
|
||||
// Objects
|
||||
properties?: { [key: string]: JSONSchemaDefinition };
|
||||
patternProperties?: { [key: string]: JSONSchemaDefinition };
|
||||
additionalProperties?: JSONSchemaDefinition;
|
||||
required?: string[];
|
||||
maxProperties?: number;
|
||||
minProperties?: number;
|
||||
dependencies?: { [key: string]: JSONSchemaDefinition | string[] };
|
||||
|
||||
// Combining schemas
|
||||
allOf?: JSONSchemaDefinition[];
|
||||
anyOf?: JSONSchemaDefinition[];
|
||||
oneOf?: JSONSchemaDefinition[];
|
||||
not?: JSONSchemaDefinition;
|
||||
if?: JSONSchemaDefinition;
|
||||
then?: JSONSchemaDefinition;
|
||||
else?: JSONSchemaDefinition;
|
||||
|
||||
// Definitions
|
||||
definitions?: { [key: string]: JSONSchemaDefinition };
|
||||
$comment?: string;
|
||||
[key: string | symbol]: any; // catch-all for custom extensions
|
||||
}
|
||||
|
||||
export function formDataToNestedObject(
|
||||
formData: FormData,
|
||||
formElement: HTMLFormElement
|
||||
): Record<string, any> {
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
formData.forEach((value, key) => {
|
||||
const inputElement = formElement.querySelector(`[name="${key}"]`) as
|
||||
| HTMLInputElement
|
||||
| HTMLTextAreaElement
|
||||
| HTMLSelectElement
|
||||
| null;
|
||||
|
||||
if (!inputElement) {
|
||||
return; // Skip if the input element is not found
|
||||
}
|
||||
|
||||
// Skip fields with empty values
|
||||
if (value === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = key
|
||||
.replace(/\[([^\]]*)\]/g, ".$1") // Convert [key] to .key
|
||||
.split(".") // Split by dots
|
||||
.filter(Boolean); // Remove empty parts
|
||||
|
||||
let current = result;
|
||||
|
||||
keys.forEach((k, i) => {
|
||||
if (i === keys.length - 1) {
|
||||
let parsedValue: any = value;
|
||||
|
||||
if (inputElement.type === "number") {
|
||||
parsedValue = !Number.isNaN(Number(value)) ? Number(value) : value;
|
||||
} else if (inputElement.type === "checkbox") {
|
||||
parsedValue = "checked" in inputElement && inputElement.checked;
|
||||
}
|
||||
|
||||
// Handle array or single value
|
||||
if (current[k] !== undefined) {
|
||||
if (!Array.isArray(current[k])) {
|
||||
current[k] = [current[k]];
|
||||
}
|
||||
current[k].push(parsedValue);
|
||||
} else {
|
||||
current[k] = parsedValue;
|
||||
}
|
||||
} else {
|
||||
// Ensure the key exists as an object
|
||||
if (current[k] === undefined || typeof current[k] !== "object") {
|
||||
current[k] = {};
|
||||
}
|
||||
current = current[k];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const cache = new Map<string, JSONSchema>();
|
||||
|
||||
export type ChangeSet = { name: string; value: any };
|
||||
|
||||
export type Validator<Err = unknown, FormData = any> = {
|
||||
validate: (schema: JSONSchema | any, data: FormData) => Promise<Err[]> | Err[];
|
||||
};
|
||||
|
||||
export type FormRenderProps<Err> = {
|
||||
errors: Err[];
|
||||
schema: JSONSchema;
|
||||
submitting: boolean;
|
||||
dirty: boolean;
|
||||
submit: () => Promise<void>;
|
||||
reset: () => void;
|
||||
resetDirty: () => void;
|
||||
};
|
||||
|
||||
export type FormRef<FormData, Err> = {
|
||||
submit: () => Promise<void>;
|
||||
validate: () => Promise<{ data: FormData; errors: Err[] }>;
|
||||
reset: () => void;
|
||||
resetDirty: () => void;
|
||||
formRef: RefObject<HTMLFormElement | null>;
|
||||
};
|
||||
|
||||
export type FormProps<FormData, ValFn, Err> = Omit<
|
||||
ComponentPropsWithoutRef<"form">,
|
||||
"onSubmit" | "onChange" | "children"
|
||||
> & {
|
||||
schema: `http${string}` | `/${string}` | JSONSchema;
|
||||
validator: Validator<Err, FormData>;
|
||||
validationMode?: "submit" | "change";
|
||||
children: (props: FormRenderProps<Err>) => ReactNode;
|
||||
onChange?: (formData: FormData, changed: ChangeSet) => void | Promise<void>;
|
||||
onSubmit?: (formData: FormData) => void | Promise<void>;
|
||||
onSubmitInvalid?: (errors: Err[], formData: FormData) => void | Promise<void>;
|
||||
resetOnSubmit?: boolean;
|
||||
revalidateOnError?: boolean;
|
||||
hiddenSubmit?: boolean;
|
||||
};
|
||||
|
||||
const FormComponent = <FormData, ValFn, Err>(
|
||||
{
|
||||
schema: initialSchema,
|
||||
validator,
|
||||
validationMode = "submit",
|
||||
children,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onSubmitInvalid,
|
||||
resetOnSubmit,
|
||||
revalidateOnError = true,
|
||||
hiddenSubmit,
|
||||
...formProps
|
||||
}: FormProps<FormData, ValFn, Err>,
|
||||
ref: ForwardedRef<FormRef<FormData, Err>>
|
||||
) => {
|
||||
const is_schema = typeof initialSchema !== "string";
|
||||
const [schema, setSchema] = useState<JSONSchema | undefined>(
|
||||
is_schema ? initialSchema : undefined
|
||||
);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [errors, setErrors] = useState<any[]>([]);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const formRef = useRef<HTMLFormElement | null>(null);
|
||||
|
||||
function resetDirty() {
|
||||
setDirty(false);
|
||||
setDirty(false);
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
submit: submit,
|
||||
validate: validate,
|
||||
reset: reset,
|
||||
resetDirty,
|
||||
formRef
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!is_schema) {
|
||||
if (cache.has(initialSchema)) {
|
||||
setSchema(cache.get(initialSchema));
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(initialSchema);
|
||||
|
||||
if (res.ok) {
|
||||
const s = (await res.json()) as JSONSchema;
|
||||
setSchema(s);
|
||||
cache.set(initialSchema, s);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [initialSchema]);
|
||||
|
||||
async function handleChangeEvent(e: React.FormEvent<HTMLFormElement>) {
|
||||
const form = formRef.current;
|
||||
if (!form) return;
|
||||
setDirty(true);
|
||||
const target = e.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null;
|
||||
|
||||
if (!target || !form.contains(target)) {
|
||||
return; // Ignore events from outside the form
|
||||
}
|
||||
|
||||
const name = target.name;
|
||||
const formData = new FormData(form);
|
||||
const data = formDataToNestedObject(formData, form) as FormData;
|
||||
const value = formData.get(name);
|
||||
|
||||
await onChange?.(data, { name, value });
|
||||
|
||||
if ((revalidateOnError && errors.length > 0) || validationMode === "change") {
|
||||
await validate();
|
||||
}
|
||||
}
|
||||
|
||||
async function validate() {
|
||||
const form = formRef.current;
|
||||
if (!form || !schema) return { data: {} as FormData, errors: [] };
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = formDataToNestedObject(formData, form) as FormData;
|
||||
|
||||
const errors = await validator.validate(schema, data);
|
||||
setErrors(errors);
|
||||
return { data, errors };
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
await submit();
|
||||
return false;
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const form = formRef.current;
|
||||
if (!form || !schema) {
|
||||
console.log("invalid", { form, schema });
|
||||
return;
|
||||
}
|
||||
|
||||
const { data, errors } = await validate();
|
||||
if (errors.length > 0) {
|
||||
await onSubmitInvalid?.(errors, data);
|
||||
} else {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
if (onSubmit) {
|
||||
await onSubmit?.(data);
|
||||
if (resetOnSubmit) {
|
||||
reset();
|
||||
}
|
||||
} else {
|
||||
form.submit();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.warn("You should wrap your submit handler in a try/catch block");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
setDirty(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
formRef.current?.reset();
|
||||
setErrors([]);
|
||||
}
|
||||
|
||||
return (
|
||||
<form {...formProps} onSubmit={handleSubmit} ref={formRef} onChange={handleChangeEvent}>
|
||||
{children({
|
||||
schema: schema as any,
|
||||
submit,
|
||||
dirty,
|
||||
reset,
|
||||
resetDirty,
|
||||
submitting,
|
||||
errors
|
||||
})}
|
||||
|
||||
{hiddenSubmit && (
|
||||
<input type="submit" style={{ visibility: "hidden" }} disabled={errors.length > 0} />
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const Form = forwardRef(FormComponent) as <
|
||||
FormData = any,
|
||||
ValidatorActual = Validator,
|
||||
Err = ValidatorActual extends Validator<infer E, FormData> ? Awaited<E> : never
|
||||
>(
|
||||
props: FormProps<FormData, ValidatorActual, Err> & {
|
||||
ref?: ForwardedRef<HTMLFormElement>;
|
||||
}
|
||||
) => ReturnType<typeof FormComponent>;
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { PrimaryFieldType } from "core";
|
||||
import { encodeSearch } from "core/utils";
|
||||
import { atom, useSetAtom } from "jotai";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { useBaseUrl } from "../client";
|
||||
import { useBknd } from "../client/BkndProvider";
|
||||
|
||||
export const routes = {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ValueError } from "@sinclair/typebox/value";
|
||||
import type { AppAuthOAuthStrategy, AppAuthSchema } from "auth/auth-schema";
|
||||
import { type TSchema, Type, Value } from "core/utils";
|
||||
import { Form, type Validator } from "json-schema-form-react";
|
||||
import { transform } from "lodash-es";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { Group, Input, Label } from "ui/components/form/Formy";
|
||||
import { Form, type Validator } from "ui/lib/json-schema-form";
|
||||
import { SocialLink } from "ui/modules/auth/SocialLink";
|
||||
|
||||
export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "onSubmit" | "action"> & {
|
||||
|
||||
Reference in New Issue
Block a user