mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 12:37:20 +00:00
Merge remote-tracking branch 'origin/release/0.10' into feat/remove-admin-config
# Conflicts: # app/src/modules/server/AdminController.tsx # app/src/ui/Admin.tsx
This commit is contained in:
@@ -24,14 +24,14 @@ export type { TSchemaActions };
|
||||
enum Fetching {
|
||||
None = 0,
|
||||
Schema = 1,
|
||||
Secrets = 2
|
||||
Secrets = 2,
|
||||
}
|
||||
|
||||
export function BkndProvider({
|
||||
includeSecrets = false,
|
||||
adminOverride,
|
||||
children,
|
||||
fallback = null
|
||||
fallback = null,
|
||||
}: { includeSecrets?: boolean; children: any; fallback?: React.ReactNode } & Pick<
|
||||
BkndContext,
|
||||
"adminOverride"
|
||||
@@ -41,25 +41,35 @@ export function BkndProvider({
|
||||
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions" | "fallback">>();
|
||||
const [fetched, setFetched] = useState(false);
|
||||
const [error, setError] = useState<boolean>();
|
||||
const errorShown = useRef<boolean>();
|
||||
const errorShown = useRef<boolean>(false);
|
||||
const fetching = useRef<Fetching>(Fetching.None);
|
||||
const [local_version, set_local_version] = useState(0);
|
||||
const api = useApi();
|
||||
|
||||
async function reloadSchema() {
|
||||
await fetchSchema(includeSecrets, true);
|
||||
await fetchSchema(includeSecrets, {
|
||||
force: true,
|
||||
fresh: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchSchema(_includeSecrets: boolean = false, force?: boolean) {
|
||||
async function fetchSchema(
|
||||
_includeSecrets: boolean = false,
|
||||
opts?: {
|
||||
force?: boolean;
|
||||
fresh?: boolean;
|
||||
},
|
||||
) {
|
||||
const requesting = withSecrets ? Fetching.Secrets : Fetching.Schema;
|
||||
if (fetching.current === requesting) return;
|
||||
|
||||
if (withSecrets && !force) return;
|
||||
if (withSecrets && opts?.force !== true) return;
|
||||
fetching.current = requesting;
|
||||
|
||||
const res = await api.system.readSchema({
|
||||
config: true,
|
||||
secrets: _includeSecrets
|
||||
secrets: _includeSecrets,
|
||||
fresh: opts?.fresh,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -80,22 +90,24 @@ export function BkndProvider({
|
||||
schema: getDefaultSchema(),
|
||||
config: getDefaultConfig(),
|
||||
permissions: [],
|
||||
fallback: true
|
||||
fallback: true,
|
||||
} as any);
|
||||
|
||||
if (adminOverride) {
|
||||
newSchema.config.server.admin = {
|
||||
...newSchema.config.server.admin,
|
||||
...adminOverride
|
||||
...adminOverride,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AppTheme } from "modules/server/AppServer";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
const ClientContext = createContext<{ baseUrl: string; api: Api }>({
|
||||
baseUrl: undefined
|
||||
baseUrl: undefined,
|
||||
} as any);
|
||||
|
||||
export type ClientProviderProps = {
|
||||
@@ -69,7 +69,7 @@ export function useBkndWindowContext(): BkndWindowContext {
|
||||
return window.__BKND__ as any;
|
||||
} else {
|
||||
return {
|
||||
logout_route: "/api/auth/logout"
|
||||
logout_route: "/api/auth/logout",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import { useApi } from "ui/client";
|
||||
|
||||
export const useApiQuery = <
|
||||
Data,
|
||||
RefineFn extends (data: ResponseObject<Data>) => unknown = (data: ResponseObject<Data>) => Data
|
||||
RefineFn extends (data: ResponseObject<Data>) => unknown = (data: ResponseObject<Data>) => Data,
|
||||
>(
|
||||
fn: (api: Api) => FetchPromise<Data>,
|
||||
options?: SWRConfiguration & { enabled?: boolean; refine?: RefineFn }
|
||||
options?: SWRConfiguration & { enabled?: boolean; refine?: RefineFn },
|
||||
) => {
|
||||
const api = useApi();
|
||||
const promise = fn(api);
|
||||
@@ -23,7 +23,7 @@ export const useApiQuery = <
|
||||
...swr,
|
||||
promise,
|
||||
key,
|
||||
api
|
||||
api,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { type Api, useApi } from "ui/client";
|
||||
export class UseEntityApiError<Payload = any> extends Error {
|
||||
constructor(
|
||||
public response: ResponseObject<Payload>,
|
||||
fallback?: string
|
||||
fallback?: string,
|
||||
) {
|
||||
let message = fallback;
|
||||
if ("error" in response) {
|
||||
@@ -26,10 +26,10 @@ export class UseEntityApiError<Payload = any> extends Error {
|
||||
export const useEntity = <
|
||||
Entity extends keyof DB | string,
|
||||
Id extends PrimaryFieldType | undefined = undefined,
|
||||
Data = Entity extends keyof DB ? DB[Entity] : EntityData
|
||||
Data = Entity extends keyof DB ? DB[Entity] : EntityData,
|
||||
>(
|
||||
entity: Entity,
|
||||
id?: Id
|
||||
id?: Id,
|
||||
) => {
|
||||
const api = useApi().data;
|
||||
|
||||
@@ -71,7 +71,7 @@ export const useEntity = <
|
||||
throw new UseEntityApiError(res, `Failed to delete entity "${entity}"`);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -80,7 +80,7 @@ export function makeKey(
|
||||
api: ModuleApi,
|
||||
entity: string,
|
||||
id?: PrimaryFieldType,
|
||||
query?: RepoQueryIn
|
||||
query?: RepoQueryIn,
|
||||
) {
|
||||
return (
|
||||
"/" +
|
||||
@@ -93,12 +93,12 @@ export function makeKey(
|
||||
|
||||
export const useEntityQuery = <
|
||||
Entity extends keyof DB | string,
|
||||
Id extends PrimaryFieldType | undefined = undefined
|
||||
Id extends PrimaryFieldType | undefined = undefined,
|
||||
>(
|
||||
entity: Entity,
|
||||
id?: Id,
|
||||
query?: RepoQueryIn,
|
||||
options?: SWRConfiguration & { enabled?: boolean; revalidateOnMutate?: boolean }
|
||||
options?: SWRConfiguration & { enabled?: boolean; revalidateOnMutate?: boolean },
|
||||
) => {
|
||||
const api = useApi().data;
|
||||
const key = makeKey(api, entity as string, id, query);
|
||||
@@ -109,13 +109,13 @@ export const useEntityQuery = <
|
||||
const swr = useSWR<T>(options?.enabled === false ? null : key, fetcher as any, {
|
||||
revalidateOnFocus: false,
|
||||
keepPreviousData: true,
|
||||
...options
|
||||
...options,
|
||||
});
|
||||
|
||||
const mutateAll = async () => {
|
||||
const entityKey = makeKey(api, entity as string);
|
||||
return mutate((key) => typeof key === "string" && key.startsWith(entityKey), undefined, {
|
||||
revalidate: true
|
||||
revalidate: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -138,13 +138,13 @@ export const useEntityQuery = <
|
||||
mutate: mutateAll,
|
||||
mutateRaw: swr.mutate,
|
||||
api,
|
||||
key
|
||||
key,
|
||||
};
|
||||
};
|
||||
|
||||
export async function mutateEntityCache<
|
||||
Entity extends keyof DB | string,
|
||||
Data = Entity extends keyof DB ? Omit<DB[Entity], "id"> : EntityData
|
||||
Data = Entity extends keyof DB ? Omit<DB[Entity], "id"> : EntityData,
|
||||
>(api: Api["data"], entity: Entity, id: PrimaryFieldType, partialData: Partial<Data>) {
|
||||
function update(prev: any, partialNext: any) {
|
||||
if (
|
||||
@@ -171,23 +171,23 @@ export async function mutateEntityCache<
|
||||
return update(data, partialData);
|
||||
},
|
||||
{
|
||||
revalidate: false
|
||||
}
|
||||
revalidate: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export const useEntityMutate = <
|
||||
Entity extends keyof DB | string,
|
||||
Id extends PrimaryFieldType | undefined = undefined,
|
||||
Data = Entity extends keyof DB ? Omit<DB[Entity], "id"> : EntityData
|
||||
Data = Entity extends keyof DB ? Omit<DB[Entity], "id"> : EntityData,
|
||||
>(
|
||||
entity: Entity,
|
||||
id?: Id,
|
||||
options?: SWRConfiguration
|
||||
options?: SWRConfiguration,
|
||||
) => {
|
||||
const { data, ...$q } = useEntityQuery<Entity, Id>(entity, id, undefined, {
|
||||
...options,
|
||||
enabled: false
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const _mutate = id
|
||||
@@ -198,6 +198,6 @@ export const useEntityMutate = <
|
||||
...$q,
|
||||
mutate: _mutate as unknown as Id extends undefined
|
||||
? (id: PrimaryFieldType, data: Partial<Data>) => Promise<void>
|
||||
: (data: Partial<Data>) => Promise<void>
|
||||
: (data: Partial<Data>) => Promise<void>,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ export {
|
||||
useBkndWindowContext,
|
||||
type ClientProviderProps,
|
||||
useApi,
|
||||
useBaseUrl
|
||||
useBaseUrl,
|
||||
} from "./ClientProvider";
|
||||
|
||||
export * from "./api/use-api";
|
||||
|
||||
@@ -18,15 +18,15 @@ export function getSchemaActions({ api, setSchema, reloadSchema }: SchemaActions
|
||||
action: string,
|
||||
module: Module,
|
||||
res: ResponseObject<ConfigUpdateResponse<Module>>,
|
||||
path?: string
|
||||
path?: string,
|
||||
): Promise<boolean> {
|
||||
const base: Partial<NotificationData> = {
|
||||
id: "schema-" + [action, module, path].join("-"),
|
||||
position: "top-right",
|
||||
autoClose: 3000
|
||||
autoClose: 3000,
|
||||
};
|
||||
|
||||
if (res.success === true) {
|
||||
if (res.success) {
|
||||
console.log("update config", action, module, path, res.body);
|
||||
if (res.body.success) {
|
||||
setSchema((prev) => {
|
||||
@@ -35,8 +35,8 @@ export function getSchemaActions({ api, setSchema, reloadSchema }: SchemaActions
|
||||
...prev,
|
||||
config: {
|
||||
...prev.config,
|
||||
[module]: res.config
|
||||
}
|
||||
[module]: res.config,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export function getSchemaActions({ api, setSchema, reloadSchema }: SchemaActions
|
||||
...base,
|
||||
title: `Config updated: ${ucFirst(module)}`,
|
||||
color: "blue",
|
||||
message: `Operation ${action.toUpperCase()} at ${module}${path ? "." + path : ""}`
|
||||
message: `Operation ${action.toUpperCase()} at ${module}${path ? "." + path : ""}`,
|
||||
});
|
||||
} else {
|
||||
notifications.show({
|
||||
@@ -54,7 +54,7 @@ export function getSchemaActions({ api, setSchema, reloadSchema }: SchemaActions
|
||||
color: "red",
|
||||
withCloseButton: true,
|
||||
autoClose: false,
|
||||
message: res.error ?? "Failed to complete config update"
|
||||
message: res.error ?? "Failed to complete config update",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export function getSchemaActions({ api, setSchema, reloadSchema }: SchemaActions
|
||||
set: async <Module extends keyof ModuleConfigs>(
|
||||
module: keyof ModuleConfigs,
|
||||
value: ModuleConfigs[Module],
|
||||
force?: boolean
|
||||
force?: boolean,
|
||||
) => {
|
||||
const res = await api.system.setConfig(module, value, force);
|
||||
return await handleConfigUpdate("set", module, res);
|
||||
@@ -74,7 +74,7 @@ export function getSchemaActions({ api, setSchema, reloadSchema }: SchemaActions
|
||||
patch: async <Module extends keyof ModuleConfigs>(
|
||||
module: Module,
|
||||
path: string,
|
||||
value: any
|
||||
value: any,
|
||||
): Promise<boolean> => {
|
||||
const res = await api.system.patchConfig(module, path, value);
|
||||
return await handleConfigUpdate("patch", module, res, path);
|
||||
@@ -82,7 +82,7 @@ export function getSchemaActions({ api, setSchema, reloadSchema }: SchemaActions
|
||||
overwrite: async <Module extends keyof ModuleConfigs>(
|
||||
module: Module,
|
||||
path: string,
|
||||
value: any
|
||||
value: any,
|
||||
) => {
|
||||
const res = await api.system.overwriteConfig(module, path, value);
|
||||
return await handleConfigUpdate("overwrite", module, res, path);
|
||||
@@ -94,6 +94,6 @@ export function getSchemaActions({ api, setSchema, reloadSchema }: SchemaActions
|
||||
remove: async <Module extends keyof ModuleConfigs>(module: Module, path: string) => {
|
||||
const res = await api.system.removeConfig(module, path);
|
||||
return await handleConfigUpdate("remove", module, res, path);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,6 +69,6 @@ export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
|
||||
register,
|
||||
logout,
|
||||
setToken,
|
||||
verify
|
||||
verify,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import type { AppAuthSchema } from "auth/auth-schema";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { routes } from "ui/lib/routes";
|
||||
|
||||
export function useBkndAuth() {
|
||||
const { config, schema, actions: bkndActions } = useBknd();
|
||||
const { config, schema, actions: bkndActions, app } = useBknd();
|
||||
|
||||
const actions = {
|
||||
config: {
|
||||
set: async (data: Partial<AppAuthSchema>) => {
|
||||
console.log("--set", data);
|
||||
if (await bkndActions.set("auth", data, true)) {
|
||||
await bkndActions.reload();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
add: async (name: string, data: any = {}) => {
|
||||
console.log("add role", name, data);
|
||||
@@ -19,15 +31,39 @@ export function useBkndAuth() {
|
||||
return await bkndActions.remove("auth", `roles.${name}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const minimum_permissions = [
|
||||
"system.access.admin",
|
||||
"system.access.api",
|
||||
"system.config.read",
|
||||
"system.config.read.secrets",
|
||||
"system.build",
|
||||
];
|
||||
const $auth = {
|
||||
roles: {
|
||||
none: Object.keys(config.auth.roles ?? {}).length === 0,
|
||||
minimum_permissions,
|
||||
has_admin: Object.entries(config.auth.roles ?? {}).some(
|
||||
([name, role]) =>
|
||||
role.implicit_allow ||
|
||||
minimum_permissions.every((p) => role.permissions?.includes(p)),
|
||||
),
|
||||
},
|
||||
routes: {
|
||||
settings: app.getSettingsPath(["auth"]),
|
||||
listUsers: app.getAbsolutePath(
|
||||
"/data/" + routes.data.entity.list(config.auth.entity_name),
|
||||
),
|
||||
},
|
||||
};
|
||||
const $auth = {};
|
||||
|
||||
return {
|
||||
$auth,
|
||||
config: config.auth,
|
||||
schema: schema.auth,
|
||||
actions
|
||||
actions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
entitiesSchema,
|
||||
entityFields,
|
||||
fieldsSchema,
|
||||
relationsSchema
|
||||
relationsSchema,
|
||||
} from "data/data-schema";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import type { TSchemaActions } from "ui/client/schema/actions";
|
||||
@@ -28,7 +28,7 @@ export function useBkndData() {
|
||||
console.log("create entity", { data });
|
||||
const validated = parse(entitiesSchema, data, {
|
||||
skipMark: true,
|
||||
forceParse: true
|
||||
forceParse: true,
|
||||
});
|
||||
console.log("validated", validated);
|
||||
// @todo: check for existing?
|
||||
@@ -46,12 +46,12 @@ export function useBkndData() {
|
||||
return await bkndActions.overwrite(
|
||||
"data",
|
||||
`entities.${entityName}.config`,
|
||||
partial
|
||||
partial,
|
||||
);
|
||||
},
|
||||
fields: entityFieldActions(bkndActions, entityName)
|
||||
fields: entityFieldActions(bkndActions, entityName),
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
add: async (relation: TAppDataRelation) => {
|
||||
@@ -59,12 +59,12 @@ export function useBkndData() {
|
||||
const name = crypto.randomUUID();
|
||||
const validated = parse(Type.Union(relationsSchema), relation, {
|
||||
skipMark: true,
|
||||
forceParse: true
|
||||
forceParse: true,
|
||||
});
|
||||
console.log("validated", validated);
|
||||
return await bkndActions.add("data", `relations.${name}`, validated);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
const $data = {
|
||||
entity: (name: string) => entities[name],
|
||||
@@ -72,8 +72,8 @@ export function useBkndData() {
|
||||
system: (name: string) => ({
|
||||
any: entities[name]?.type === "system",
|
||||
users: name === config.auth.entity_name,
|
||||
media: name === config.media.entity_name
|
||||
})
|
||||
media: name === config.media.entity_name,
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -82,7 +82,7 @@ export function useBkndData() {
|
||||
relations: app.relations,
|
||||
config: config.data,
|
||||
schema: schema.data,
|
||||
actions
|
||||
actions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ const modals = {
|
||||
createEntity: () =>
|
||||
bkndModals.open(bkndModals.ids.dataCreate, {
|
||||
initialPath: ["entities", "entity"],
|
||||
initialState: { action: "entity" }
|
||||
initialState: { action: "entity" },
|
||||
}),
|
||||
createRelation: (entity?: string) =>
|
||||
bkndModals.open(bkndModals.ids.dataCreate, {
|
||||
@@ -99,9 +99,9 @@ const modals = {
|
||||
initialState: {
|
||||
action: "relation",
|
||||
relations: {
|
||||
create: [{ source: entity, type: "n:1" } as any]
|
||||
}
|
||||
}
|
||||
create: [{ source: entity, type: "n:1" } as any],
|
||||
},
|
||||
},
|
||||
}),
|
||||
createMedia: (entity?: string) =>
|
||||
bkndModals.open(bkndModals.ids.dataCreate, {
|
||||
@@ -109,10 +109,10 @@ const modals = {
|
||||
initialState: {
|
||||
action: "template-media",
|
||||
initial: {
|
||||
entity
|
||||
}
|
||||
}
|
||||
})
|
||||
entity,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
function entityFieldActions(bkndActions: TSchemaActions, entityName: string) {
|
||||
@@ -121,7 +121,7 @@ function entityFieldActions(bkndActions: TSchemaActions, entityName: string) {
|
||||
console.log("create field", { name, field });
|
||||
const validated = parse(fieldsSchema, field, {
|
||||
skipMark: true,
|
||||
forceParse: true
|
||||
forceParse: true,
|
||||
});
|
||||
console.log("validated", validated);
|
||||
return await bkndActions.add("data", `entities.${entityName}.fields.${name}`, validated);
|
||||
@@ -132,12 +132,12 @@ function entityFieldActions(bkndActions: TSchemaActions, entityName: string) {
|
||||
try {
|
||||
const validated = parse(entityFields, fields, {
|
||||
skipMark: true,
|
||||
forceParse: true
|
||||
forceParse: true,
|
||||
});
|
||||
const res = await bkndActions.overwrite(
|
||||
"data",
|
||||
`entities.${entityName}.fields`,
|
||||
validated
|
||||
validated,
|
||||
);
|
||||
console.log("res", res);
|
||||
//bkndActions.set("data", "entities", fields);
|
||||
@@ -149,6 +149,6 @@ function entityFieldActions(bkndActions: TSchemaActions, entityName: string) {
|
||||
alert("An error occured, check console. There will be nice error handling soon.");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ export function useFlows() {
|
||||
console.log("parsed", parsed);
|
||||
const res = await bkndActions.add("flows", `flows.${name}`, parsed);
|
||||
console.log("res", res);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { flows: app.flows, config: config.flows, actions };
|
||||
|
||||
@@ -13,8 +13,8 @@ export function useBkndMedia() {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
const $media = {};
|
||||
|
||||
@@ -22,6 +22,6 @@ export function useBkndMedia() {
|
||||
$media,
|
||||
config: config.media,
|
||||
schema: schema.media,
|
||||
actions
|
||||
actions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,15 +9,15 @@ export function useBkndSystem() {
|
||||
theme: {
|
||||
set: async (scheme: "light" | "dark") => {
|
||||
return await bkndActions.patch("server", "admin", {
|
||||
color_scheme: scheme
|
||||
color_scheme: scheme,
|
||||
});
|
||||
},
|
||||
toggle: async () => {
|
||||
return await bkndActions.patch("server", "admin", {
|
||||
color_scheme: theme === "light" ? "dark" : "light"
|
||||
color_scheme: theme === "light" ? "dark" : "light",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
const $system = {};
|
||||
|
||||
@@ -26,7 +26,7 @@ export function useBkndSystem() {
|
||||
config: config.server,
|
||||
schema: schema.server,
|
||||
theme,
|
||||
actions
|
||||
actions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { AppTheme } from "modules/server/AppServer";
|
||||
import { useBkndWindowContext } from "ui/client/ClientProvider";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
|
||||
export function useTheme(fallback: AppTheme = "system"): { theme: AppTheme } {
|
||||
export function useTheme(fallback: AppTheme = "system") {
|
||||
const b = useBknd();
|
||||
const winCtx = useBkndWindowContext();
|
||||
|
||||
@@ -14,13 +14,16 @@ export function useTheme(fallback: AppTheme = "system"): { theme: AppTheme } {
|
||||
const override = b?.adminOverride?.color_scheme;
|
||||
const config = b?.config.server.admin.color_scheme;
|
||||
const win = winCtx.color_scheme;
|
||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
const prefersDark =
|
||||
typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
||||
const theme = override ?? config ?? win ?? fallback;
|
||||
|
||||
if (theme === "system") {
|
||||
return { theme: prefersDark ? "dark" : "light" };
|
||||
}
|
||||
|
||||
return { theme };
|
||||
return {
|
||||
theme: (theme === "system" ? (prefersDark ? "dark" : "light") : theme) as AppTheme,
|
||||
prefersDark,
|
||||
override,
|
||||
config,
|
||||
win,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ export function useSetTheme(initialTheme: AppTheme = "light") {
|
||||
fetch("/api/system/config/patch/server/admin", {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ color_scheme: newTheme })
|
||||
body: JSON.stringify({ color_scheme: newTheme }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
|
||||
@@ -7,10 +7,10 @@ export function Context() {
|
||||
<div>
|
||||
{JSON.stringify(
|
||||
{
|
||||
baseurl
|
||||
baseurl,
|
||||
},
|
||||
null,
|
||||
2
|
||||
2,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,13 +7,13 @@ import { Link } from "ui/components/wouter/Link";
|
||||
const sizes = {
|
||||
small: "px-2 py-1.5 rounded-md gap-1 text-sm",
|
||||
default: "px-3 py-2.5 rounded-md gap-1.5",
|
||||
large: "px-4 py-3 rounded-md gap-2.5 text-lg"
|
||||
large: "px-4 py-3 rounded-md gap-2.5 text-lg",
|
||||
};
|
||||
|
||||
const iconSizes = {
|
||||
small: 12,
|
||||
default: 16,
|
||||
large: 20
|
||||
large: 20,
|
||||
};
|
||||
|
||||
const styles = {
|
||||
@@ -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,10 +51,10 @@ 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
|
||||
props.className,
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
@@ -66,7 +66,7 @@ const Base = ({
|
||||
)}
|
||||
{IconRight && <IconRight size={iconSize} {...iconProps} />}
|
||||
</>
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
export type ButtonProps = React.ComponentPropsWithoutRef<"button"> & BaseProps;
|
||||
|
||||
@@ -12,7 +12,7 @@ const styles = {
|
||||
xs: { className: "p-0.5", size: 13 },
|
||||
sm: { className: "p-0.5", size: 15 },
|
||||
md: { className: "p-1", size: 18 },
|
||||
lg: { className: "p-1.5", size: 22 }
|
||||
lg: { className: "p-1.5", size: 22 },
|
||||
} as const;
|
||||
|
||||
interface IconButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||
@@ -38,5 +38,5 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
addEdge,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
useReactFlow
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
||||
import { useBkndSystemTheme } from "ui/client/schema/system/use-bknd-system";
|
||||
@@ -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;
|
||||
};
|
||||
@@ -127,9 +127,9 @@ export function Canvas({
|
||||
data: { label: "" },
|
||||
position: screenToFlowPosition({
|
||||
x: clientX,
|
||||
y: clientY
|
||||
y: clientY,
|
||||
}),
|
||||
origin: [0.0, 0.0]
|
||||
origin: [0.0, 0.0],
|
||||
});
|
||||
|
||||
setNodes((nds) => nds.concat(newNode as any));
|
||||
@@ -138,13 +138,13 @@ export function Canvas({
|
||||
onDropNewNode({
|
||||
id: newNode.id,
|
||||
source: connectionState.fromNode.id,
|
||||
target: newNode.id
|
||||
})
|
||||
)
|
||||
target: newNode.id,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
[screenToFlowPosition]
|
||||
[screenToFlowPosition],
|
||||
);
|
||||
//console.log("edges1", edges);
|
||||
|
||||
@@ -163,12 +163,12 @@ export function Canvas({
|
||||
: ""
|
||||
}
|
||||
proOptions={{
|
||||
hideAttribution: true
|
||||
hideAttribution: true,
|
||||
}}
|
||||
fitView
|
||||
fitViewOptions={{
|
||||
maxZoom: 1.5,
|
||||
...props.fitViewOptions
|
||||
...props.fitViewOptions,
|
||||
}}
|
||||
nodeDragThreshold={25}
|
||||
panOnScrollSpeed={1}
|
||||
|
||||
@@ -14,7 +14,7 @@ export function DefaultNode({ selected, children, className, ...props }: TDefaul
|
||||
className={twMerge(
|
||||
"relative w-80 shadow-lg rounded-lg bg-background",
|
||||
selected && "outline outline-blue-500/25",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -31,7 +31,7 @@ const Header: React.FC<TDefaultNodeHeaderProps> = ({ className, label, children,
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"flex flex-row bg-primary/15 justify-center items-center rounded-tl-lg rounded-tr-lg py-1 px-2 drag-handle",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children ? (
|
||||
|
||||
@@ -29,7 +29,7 @@ export const layoutWithDagre = ({ nodes, edges, graph }: LayoutProps) => {
|
||||
nodes.forEach((node) => {
|
||||
dagreGraph.setNode(node.id, {
|
||||
width: node.width,
|
||||
height: node.height
|
||||
height: node.height,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,9 +45,9 @@ export const layoutWithDagre = ({ nodes, edges, graph }: LayoutProps) => {
|
||||
return {
|
||||
...node,
|
||||
x: position.x - (node.width ?? 0) / 2,
|
||||
y: position.y - (node.height ?? 0) / 2
|
||||
y: position.y - (node.height ?? 0) / 2,
|
||||
};
|
||||
}),
|
||||
edges
|
||||
edges,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ const Wrapper = ({ children, className, ...props }: ComponentPropsWithoutRef<"di
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"flex flex-row bg-lightest border ring-2 ring-muted/5 border-muted rounded-full items-center p-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -70,7 +70,7 @@ const Text = forwardRef<any, ComponentPropsWithoutRef<"span"> & { mono?: boolean
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
Panel.Wrapper = Wrapper;
|
||||
|
||||
@@ -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,13 +16,12 @@ export default function CodeEditor({
|
||||
_extensions = {},
|
||||
...props
|
||||
}: CodeEditorProps) {
|
||||
const b = useBknd();
|
||||
const theme = b.app.getAdminConfig().color_scheme;
|
||||
const { theme } = useTheme();
|
||||
const _basicSetup: Partial<ReactCodeMirrorProps["basicSetup"]> = !editable
|
||||
? {
|
||||
...(typeof basicSetup === "object" ? basicSetup : {}),
|
||||
highlightActiveLine: false,
|
||||
highlightActiveLineGutter: false
|
||||
highlightActiveLineGutter: false,
|
||||
}
|
||||
: basicSetup;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export function JsonEditor({ editable, className, ...props }: CodeEditorProps) {
|
||||
className={twMerge(
|
||||
"flex w-full border border-muted",
|
||||
!editable && "opacity-70",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
editable={editable}
|
||||
_extensions={{ json: true }}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const JsonViewer = ({
|
||||
expand = 0,
|
||||
showSize = false,
|
||||
showCopy = false,
|
||||
className
|
||||
className,
|
||||
}: {
|
||||
json: object;
|
||||
title?: string;
|
||||
@@ -61,7 +61,7 @@ export const JsonViewer = ({
|
||||
"text-zinc-400 font-mono font-bold text-lg after:content-['▾'] mr-1.5",
|
||||
expandIcon:
|
||||
"text-zinc-400 font-mono font-bold text-lg after:content-['▸'] mr-1.5",
|
||||
noQuotesForStringValues: false
|
||||
noQuotesForStringValues: false,
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -80,7 +80,7 @@ const filters = [
|
||||
{ label: "url_encode" },
|
||||
{ label: "where" },
|
||||
{ label: "where_exp" },
|
||||
{ label: "xml_escape" }
|
||||
{ label: "xml_escape" },
|
||||
];
|
||||
|
||||
const tags = [
|
||||
@@ -103,7 +103,7 @@ const tags = [
|
||||
{ label: "render" },
|
||||
{ label: "tablerow" },
|
||||
{ label: "unless" },
|
||||
{ label: "when" }
|
||||
{ label: "when" },
|
||||
];
|
||||
|
||||
export function LiquidJsEditor({ editable, ...props }: CodeEditorProps) {
|
||||
@@ -112,11 +112,11 @@ export function LiquidJsEditor({ editable, ...props }: CodeEditorProps) {
|
||||
<CodeEditor
|
||||
className={twMerge(
|
||||
"flex w-full border border-muted bg-white rounded-lg",
|
||||
!editable && "opacity-70"
|
||||
!editable && "opacity-70",
|
||||
)}
|
||||
editable={editable}
|
||||
_extensions={{
|
||||
liquid: { filters, tags }
|
||||
liquid: { filters, tags },
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -22,11 +22,13 @@ const Base: React.FC<AlertProps> = ({
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"flex flex-row items-center dark:bg-amber-300/20 bg-amber-200 p-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{title && <b className="mr-2">{title}:</b>}
|
||||
{message || children}
|
||||
<p>
|
||||
{title && <b>{title}: </b>}
|
||||
{message || children}
|
||||
</p>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
@@ -50,5 +52,5 @@ export const Alert = {
|
||||
Warning,
|
||||
Exception,
|
||||
Success,
|
||||
Info
|
||||
Info,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Empty: React.FC<EmptyProps> = ({
|
||||
description = "Check back later my friend.",
|
||||
primary,
|
||||
secondary,
|
||||
className
|
||||
className,
|
||||
}) => (
|
||||
<div className={twMerge("flex flex-col h-full w-full justify-center items-center", className)}>
|
||||
<div className="flex flex-col gap-3 items-center max-w-80">
|
||||
|
||||
67
app/src/ui/components/display/ErrorBoundary.tsx
Normal file
67
app/src/ui/components/display/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { Component, type ErrorInfo, type ReactNode } from "react";
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
suppressError?: boolean;
|
||||
fallback?:
|
||||
| (({ error, resetError }: { error: Error; resetError: () => void }) => ReactNode)
|
||||
| ReactNode;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error?: Error | undefined;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: undefined };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
const type = this.props.suppressError ? "warn" : "error";
|
||||
console[type]("ErrorBoundary caught an error:", error, errorInfo);
|
||||
}
|
||||
|
||||
resetError = () => {
|
||||
this.setState({ hasError: false, error: undefined });
|
||||
};
|
||||
|
||||
private renderFallback() {
|
||||
if (this.props.fallback) {
|
||||
return typeof this.props.fallback === "function"
|
||||
? this.props.fallback({ error: this.state.error!, resetError: this.resetError })
|
||||
: this.props.fallback;
|
||||
}
|
||||
return <BaseError>Error</BaseError>;
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this.state.hasError) {
|
||||
return this.renderFallback();
|
||||
}
|
||||
|
||||
if (this.props.suppressError) {
|
||||
try {
|
||||
return this.props.children;
|
||||
} catch (e) {
|
||||
return this.renderFallback();
|
||||
}
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const BaseError = ({ children }: { children: ReactNode }) => (
|
||||
<div className="bg-red-700 text-white py-1 px-2 rounded-md leading-none font-mono">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ErrorBoundary;
|
||||
18
app/src/ui/components/display/Icon.tsx
Normal file
18
app/src/ui/components/display/Icon.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { TbAlertCircle } from "react-icons/tb";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type IconProps = {
|
||||
className?: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const Warning = ({ className, ...props }: IconProps) => (
|
||||
<TbAlertCircle
|
||||
{...props}
|
||||
className={twMerge("dark:text-amber-300 text-amber-700 cursor-help", className)}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Icon = {
|
||||
Warning,
|
||||
};
|
||||
@@ -11,7 +11,7 @@ export function Logo({
|
||||
|
||||
const dim = {
|
||||
width: Math.round(578 * scale),
|
||||
height: Math.round(188 * scale)
|
||||
height: Math.round(188 * scale),
|
||||
} as const;
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,5 +20,5 @@ const MissingPermission = ({
|
||||
export const Message = {
|
||||
NotFound,
|
||||
NotAllowed,
|
||||
MissingPermission
|
||||
MissingPermission,
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ export function FloatingSelect({ data, label, description }: FloatingSelectProps
|
||||
key={item}
|
||||
className={twMerge(
|
||||
"transition-colors duration-100 px-2.5 py-2 leading-none rounded-lg text-md",
|
||||
active === index && "text-white"
|
||||
active === index && "text-white",
|
||||
)}
|
||||
ref={setControlRef(index)}
|
||||
onClick={() => setActive(index)}
|
||||
|
||||
@@ -26,5 +26,5 @@ export const BooleanInputMantine = forwardRef<HTMLInputElement, React.ComponentP
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import clsx from "clsx";
|
||||
import { getBrowser } from "core/utils";
|
||||
import type { Field } from "data";
|
||||
import { Switch as RadixSwitch } from "radix-ui";
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState
|
||||
useState,
|
||||
} from "react";
|
||||
import { TbCalendar, TbChevronDown, TbInfoCircle } from "react-icons/tb";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@@ -31,7 +32,7 @@ export const Group = <E extends ElementType = "div">({
|
||||
as === "fieldset" && "border border-primary/10 p-3 rounded-md",
|
||||
as === "fieldset" && error && "border-red-500",
|
||||
error && "text-red-500",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -92,7 +93,7 @@ export const Input = forwardRef<HTMLInputElement, React.ComponentProps<"input">>
|
||||
disabledOrReadonly && "bg-muted/50 text-primary/50",
|
||||
!disabledOrReadonly &&
|
||||
"focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -107,11 +108,11 @@ export const Textarea = forwardRef<HTMLTextAreaElement, React.ComponentProps<"te
|
||||
ref={ref}
|
||||
className={twMerge(
|
||||
"bg-muted/40 min-h-11 rounded-md py-2.5 px-4 focus:bg-muted outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const DateInput = forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
@@ -144,7 +145,7 @@ export const DateInput = forwardRef<HTMLInputElement, React.ComponentProps<"inpu
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const BooleanInput = forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
@@ -173,10 +174,25 @@ export const BooleanInput = forwardRef<HTMLInputElement, React.ComponentProps<"i
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export type SwitchValue = boolean | 1 | 0 | "true" | "false" | "on" | "off";
|
||||
const SwitchSizes = {
|
||||
xs: {
|
||||
root: "h-5 w-8",
|
||||
thumb: "data-[state=checked]:left-[calc(100%-1rem)]",
|
||||
},
|
||||
sm: {
|
||||
root: "h-6 w-10",
|
||||
thumb: "data-[state=checked]:left-[calc(100%-1.25rem)]",
|
||||
},
|
||||
md: {
|
||||
root: "h-7 w-12",
|
||||
thumb: "data-[state=checked]:left-[calc(100%-1.5rem)]",
|
||||
},
|
||||
};
|
||||
|
||||
export const Switch = forwardRef<
|
||||
HTMLButtonElement,
|
||||
Pick<
|
||||
@@ -184,14 +200,20 @@ export const Switch = forwardRef<
|
||||
"name" | "required" | "disabled" | "checked" | "defaultChecked" | "id" | "type"
|
||||
> & {
|
||||
value?: SwitchValue;
|
||||
size?: keyof typeof SwitchSizes;
|
||||
onChange?: (e: { target: { value: boolean } }) => void;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
}
|
||||
>(({ type, required, ...props }, ref) => {
|
||||
return (
|
||||
<RadixSwitch.Root
|
||||
className="relative h-7 w-12 cursor-pointer rounded-full bg-muted border-2 border-transparent outline-none data-[state=checked]:bg-primary/75 appearance-none transition-colors hover:bg-muted/80"
|
||||
className={clsx(
|
||||
"relative cursor-pointer rounded-full bg-muted border-2 border-transparent outline-none ring-1 dark:ring-primary/10 ring-primary/20 data-[state=checked]:ring-primary/60 data-[state=checked]:bg-primary/60 appearance-none transition-colors hover:bg-muted/80",
|
||||
SwitchSizes[props.size ?? "md"].root,
|
||||
props.disabled && "opacity-50 !cursor-not-allowed",
|
||||
)}
|
||||
onCheckedChange={(bool) => {
|
||||
console.log("setting", bool);
|
||||
props.onChange?.({ target: { value: bool } });
|
||||
}}
|
||||
{...(props as any)}
|
||||
@@ -204,7 +226,12 @@ export const Switch = forwardRef<
|
||||
}
|
||||
ref={ref}
|
||||
>
|
||||
<RadixSwitch.Thumb className="absolute top-0 left-0 h-full aspect-square rounded-full bg-background transition-[left,right] duration-100 border border-muted data-[state=checked]:left-[calc(100%-1.5rem)]" />
|
||||
<RadixSwitch.Thumb
|
||||
className={clsx(
|
||||
"absolute top-0 left-0 h-full aspect-square rounded-full bg-primary/30 data-[state=checked]:bg-background transition-[left,right] duration-100 border border-muted",
|
||||
SwitchSizes[props.size ?? "md"].thumb,
|
||||
)}
|
||||
/>
|
||||
</RadixSwitch.Root>
|
||||
);
|
||||
});
|
||||
@@ -223,7 +250,7 @@ export const Select = forwardRef<
|
||||
"bg-muted/40 focus:bg-muted rounded-md py-2.5 px-4 outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50",
|
||||
"appearance-none h-11 w-full",
|
||||
!props.multiple && "border-r-8 border-r-transparent",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{options ? (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
Input,
|
||||
SegmentedControl as MantineSegmentedControl,
|
||||
type SegmentedControlProps as MantineSegmentedControlProps
|
||||
type SegmentedControlProps as MantineSegmentedControlProps,
|
||||
} from "@mantine/core";
|
||||
|
||||
type SegmentedControlProps = MantineSegmentedControlProps & {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
NumberInput as $NumberInput,
|
||||
type NumberInputProps as $NumberInputProps
|
||||
type NumberInputProps as $NumberInputProps,
|
||||
} from "@mantine/core";
|
||||
import { type FieldValues, type UseControllerProps, useController } from "react-hook-form";
|
||||
|
||||
@@ -18,13 +18,13 @@ export function MantineNumberInput<T extends FieldValues>({
|
||||
}: MantineNumberInputProps<T>) {
|
||||
const {
|
||||
field: { value, onChange: fieldOnChange, ...field },
|
||||
fieldState
|
||||
fieldState,
|
||||
} = useController<T>({
|
||||
name,
|
||||
control,
|
||||
defaultValue,
|
||||
rules,
|
||||
shouldUnregister
|
||||
shouldUnregister,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
Radio as $Radio,
|
||||
RadioGroup as $RadioGroup,
|
||||
type RadioGroupProps as $RadioGroupProps,
|
||||
type RadioProps as $RadioProps
|
||||
type RadioProps as $RadioProps,
|
||||
} from "@mantine/core";
|
||||
import { type FieldValues, type UseControllerProps, useController } from "react-hook-form";
|
||||
|
||||
@@ -19,13 +19,13 @@ export function MantineRadio<T extends FieldValues>({
|
||||
...props
|
||||
}: RadioProps<T>) {
|
||||
const {
|
||||
field: { value, onChange: fieldOnChange, ...field }
|
||||
field: { value, onChange: fieldOnChange, ...field },
|
||||
} = useController<T>({
|
||||
name,
|
||||
control,
|
||||
defaultValue,
|
||||
rules,
|
||||
shouldUnregister
|
||||
shouldUnregister,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -55,13 +55,13 @@ function RadioGroup<T extends FieldValues>({
|
||||
}: RadioGroupProps<T>) {
|
||||
const {
|
||||
field: { value, onChange: fieldOnChange, ...field },
|
||||
fieldState
|
||||
fieldState,
|
||||
} = useController<T>({
|
||||
name,
|
||||
control,
|
||||
defaultValue,
|
||||
rules,
|
||||
shouldUnregister
|
||||
shouldUnregister,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
SegmentedControl as $SegmentedControl,
|
||||
type SegmentedControlProps as $SegmentedControlProps,
|
||||
Input
|
||||
Input,
|
||||
} from "@mantine/core";
|
||||
import { type FieldValues, type UseControllerProps, useController } from "react-hook-form";
|
||||
|
||||
@@ -26,13 +26,13 @@ export function MantineSegmentedControl<T extends FieldValues>({
|
||||
...props
|
||||
}: MantineSegmentedControlProps<T>) {
|
||||
const {
|
||||
field: { value, onChange: fieldOnChange, ...field }
|
||||
field: { value, onChange: fieldOnChange, ...field },
|
||||
} = useController<T>({
|
||||
name,
|
||||
control,
|
||||
defaultValue,
|
||||
rules,
|
||||
shouldUnregister
|
||||
shouldUnregister,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -16,13 +16,13 @@ export function MantineSelect<T extends FieldValues>({
|
||||
}: MantineSelectProps<T>) {
|
||||
const {
|
||||
field: { value, onChange: fieldOnChange, ...field },
|
||||
fieldState
|
||||
fieldState,
|
||||
} = useController<T>({
|
||||
name,
|
||||
control,
|
||||
defaultValue,
|
||||
rules,
|
||||
shouldUnregister
|
||||
shouldUnregister,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -34,8 +34,8 @@ export function MantineSelect<T extends FieldValues>({
|
||||
...new Event("change", { bubbles: true, cancelable: true }),
|
||||
target: {
|
||||
value: e,
|
||||
name: field.name
|
||||
}
|
||||
name: field.name,
|
||||
},
|
||||
});
|
||||
// @ts-ignore
|
||||
onChange?.(e);
|
||||
|
||||
@@ -15,13 +15,13 @@ export function MantineSwitch<T extends FieldValues>({
|
||||
}: SwitchProps<T>) {
|
||||
const {
|
||||
field: { value, onChange: fieldOnChange, ...field },
|
||||
fieldState
|
||||
fieldState,
|
||||
} = useController<T>({
|
||||
name,
|
||||
control,
|
||||
defaultValue,
|
||||
rules,
|
||||
shouldUnregister
|
||||
shouldUnregister,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,6 @@ import { getLabel, getMultiSchemaMatched } from "./utils";
|
||||
|
||||
export type AnyOfFieldRootProps = {
|
||||
path?: string;
|
||||
schema?: JsonSchema;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
@@ -34,14 +33,14 @@ export const useAnyOfContext = () => {
|
||||
|
||||
const selectedAtom = atom<number | null>(null);
|
||||
|
||||
const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) => {
|
||||
const Root = ({ path = "", children }: AnyOfFieldRootProps) => {
|
||||
const {
|
||||
setValue,
|
||||
lib,
|
||||
pointer,
|
||||
value: { matchedIndex, schemas },
|
||||
schema
|
||||
} = useDerivedFieldContext(path, _schema, (ctx) => {
|
||||
schema,
|
||||
} = useDerivedFieldContext(path, (ctx) => {
|
||||
const [matchedIndex, schemas = []] = getMultiSchemaMatched(ctx.schema, ctx.value);
|
||||
return { matchedIndex, schemas };
|
||||
});
|
||||
@@ -59,7 +58,7 @@ const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) =>
|
||||
const options = schemas.map((s, i) => s.title ?? `Option ${i + 1}`);
|
||||
const selectSchema = {
|
||||
type: "string",
|
||||
enum: options
|
||||
enum: options,
|
||||
} satisfies JsonSchema;
|
||||
|
||||
const selectedSchema = selected !== null ? (schemas[selected] as JsonSchema) : undefined;
|
||||
@@ -70,7 +69,7 @@ const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) =>
|
||||
selectedSchema,
|
||||
schema,
|
||||
schemas,
|
||||
selected
|
||||
selected,
|
||||
};
|
||||
}, [selected]);
|
||||
|
||||
@@ -81,7 +80,7 @@ const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) =>
|
||||
...context,
|
||||
select,
|
||||
path,
|
||||
errors
|
||||
errors,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -115,7 +114,7 @@ const Select = () => {
|
||||
};
|
||||
|
||||
// @todo: add local validation for AnyOf fields
|
||||
const Field = ({ name, label, schema, ...props }: Partial<FormFieldProps>) => {
|
||||
const Field = ({ name, label, ...props }: Partial<FormFieldProps>) => {
|
||||
const { selected, selectedSchema, path, errors } = useAnyOfContext();
|
||||
if (selected === null) return null;
|
||||
return (
|
||||
@@ -131,7 +130,7 @@ export const AnyOf = {
|
||||
Root,
|
||||
Select,
|
||||
Field,
|
||||
useContext: useAnyOfContext
|
||||
useContext: useAnyOfContext,
|
||||
};
|
||||
|
||||
export const AnyOfField = (props: Omit<AnyOfFieldRootProps, "children">) => {
|
||||
|
||||
@@ -10,12 +10,8 @@ import { FieldWrapper } from "./FieldWrapper";
|
||||
import { useDerivedFieldContext, useFormValue } from "./Form";
|
||||
import { coerce, getMultiSchema, getMultiSchemaMatched, isEqual, suffixPath } from "./utils";
|
||||
|
||||
export const ArrayField = ({
|
||||
path = "",
|
||||
schema: _schema
|
||||
}: { path?: string; schema?: JsonSchema }) => {
|
||||
const { setValue, pointer, required, ...ctx } = useDerivedFieldContext(path, _schema);
|
||||
const schema = _schema ?? ctx.schema;
|
||||
export const ArrayField = ({ path = "" }: { path?: string }) => {
|
||||
const { setValue, pointer, required, schema, ...ctx } = useDerivedFieldContext(path);
|
||||
if (!schema || typeof schema === "undefined") return `ArrayField(${path}): no schema ${pointer}`;
|
||||
|
||||
// if unique items with enum
|
||||
@@ -55,7 +51,7 @@ export const ArrayField = ({
|
||||
};
|
||||
|
||||
const ArrayItem = memo(({ path, index, schema }: any) => {
|
||||
const { value, ...ctx } = useDerivedFieldContext(path, schema, (ctx) => {
|
||||
const { value, ...ctx } = useDerivedFieldContext(path, (ctx) => {
|
||||
return ctx.value?.[index];
|
||||
});
|
||||
const itemPath = suffixPath(path, index);
|
||||
@@ -76,7 +72,7 @@ const ArrayItem = memo(({ path, index, schema }: any) => {
|
||||
|
||||
const DeleteButton = useMemo(
|
||||
() => <IconButton Icon={IconTrash} onClick={() => handleDelete(itemPath)} size="sm" />,
|
||||
[itemPath]
|
||||
[itemPath],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -99,7 +95,7 @@ const ArrayIterator = memo(
|
||||
({ name, children }: any) => {
|
||||
return children(useFormValue(name));
|
||||
},
|
||||
(prev, next) => prev.value?.length === next.value?.length
|
||||
(prev, next) => prev.value?.length === next.value?.length,
|
||||
);
|
||||
|
||||
const ArrayAdd = ({ schema, path }: { schema: JsonSchema; path: string }) => {
|
||||
@@ -107,7 +103,7 @@ const ArrayAdd = ({ schema, path }: { schema: JsonSchema; path: string }) => {
|
||||
setValue,
|
||||
value: { currentIndex },
|
||||
...ctx
|
||||
} = useDerivedFieldContext(path, schema, (ctx) => {
|
||||
} = useDerivedFieldContext(path, (ctx) => {
|
||||
return { currentIndex: ctx.value?.length ?? 0 };
|
||||
});
|
||||
const itemsMultiSchema = getMultiSchema(schema.items);
|
||||
@@ -121,11 +117,11 @@ const ArrayAdd = ({ schema, path }: { schema: JsonSchema; path: string }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownWrapperProps={{
|
||||
className: "min-w-0"
|
||||
className: "min-w-0",
|
||||
}}
|
||||
items={itemsMultiSchema.map((s, i) => ({
|
||||
label: s!.title ?? `Option ${i + 1}`,
|
||||
onClick: () => handleAdd(ctx.lib.getTemplate(undefined, s!))
|
||||
onClick: () => handleAdd(ctx.lib.getTemplate(undefined, s!)),
|
||||
}))}
|
||||
onClickItem={console.log}
|
||||
>
|
||||
|
||||
@@ -1,24 +1,40 @@
|
||||
import type { JsonSchema } from "json-schema-library";
|
||||
import type { ChangeEvent, ComponentPropsWithoutRef } from "react";
|
||||
import ErrorBoundary from "ui/components/display/ErrorBoundary";
|
||||
import * as Formy from "ui/components/form/Formy";
|
||||
import { useEvent } from "ui/hooks/use-event";
|
||||
import { ArrayField } from "./ArrayField";
|
||||
import { FieldWrapper } from "./FieldWrapper";
|
||||
import { FieldWrapper, type FieldwrapperProps } from "./FieldWrapper";
|
||||
import { useDerivedFieldContext, useFormValue } from "./Form";
|
||||
import { ObjectField } from "./ObjectField";
|
||||
import { coerce, isType, isTypeSchema } from "./utils";
|
||||
|
||||
export type FieldProps = {
|
||||
name: string;
|
||||
schema?: JsonSchema;
|
||||
onChange?: (e: ChangeEvent<any>) => void;
|
||||
label?: string | false;
|
||||
hidden?: boolean;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
} & Omit<FieldwrapperProps, "children" | "schema">;
|
||||
|
||||
export const Field = (props: FieldProps) => {
|
||||
return (
|
||||
<ErrorBoundary fallback={fieldErrorBoundary(props)}>
|
||||
<FieldImpl {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }: FieldProps) => {
|
||||
const { path, setValue, required, ...ctx } = useDerivedFieldContext(name, _schema);
|
||||
const schema = _schema ?? ctx.schema;
|
||||
const fieldErrorBoundary =
|
||||
({ name }: FieldProps) =>
|
||||
({ error }: { error: Error }) => (
|
||||
<Pre>
|
||||
Field "{name}" error: {error.message}
|
||||
</Pre>
|
||||
);
|
||||
|
||||
const FieldImpl = ({ name, onChange, placeholder, required: _required, ...props }: FieldProps) => {
|
||||
const { path, setValue, schema, ...ctx } = useDerivedFieldContext(name);
|
||||
const required = typeof _required === "boolean" ? _required : ctx.required;
|
||||
//console.log("Field", { name, path, schema });
|
||||
if (!isTypeSchema(schema))
|
||||
return (
|
||||
<Pre>
|
||||
@@ -27,14 +43,14 @@ export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }
|
||||
);
|
||||
|
||||
if (isType(schema.type, "object")) {
|
||||
return <ObjectField path={name} schema={schema} />;
|
||||
return <ObjectField path={name} />;
|
||||
}
|
||||
|
||||
if (isType(schema.type, "array")) {
|
||||
return <ArrayField path={name} schema={schema} />;
|
||||
return <ArrayField path={name} />;
|
||||
}
|
||||
|
||||
const disabled = schema.readOnly ?? "const" in schema ?? false;
|
||||
const disabled = props.disabled ?? schema.readOnly ?? "const" in schema ?? false;
|
||||
|
||||
const handleChange = useEvent((e: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = coerce(e.target.value, schema as any, { required });
|
||||
@@ -46,12 +62,13 @@ export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }
|
||||
});
|
||||
|
||||
return (
|
||||
<FieldWrapper name={name} label={_label} required={required} schema={schema} hidden={hidden}>
|
||||
<FieldWrapper name={name} required={required} schema={schema} {...props}>
|
||||
<FieldComponent
|
||||
schema={schema}
|
||||
name={name}
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange ?? handleChange}
|
||||
/>
|
||||
</FieldWrapper>
|
||||
@@ -73,7 +90,11 @@ export const FieldComponent = ({
|
||||
const props = {
|
||||
..._props,
|
||||
// allow override
|
||||
value: typeof _props.value !== "undefined" ? _props.value : value
|
||||
value: typeof _props.value !== "undefined" ? _props.value : value,
|
||||
placeholder:
|
||||
(_props.placeholder ?? typeof schema.default !== "undefined")
|
||||
? String(schema.default)
|
||||
: "",
|
||||
};
|
||||
|
||||
if (schema.enum) {
|
||||
@@ -84,7 +105,7 @@ export const FieldComponent = ({
|
||||
const additional = {
|
||||
min: schema.minimum,
|
||||
max: schema.maximum,
|
||||
step: schema.multipleOf
|
||||
step: schema.multipleOf,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -114,7 +135,7 @@ export const FieldComponent = ({
|
||||
const date = new Date(e.target.value);
|
||||
props.onChange?.({
|
||||
// @ts-ignore
|
||||
target: { value: date.toISOString() }
|
||||
target: { value: date.toISOString() },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -128,7 +149,7 @@ export const FieldComponent = ({
|
||||
const additional = {
|
||||
maxLength: schema.maxLength,
|
||||
minLength: schema.minLength,
|
||||
pattern: schema.pattern
|
||||
pattern: schema.pattern,
|
||||
} as any;
|
||||
|
||||
if (schema.format) {
|
||||
|
||||
@@ -7,14 +7,14 @@ import * as Formy from "ui/components/form/Formy";
|
||||
import {
|
||||
useFormContext,
|
||||
useFormError,
|
||||
useFormValue
|
||||
useFormValue,
|
||||
} from "ui/components/form/json-schema-form/Form";
|
||||
import { Popover } from "ui/components/overlay/Popover";
|
||||
import { getLabel } from "./utils";
|
||||
|
||||
export type FieldwrapperProps = {
|
||||
name: string;
|
||||
label?: string | false;
|
||||
label?: string | ReactNode | false;
|
||||
required?: boolean;
|
||||
schema?: JsonSchema;
|
||||
debug?: object | boolean;
|
||||
@@ -22,6 +22,8 @@ export type FieldwrapperProps = {
|
||||
hidden?: boolean;
|
||||
children: ReactElement | ReactNode;
|
||||
errorPlacement?: "top" | "bottom";
|
||||
description?: string;
|
||||
descriptionPlacement?: "top" | "bottom";
|
||||
};
|
||||
|
||||
export function FieldWrapper({
|
||||
@@ -32,18 +34,26 @@ export function FieldWrapper({
|
||||
wrapper,
|
||||
hidden,
|
||||
errorPlacement = "bottom",
|
||||
children
|
||||
descriptionPlacement = "bottom",
|
||||
children,
|
||||
...props
|
||||
}: FieldwrapperProps) {
|
||||
const errors = useFormError(name, { strict: true });
|
||||
const examples = schema?.examples || [];
|
||||
const examplesId = `${name}-examples`;
|
||||
const description = schema?.description;
|
||||
const description = props?.description ?? schema?.description;
|
||||
const label = typeof _label !== "undefined" ? _label : schema ? getLabel(name, schema) : name;
|
||||
|
||||
const Errors = errors.length > 0 && (
|
||||
<Formy.ErrorMessage>{errors.map((e) => e.message).join(", ")}</Formy.ErrorMessage>
|
||||
);
|
||||
|
||||
const Description = description && (
|
||||
<Formy.Help className={descriptionPlacement === "top" ? "-mt-1 mb-1" : "mb-2"}>
|
||||
{description}
|
||||
</Formy.Help>
|
||||
);
|
||||
|
||||
return (
|
||||
<Formy.Group
|
||||
error={errors.length > 0}
|
||||
@@ -62,13 +72,14 @@ export function FieldWrapper({
|
||||
{label} {required && <span className="font-medium opacity-30">*</span>}
|
||||
</Formy.Label>
|
||||
)}
|
||||
{descriptionPlacement === "top" && Description}
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="flex flex-1 flex-col gap-3">
|
||||
{Children.count(children) === 1 && isValidElement(children)
|
||||
? cloneElement(children, {
|
||||
// @ts-ignore
|
||||
list: examples.length > 0 ? examplesId : undefined
|
||||
list: examples.length > 0 ? examplesId : undefined,
|
||||
})
|
||||
: children}
|
||||
{examples.length > 0 && (
|
||||
@@ -80,7 +91,7 @@ export function FieldWrapper({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{description && <Formy.Help>{description}</Formy.Help>}
|
||||
{descriptionPlacement === "bottom" && Description}
|
||||
{errorPlacement === "bottom" && Errors}
|
||||
</Formy.Group>
|
||||
);
|
||||
@@ -89,7 +100,7 @@ export function FieldWrapper({
|
||||
const FieldDebug = ({
|
||||
name,
|
||||
schema,
|
||||
required
|
||||
required,
|
||||
}: Pick<FieldwrapperProps, "name" | "schema" | "required">) => {
|
||||
const { options } = useFormContext();
|
||||
if (!options?.debug) return null;
|
||||
@@ -100,7 +111,7 @@ const FieldDebug = ({
|
||||
<div className="absolute top-0 right-0">
|
||||
<Popover
|
||||
overlayProps={{
|
||||
className: "max-w-none"
|
||||
className: "max-w-none",
|
||||
}}
|
||||
position="bottom-end"
|
||||
target={({ toggle }) => (
|
||||
@@ -111,7 +122,7 @@ const FieldDebug = ({
|
||||
value,
|
||||
required,
|
||||
schema,
|
||||
errors
|
||||
errors,
|
||||
}}
|
||||
expand={6}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
getDefaultStore,
|
||||
useAtom,
|
||||
useAtomValue,
|
||||
useSetAtom
|
||||
useSetAtom,
|
||||
} from "jotai";
|
||||
import { selectAtom } from "jotai/utils";
|
||||
import { Draft2019, type JsonError, type JsonSchema as LibJsonSchema } from "json-schema-library";
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef
|
||||
useRef,
|
||||
} from "react";
|
||||
import { JsonViewer } from "ui/components/code/JsonViewer";
|
||||
import { useEvent } from "ui/hooks/use-event";
|
||||
@@ -32,10 +32,10 @@ import {
|
||||
omitSchema,
|
||||
pathToPointer,
|
||||
prefixPath,
|
||||
prefixPointer
|
||||
prefixPointer,
|
||||
} from "./utils";
|
||||
|
||||
type JSONSchema = Exclude<$JSONSchema, boolean>;
|
||||
export type JSONSchema = Exclude<$JSONSchema, boolean>;
|
||||
type FormState<Data = any> = {
|
||||
dirty: boolean;
|
||||
submitting: boolean;
|
||||
@@ -67,7 +67,7 @@ FormContext.displayName = "FormContext";
|
||||
|
||||
export function Form<
|
||||
const Schema extends JSONSchema,
|
||||
const Data = Schema extends JSONSchema ? FromSchema<Schema> : any
|
||||
const Data = Schema extends JSONSchema ? FromSchema<Schema> : any,
|
||||
>({
|
||||
schema: _schema,
|
||||
initialValues: _initialValues,
|
||||
@@ -101,7 +101,7 @@ export function Form<
|
||||
dirty: false,
|
||||
submitting: false,
|
||||
errors: [] as JsonError[],
|
||||
data: initialValues
|
||||
data: initialValues,
|
||||
});
|
||||
}, [initialValues]);
|
||||
const setFormState = useSetAtom(_formStateAtom);
|
||||
@@ -188,9 +188,9 @@ export function Form<
|
||||
lib,
|
||||
options,
|
||||
root: "",
|
||||
path: ""
|
||||
path: "",
|
||||
}),
|
||||
[schema, initialValues]
|
||||
[schema, initialValues],
|
||||
) as any;
|
||||
|
||||
return (
|
||||
@@ -236,7 +236,7 @@ export function FormContextOverride({
|
||||
const context = {
|
||||
...ctx,
|
||||
...overrides,
|
||||
...additional
|
||||
...additional,
|
||||
};
|
||||
|
||||
return <FormContext.Provider value={context}>{children}</FormContext.Provider>;
|
||||
@@ -255,12 +255,12 @@ export function useFormValue(name: string, opts?: { strict?: boolean }) {
|
||||
const pointer = pathToPointer(prefixedName);
|
||||
return {
|
||||
value: getPath(state.data, prefixedName),
|
||||
errors: state.errors.filter((error) => error.data.pointer.startsWith(pointer))
|
||||
errors: state.errors.filter((error) => error.data.pointer.startsWith(pointer)),
|
||||
};
|
||||
},
|
||||
[name]
|
||||
[name],
|
||||
),
|
||||
isEqual
|
||||
isEqual,
|
||||
);
|
||||
return useAtom(selected)[0];
|
||||
}
|
||||
@@ -279,18 +279,19 @@ export function useFormError(name: string, opt?: { strict?: boolean; debug?: boo
|
||||
: error.data.pointer.startsWith(pointer);
|
||||
});
|
||||
},
|
||||
[name]
|
||||
[name],
|
||||
),
|
||||
isEqual
|
||||
isEqual,
|
||||
);
|
||||
return useAtom(selected)[0];
|
||||
}
|
||||
|
||||
export function useFormStateSelector<Data = any, Reduced = Data>(
|
||||
selector: (state: FormState<Data>) => Reduced
|
||||
selector: (state: FormState<Data>) => Reduced,
|
||||
deps: any[] = [],
|
||||
): Reduced {
|
||||
const { _formStateAtom } = useFormContext();
|
||||
const selected = selectAtom(_formStateAtom, useCallback(selector, []), isEqual);
|
||||
const selected = selectAtom(_formStateAtom, useCallback(selector, deps), isEqual);
|
||||
return useAtom(selected)[0];
|
||||
}
|
||||
|
||||
@@ -298,7 +299,6 @@ type SelectorFn<Ctx = any, Refined = any> = (state: Ctx) => Refined;
|
||||
|
||||
export function useDerivedFieldContext<Data = any, Reduced = undefined>(
|
||||
path,
|
||||
_schema?: LibJsonSchema,
|
||||
deriveFn?: SelectorFn<
|
||||
FormContext<Data> & {
|
||||
pointer: string;
|
||||
@@ -307,7 +307,8 @@ export function useDerivedFieldContext<Data = any, Reduced = undefined>(
|
||||
path: string;
|
||||
},
|
||||
Reduced
|
||||
>
|
||||
>,
|
||||
_schema?: JSONSchema,
|
||||
): FormContext<Data> & {
|
||||
value: Reduced;
|
||||
pointer: string;
|
||||
@@ -324,9 +325,6 @@ export function useDerivedFieldContext<Data = any, Reduced = undefined>(
|
||||
const prefixedName = prefixPath(path, root);
|
||||
const prefixedPointer = pathToPointer(prefixedName);
|
||||
const value = getPath(state.data, prefixedName);
|
||||
/*const errors = state.errors.filter((error) =>
|
||||
error.data.pointer.startsWith(prefixedPointer)
|
||||
);*/
|
||||
const fieldSchema =
|
||||
pointer === "#/"
|
||||
? (schema as LibJsonSchema)
|
||||
@@ -339,29 +337,29 @@ export function useDerivedFieldContext<Data = any, Reduced = undefined>(
|
||||
root,
|
||||
schema: fieldSchema as LibJsonSchema,
|
||||
pointer,
|
||||
required
|
||||
required,
|
||||
};
|
||||
const derived = deriveFn?.({ ...context, _formStateAtom, lib, value });
|
||||
|
||||
return {
|
||||
...context,
|
||||
value: derived
|
||||
value: derived,
|
||||
};
|
||||
},
|
||||
[path, schema ?? {}, root]
|
||||
[path, schema ?? {}, root],
|
||||
),
|
||||
isEqual
|
||||
isEqual,
|
||||
);
|
||||
return {
|
||||
...useAtomValue(selected),
|
||||
_formStateAtom,
|
||||
lib
|
||||
lib,
|
||||
} as any;
|
||||
}
|
||||
|
||||
export function Subscribe<Data = any, Refined = Data>({
|
||||
children,
|
||||
selector
|
||||
selector,
|
||||
}: {
|
||||
children: (state: Refined) => ReactNode;
|
||||
selector?: SelectorFn<FormState<Data>, Refined>;
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import type { JSONSchema } from "json-schema-to-ts";
|
||||
import { isTypeSchema } from "ui/components/form/json-schema-form/utils";
|
||||
import { AnyOfField } from "./AnyOfField";
|
||||
import { Field } from "./Field";
|
||||
import { FieldWrapper, type FieldwrapperProps } from "./FieldWrapper";
|
||||
import { useDerivedFieldContext } from "./Form";
|
||||
import { type JSONSchema, useDerivedFieldContext } from "./Form";
|
||||
|
||||
export type ObjectFieldProps = {
|
||||
path?: string;
|
||||
schema?: Exclude<JSONSchema, boolean>;
|
||||
label?: string | false;
|
||||
wrapperProps?: Partial<FieldwrapperProps>;
|
||||
};
|
||||
|
||||
export const ObjectField = ({
|
||||
path = "",
|
||||
schema: _schema,
|
||||
label: _label,
|
||||
wrapperProps = {}
|
||||
}: ObjectFieldProps) => {
|
||||
const ctx = useDerivedFieldContext(path, _schema);
|
||||
const schema = _schema ?? ctx.schema;
|
||||
export const ObjectField = ({ path = "", label: _label, wrapperProps = {} }: ObjectFieldProps) => {
|
||||
const { schema, ...ctx } = useDerivedFieldContext(path);
|
||||
if (!isTypeSchema(schema)) return `ObjectField "${path}": no schema`;
|
||||
const properties = schema.properties ?? {};
|
||||
const properties = Object.entries(schema.properties ?? {}) as [string, JSONSchema][];
|
||||
|
||||
return (
|
||||
<FieldWrapper
|
||||
@@ -31,17 +23,20 @@ export const ObjectField = ({
|
||||
errorPlacement="top"
|
||||
{...wrapperProps}
|
||||
>
|
||||
{Object.keys(properties).map((prop) => {
|
||||
const schema = properties[prop];
|
||||
const name = [path, prop].filter(Boolean).join(".");
|
||||
if (typeof schema === "undefined" || typeof schema === "boolean") return;
|
||||
{properties.length === 0 ? (
|
||||
<i className="opacity-50">No properties</i>
|
||||
) : (
|
||||
properties.map(([prop, schema]) => {
|
||||
const name = [path, prop].filter(Boolean).join(".");
|
||||
if (typeof schema === "undefined" || typeof schema === "boolean") return;
|
||||
|
||||
if (schema.anyOf || schema.oneOf) {
|
||||
return <AnyOfField key={name} path={name} />;
|
||||
}
|
||||
if (schema.anyOf || schema.oneOf) {
|
||||
return <AnyOfField key={name} path={name} />;
|
||||
}
|
||||
|
||||
return <Field key={name} name={name} />;
|
||||
})}
|
||||
return <Field key={name} name={name} />;
|
||||
})
|
||||
)}
|
||||
</FieldWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -102,7 +102,7 @@ export function getMultiSchema(schema: JsonSchema): JsonSchema[] | undefined {
|
||||
|
||||
export function getMultiSchemaMatched(
|
||||
schema: JsonSchema,
|
||||
data: any
|
||||
data: any,
|
||||
): [number, JsonSchema[], JsonSchema | undefined] {
|
||||
const multiSchema = getMultiSchema(schema);
|
||||
//console.log("getMultiSchemaMatched", schema, data, multiSchema);
|
||||
@@ -124,7 +124,7 @@ export function omitSchema<Given extends JSONSchema>(_schema: Given, keys: strin
|
||||
|
||||
const updated = {
|
||||
...schema,
|
||||
properties: omitKeys(schema.properties, keys)
|
||||
properties: omitKeys(schema.properties, keys),
|
||||
};
|
||||
if (updated.required) {
|
||||
updated.required = updated.required.filter((key) => !keys.includes(key as any));
|
||||
|
||||
@@ -40,7 +40,7 @@ export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>
|
||||
cleanOnChange,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const formRef = useRef<Form<any, RJSFSchema, any>>(null);
|
||||
const id = useId();
|
||||
@@ -67,32 +67,32 @@ export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>
|
||||
formData: () => value,
|
||||
validateForm: () => formRef.current!.validateForm(),
|
||||
silentValidate: () => isValid(value),
|
||||
cancel: () => formRef.current!.reset()
|
||||
cancel: () => formRef.current!.reset(),
|
||||
}),
|
||||
[value]
|
||||
[value],
|
||||
);
|
||||
|
||||
const _uiSchema: UiSchema = {
|
||||
...uiSchema,
|
||||
"ui:globalOptions": {
|
||||
...uiSchema?.["ui:globalOptions"],
|
||||
enableMarkdownInDescription: true
|
||||
enableMarkdownInDescription: true,
|
||||
},
|
||||
"ui:submitButtonOptions": {
|
||||
norender: true
|
||||
}
|
||||
norender: true,
|
||||
},
|
||||
};
|
||||
const _fields: any = {
|
||||
...Fields,
|
||||
...fields
|
||||
...fields,
|
||||
};
|
||||
const _templates: any = {
|
||||
...Templates,
|
||||
...templates
|
||||
...templates,
|
||||
};
|
||||
const _widgets: any = {
|
||||
...Widgets,
|
||||
...widgets
|
||||
...widgets,
|
||||
};
|
||||
//console.log("schema", schema, removeTitleFromSchema(schema));
|
||||
|
||||
@@ -115,7 +115,7 @@ export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>
|
||||
validator={validator as any}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
function removeTitleFromSchema(schema: any): any {
|
||||
// Create a deep copy of the schema using lodash
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
StrictRJSFSchema,
|
||||
UiSchema,
|
||||
ValidationData,
|
||||
ValidatorType
|
||||
ValidatorType,
|
||||
} from "@rjsf/utils";
|
||||
import { toErrorSchema } from "@rjsf/utils";
|
||||
import { get } from "lodash-es";
|
||||
@@ -49,7 +49,7 @@ const validate = true;
|
||||
export class JsonSchemaValidator<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
F extends FormContextType = any,
|
||||
> implements ValidatorType
|
||||
{
|
||||
// @ts-ignore
|
||||
@@ -69,7 +69,7 @@ export class JsonSchemaValidator<
|
||||
schema: S,
|
||||
customValidate?: CustomValidator,
|
||||
transformErrors?: ErrorTransformer,
|
||||
uiSchema?: UiSchema
|
||||
uiSchema?: UiSchema,
|
||||
): ValidationData<T> {
|
||||
if (!validate) return { errors: [], errorSchema: {} as any };
|
||||
|
||||
@@ -80,7 +80,7 @@ export class JsonSchemaValidator<
|
||||
schema,
|
||||
customValidate,
|
||||
transformErrors,
|
||||
uiSchema
|
||||
uiSchema,
|
||||
);
|
||||
const { errors } = this.rawValidation(schema, formData);
|
||||
debug && console.log("errors", { errors });
|
||||
@@ -97,14 +97,14 @@ export class JsonSchemaValidator<
|
||||
message: errorText,
|
||||
property: "." + error.instanceLocation.replace(/^#\/?/, "").split("/").join("."),
|
||||
schemaPath: error.keywordLocation,
|
||||
stack: error.error
|
||||
stack: error.error,
|
||||
};
|
||||
});
|
||||
debug && console.log("transformed", transformedErrors);
|
||||
|
||||
return {
|
||||
errors: transformedErrors,
|
||||
errorSchema: toErrorSchema(transformedErrors)
|
||||
errorSchema: toErrorSchema(transformedErrors),
|
||||
} as any;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
getDiscriminatorFieldFromSchema,
|
||||
getUiOptions,
|
||||
getWidget,
|
||||
mergeSchemas
|
||||
mergeSchemas,
|
||||
} from "@rjsf/utils";
|
||||
import { get, isEmpty, omit } from "lodash-es";
|
||||
import { Component } from "react";
|
||||
@@ -35,7 +35,7 @@ type AnyOfFieldState<S extends StrictRJSFSchema = RJSFSchema> = {
|
||||
class MultiSchemaField<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
F extends FormContextType = any,
|
||||
> extends Component<FieldProps<T, S, F>, AnyOfFieldState<S>> {
|
||||
/** Constructs an `AnyOfField` with the given `props` to initialize the initially selected option in state
|
||||
*
|
||||
@@ -47,7 +47,7 @@ class MultiSchemaField<
|
||||
const {
|
||||
formData,
|
||||
options,
|
||||
registry: { schemaUtils }
|
||||
registry: { schemaUtils },
|
||||
} = this.props;
|
||||
// cache the retrieved options in state in case they have $refs to save doing it later
|
||||
//console.log("multi schema", { formData, options, props });
|
||||
@@ -55,7 +55,7 @@ class MultiSchemaField<
|
||||
|
||||
this.state = {
|
||||
retrievedOptions,
|
||||
selectedOption: this.getMatchingOption(0, formData, retrievedOptions)
|
||||
selectedOption: this.getMatchingOption(0, formData, retrievedOptions),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,18 +67,18 @@ class MultiSchemaField<
|
||||
*/
|
||||
override componentDidUpdate(
|
||||
prevProps: Readonly<FieldProps<T, S, F>>,
|
||||
prevState: Readonly<AnyOfFieldState>
|
||||
prevState: Readonly<AnyOfFieldState>,
|
||||
) {
|
||||
const { formData, options, idSchema } = this.props;
|
||||
const { selectedOption } = this.state;
|
||||
let newState = this.state;
|
||||
if (!deepEquals(prevProps.options, options)) {
|
||||
const {
|
||||
registry: { schemaUtils }
|
||||
registry: { schemaUtils },
|
||||
} = this.props;
|
||||
// re-cache the retrieved options in state in case they have $refs to save doing it later
|
||||
const retrievedOptions = options.map((opt: S) =>
|
||||
schemaUtils.retrieveSchema(opt, formData)
|
||||
schemaUtils.retrieveSchema(opt, formData),
|
||||
);
|
||||
newState = { selectedOption, retrievedOptions };
|
||||
}
|
||||
@@ -104,7 +104,7 @@ class MultiSchemaField<
|
||||
getMatchingOption(selectedOption: number, formData: T | undefined, options: S[]) {
|
||||
const {
|
||||
schema,
|
||||
registry: { schemaUtils }
|
||||
registry: { schemaUtils },
|
||||
} = this.props;
|
||||
|
||||
const discriminator = getDiscriminatorFieldFromSchema<S>(schema);
|
||||
@@ -112,7 +112,7 @@ class MultiSchemaField<
|
||||
formData,
|
||||
options,
|
||||
selectedOption,
|
||||
discriminator
|
||||
discriminator,
|
||||
);
|
||||
return option;
|
||||
}
|
||||
@@ -143,7 +143,7 @@ class MultiSchemaField<
|
||||
newFormData = schemaUtils.getDefaultFormState(
|
||||
newOption,
|
||||
newFormData,
|
||||
"excludeObjectChildren"
|
||||
"excludeObjectChildren",
|
||||
) as T;
|
||||
}
|
||||
onChange(newFormData, undefined, this.getFieldId());
|
||||
@@ -169,7 +169,7 @@ class MultiSchemaField<
|
||||
registry,
|
||||
schema,
|
||||
uiSchema,
|
||||
readonly
|
||||
readonly,
|
||||
} = this.props;
|
||||
|
||||
const { widgets, fields, translateString, globalUiOptions, schemaUtils } = registry;
|
||||
@@ -241,7 +241,7 @@ class MultiSchemaField<
|
||||
return {
|
||||
label:
|
||||
uiTitle || translateString(translateEnum, translateParams.concat(String(index + 1))),
|
||||
value: index
|
||||
value: index,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -255,8 +255,8 @@ class MultiSchemaField<
|
||||
...optionUiSchema,
|
||||
"ui:options": {
|
||||
...optionUiSchema?.["ui:options"],
|
||||
hideLabel: true
|
||||
}
|
||||
hideLabel: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -265,7 +265,7 @@ class MultiSchemaField<
|
||||
<div
|
||||
className={twMerge(
|
||||
"panel multischema flex",
|
||||
flexDirection === "row" ? "flex-row gap-3" : "flex-col gap-2"
|
||||
flexDirection === "row" ? "flex-row gap-3" : "flex-col gap-2",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center panel-select">
|
||||
|
||||
@@ -6,5 +6,5 @@ export const fields = {
|
||||
AnyOfField: MultiSchemaField,
|
||||
OneOfField: MultiSchemaField,
|
||||
JsonField,
|
||||
LiquidJsField
|
||||
LiquidJsField,
|
||||
};
|
||||
|
||||
@@ -5,8 +5,8 @@ export type { JsonSchemaFormProps, JsonSchemaFormRef };
|
||||
|
||||
const Module = lazy(() =>
|
||||
import("./JsonSchemaForm").then((m) => ({
|
||||
default: m.JsonSchemaForm
|
||||
}))
|
||||
default: m.JsonSchemaForm,
|
||||
})),
|
||||
);
|
||||
|
||||
export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>((props, ref) => {
|
||||
|
||||
@@ -1,264 +1,265 @@
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.json-form {
|
||||
@apply flex flex-col flex-grow;
|
||||
@apply flex flex-col flex-grow;
|
||||
|
||||
/* dirty fix preventing the first fieldset to wrap */
|
||||
&.mute-root {
|
||||
& > div > div > div > fieldset:first-child {
|
||||
@apply border-none p-0;
|
||||
}
|
||||
}
|
||||
/* dirty fix preventing the first fieldset to wrap */
|
||||
&.mute-root {
|
||||
& > div > div > div > fieldset:first-child {
|
||||
@apply border-none p-0;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.fieldset-alternative) {
|
||||
fieldset {
|
||||
@apply flex flex-grow flex-col gap-3.5 border border-solid border-muted p-3 rounded;
|
||||
&:not(.fieldset-alternative) {
|
||||
fieldset {
|
||||
@apply flex flex-grow flex-col gap-3.5 border border-solid border-muted p-3 rounded;
|
||||
|
||||
.title-field {
|
||||
@apply bg-primary/10 px-3 text-sm font-medium py-1 rounded-full;
|
||||
align-self: flex-start;
|
||||
.title-field {
|
||||
@apply bg-primary/10 px-3 text-sm font-medium py-1 rounded-full;
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* alternative */
|
||||
&.fieldset-alternative {
|
||||
fieldset {
|
||||
@apply flex flex-grow flex-col gap-3.5;
|
||||
&:has(> legend) {
|
||||
@apply mt-3 border-l-4 border-solid border-muted/50 p-3 pb-0 pt-0;
|
||||
}
|
||||
|
||||
.title-field {
|
||||
@apply bg-muted/50 text-sm font-medium py-1 table ml-[-14px] pl-4 pr-3 mb-3 mt-3;
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.multischema {
|
||||
@apply mt-3;
|
||||
|
||||
fieldset {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hide-required-mark {
|
||||
.control-label span.required {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
@apply flex flex-col gap-1;
|
||||
&:not(.field) {
|
||||
@apply flex-grow;
|
||||
}
|
||||
|
||||
/* hide empty description if markdown is enabled */
|
||||
.field-description:has(> span:empty) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.control-label span.required {
|
||||
@apply ml-1 opacity-50;
|
||||
}
|
||||
|
||||
&.field.has-error {
|
||||
@apply text-red-500;
|
||||
|
||||
.control-label {
|
||||
@apply font-bold;
|
||||
}
|
||||
.error-detail:not(:only-child) {
|
||||
@apply font-bold list-disc pl-6;
|
||||
}
|
||||
.error-detail:only-child {
|
||||
@apply font-bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-description {
|
||||
@apply text-primary/70 text-sm;
|
||||
}
|
||||
|
||||
/* input but not radio */
|
||||
input:not([type="radio"]):not([type="checkbox"]) {
|
||||
@apply flex bg-muted/40 h-11 rounded-md outline-none;
|
||||
@apply py-2.5 px-4;
|
||||
width: 100%;
|
||||
|
||||
&:not([disabled]):not([readonly]) {
|
||||
@apply focus:outline-none focus:ring-2 focus:bg-muted focus:ring-zinc-500 focus:border-transparent transition-all;
|
||||
}
|
||||
&[disabled],
|
||||
&[readonly] {
|
||||
@apply bg-muted/50 text-primary/50 cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
@apply flex bg-muted/40 focus:bg-muted rounded-md outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50;
|
||||
@apply py-2.5 px-4;
|
||||
width: 100%;
|
||||
}
|
||||
.checkbox {
|
||||
label,
|
||||
label > span {
|
||||
@apply flex flex-row gap-2;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
@apply bg-muted/40 focus:bg-muted rounded-md py-2.5 pr-4 pl-2.5 outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all;
|
||||
@apply disabled:bg-muted/70 disabled:text-primary/70;
|
||||
@apply w-full border-r-8 border-r-transparent;
|
||||
|
||||
&:not([multiple]) {
|
||||
@apply h-11;
|
||||
}
|
||||
|
||||
&[multiple] {
|
||||
option {
|
||||
@apply py-1.5 px-2.5 bg-transparent;
|
||||
&:checked {
|
||||
@apply bg-primary/20;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* alternative */
|
||||
&.fieldset-alternative {
|
||||
fieldset {
|
||||
@apply flex flex-grow flex-col gap-3.5;
|
||||
&:has(> legend) {
|
||||
@apply mt-3 border-l-4 border-solid border-muted/50 p-3 pb-0 pt-0;
|
||||
.btn {
|
||||
@apply w-5 h-5 bg-amber-500;
|
||||
}
|
||||
|
||||
.field-radio-group {
|
||||
@apply flex flex-row gap-2;
|
||||
}
|
||||
|
||||
&.noborder-first-fieldset {
|
||||
fieldset#root {
|
||||
@apply border-none p-0;
|
||||
}
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
.form-group {
|
||||
@apply flex-row gap-2;
|
||||
}
|
||||
.form-control,
|
||||
.panel {
|
||||
@apply flex-grow;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
@apply w-32 flex h-11 items-center;
|
||||
}
|
||||
input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
fieldset#root {
|
||||
@apply gap-6;
|
||||
}
|
||||
|
||||
fieldset.object-field {
|
||||
@apply gap-2;
|
||||
}
|
||||
|
||||
.additional-children {
|
||||
.checkbox {
|
||||
@apply w-full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hide-multi-labels {
|
||||
.control-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.multischema {
|
||||
.form-control {
|
||||
@apply flex-shrink;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
/*@apply flex flex-col gap-2;*/
|
||||
|
||||
/*.control-label { display: none; }*/
|
||||
|
||||
& > .field-radio-group {
|
||||
@apply flex flex-row gap-3;
|
||||
|
||||
.radio,
|
||||
.radio-inline {
|
||||
@apply text-sm border-b border-b-transparent;
|
||||
@apply font-mono text-primary/70;
|
||||
|
||||
input {
|
||||
@apply appearance-none;
|
||||
}
|
||||
|
||||
.title-field {
|
||||
@apply bg-muted/50 text-sm font-medium py-1 table ml-[-14px] pl-4 pr-3 mb-3 mt-3;
|
||||
align-self: flex-start;
|
||||
&.checked {
|
||||
@apply border-b-primary/70 text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multischema {
|
||||
@apply mt-3;
|
||||
|
||||
fieldset {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hide-required-mark {
|
||||
.control-label span.required {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
@apply flex flex-col gap-1;
|
||||
&:not(.field) {
|
||||
@apply flex-grow;
|
||||
}
|
||||
|
||||
/* hide empty description if markdown is enabled */
|
||||
.field-description:has(> span:empty) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.control-label span.required {
|
||||
@apply ml-1 opacity-50;
|
||||
}
|
||||
|
||||
&.field.has-error {
|
||||
@apply text-red-500;
|
||||
|
||||
.control-label {
|
||||
@apply font-bold;
|
||||
}
|
||||
.error-detail:not(:only-child) {
|
||||
@apply font-bold list-disc pl-6;
|
||||
}
|
||||
.error-detail:only-child {
|
||||
@apply font-bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-description {
|
||||
@apply text-primary/70 text-sm;
|
||||
}
|
||||
|
||||
/* input but not radio */
|
||||
input:not([type="radio"]):not([type="checkbox"]) {
|
||||
@apply flex bg-muted/40 h-11 rounded-md outline-none;
|
||||
@apply py-2.5 px-4;
|
||||
width: 100%;
|
||||
|
||||
&:not([disabled]):not([readonly]) {
|
||||
@apply focus:outline-none focus:ring-2 focus:bg-muted focus:ring-zinc-500 focus:border-transparent transition-all;
|
||||
}
|
||||
&[disabled], &[readonly] {
|
||||
@apply bg-muted/50 text-primary/50 cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
@apply flex bg-muted/40 focus:bg-muted rounded-md outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50;
|
||||
@apply py-2.5 px-4;
|
||||
width: 100%;
|
||||
}
|
||||
.checkbox {
|
||||
label, label > span {
|
||||
@apply flex flex-row gap-2;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
@apply bg-muted/40 focus:bg-muted rounded-md py-2.5 pr-4 pl-2.5 outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all;
|
||||
@apply disabled:bg-muted/70 disabled:text-primary/70;
|
||||
@apply w-full border-r-8 border-r-transparent;
|
||||
|
||||
&:not([multiple]) {
|
||||
@apply h-11;
|
||||
}
|
||||
|
||||
&[multiple] {
|
||||
option {
|
||||
@apply py-1.5 px-2.5 bg-transparent;
|
||||
&:checked {
|
||||
@apply bg-primary/20;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply w-5 h-5 bg-amber-500;
|
||||
}
|
||||
|
||||
.field-radio-group {
|
||||
@apply flex flex-row gap-2;
|
||||
}
|
||||
|
||||
&.noborder-first-fieldset {
|
||||
fieldset#root {
|
||||
@apply border-none p-0;
|
||||
}
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
.form-group {
|
||||
@apply flex-row gap-2;
|
||||
}
|
||||
.form-control, .panel {
|
||||
@apply flex-grow;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
@apply w-32 flex h-11 items-center;
|
||||
}
|
||||
input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
fieldset#root {
|
||||
@apply gap-6;
|
||||
}
|
||||
|
||||
fieldset.object-field {
|
||||
@apply gap-2;
|
||||
}
|
||||
|
||||
.additional-children {
|
||||
.checkbox {
|
||||
@apply w-full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hide-multi-labels {
|
||||
.control-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.multischema {
|
||||
.form-control {
|
||||
@apply flex-shrink;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
/*@apply flex flex-col gap-2;*/
|
||||
|
||||
/*.control-label { display: none; }*/
|
||||
|
||||
& > .field-radio-group {
|
||||
@apply flex flex-row gap-3;
|
||||
|
||||
.radio, .radio-inline {
|
||||
@apply text-sm border-b border-b-transparent;
|
||||
@apply font-mono text-primary/70;
|
||||
|
||||
input {
|
||||
@apply appearance-none;
|
||||
}
|
||||
&.checked {
|
||||
@apply border-b-primary/70 text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* :not(.panel-select) .control-label {
|
||||
/* :not(.panel-select) .control-label {
|
||||
display: none;
|
||||
} */
|
||||
|
||||
.panel-select select {
|
||||
@apply py-1 pr-1 pl-1.5 text-sm;
|
||||
@apply h-auto w-auto;
|
||||
}
|
||||
}
|
||||
.panel-select select {
|
||||
@apply py-1 pr-1 pl-1.5 text-sm;
|
||||
@apply h-auto w-auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.legacy {
|
||||
/* first fieldset */
|
||||
& > .form-group.field-object > div > fieldset {
|
||||
@apply border-none p-0;
|
||||
}
|
||||
|
||||
|
||||
&.legacy {
|
||||
/* first fieldset */
|
||||
& > .form-group.field-object>div>fieldset {
|
||||
@apply border-none p-0;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
.col-xs-5 {
|
||||
display: flex;
|
||||
width: 50%;
|
||||
}
|
||||
.form-additional {
|
||||
fieldset {
|
||||
/* padding: 0;
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
.col-xs-5 {
|
||||
display: flex;
|
||||
width: 50%;
|
||||
}
|
||||
.form-additional {
|
||||
fieldset {
|
||||
/* padding: 0;
|
||||
border: none; */
|
||||
|
||||
/* legend {
|
||||
/* legend {
|
||||
display: none;
|
||||
} */
|
||||
}
|
||||
&.additional-start {
|
||||
> label {
|
||||
display: none;
|
||||
}
|
||||
&.additional-start {
|
||||
> label {
|
||||
display: none;
|
||||
}
|
||||
/* > label + div > fieldset:first-child {
|
||||
/* > label + div > fieldset:first-child {
|
||||
display: none;
|
||||
} */
|
||||
}
|
||||
}
|
||||
.field-object + .field-object {
|
||||
@apply mt-3 pt-4 border-t border-muted;
|
||||
}
|
||||
.panel>.field-object>label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.field-object + .field-object {
|
||||
@apply mt-3 pt-4 border-t border-muted;
|
||||
}
|
||||
.panel > .field-object > label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type RJSFSchema,
|
||||
type StrictRJSFSchema,
|
||||
getTemplate,
|
||||
getUiOptions
|
||||
getUiOptions,
|
||||
} from "@rjsf/utils";
|
||||
import { cloneElement } from "react";
|
||||
|
||||
@@ -16,7 +16,7 @@ import { cloneElement } from "react";
|
||||
export default function ArrayFieldTemplate<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
F extends FormContextType = any,
|
||||
>(props: ArrayFieldTemplateProps<T, S, F>) {
|
||||
const {
|
||||
canAdd,
|
||||
@@ -30,27 +30,27 @@ export default function ArrayFieldTemplate<
|
||||
registry,
|
||||
required,
|
||||
schema,
|
||||
title
|
||||
title,
|
||||
} = props;
|
||||
const uiOptions = getUiOptions<T, S, F>(uiSchema);
|
||||
const ArrayFieldDescriptionTemplate = getTemplate<"ArrayFieldDescriptionTemplate", T, S, F>(
|
||||
"ArrayFieldDescriptionTemplate",
|
||||
registry,
|
||||
uiOptions
|
||||
uiOptions,
|
||||
);
|
||||
const ArrayFieldItemTemplate = getTemplate<"ArrayFieldItemTemplate", T, S, F>(
|
||||
"ArrayFieldItemTemplate",
|
||||
registry,
|
||||
uiOptions
|
||||
uiOptions,
|
||||
);
|
||||
const ArrayFieldTitleTemplate = getTemplate<"ArrayFieldTitleTemplate", T, S, F>(
|
||||
"ArrayFieldTitleTemplate",
|
||||
registry,
|
||||
uiOptions
|
||||
uiOptions,
|
||||
);
|
||||
// Button templates are not overridden in the uiSchema
|
||||
const {
|
||||
ButtonTemplates: { AddButton }
|
||||
ButtonTemplates: { AddButton },
|
||||
} = registry.templates;
|
||||
return (
|
||||
<fieldset className={className} id={idSchema.$id}>
|
||||
@@ -74,15 +74,16 @@ export default function ArrayFieldTemplate<
|
||||
{items.map(
|
||||
({ key, children, ...itemProps }: ArrayFieldTemplateItemType<T, S, F>) => {
|
||||
const newChildren = cloneElement(children, {
|
||||
// @ts-ignore
|
||||
...children.props,
|
||||
name: undefined,
|
||||
title: undefined
|
||||
title: undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<ArrayFieldItemTemplate key={key} {...itemProps} children={newChildren} />
|
||||
);
|
||||
}
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type StrictRJSFSchema,
|
||||
ariaDescribedByIds,
|
||||
examplesId,
|
||||
getInputProps
|
||||
getInputProps,
|
||||
} from "@rjsf/utils";
|
||||
import { type ChangeEvent, type FocusEvent, useCallback } from "react";
|
||||
import { Label } from "./FieldTemplate";
|
||||
@@ -19,7 +19,7 @@ import { Label } from "./FieldTemplate";
|
||||
export default function BaseInputTemplate<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
F extends FormContextType = any,
|
||||
>(props: BaseInputTemplateProps<T, S, F>) {
|
||||
const {
|
||||
id,
|
||||
@@ -52,7 +52,7 @@ export default function BaseInputTemplate<
|
||||
}
|
||||
const inputProps = {
|
||||
...rest,
|
||||
...getInputProps<T, S, F>(schema, type, options)
|
||||
...getInputProps<T, S, F>(schema, type, options),
|
||||
};
|
||||
|
||||
let inputValue;
|
||||
@@ -65,15 +65,15 @@ export default function BaseInputTemplate<
|
||||
const _onChange = useCallback(
|
||||
({ target: { value } }: ChangeEvent<HTMLInputElement>) =>
|
||||
onChange(value === "" ? options.emptyValue : value),
|
||||
[onChange, options]
|
||||
[onChange, options],
|
||||
);
|
||||
const _onBlur = useCallback(
|
||||
({ target }: FocusEvent<HTMLInputElement>) => onBlur(id, target && target.value),
|
||||
[onBlur, id]
|
||||
[onBlur, id],
|
||||
);
|
||||
const _onFocus = useCallback(
|
||||
({ target }: FocusEvent<HTMLInputElement>) => onFocus(id, target && target.value),
|
||||
[onFocus, id]
|
||||
[onFocus, id],
|
||||
);
|
||||
|
||||
const shouldHideLabel =
|
||||
@@ -108,7 +108,7 @@ export default function BaseInputTemplate<
|
||||
.concat(
|
||||
schema.default && !schema.examples.includes(schema.default)
|
||||
? ([schema.default] as string[])
|
||||
: []
|
||||
: [],
|
||||
)
|
||||
.map((example: any) => {
|
||||
return <option key={example} value={example} />;
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
type RJSFSchema,
|
||||
type StrictRJSFSchema,
|
||||
getTemplate,
|
||||
getUiOptions
|
||||
getUiOptions,
|
||||
} from "@rjsf/utils";
|
||||
import { identifierToHumanReadable } from "core/utils";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@@ -45,7 +45,7 @@ export function Label(props: LabelProps) {
|
||||
export function FieldTemplate<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
F extends FormContextType = any,
|
||||
>(props: FieldTemplateProps<T, S, F>) {
|
||||
const {
|
||||
id,
|
||||
@@ -58,14 +58,14 @@ export function FieldTemplate<
|
||||
required,
|
||||
displayLabel,
|
||||
registry,
|
||||
uiSchema
|
||||
uiSchema,
|
||||
} = props;
|
||||
const uiOptions = getUiOptions(uiSchema, registry.globalUiOptions);
|
||||
//console.log("field---", uiOptions);
|
||||
const WrapIfAdditionalTemplate = getTemplate<"WrapIfAdditionalTemplate", T, S, F>(
|
||||
"WrapIfAdditionalTemplate",
|
||||
registry,
|
||||
uiOptions
|
||||
uiOptions,
|
||||
);
|
||||
if (hidden) {
|
||||
return <div className="hidden">{children}</div>;
|
||||
@@ -81,7 +81,7 @@ export function FieldTemplate<
|
||||
"flex flex-grow additional-children",
|
||||
uiOptions.flexDirection === "row"
|
||||
? "flex-row items-center gap-3"
|
||||
: "flex-col flex-grow gap-2"
|
||||
: "flex-col flex-grow gap-2",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
descriptionId,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
titleId
|
||||
titleId,
|
||||
} from "@rjsf/utils";
|
||||
|
||||
/** The `ObjectFieldTemplate` is the template to use to render all the inner properties of an object along with the
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
export default function ObjectFieldTemplate<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
F extends FormContextType = any,
|
||||
>(props: ObjectFieldTemplateProps<T, S, F>) {
|
||||
const {
|
||||
description,
|
||||
@@ -34,18 +34,18 @@ export default function ObjectFieldTemplate<
|
||||
required,
|
||||
schema,
|
||||
title,
|
||||
uiSchema
|
||||
uiSchema,
|
||||
} = props;
|
||||
const options = getUiOptions<T, S, F>(uiSchema);
|
||||
const TitleFieldTemplate = getTemplate<"TitleFieldTemplate", T, S, F>(
|
||||
"TitleFieldTemplate",
|
||||
registry,
|
||||
options
|
||||
options,
|
||||
);
|
||||
const DescriptionFieldTemplate = getTemplate<"DescriptionFieldTemplate", T, S, F>(
|
||||
"DescriptionFieldTemplate",
|
||||
registry,
|
||||
options
|
||||
options,
|
||||
);
|
||||
|
||||
/* if (properties.length === 0) {
|
||||
@@ -59,7 +59,7 @@ export default function ObjectFieldTemplate<
|
||||
|
||||
// Button templates are not overridden in the uiSchema
|
||||
const {
|
||||
ButtonTemplates: { AddButton }
|
||||
ButtonTemplates: { AddButton },
|
||||
} = registry.templates;
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,7 @@ const REQUIRED_FIELD_SYMBOL = "*";
|
||||
export default function TitleField<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
F extends FormContextType = any,
|
||||
>(props: TitleFieldProps<T, S, F>) {
|
||||
const { id, title, required } = props;
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
type RJSFSchema,
|
||||
type StrictRJSFSchema,
|
||||
TranslatableString,
|
||||
type WrapIfAdditionalTemplateProps
|
||||
type WrapIfAdditionalTemplateProps,
|
||||
} from "@rjsf/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useState } from "react";
|
||||
export default function WrapIfAdditionalTemplate<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
F extends FormContextType = any,
|
||||
>(props: WrapIfAdditionalTemplateProps<T, S, F>) {
|
||||
const {
|
||||
id,
|
||||
@@ -31,7 +31,7 @@ export default function WrapIfAdditionalTemplate<
|
||||
schema,
|
||||
children,
|
||||
uiSchema,
|
||||
registry
|
||||
registry,
|
||||
} = props;
|
||||
const { templates, translateString } = registry;
|
||||
// Button templates are not overridden in the uiSchema
|
||||
|
||||
@@ -15,5 +15,5 @@ export const templates = {
|
||||
TitleFieldTemplate,
|
||||
ObjectFieldTemplate,
|
||||
BaseInputTemplate,
|
||||
WrapIfAdditionalTemplate
|
||||
WrapIfAdditionalTemplate,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
StrictRJSFSchema,
|
||||
UiSchema,
|
||||
ValidationData,
|
||||
ValidatorType
|
||||
ValidatorType,
|
||||
} from "@rjsf/utils";
|
||||
import { toErrorSchema } from "@rjsf/utils";
|
||||
|
||||
@@ -33,7 +33,7 @@ export class RJSFTypeboxValidator<T = any, S extends StrictRJSFSchema = RJSFSche
|
||||
|
||||
return {
|
||||
errors: [...Errors(tbSchema, formData)],
|
||||
validationError: null as any
|
||||
validationError: null as any,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export class RJSFTypeboxValidator<T = any, S extends StrictRJSFSchema = RJSFSche
|
||||
schema: S,
|
||||
customValidate?: CustomValidator,
|
||||
transformErrors?: ErrorTransformer,
|
||||
uiSchema?: UiSchema
|
||||
uiSchema?: UiSchema,
|
||||
): ValidationData<T> {
|
||||
const { errors } = this.rawValidation(schema, formData);
|
||||
|
||||
@@ -54,13 +54,13 @@ export class RJSFTypeboxValidator<T = any, S extends StrictRJSFSchema = RJSFSche
|
||||
message: error.message,
|
||||
property: "." + schemaLocation,
|
||||
schemaPath: error.path,
|
||||
stack: error.message
|
||||
stack: error.message,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
errors: transformedErrors,
|
||||
errorSchema: toErrorSchema(transformedErrors)
|
||||
errorSchema: toErrorSchema(transformedErrors),
|
||||
} as any;
|
||||
}
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ function FromConst<T extends SConst>(T: T) {
|
||||
// ------------------------------------------------------------------
|
||||
type TFromPropertiesIsOptional<
|
||||
K extends PropertyKey,
|
||||
R extends string | unknown
|
||||
R extends string | unknown,
|
||||
> = unknown extends R ? true : K extends R ? false : true;
|
||||
// prettier-ignore
|
||||
// biome-ignore format:
|
||||
@@ -243,13 +243,13 @@ function FromObject<T extends SObject>(T: T): TFromObject<T> {
|
||||
// biome-ignore lint: reason
|
||||
T.required && T.required.includes(K)
|
||||
? FromSchema(T.properties[K])
|
||||
: Type.Optional(FromSchema(T.properties[K]))
|
||||
: Type.Optional(FromSchema(T.properties[K])),
|
||||
};
|
||||
}, {} as Type.TProperties);
|
||||
|
||||
if ("additionalProperties" in T) {
|
||||
return Type.Object(properties, {
|
||||
additionalProperties: FromSchema(T.additionalProperties)
|
||||
additionalProperties: FromSchema(T.additionalProperties),
|
||||
}) as never;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { type ChangeEvent, useCallback } from "react";
|
||||
export function CheckboxWidget<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
F extends FormContextType = any,
|
||||
>({
|
||||
schema,
|
||||
uiSchema,
|
||||
@@ -33,7 +33,7 @@ export function CheckboxWidget<
|
||||
});*/
|
||||
const handleChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => onChange(event.target.checked),
|
||||
[onChange]
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
enumOptionsIsSelected,
|
||||
enumOptionsSelectValue,
|
||||
enumOptionsValueForIndex,
|
||||
optionId
|
||||
optionId,
|
||||
} from "@rjsf/utils";
|
||||
import { type ChangeEvent, type FocusEvent, useCallback } from "react";
|
||||
|
||||
@@ -20,7 +20,7 @@ import { type ChangeEvent, type FocusEvent, useCallback } from "react";
|
||||
function CheckboxesWidget<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
F extends FormContextType = any,
|
||||
>({
|
||||
id,
|
||||
disabled,
|
||||
@@ -30,20 +30,20 @@ function CheckboxesWidget<
|
||||
readonly,
|
||||
onChange,
|
||||
onBlur,
|
||||
onFocus
|
||||
onFocus,
|
||||
}: WidgetProps<T, S, F>) {
|
||||
const checkboxesValues = Array.isArray(value) ? value : [value];
|
||||
|
||||
const handleBlur = useCallback(
|
||||
({ target }: FocusEvent<HTMLInputElement>) =>
|
||||
onBlur(id, enumOptionsValueForIndex<S>(target?.value, enumOptions, emptyValue)),
|
||||
[onBlur, id]
|
||||
[onBlur, id],
|
||||
);
|
||||
|
||||
const handleFocus = useCallback(
|
||||
({ target }: FocusEvent<HTMLInputElement>) =>
|
||||
onFocus(id, enumOptionsValueForIndex<S>(target?.value, enumOptions, emptyValue)),
|
||||
[onFocus, id]
|
||||
[onFocus, id],
|
||||
);
|
||||
return (
|
||||
<div className="checkboxes" id={id}>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ariaDescribedByIds,
|
||||
enumOptionsIsSelected,
|
||||
enumOptionsValueForIndex,
|
||||
optionId
|
||||
optionId,
|
||||
} from "@rjsf/utils";
|
||||
import { type FocusEvent, useCallback } from "react";
|
||||
|
||||
@@ -18,7 +18,7 @@ import { type FocusEvent, useCallback } from "react";
|
||||
function RadioWidget<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
F extends FormContextType = any,
|
||||
>({
|
||||
options,
|
||||
value,
|
||||
@@ -29,20 +29,20 @@ function RadioWidget<
|
||||
onBlur,
|
||||
onFocus,
|
||||
onChange,
|
||||
id
|
||||
id,
|
||||
}: WidgetProps<T, S, F>) {
|
||||
const { enumOptions, enumDisabled, inline, emptyValue } = options;
|
||||
|
||||
const handleBlur = useCallback(
|
||||
({ target: { value } }: FocusEvent<HTMLInputElement>) =>
|
||||
onBlur(id, enumOptionsValueForIndex<S>(value, enumOptions, emptyValue)),
|
||||
[onBlur, id]
|
||||
[onBlur, id],
|
||||
);
|
||||
|
||||
const handleFocus = useCallback(
|
||||
({ target: { value } }: FocusEvent<HTMLInputElement>) =>
|
||||
onFocus(id, enumOptionsValueForIndex<S>(value, enumOptions, emptyValue)),
|
||||
[onFocus, id]
|
||||
[onFocus, id],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type WidgetProps,
|
||||
ariaDescribedByIds,
|
||||
enumOptionsIndexForValue,
|
||||
enumOptionsValueForIndex
|
||||
enumOptionsValueForIndex,
|
||||
} from "@rjsf/utils";
|
||||
import { type ChangeEvent, type FocusEvent, type SyntheticEvent, useCallback } from "react";
|
||||
|
||||
@@ -27,7 +27,7 @@ function getValue(event: SyntheticEvent<HTMLSelectElement>, multiple: boolean) {
|
||||
function SelectWidget<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
F extends FormContextType = any,
|
||||
>({
|
||||
schema,
|
||||
id,
|
||||
@@ -41,7 +41,7 @@ function SelectWidget<
|
||||
onChange,
|
||||
onBlur,
|
||||
onFocus,
|
||||
placeholder
|
||||
placeholder,
|
||||
}: WidgetProps<T, S, F>) {
|
||||
const { enumOptions, enumDisabled, emptyValue: optEmptyVal } = options;
|
||||
const emptyValue = multiple ? [] : "";
|
||||
@@ -51,7 +51,7 @@ function SelectWidget<
|
||||
const newValue = getValue(event, multiple);
|
||||
return onFocus(id, enumOptionsValueForIndex<S>(newValue, enumOptions, optEmptyVal));
|
||||
},
|
||||
[onFocus, id, schema, multiple, enumOptions, optEmptyVal]
|
||||
[onFocus, id, schema, multiple, enumOptions, optEmptyVal],
|
||||
);
|
||||
|
||||
const handleBlur = useCallback(
|
||||
@@ -59,7 +59,7 @@ function SelectWidget<
|
||||
const newValue = getValue(event, multiple);
|
||||
return onBlur(id, enumOptionsValueForIndex<S>(newValue, enumOptions, optEmptyVal));
|
||||
},
|
||||
[onBlur, id, schema, multiple, enumOptions, optEmptyVal]
|
||||
[onBlur, id, schema, multiple, enumOptions, optEmptyVal],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
@@ -67,7 +67,7 @@ function SelectWidget<
|
||||
const newValue = getValue(event, multiple);
|
||||
return onChange(enumOptionsValueForIndex<S>(newValue, enumOptions, optEmptyVal));
|
||||
},
|
||||
[onChange, schema, multiple, enumOptions, optEmptyVal]
|
||||
[onChange, schema, multiple, enumOptions, optEmptyVal],
|
||||
);
|
||||
|
||||
const selectedIndexes = enumOptionsIndexForValue<S>(value, enumOptions, multiple);
|
||||
|
||||
@@ -26,5 +26,5 @@ export const widgets = {
|
||||
CheckboxWidget: WithLabel(CheckboxWidget),
|
||||
SelectWidget: WithLabel(SelectWidget, "select"),
|
||||
CheckboxesWidget: WithLabel(CheckboxesWidget),
|
||||
JsonWidget: WithLabel(JsonWidget)
|
||||
JsonWidget: WithLabel(JsonWidget),
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
type FormEvent,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
useState,
|
||||
} from "react";
|
||||
import { useEvent } from "ui/hooks/use-event";
|
||||
import {
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
coerce,
|
||||
getFormTarget,
|
||||
getTargetsByName,
|
||||
setPath
|
||||
setPath,
|
||||
} from "./utils";
|
||||
|
||||
export type NativeFormProps = {
|
||||
@@ -25,12 +25,12 @@ export type NativeFormProps = {
|
||||
onSubmit?: (data: any, ctx: { event: FormEvent<HTMLFormElement> }) => Promise<void> | void;
|
||||
onSubmitInvalid?: (
|
||||
errors: InputError[],
|
||||
ctx: { event: FormEvent<HTMLFormElement> }
|
||||
ctx: { event: FormEvent<HTMLFormElement> },
|
||||
) => Promise<void> | void;
|
||||
onError?: (errors: InputError[]) => void;
|
||||
onChange?: (
|
||||
data: any,
|
||||
ctx: { event: ChangeEvent<HTMLFormElement>; key: string; value: any; errors: InputError[] }
|
||||
ctx: { event: ChangeEvent<HTMLFormElement>; key: string; value: any; errors: InputError[] },
|
||||
) => Promise<void> | void;
|
||||
clean?: CleanOptions | true;
|
||||
} & Omit<ComponentPropsWithoutRef<"form">, "onChange" | "onSubmit">;
|
||||
@@ -77,13 +77,13 @@ export function NativeForm({
|
||||
const validateElement = useEvent((el: InputElement | null, opts?: { report?: boolean }) => {
|
||||
if (props.noValidate || !el || !("name" in el)) return;
|
||||
const errorElement = formRef.current?.querySelector(
|
||||
errorFieldSelector?.(el.name) ?? `[data-role="input-error"][data-name="${el.name}"]`
|
||||
errorFieldSelector?.(el.name) ?? `[data-role="input-error"][data-name="${el.name}"]`,
|
||||
);
|
||||
|
||||
if (!el.checkValidity()) {
|
||||
const error = {
|
||||
name: el.name,
|
||||
message: el.validationMessage
|
||||
message: el.validationMessage,
|
||||
};
|
||||
|
||||
setErrors((prev) => [...prev.filter((e) => e.name !== el.name), error]);
|
||||
@@ -165,7 +165,7 @@ export function NativeForm({
|
||||
event: e,
|
||||
key: target.name,
|
||||
value: target.value,
|
||||
errors
|
||||
errors,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -70,14 +70,14 @@ export type CleanOptions = {
|
||||
};
|
||||
export function cleanObject<Obj extends { [key: string]: any }>(
|
||||
obj: Obj,
|
||||
_opts?: CleanOptions
|
||||
_opts?: CleanOptions,
|
||||
): Obj {
|
||||
if (!obj) return obj;
|
||||
const _empty = [null, undefined, ""];
|
||||
const opts = {
|
||||
empty: _opts?.empty ?? _empty,
|
||||
emptyInArray: _opts?.emptyInArray ?? _empty,
|
||||
keepEmptyArray: _opts?.keepEmptyArray ?? false
|
||||
keepEmptyArray: _opts?.keepEmptyArray ?? false,
|
||||
};
|
||||
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type DraggableRubric,
|
||||
type DraggableStateSnapshot,
|
||||
Droppable,
|
||||
type DroppableProps
|
||||
type DroppableProps,
|
||||
} from "@hello-pangea/dnd";
|
||||
import type { ElementProps } from "@mantine/core";
|
||||
import { useListState } from "@mantine/hooks";
|
||||
|
||||
@@ -21,13 +21,13 @@ export type Modal2Props = Omit<ModalProps, "opened" | "onClose"> & {
|
||||
export const Modal2 = forwardRef<Modal2Ref, Modal2Props>(
|
||||
(
|
||||
{ classNames, children, opened: initialOpened, closeOnClickOutside = false, ...props },
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const [opened, { open, close }] = useDisclosure(initialOpened);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open,
|
||||
close
|
||||
close,
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -42,18 +42,18 @@ export const Modal2 = forwardRef<Modal2Ref, Modal2Props>(
|
||||
classNames={{
|
||||
...classNames,
|
||||
root: "bknd-admin",
|
||||
content: "rounded-lg select-none"
|
||||
content: "rounded-lg select-none",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const ModalTitle = ({ path, onClose }: { path: string[]; onClose: () => void }) => {
|
||||
return (
|
||||
<div className="py-3 px-5 font-bold bg-primary/5 flex flex-row justify-between items-center sticky top-0 left-0 right-0 z-10 border-b border-b-muted">
|
||||
<div className="py-3 px-5 font-bold bg-lightest flex flex-row justify-between items-center sticky top-0 left-0 right-0 z-10 border-none">
|
||||
<div className="flex flex-row gap-1">
|
||||
{path.map((p, i) => {
|
||||
const last = i + 1 === path.length;
|
||||
@@ -76,7 +76,7 @@ export const ModalBody = ({ children, className }: { children?: any; className?:
|
||||
<ScrollArea.Root
|
||||
className={twMerge("flex flex-col min-h-96", className)}
|
||||
style={{
|
||||
maxHeight: "calc(80vh)"
|
||||
maxHeight: "calc(80vh)",
|
||||
}}
|
||||
>
|
||||
<ScrollArea.Viewport className="w-full h-full">
|
||||
@@ -104,7 +104,7 @@ export const ModalFooter = ({
|
||||
prev,
|
||||
nextLabel = "Next",
|
||||
prevLabel = "Back",
|
||||
debug
|
||||
debug,
|
||||
}: {
|
||||
next: any;
|
||||
prev: any;
|
||||
@@ -123,7 +123,7 @@ export const ModalFooter = ({
|
||||
shadow="md"
|
||||
opened={opened}
|
||||
classNames={{
|
||||
dropdown: "!px-1 !pr-2.5 !py-2 text-sm"
|
||||
dropdown: "!px-1 !pr-2.5 !py-2 text-sm",
|
||||
}}
|
||||
>
|
||||
<Popover.Target>
|
||||
|
||||
@@ -4,14 +4,15 @@ import {
|
||||
type ComponentPropsWithoutRef,
|
||||
Fragment,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
cloneElement,
|
||||
useState
|
||||
useState,
|
||||
} from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useEvent } from "ui/hooks/use-event";
|
||||
|
||||
export type DropdownItem =
|
||||
| (() => JSX.Element)
|
||||
| (() => ReactNode)
|
||||
| {
|
||||
label: string | ReactElement;
|
||||
icon?: any;
|
||||
@@ -36,7 +37,7 @@ export type DropdownProps = {
|
||||
onClickItem?: (item: DropdownItem) => void;
|
||||
renderItem?: (
|
||||
item: DropdownItem,
|
||||
props: { key: number; onClick: () => void }
|
||||
props: { key: number; onClick: () => void },
|
||||
) => DropdownClickableChild;
|
||||
};
|
||||
|
||||
@@ -52,7 +53,7 @@ export function Dropdown({
|
||||
onClickItem,
|
||||
renderItem,
|
||||
itemsClassName,
|
||||
className
|
||||
className,
|
||||
}: DropdownProps) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const [position, setPosition] = useState(initialPosition);
|
||||
@@ -61,7 +62,7 @@ export function Dropdown({
|
||||
const [_offset, _setOffset] = useState(0);
|
||||
|
||||
const toggle = useEvent((delay: number = 50) =>
|
||||
setTimeout(() => setOpen((prev) => !prev), typeof delay === "number" ? delay : 0)
|
||||
setTimeout(() => setOpen((prev) => !prev), typeof delay === "number" ? delay : 0),
|
||||
);
|
||||
|
||||
const onClickHandler = openEvent === "onClick" ? toggle : undefined;
|
||||
@@ -106,7 +107,7 @@ export function Dropdown({
|
||||
"bottom-start": { top: "100%", left: _offset, marginTop: offset },
|
||||
"bottom-end": { right: _offset, top: "100%", marginTop: offset },
|
||||
"top-start": { bottom: "100%", marginBottom: offset },
|
||||
"top-end": { bottom: "100%", right: _offset, marginBottom: offset }
|
||||
"top-end": { bottom: "100%", right: _offset, marginBottom: offset },
|
||||
}[position];
|
||||
|
||||
const internalOnClickItem = useEvent((item) => {
|
||||
@@ -132,7 +133,7 @@ export function Dropdown({
|
||||
"flex flex-row flex-nowrap text-nowrap items-center outline-none cursor-pointer px-2.5 rounded-md link leading-none h-8",
|
||||
itemsClassName,
|
||||
item.disabled ? "opacity-50 cursor-not-allowed" : "hover:bg-primary/10",
|
||||
item.destructive && "text-red-500 hover:bg-red-600 hover:text-white"
|
||||
item.destructive && "text-red-500 hover:bg-red-600 hover:text-white",
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
@@ -159,7 +160,7 @@ export function Dropdown({
|
||||
{...dropdownWrapperProps}
|
||||
className={twMerge(
|
||||
"absolute z-30 flex flex-col bg-background border border-muted px-1 py-1 rounded-lg shadow-lg min-w-full",
|
||||
dropdownWrapperProps?.className
|
||||
dropdownWrapperProps?.className,
|
||||
)}
|
||||
style={dropdownStyle}
|
||||
>
|
||||
@@ -167,7 +168,7 @@ export function Dropdown({
|
||||
<div className="text-sm font-bold px-2.5 mb-1 mt-1 opacity-50">{title}</div>
|
||||
)}
|
||||
{menuItems.map((item, i) =>
|
||||
itemRenderer(item, { key: i, onClick: () => internalOnClickItem(item) })
|
||||
itemRenderer(item, { key: i, onClick: () => internalOnClickItem(item) }),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function Modal({
|
||||
onClose = () => null,
|
||||
allowBackdropClose = true,
|
||||
className,
|
||||
stickToTop
|
||||
stickToTop,
|
||||
}: ModalProps) {
|
||||
const clickoutsideRef = useClickOutside(() => {
|
||||
if (allowBackdropClose) onClose();
|
||||
@@ -29,13 +29,13 @@ export function Modal({
|
||||
<div
|
||||
className={twMerge(
|
||||
"w-full h-full fixed bottom-0 top-0 right-0 left-0 bg-background/60 flex justify-center backdrop-blur-sm z-10",
|
||||
stickToTop ? "items-start" : "items-center"
|
||||
stickToTop ? "items-start" : "items-center",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"z-20 flex flex-col bg-background rounded-lg shadow-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
ref={clickoutsideRef}
|
||||
>
|
||||
|
||||
@@ -20,20 +20,20 @@ export function Popover({
|
||||
backdrop = false,
|
||||
position = "bottom-start",
|
||||
overlayProps,
|
||||
className
|
||||
className,
|
||||
}: PopoverProps) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const clickoutsideRef = useClickOutside(() => setOpen(false));
|
||||
|
||||
const toggle = useEvent((delay: number = 50) =>
|
||||
setTimeout(() => setOpen((prev) => !prev), typeof delay === "number" ? delay : 0)
|
||||
setTimeout(() => setOpen((prev) => !prev), typeof delay === "number" ? delay : 0),
|
||||
);
|
||||
|
||||
const pos = {
|
||||
"bottom-start": "mt-1 top-[100%]",
|
||||
"bottom-end": "right-0 top-[100%] mt-1",
|
||||
"top-start": "bottom-[100%] mb-1",
|
||||
"top-end": "bottom-[100%] right-0 mb-1"
|
||||
"top-end": "bottom-[100%] right-0 mb-1",
|
||||
}[position];
|
||||
|
||||
return (
|
||||
@@ -49,7 +49,7 @@ export function Popover({
|
||||
className={twMerge(
|
||||
"animate-fade-in absolute z-20 flex flex-col bg-background border border-muted px-1 py-1 rounded-lg shadow-lg backdrop-blur-sm min-w-full max-w-20",
|
||||
pos,
|
||||
overlayProps?.className
|
||||
overlayProps?.className,
|
||||
)}
|
||||
>
|
||||
{target({ toggle })}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
type SetStateAction,
|
||||
createContext,
|
||||
useContext,
|
||||
useState
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
export type TStepsProps = {
|
||||
@@ -30,7 +30,7 @@ export function Steps({ children, initialPath = [], initialState = {}, lastBack
|
||||
const [state, setState] = useState<any>(initialState);
|
||||
const [path, setPath] = useState<string[]>(initialPath);
|
||||
const steps: any[] = Children.toArray(children).filter(
|
||||
(child: any) => child.props.disabled !== true
|
||||
(child: any) => child.props.disabled !== true,
|
||||
);
|
||||
|
||||
function stepBack() {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
TbDotsVertical,
|
||||
TbSelector,
|
||||
TbSquare,
|
||||
TbSquareCheckFilled
|
||||
TbSquareCheckFilled,
|
||||
} from "react-icons/tb";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
@@ -69,7 +69,7 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
|
||||
renderHeader,
|
||||
rowActions,
|
||||
renderValue,
|
||||
onClickNew
|
||||
onClickNew,
|
||||
}: DataTableProps<Data>) {
|
||||
total = total || data?.length || 0;
|
||||
page = page || 1;
|
||||
@@ -105,7 +105,7 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
|
||||
type="button"
|
||||
className={twMerge(
|
||||
"link hover:bg-primary/5 py-1.5 rounded-md inline-flex flex-row justify-start items-center gap-1",
|
||||
onClickSort ? "pl-2.5 pr-1" : "px-2.5"
|
||||
onClickSort ? "pl-2.5 pr-1" : "px-2.5",
|
||||
)}
|
||||
onClick={() => onClickSort?.(property)}
|
||||
>
|
||||
@@ -210,7 +210,7 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
|
||||
<Dropdown
|
||||
items={perPageOptions.map((perPage) => ({
|
||||
label: String(perPage),
|
||||
perPage
|
||||
perPage,
|
||||
}))}
|
||||
position="top-end"
|
||||
onClickItem={(item: any) => onClickPerPage?.(item.perPage)}
|
||||
@@ -254,7 +254,7 @@ export const CellValue = ({ value, property }) => {
|
||||
|
||||
const SortIndicator = ({
|
||||
sort,
|
||||
field
|
||||
field,
|
||||
}: {
|
||||
sort: Pick<DataTableProps<any>, "sort">["sort"];
|
||||
field: string;
|
||||
@@ -292,7 +292,7 @@ const TableNav: React.FC<TableNavProps> = ({ current, total, onClick }: TableNav
|
||||
{ value: 1, Icon: TbChevronsLeft, disabled: current === 1 },
|
||||
{ value: current - 1, Icon: TbChevronLeft, disabled: current === 1 },
|
||||
{ value: current + 1, Icon: TbChevronRight, disabled: current === total },
|
||||
{ value: total, Icon: TbChevronsRight, disabled: current === total }
|
||||
{ value: total, Icon: TbChevronsRight, disabled: current === total },
|
||||
] as const;
|
||||
|
||||
return navMap.map((nav, key) => (
|
||||
|
||||
@@ -38,10 +38,22 @@ const useLocationFromRouter = (router) => {
|
||||
// (This is achieved via `useEvent`.)
|
||||
return [
|
||||
unescape(relativePath(router.base, location)),
|
||||
useEvent((to, navOpts) => navigate(absolutePath(to, router.base), navOpts))
|
||||
useEvent((to, navOpts) => navigate(absolutePath(to, router.base), navOpts)),
|
||||
];
|
||||
};
|
||||
|
||||
export function isLinkActive(href: string, strictness?: number) {
|
||||
const path = window.location.pathname;
|
||||
|
||||
if (!strictness || strictness === 0) {
|
||||
return path.includes(href);
|
||||
} else if (strictness === 1) {
|
||||
return path === href || path.endsWith(href) || path.includes(href + "/");
|
||||
}
|
||||
|
||||
return path === href;
|
||||
}
|
||||
|
||||
export function Link({
|
||||
className,
|
||||
native,
|
||||
@@ -64,7 +76,7 @@ export function Link({
|
||||
const href = router
|
||||
.hrefs(
|
||||
_href[0] === "~" ? _href.slice(1) : router.base + _href,
|
||||
router // pass router as a second argument for convinience
|
||||
router, // pass router as a second argument for convinience
|
||||
)
|
||||
.replace("//", "/");
|
||||
const absPath = absolutePath(path, router.base).replace("//", "/");
|
||||
|
||||
@@ -6,7 +6,7 @@ export function useFlows() {
|
||||
|
||||
return {
|
||||
flows: app.flows,
|
||||
config: app.config.flows
|
||||
config: app.config.flows,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,6 @@ export function useFlow(name: string) {
|
||||
|
||||
return {
|
||||
flow: flow!,
|
||||
config: app.config.flows[name]
|
||||
config: app.config.flows[name],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@ export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "onSubmit" |
|
||||
const validator = new TypeboxValidator();
|
||||
const schema = Type.Object({
|
||||
email: Type.String({
|
||||
pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"
|
||||
pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
|
||||
}),
|
||||
password: Type.String({
|
||||
minLength: 8 // @todo: this should be configurable
|
||||
})
|
||||
minLength: 8, // @todo: this should be configurable
|
||||
}),
|
||||
});
|
||||
|
||||
export function AuthForm({
|
||||
@@ -49,7 +49,7 @@ export function AuthForm({
|
||||
const basepath = auth?.basepath ?? "/api/auth";
|
||||
const password = {
|
||||
action: `${basepath}/password/${action}`,
|
||||
strategy: auth?.strategies?.password ?? ({ type: "password" } as const)
|
||||
strategy: auth?.strategies?.password ?? ({ type: "password" } as const),
|
||||
};
|
||||
|
||||
const oauth = transform(
|
||||
@@ -59,7 +59,7 @@ export function AuthForm({
|
||||
result[key] = value.config;
|
||||
}
|
||||
},
|
||||
{}
|
||||
{},
|
||||
) as Record<string, AppAuthOAuthStrategy>;
|
||||
const has_oauth = Object.keys(oauth).length > 0;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export function AuthScreen({
|
||||
action = "login",
|
||||
logo,
|
||||
intro,
|
||||
formOnly
|
||||
formOnly,
|
||||
}: AuthScreenProps) {
|
||||
const { strategies, basepath, loading } = useAuthStrategies();
|
||||
const Form = <AuthForm auth={{ basepath, strategies }} method={method} action={action} />;
|
||||
|
||||
@@ -20,7 +20,7 @@ export function SocialLink({
|
||||
action,
|
||||
method = "POST",
|
||||
basepath = "/api/auth",
|
||||
children
|
||||
children,
|
||||
}: SocialLinkProps) {
|
||||
const url = [basepath, provider, action].join("/");
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SocialLink } from "./SocialLink";
|
||||
export const Auth = {
|
||||
Screen: AuthScreen,
|
||||
Form: AuthForm,
|
||||
SocialLink: SocialLink
|
||||
SocialLink: SocialLink,
|
||||
};
|
||||
|
||||
export { useAuthStrategies };
|
||||
|
||||
@@ -2,11 +2,12 @@ import type { DB } from "core";
|
||||
import {
|
||||
type ComponentPropsWithRef,
|
||||
type ComponentPropsWithoutRef,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
memo,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
useState,
|
||||
} from "react";
|
||||
import { TbDots } from "react-icons/tb";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@@ -27,7 +28,7 @@ export type FileState = {
|
||||
export type FileStateWithData = FileState & { data: DB["media"] };
|
||||
|
||||
export type DropzoneRenderProps = {
|
||||
wrapperRef: RefObject<HTMLDivElement>;
|
||||
wrapperRef: RefObject<HTMLDivElement | null>;
|
||||
inputProps: ComponentPropsWithRef<"input">;
|
||||
state: {
|
||||
files: FileState[];
|
||||
@@ -49,6 +50,7 @@ export type DropzoneProps = {
|
||||
initialItems?: FileState[];
|
||||
flow?: "start" | "end";
|
||||
maxItems?: number;
|
||||
allowedMimeTypes?: string[];
|
||||
overwrite?: boolean;
|
||||
autoUpload?: boolean;
|
||||
onRejected?: (files: FileWithPath[]) => void;
|
||||
@@ -58,7 +60,7 @@ export type DropzoneProps = {
|
||||
show?: boolean;
|
||||
text?: string;
|
||||
};
|
||||
children?: (props: DropzoneRenderProps) => JSX.Element;
|
||||
children?: (props: DropzoneRenderProps) => ReactNode;
|
||||
};
|
||||
|
||||
function handleUploadError(e: unknown) {
|
||||
@@ -75,6 +77,7 @@ export function Dropzone({
|
||||
handleDelete,
|
||||
initialItems = [],
|
||||
flow = "start",
|
||||
allowedMimeTypes,
|
||||
maxItems,
|
||||
overwrite,
|
||||
autoUpload,
|
||||
@@ -82,7 +85,7 @@ export function Dropzone({
|
||||
onRejected,
|
||||
onDeleted,
|
||||
onUploaded,
|
||||
children
|
||||
children,
|
||||
}: DropzoneProps) {
|
||||
const [files, setFiles] = useState<FileState[]>(initialItems);
|
||||
const [uploading, setUploading] = useState<boolean>(false);
|
||||
@@ -109,8 +112,26 @@ export function Dropzone({
|
||||
return added > remaining;
|
||||
}
|
||||
|
||||
function isAllowed(i: DataTransferItem | DataTransferItem[] | File | File[]): boolean {
|
||||
const items = Array.isArray(i) ? i : [i];
|
||||
const specs = items.map((item) => ({
|
||||
kind: "kind" in item ? item.kind : "file",
|
||||
type: item.type,
|
||||
size: "size" in item ? item.size : 0,
|
||||
}));
|
||||
|
||||
return specs.every((spec) => {
|
||||
if (spec.kind !== "file") {
|
||||
return false;
|
||||
}
|
||||
return !(allowedMimeTypes && !allowedMimeTypes.includes(spec.type));
|
||||
});
|
||||
}
|
||||
|
||||
const { isOver, handleFileInputChange, ref } = useDropzone({
|
||||
onDropped: (newFiles: FileWithPath[]) => {
|
||||
if (!isAllowed(newFiles)) return;
|
||||
|
||||
let to_drop = 0;
|
||||
const added = newFiles.length;
|
||||
|
||||
@@ -144,7 +165,7 @@ export function Dropzone({
|
||||
size: f.size,
|
||||
type: f.type,
|
||||
state: "pending",
|
||||
progress: 0
|
||||
progress: 0,
|
||||
}));
|
||||
|
||||
return flow === "start" ? [...filteredFiles, ..._prev] : [..._prev, ...filteredFiles];
|
||||
@@ -155,12 +176,17 @@ export function Dropzone({
|
||||
}
|
||||
},
|
||||
onOver: (items) => {
|
||||
if (!isAllowed(items)) {
|
||||
setIsOverAccepted(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const max_reached = isMaxReached(items.length);
|
||||
setIsOverAccepted(!max_reached);
|
||||
},
|
||||
onLeave: () => {
|
||||
setIsOverAccepted(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -198,11 +224,11 @@ export function Dropzone({
|
||||
return {
|
||||
...f,
|
||||
state,
|
||||
progress: progress ?? f.progress
|
||||
progress: progress ?? f.progress,
|
||||
};
|
||||
}
|
||||
return f;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -212,11 +238,11 @@ export function Dropzone({
|
||||
if (f.path === prevPath) {
|
||||
return {
|
||||
...f,
|
||||
...newState
|
||||
...newState,
|
||||
};
|
||||
}
|
||||
return f;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -268,7 +294,7 @@ export function Dropzone({
|
||||
console.log(`Progress: ${percentComplete.toFixed(2)}%`);
|
||||
} else {
|
||||
console.log(
|
||||
"Unable to compute progress information since the total size is unknown"
|
||||
"Unable to compute progress information since the total size is unknown",
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -286,7 +312,7 @@ export function Dropzone({
|
||||
const newState = {
|
||||
...response.state,
|
||||
progress: 1,
|
||||
state: "uploaded"
|
||||
state: "uploaded",
|
||||
};
|
||||
|
||||
replaceFileState(file.path, newState);
|
||||
@@ -339,7 +365,7 @@ export function Dropzone({
|
||||
|
||||
const openFileInput = () => inputRef.current?.click();
|
||||
const showPlaceholder = Boolean(
|
||||
placeholder?.show === true || !maxItems || (maxItems && files.length < maxItems)
|
||||
placeholder?.show === true || !maxItems || (maxItems && files.length < maxItems),
|
||||
);
|
||||
|
||||
const renderProps: DropzoneRenderProps = {
|
||||
@@ -348,25 +374,25 @@ export function Dropzone({
|
||||
ref: inputRef,
|
||||
type: "file",
|
||||
multiple: !maxItems || maxItems > 1,
|
||||
onChange: handleFileInputChange
|
||||
onChange: handleFileInputChange,
|
||||
},
|
||||
state: {
|
||||
files,
|
||||
isOver,
|
||||
isOverAccepted,
|
||||
showPlaceholder
|
||||
showPlaceholder,
|
||||
},
|
||||
actions: {
|
||||
uploadFile,
|
||||
deleteFile,
|
||||
openFileInput
|
||||
openFileInput,
|
||||
},
|
||||
dropzoneProps: {
|
||||
maxItems,
|
||||
placeholder,
|
||||
autoUpload,
|
||||
flow
|
||||
}
|
||||
flow,
|
||||
},
|
||||
};
|
||||
|
||||
return children ? children(renderProps) : <DropzoneInner {...renderProps} />;
|
||||
@@ -377,7 +403,7 @@ const DropzoneInner = ({
|
||||
inputProps,
|
||||
state: { files, isOver, isOverAccepted, showPlaceholder },
|
||||
actions: { uploadFile, deleteFile, openFileInput },
|
||||
dropzoneProps: { placeholder, flow }
|
||||
dropzoneProps: { placeholder, flow },
|
||||
}: DropzoneRenderProps) => {
|
||||
const Placeholder = showPlaceholder && (
|
||||
<UploadPlaceholder onClick={openFileInput} text={placeholder?.text} />
|
||||
@@ -397,7 +423,7 @@ const DropzoneInner = ({
|
||||
className={twMerge(
|
||||
"dropzone w-full h-full align-start flex flex-col select-none",
|
||||
isOver && isOverAccepted && "bg-green-200/10",
|
||||
isOver && !isOverAccepted && "bg-red-200/40 cursor-not-allowed"
|
||||
isOver && !isOverAccepted && "bg-red-200/40 cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
<div className="hidden">
|
||||
@@ -434,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;
|
||||
@@ -453,7 +479,7 @@ const Wrapper = ({ file, fallback, ...props }: PreviewComponentProps) => {
|
||||
};
|
||||
export const PreviewWrapperMemoized = memo(
|
||||
Wrapper,
|
||||
(prev, next) => prev.file.path === next.file.path
|
||||
(prev, next) => prev.file.path === next.file.path,
|
||||
);
|
||||
|
||||
type PreviewProps = {
|
||||
@@ -461,16 +487,16 @@ type PreviewProps = {
|
||||
handleUpload: (file: FileState) => Promise<void>;
|
||||
handleDelete: (file: FileState) => Promise<void>;
|
||||
};
|
||||
const Preview: React.FC<PreviewProps> = ({ file, handleUpload, handleDelete }) => {
|
||||
const Preview = ({ file, handleUpload, handleDelete }: PreviewProps) => {
|
||||
const dropdownItems = [
|
||||
["initial", "uploaded"].includes(file.state) && {
|
||||
label: "Delete",
|
||||
onClick: () => handleDelete(file)
|
||||
onClick: () => handleDelete(file),
|
||||
},
|
||||
["initial", "pending"].includes(file.state) && {
|
||||
label: "Upload",
|
||||
onClick: () => handleUpload(file)
|
||||
}
|
||||
onClick: () => handleUpload(file),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -478,7 +504,7 @@ const Preview: React.FC<PreviewProps> = ({ file, handleUpload, handleDelete }) =
|
||||
className={twMerge(
|
||||
"w-[49%] md:w-60 flex flex-col border border-muted relative",
|
||||
file.state === "failed" && "border-red-500 bg-red-200/20",
|
||||
file.state === "deleting" && "opacity-70"
|
||||
file.state === "deleting" && "opacity-70",
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-2 right-2">
|
||||
|
||||
@@ -38,7 +38,7 @@ export function DropzoneContainer({
|
||||
const baseUrl = api.baseUrl;
|
||||
const defaultQuery = {
|
||||
limit: query?.limit ? query?.limit : props.maxItems ? props.maxItems : 50,
|
||||
sort: "-id"
|
||||
sort: "-id",
|
||||
};
|
||||
const entity_name = (media?.entity_name ?? "media") as "media";
|
||||
//console.log("dropzone:baseUrl", baseUrl);
|
||||
@@ -51,12 +51,12 @@ export function DropzoneContainer({
|
||||
where: {
|
||||
reference: `${entity.name}.${entity.field}`,
|
||||
entity_id: entity.id,
|
||||
...query?.where
|
||||
}
|
||||
...query?.where,
|
||||
},
|
||||
})
|
||||
: api.data.readMany(entity_name, {
|
||||
...defaultQuery,
|
||||
...query
|
||||
...query,
|
||||
});
|
||||
|
||||
const $q = useApiQuery(selectApi, { enabled: initialItems !== false && !initialItems });
|
||||
@@ -69,7 +69,7 @@ export function DropzoneContainer({
|
||||
return {
|
||||
url,
|
||||
headers: api.media.getUploadHeaders(),
|
||||
method: "POST"
|
||||
method: "POST",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -81,7 +81,9 @@ export function DropzoneContainer({
|
||||
return api.media.deleteFile(file.path);
|
||||
});
|
||||
|
||||
const actualItems = (initialItems || $q.data || []) as MediaFieldSchema[];
|
||||
const actualItems = (initialItems ??
|
||||
(Array.isArray($q.data) ? $q.data : []) ??
|
||||
[]) as MediaFieldSchema[];
|
||||
const _initialItems = mediaItemsToFileStates(actualItems, { baseUrl });
|
||||
|
||||
const key = id + JSON.stringify(_initialItems);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { guess } from "media/storage/mime-types-tiny";
|
||||
const FILES_TO_IGNORE = [
|
||||
// Thumbnail cache files for macOS and Windows
|
||||
".DS_Store", // macOs
|
||||
"Thumbs.db" // Windows
|
||||
"Thumbs.db", // Windows
|
||||
];
|
||||
|
||||
export function toFileWithPath(file: FileWithPath, path?: string): FileWithPath {
|
||||
@@ -29,7 +29,7 @@ export function toFileWithPath(file: FileWithPath, path?: string): FileWithPath
|
||||
: file.name,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
enumerable: true
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ function withMimeType(file: FileWithPath) {
|
||||
value: type,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
enumerable: true
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -80,10 +80,8 @@ export interface FileWithPath extends File {
|
||||
export async function fromEvent(evt: Event | any): Promise<(FileWithPath | DataTransferItem)[]> {
|
||||
if (isObject<DragEvent>(evt) && isDataTransfer(evt.dataTransfer)) {
|
||||
return getDataTransferFiles(evt.dataTransfer, evt.type);
|
||||
// biome-ignore lint/style/noUselessElse: not useless
|
||||
} else if (isChangeEvt(evt)) {
|
||||
return getInputFiles(evt);
|
||||
// biome-ignore lint/style/noUselessElse: not useless
|
||||
} else if (
|
||||
Array.isArray(evt) &&
|
||||
evt.every((item) => "getFile" in item && typeof item.getFile === "function")
|
||||
@@ -107,7 +105,7 @@ function isObject<T>(v: any): v is T {
|
||||
|
||||
function getInputFiles(evt: Event) {
|
||||
return fromList<FileWithPath>((evt.target as HTMLInputElement).files).map((file) =>
|
||||
toFileWithPath(file)
|
||||
toFileWithPath(file),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,9 +179,9 @@ function flatten<T>(items: any[]): T[] {
|
||||
(acc, files) => [
|
||||
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
|
||||
...acc,
|
||||
...(Array.isArray(files) ? flatten(files) : [files])
|
||||
...(Array.isArray(files) ? flatten(files) : [files]),
|
||||
],
|
||||
[]
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -231,7 +229,7 @@ function fromDirEntry(entry: any) {
|
||||
},
|
||||
(err: any) => {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -249,7 +247,7 @@ async function fromFileEntry(entry: any) {
|
||||
},
|
||||
(err: any) => {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export function mediaItemToFileState(
|
||||
options: {
|
||||
overrides?: Partial<FileState>;
|
||||
baseUrl?: string;
|
||||
} = { overrides: {}, baseUrl: "" }
|
||||
} = { overrides: {}, baseUrl: "" },
|
||||
): FileState {
|
||||
return {
|
||||
body: `${options.baseUrl}/api/media/file/${item.path}`,
|
||||
@@ -16,7 +16,7 @@ export function mediaItemToFileState(
|
||||
type: item.mime_type ?? "",
|
||||
state: "uploaded",
|
||||
progress: 0,
|
||||
...options.overrides
|
||||
...options.overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export function mediaItemsToFileStates(
|
||||
options: {
|
||||
overrides?: Partial<FileState>;
|
||||
baseUrl?: string;
|
||||
} = { overrides: {}, baseUrl: "" }
|
||||
} = { overrides: {}, baseUrl: "" },
|
||||
): FileState[] {
|
||||
return items.map((item) => mediaItemToFileState(item, options));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DropzoneContainer, useDropzone } from "./DropzoneContainer";
|
||||
export const Media = {
|
||||
Dropzone: DropzoneContainer,
|
||||
Preview: PreviewWrapperMemoized,
|
||||
useDropzone: useDropzone
|
||||
useDropzone: useDropzone,
|
||||
};
|
||||
|
||||
export { useDropzone as useMediaDropzone };
|
||||
@@ -14,6 +14,6 @@ export type {
|
||||
FileState,
|
||||
FileStateWithData,
|
||||
DropzoneProps,
|
||||
DropzoneRenderProps
|
||||
DropzoneRenderProps,
|
||||
} from "./Dropzone";
|
||||
export type { DropzoneContainerProps } from "./DropzoneContainer";
|
||||
|
||||
@@ -3,13 +3,13 @@ import { type FileWithPath, fromEvent } from "./file-selector";
|
||||
|
||||
type DropzoneProps = {
|
||||
onDropped: (files: FileWithPath[]) => void;
|
||||
onOver?: (items: DataTransferItem[]) => void;
|
||||
onOver?: (items: DataTransferItem[], event: DragEvent) => void;
|
||||
onLeave?: () => void;
|
||||
};
|
||||
|
||||
const events = {
|
||||
enter: ["dragenter", "dragover", "dragstart"],
|
||||
leave: ["dragleave", "drop"]
|
||||
enter: ["dragenter", "dragover", "dragstart"] as const,
|
||||
leave: ["dragleave", "drop"] as const,
|
||||
};
|
||||
const allEvents = [...events.enter, ...events.leave];
|
||||
|
||||
@@ -24,10 +24,10 @@ export function useDropzone({ onDropped, onOver, onLeave }: DropzoneProps) {
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const toggleHighlight = useCallback(async (e: Event) => {
|
||||
const _isOver = events.enter.includes(e.type);
|
||||
const toggleHighlight = useCallback(async (e: DragEvent) => {
|
||||
const _isOver = events.enter.includes(e.type as any);
|
||||
if (onOver && _isOver !== isOver && !onOverCalled.current) {
|
||||
onOver((await fromEvent(e)) as DataTransferItem[]);
|
||||
onOver((await fromEvent(e)) as DataTransferItem[], e);
|
||||
onOverCalled.current = true;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export function useDropzone({ onDropped, onOver, onLeave }: DropzoneProps) {
|
||||
onDropped?.(files as any);
|
||||
onOverCalled.current = false;
|
||||
},
|
||||
[onDropped]
|
||||
[onDropped],
|
||||
);
|
||||
|
||||
const handleFileInputChange = useCallback(
|
||||
@@ -53,7 +53,7 @@ export function useDropzone({ onDropped, onOver, onLeave }: DropzoneProps) {
|
||||
const files = await fromEvent(e);
|
||||
onDropped?.(files as any);
|
||||
},
|
||||
[onDropped]
|
||||
[onDropped],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
// there is no lifecycle or Hook in React that we can use to switch
|
||||
// .current at the right timing."
|
||||
// So we will have to make do with this "close enough" approach for now.
|
||||
import { useInsertionEffect, useRef } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export const useEvent = <Fn>(fn: Fn | ((...args: any[]) => any) | undefined): Fn => {
|
||||
const ref = useRef([fn, (...args) => ref[0](...args)]).current;
|
||||
// Per Dan Abramov: useInsertionEffect executes marginally closer to the
|
||||
// correct timing for ref synchronization than useLayoutEffect on React 18.
|
||||
// See: https://github.com/facebook/react/pull/25881#issuecomment-1356244360
|
||||
useInsertionEffect(() => {
|
||||
useEffect(() => {
|
||||
ref[0] = fn;
|
||||
});
|
||||
}, []);
|
||||
return ref[1];
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Type,
|
||||
decodeSearch,
|
||||
encodeSearch,
|
||||
parseDecode
|
||||
parseDecode,
|
||||
} from "core/utils";
|
||||
import { isEqual, transform } from "lodash-es";
|
||||
import { useLocation, useSearch as useWouterSearch } from "wouter";
|
||||
@@ -13,7 +13,7 @@ import { useLocation, useSearch as useWouterSearch } from "wouter";
|
||||
// @todo: migrate to Typebox
|
||||
export function useSearch<Schema extends TSchema = TSchema>(
|
||||
schema: Schema,
|
||||
defaultValue?: Partial<StaticDecode<Schema>>
|
||||
defaultValue?: Partial<StaticDecode<Schema>>,
|
||||
) {
|
||||
const searchString = useWouterSearch();
|
||||
const [location, navigate] = useLocation();
|
||||
@@ -34,7 +34,7 @@ export function useSearch<Schema extends TSchema = TSchema>(
|
||||
if (defaultValue && isEqual(value, defaultValue[key])) return;
|
||||
result[key] = value;
|
||||
},
|
||||
{} as Static<Schema>
|
||||
{} as Static<Schema>,
|
||||
);
|
||||
const encoded = encodeSearch(search, { encode: false });
|
||||
navigate(location + (encoded.length > 0 ? "?" + encoded : ""));
|
||||
@@ -42,6 +42,6 @@ export function useSearch<Schema extends TSchema = TSchema>(
|
||||
|
||||
return {
|
||||
value: value as Required<StaticDecode<Schema>>,
|
||||
set
|
||||
set,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type ComponentPropsWithoutRef,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
useState,
|
||||
} from "react";
|
||||
import type { IconType } from "react-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@@ -48,9 +48,9 @@ export const NavLink = <E extends React.ElementType = "a">({
|
||||
<Tag
|
||||
{...otherProps}
|
||||
className={twMerge(
|
||||
"px-6 py-2 [&.active]:bg-muted [&.active]:hover:bg-primary/15 hover:bg-primary/5 flex flex-row items-center rounded-full gap-2.5 link",
|
||||
"px-6 py-2 [&.active]:bg-muted [&.active]:hover:bg-primary/15 hover:bg-primary/5 flex flex-row items-center rounded-full gap-2.5 link transition-colors",
|
||||
disabled && "opacity-50 cursor-not-allowed",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{Icon && <Icon size={18} />}
|
||||
@@ -65,7 +65,7 @@ export function Content({ children, center }: { children: React.ReactNode; cente
|
||||
data-shell="content"
|
||||
className={twMerge(
|
||||
"flex flex-1 flex-row w-dvw h-full",
|
||||
center && "justify-center items-center"
|
||||
center && "justify-center items-center",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -80,7 +80,7 @@ export function Main({ children }) {
|
||||
data-shell="main"
|
||||
className={twMerge(
|
||||
"flex flex-col flex-grow w-1 flex-shrink-1",
|
||||
sidebar.open && "max-w-[calc(100%-350px)]"
|
||||
sidebar.open && "md:max-w-[calc(100%-350px)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -155,13 +155,13 @@ export function SectionHeader({ children, right, className, scrollable, sticky }
|
||||
className={twMerge(
|
||||
"flex flex-row h-14 flex-shrink-0 py-2 pl-5 pr-3 border-muted border-b items-center justify-between bg-muted/10",
|
||||
sticky && "sticky top-0 bottom-10 z-10",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"",
|
||||
scrollable && "overflow-x-scroll overflow-y-visible app-scrollbar"
|
||||
scrollable && "overflow-x-scroll overflow-y-visible app-scrollbar",
|
||||
)}
|
||||
>
|
||||
{typeof children === "string" ? (
|
||||
@@ -202,11 +202,11 @@ export const SidebarLink = <E extends React.ElementType = "a">({
|
||||
<Tag
|
||||
{...otherProps}
|
||||
className={twMerge(
|
||||
"flex flex-row px-4 py-2.5 items-center gap-2",
|
||||
"flex flex-row px-4 items-center gap-2 h-12",
|
||||
!disabled &&
|
||||
"cursor-pointer rounded-md [&.active]:bg-primary/10 [&.active]:hover:bg-primary/15 [&.active]:font-medium hover:bg-primary/5 focus:bg-primary/5 link",
|
||||
disabled && "opacity-50 cursor-not-allowed pointer-events-none",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -243,7 +243,7 @@ export const SectionHeaderLink = <E extends React.ElementType = "a">({
|
||||
? "bg-background hover:bg-background text-primary border border-muted border-b-0"
|
||||
: "link",
|
||||
badge && "pr-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -283,7 +283,7 @@ export const SectionHeaderTabs = ({ title, items }: SectionHeaderTabsProps) => {
|
||||
|
||||
export function Scrollable({
|
||||
children,
|
||||
initialOffset = 64
|
||||
initialOffset = 64,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
initialOffset?: number;
|
||||
@@ -330,7 +330,7 @@ export const SectionHeaderAccordionItem = ({
|
||||
toggle,
|
||||
ActiveIcon = IconChevronUp,
|
||||
children,
|
||||
renderHeaderRight
|
||||
renderHeaderRight,
|
||||
}: {
|
||||
title: string;
|
||||
open: boolean;
|
||||
@@ -345,12 +345,12 @@ export const SectionHeaderAccordionItem = ({
|
||||
"flex flex-col flex-animate overflow-hidden",
|
||||
open
|
||||
? "flex-open border-b border-b-muted"
|
||||
: "flex-initial cursor-pointer hover:bg-primary/5"
|
||||
: "flex-initial cursor-pointer hover:bg-primary/5",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex flex-row bg-muted/10 border-muted border-b h-14 py-4 pr-4 pl-2 items-center gap-2"
|
||||
"flex flex-row bg-muted/10 border-muted border-b h-14 py-4 pr-4 pl-2 items-center gap-2",
|
||||
)}
|
||||
onClick={toggle}
|
||||
>
|
||||
@@ -362,7 +362,7 @@ export const SectionHeaderAccordionItem = ({
|
||||
<div
|
||||
className={twMerge(
|
||||
"overflow-y-scroll transition-all",
|
||||
open ? " flex-grow" : "h-0 opacity-0"
|
||||
open ? " flex-grow" : "h-0 opacity-0",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -42,10 +42,10 @@ export const Breadcrumbs = ({ path: _path, backTo, onBack }: BreadcrumbsProps) =
|
||||
return {
|
||||
last,
|
||||
href,
|
||||
string
|
||||
string,
|
||||
};
|
||||
}),
|
||||
[path, loc]
|
||||
[path, loc],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -82,9 +82,9 @@ const CrumbsMobile = ({ crumbs }) => {
|
||||
() =>
|
||||
crumbs.slice(1, -1).map((c) => ({
|
||||
label: c.string,
|
||||
href: c.href
|
||||
href: c.href,
|
||||
})),
|
||||
[crumbs]
|
||||
[crumbs],
|
||||
);
|
||||
const onClick = useEvent((item) => navigate(`~/${item.href}`));
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -46,10 +45,10 @@ export const Breadcrumbs2 = ({ path: _path, backTo, onBack }: Breadcrumbs2Props)
|
||||
|
||||
return {
|
||||
last,
|
||||
...p
|
||||
...p,
|
||||
};
|
||||
}),
|
||||
[path]
|
||||
[path],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -86,9 +85,9 @@ const CrumbsMobile = ({ crumbs }) => {
|
||||
() =>
|
||||
crumbs.slice(1, -1).map((c) => ({
|
||||
label: c.string,
|
||||
href: c.href
|
||||
href: c.href,
|
||||
})),
|
||||
[crumbs]
|
||||
[crumbs],
|
||||
);
|
||||
const onClick = useEvent((item) => navigate(`~/${item.href}`));
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
TbPhoto,
|
||||
TbSelector,
|
||||
TbUser,
|
||||
TbX
|
||||
TbX,
|
||||
} from "react-icons/tb";
|
||||
import { useAuth, useBkndWindowContext } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
@@ -47,10 +47,10 @@ export function HeaderNavigation() {
|
||||
{ label: "Data", href: "/data", Icon: TbDatabase },
|
||||
{ label: "Auth", href: "/auth", Icon: TbFingerprint },
|
||||
{ label: "Media", href: "/media", Icon: TbPhoto },
|
||||
{ label: "Flows", href: "/flows", Icon: TbHierarchy2 }
|
||||
{ label: "Flows", href: "/flows", Icon: TbHierarchy2 },
|
||||
];
|
||||
const activeItem = items.find((item) =>
|
||||
item.exact ? location === item.href : location.startsWith(item.href)
|
||||
item.exact ? location === item.href : location.startsWith(item.href),
|
||||
);
|
||||
|
||||
const handleItemClick = useEvent((item) => {
|
||||
@@ -158,7 +158,7 @@ function UserMenu() {
|
||||
}
|
||||
|
||||
const items: DropdownItem[] = [
|
||||
{ label: "Settings", onClick: () => navigate("/settings"), icon: IconSettings }
|
||||
{ label: "Settings", onClick: () => navigate("/settings"), icon: IconSettings },
|
||||
];
|
||||
|
||||
if (config.auth.enabled) {
|
||||
@@ -168,7 +168,7 @@ function UserMenu() {
|
||||
items.push({
|
||||
label: `Logout ${auth.user.email}`,
|
||||
onClick: handleLogout,
|
||||
icon: IconKeyOff
|
||||
icon: IconKeyOff,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -200,7 +200,7 @@ function UserMenuThemeToggler() {
|
||||
className="w-full"
|
||||
data={[
|
||||
{ value: "light", label: "Light" },
|
||||
{ value: "dark", label: "Dark" }
|
||||
{ value: "dark", label: "Dark" },
|
||||
]}
|
||||
value={theme}
|
||||
onChange={toggle}
|
||||
|
||||
@@ -8,11 +8,13 @@ import {
|
||||
SegmentedControl,
|
||||
Select,
|
||||
Switch,
|
||||
Tabs,
|
||||
TagsInput,
|
||||
TextInput,
|
||||
Textarea,
|
||||
createTheme
|
||||
createTheme,
|
||||
} from "@mantine/core";
|
||||
import { Notifications } from "@mantine/notifications";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
// default: https://github.com/mantinedev/mantine/blob/master/packages/%40mantine/core/src/core/MantineProvider/default-theme.ts
|
||||
@@ -25,11 +27,11 @@ export function createMantineTheme(scheme: "light" | "dark"): {
|
||||
const dark = !light;
|
||||
const baseComboboxProps: ComboboxProps = {
|
||||
offset: 2,
|
||||
transitionProps: { transition: "pop", duration: 75 }
|
||||
transitionProps: { transition: "pop", duration: 75 },
|
||||
};
|
||||
|
||||
const input =
|
||||
"bg-muted/40 border-transparent disabled:bg-muted/50 disabled:text-primary/50 focus:border-zinc-500";
|
||||
"!bg-muted/40 border-transparent disabled:bg-muted/50 disabled:text-primary/50 focus:border-zinc-500";
|
||||
|
||||
return {
|
||||
theme: createTheme({
|
||||
@@ -38,93 +40,107 @@ export function createMantineTheme(scheme: "light" | "dark"): {
|
||||
vars: (theme, props) => ({
|
||||
// https://mantine.dev/styles/styles-api/
|
||||
root: {
|
||||
"--button-height": "auto"
|
||||
}
|
||||
"--button-height": "auto",
|
||||
},
|
||||
}),
|
||||
classNames: (theme, props) => ({
|
||||
root: twMerge("px-3 py-2 rounded-md h-auto")
|
||||
root: twMerge("px-3 py-2 rounded-md h-auto"),
|
||||
}),
|
||||
defaultProps: {
|
||||
size: "md",
|
||||
variant: light ? "filled" : "white"
|
||||
}
|
||||
variant: light ? "filled" : "white",
|
||||
},
|
||||
}),
|
||||
Switch: Switch.extend({
|
||||
defaultProps: {
|
||||
size: "md",
|
||||
color: light ? "dark" : "blue"
|
||||
}
|
||||
color: light ? "dark" : "blue",
|
||||
},
|
||||
}),
|
||||
Select: Select.extend({
|
||||
classNames: (theme, props) => ({
|
||||
//input: "focus:border-primary/50 bg-transparent disabled:text-primary",
|
||||
input,
|
||||
dropdown: `bknd-admin ${scheme} bg-background border-primary/20`
|
||||
dropdown: `bknd-admin ${scheme} bg-background border-primary/20`,
|
||||
}),
|
||||
defaultProps: {
|
||||
checkIconPosition: "right",
|
||||
comboboxProps: baseComboboxProps
|
||||
}
|
||||
comboboxProps: baseComboboxProps,
|
||||
},
|
||||
}),
|
||||
TagsInput: TagsInput.extend({
|
||||
defaultProps: {
|
||||
comboboxProps: baseComboboxProps
|
||||
}
|
||||
comboboxProps: baseComboboxProps,
|
||||
},
|
||||
}),
|
||||
Radio: Radio.extend({
|
||||
defaultProps: {
|
||||
classNames: {
|
||||
body: "items-center"
|
||||
}
|
||||
}
|
||||
body: "items-center",
|
||||
},
|
||||
},
|
||||
}),
|
||||
TextInput: TextInput.extend({
|
||||
classNames: (theme, props) => ({
|
||||
wrapper: "leading-none",
|
||||
//input: "focus:border-primary/50 bg-transparent disabled:text-primary"
|
||||
input
|
||||
})
|
||||
input,
|
||||
}),
|
||||
}),
|
||||
NumberInput: NumberInput.extend({
|
||||
classNames: (theme, props) => ({
|
||||
wrapper: "leading-none",
|
||||
input
|
||||
})
|
||||
input,
|
||||
}),
|
||||
}),
|
||||
Textarea: Textarea.extend({
|
||||
classNames: (theme, props) => ({
|
||||
wrapper: "leading-none",
|
||||
input
|
||||
})
|
||||
input,
|
||||
}),
|
||||
}),
|
||||
Modal: Modal.extend({
|
||||
classNames: (theme, props) => ({
|
||||
...props.classNames,
|
||||
root: `bknd-admin ${scheme} ${props.className ?? ""} `,
|
||||
content: "bg-lightest border border-primary/10",
|
||||
overlay: "backdrop-blur"
|
||||
})
|
||||
root: `bknd-admin ${scheme} ${props.className ?? ""}`,
|
||||
content: "!bg-background !rounded-lg !select-none",
|
||||
overlay: "!backdrop-blur-sm",
|
||||
}),
|
||||
}),
|
||||
Tabs: Tabs.extend({
|
||||
vars: (theme, props) => ({
|
||||
// https://mantine.dev/styles/styles-api/
|
||||
root: {
|
||||
"--tabs-color": "border-primary",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
Menu: Menu.extend({
|
||||
defaultProps: {
|
||||
offset: 2
|
||||
offset: 2,
|
||||
},
|
||||
|
||||
classNames: (theme, props) => ({
|
||||
dropdown: "!rounded-lg !px-1",
|
||||
item: "!rounded-md !text-[14px]"
|
||||
})
|
||||
item: "!rounded-md !text-[14px]",
|
||||
}),
|
||||
}),
|
||||
SegmentedControl: SegmentedControl.extend({
|
||||
classNames: (theme, props) => ({
|
||||
root: light ? "bg-primary/5" : "bg-lightest/60",
|
||||
indicator: light ? "bg-background" : "bg-primary/15"
|
||||
})
|
||||
})
|
||||
indicator: light ? "bg-background" : "bg-primary/15",
|
||||
}),
|
||||
}),
|
||||
Notifications: Notifications.extend({
|
||||
classNames: (theme, props) => {
|
||||
return {
|
||||
notification: "-top-4 -right-4",
|
||||
};
|
||||
},
|
||||
}),
|
||||
},
|
||||
primaryColor: "dark",
|
||||
primaryShade: 9
|
||||
primaryShade: 9,
|
||||
}),
|
||||
forceColorScheme: scheme
|
||||
forceColorScheme: scheme,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,37 +9,37 @@ export const routes = {
|
||||
entity: {
|
||||
list: (entity: string) => `/entity/${entity}`,
|
||||
create: (entity: string) => `/entity/${entity}/create`,
|
||||
edit: (entity: string, id: PrimaryFieldType) => `/entity/${entity}/edit/${id}`
|
||||
edit: (entity: string, id: PrimaryFieldType) => `/entity/${entity}/edit/${id}`,
|
||||
},
|
||||
schema: {
|
||||
root: () => "/schema",
|
||||
entity: (entity: string) => `/schema/entity/${entity}`
|
||||
}
|
||||
entity: (entity: string) => `/schema/entity/${entity}`,
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
root: () => "/auth",
|
||||
users: {
|
||||
list: () => "/users",
|
||||
edit: (id: PrimaryFieldType) => `/users/edit/${id}`
|
||||
edit: (id: PrimaryFieldType) => `/users/edit/${id}`,
|
||||
},
|
||||
roles: {
|
||||
list: () => "/roles",
|
||||
edit: (role: string) => `/roles/edit/${role}`
|
||||
edit: (role: string) => `/roles/edit/${role}`,
|
||||
},
|
||||
settings: () => "/settings",
|
||||
strategies: () => "/strategies"
|
||||
strategies: () => "/strategies",
|
||||
},
|
||||
flows: {
|
||||
root: () => "/flows",
|
||||
flows: {
|
||||
list: () => "/",
|
||||
edit: (id: PrimaryFieldType) => `/flow/${id}`
|
||||
}
|
||||
edit: (id: PrimaryFieldType) => `/flow/${id}`,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
root: () => "/settings",
|
||||
path: (path: string[]) => `/settings/${path.join("/")}`
|
||||
}
|
||||
path: (path: string[]) => `/settings/${path.join("/")}`,
|
||||
},
|
||||
};
|
||||
|
||||
export function withQuery(url: string, query: object) {
|
||||
@@ -53,6 +53,14 @@ export function withAbsolute(url: string) {
|
||||
return `~/${basepath}/${url}`.replace(/\/+/g, "/");
|
||||
}
|
||||
|
||||
export function useRouteNavigate() {
|
||||
const [navigate] = useNavigate();
|
||||
|
||||
return (fn: (r: typeof routes) => string, options?: Parameters<typeof navigate>[1]) => {
|
||||
navigate(fn(routes), options);
|
||||
};
|
||||
}
|
||||
|
||||
export function useNavigate() {
|
||||
const [location, navigate] = useLocation();
|
||||
const router = useRouter();
|
||||
@@ -70,7 +78,7 @@ export function useNavigate() {
|
||||
transition?: boolean;
|
||||
}
|
||||
| { reload: true }
|
||||
| { target: string }
|
||||
| { target: string },
|
||||
) => {
|
||||
const wrap = (fn: () => void) => {
|
||||
fn();
|
||||
@@ -97,11 +105,11 @@ export function useNavigate() {
|
||||
const _url = options?.absolute ? `~/${basepath}${url}`.replace(/\/+/g, "/") : url;
|
||||
navigate(options?.query ? withQuery(_url, options?.query) : _url, {
|
||||
replace: options?.replace,
|
||||
state: options?.state
|
||||
state: options?.state,
|
||||
});
|
||||
});
|
||||
},
|
||||
location
|
||||
location,
|
||||
] as const;
|
||||
}
|
||||
|
||||
@@ -110,7 +118,7 @@ export function useGoBack(
|
||||
options?: {
|
||||
native?: boolean;
|
||||
absolute?: boolean;
|
||||
}
|
||||
},
|
||||
) {
|
||||
const { app } = useBknd();
|
||||
const basepath = app.getAdminConfig().basepath;
|
||||
@@ -153,6 +161,6 @@ export function useGoBack(
|
||||
return {
|
||||
same,
|
||||
canGoBack,
|
||||
goBack
|
||||
goBack,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,14 +4,23 @@ import Admin from "./Admin";
|
||||
import "./main.css";
|
||||
import "./styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<Admin withProvider />
|
||||
</React.StrictMode>
|
||||
);
|
||||
function render() {
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<Admin withProvider />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
}
|
||||
|
||||
if ("startViewTransition" in document) {
|
||||
document.startViewTransition(render);
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
|
||||
// REGISTER ERROR OVERLAY
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const showOverlay = true;
|
||||
if (process.env.NODE_ENV !== "production" && showOverlay) {
|
||||
const showErrorOverlay = (err) => {
|
||||
// must be within function call because that's when the element is defined for sure.
|
||||
const ErrorOverlay = customElements.get("vite-error-overlay");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type ModalProps, Tabs } from "@mantine/core";
|
||||
import type { ContextModalProps } from "@mantine/modals";
|
||||
import clsx from "clsx";
|
||||
import { transformObject } from "core/utils";
|
||||
import type { ComponentProps } from "react";
|
||||
import { JsonViewer } from "../../components/code/JsonViewer";
|
||||
@@ -24,13 +25,13 @@ export function DebugModal({ innerProps }: ContextModalProps<DebugProps>) {
|
||||
value: item,
|
||||
expand: 10,
|
||||
showCopy: true,
|
||||
...jsonViewerProps
|
||||
...jsonViewerProps,
|
||||
};
|
||||
});
|
||||
|
||||
const count = Object.keys(tabs).length;
|
||||
function renderTab({ value, label, ...props }: (typeof tabs)[keyof typeof tabs]) {
|
||||
return <JsonViewer json={value as any} {...props} />;
|
||||
function renderTab({ value, label, className, ...props }: (typeof tabs)[keyof typeof tabs]) {
|
||||
return <JsonViewer json={value as any} className={clsx("text-sm", className)} {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -57,7 +58,7 @@ export function DebugModal({ innerProps }: ContextModalProps<DebugProps>) {
|
||||
// @ts-expect-error
|
||||
...tabs[Object.keys(tabs)[0]],
|
||||
// @ts-expect-error
|
||||
title: tabs[Object.keys(tabs)[0]].label
|
||||
title: tabs[Object.keys(tabs)[0]].label,
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
@@ -69,6 +70,6 @@ DebugModal.modalProps = {
|
||||
withCloseButton: false,
|
||||
size: "lg",
|
||||
classNames: {
|
||||
body: "!p-0"
|
||||
}
|
||||
body: "!p-0",
|
||||
},
|
||||
} satisfies Omit<ModalProps, "opened" | "onClose">;
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ReactNode } from "react";
|
||||
export function OverlayModal({
|
||||
context,
|
||||
id,
|
||||
innerProps: { content }
|
||||
innerProps: { content },
|
||||
}: ContextModalProps<{ content?: ReactNode }>) {
|
||||
return content;
|
||||
}
|
||||
@@ -17,6 +17,6 @@ OverlayModal.modalProps = {
|
||||
root: "bknd-admin",
|
||||
content: "text-center justify-center",
|
||||
title: "font-bold !text-md",
|
||||
body: "py-3 px-5 gap-4 flex flex-col"
|
||||
}
|
||||
body: "py-3 px-5 gap-4 flex flex-col",
|
||||
},
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user