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:
dswbx
2025-03-11 13:56:27 +01:00
498 changed files with 14118 additions and 5427 deletions

View File

@@ -1,23 +1,23 @@
import { IconFingerprint } from "@tabler/icons-react";
import { TbSettings } from "react-icons/tb";
import { useBknd } from "ui/client/bknd";
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
import { IconButton } from "ui/components/buttons/IconButton";
import { Empty } from "ui/components/display/Empty";
import { Icon } from "ui/components/display/Icon";
import { Link } from "ui/components/wouter/Link";
import { useBrowserTitle } from "ui/hooks/use-browser-title";
import * as AppShell from "ui/layouts/AppShell/AppShell";
import { routes, useNavigate } from "ui/lib/routes";
import { routes } from "ui/lib/routes";
export function AuthRoot({ children }) {
const { app, config } = useBknd();
const users_entity = config.auth.entity_name;
const { config, $auth } = useBkndAuth();
return (
<>
<AppShell.Sidebar>
<AppShell.SectionHeader
right={
<Link href={app.getSettingsPath(["auth"])}>
<Link href={$auth.routes.settings}>
<IconButton Icon={TbSettings} />
</Link>
}
@@ -32,23 +32,42 @@ export function AuthRoot({ children }) {
</AppShell.SidebarLink>
<AppShell.SidebarLink
as={Link}
href={app.getAbsolutePath("/data/" + routes.data.entity.list(users_entity))}
disabled={!config.auth.enabled}
href={$auth.routes.listUsers}
disabled={!config.enabled}
className="justify-between"
>
Users
{!config.enabled && <AuthWarning title="Auth is not enabled." />}
</AppShell.SidebarLink>
<AppShell.SidebarLink
as={Link}
href={routes.auth.roles.list()}
disabled={!config.auth.enabled}
disabled={!config.enabled}
className="justify-between"
>
Roles & Permissions
{!config.enabled ? (
<AuthWarning title="Auth is not enabled." />
) : $auth.roles.none ? (
<AuthWarning title="No roles defined." />
) : !$auth.roles.has_admin ? (
<AuthWarning title="No admin role defined." />
) : null}
</AppShell.SidebarLink>
<AppShell.SidebarLink as={Link} href={routes.auth.strategies()} disabled>
<AppShell.SidebarLink
as={Link}
href={routes.auth.strategies()}
disabled={!config.enabled}
className="justify-between"
>
Strategies
{!config.enabled && <AuthWarning title="Auth is not enabled." />}
</AppShell.SidebarLink>
{/*<AppShell.SidebarLink as={Link} href={routes.auth.settings()}>*/}
<AppShell.SidebarLink as={Link} href={app.getSettingsPath(["auth"])}>
<AppShell.SidebarLink
as={Link}
href={routes.auth.settings()}
className="justify-between"
>
Settings
</AppShell.SidebarLink>
</nav>
@@ -60,6 +79,10 @@ export function AuthRoot({ children }) {
);
}
const AuthWarning = ({ title }) => (
<Icon.Warning title={title} className="size-5 pointer-events-auto" />
);
export function AuthEmpty() {
useBrowserTitle(["Auth"]);
return <Empty Icon={IconFingerprint} title="Not implemented yet" />;

View File

@@ -1,19 +1,22 @@
import clsx from "clsx";
import { TbArrowRight, TbCircle, TbCircleCheckFilled, TbFingerprint } from "react-icons/tb";
import { useApiQuery } from "ui/client";
import { useBknd } from "ui/client/bknd";
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
import { ButtonLink, type ButtonLinkProps } from "ui/components/buttons/Button";
import { IconButton } from "ui/components/buttons/IconButton";
import { Alert } from "ui/components/display/Alert";
import * as AppShell from "ui/layouts/AppShell/AppShell";
import { routes } from "ui/lib/routes";
import { routes, useNavigate } from "ui/lib/routes";
export function AuthIndex() {
const { app } = useBknd();
const {
config: { roles, strategies, entity_name, enabled }
config: { roles, strategies, entity_name, enabled },
} = useBkndAuth();
const users_entity = entity_name;
const $q = useApiQuery((api) => api.data.count(users_entity), {
enabled
enabled,
});
const usersTotal = $q.data?.count ?? 0;
const rolesTotal = Object.keys(roles ?? {}).length ?? 0;
@@ -21,7 +24,7 @@ export function AuthIndex() {
const usersLink = app.getAbsolutePath("/data/" + routes.data.entity.list(users_entity));
const rolesLink = routes.auth.roles.list();
const strategiesLink = app.getSettingsPath(["auth", "strategies"]);
const strategiesLink = routes.auth.strategies();
return (
<>
@@ -32,17 +35,31 @@ export function AuthIndex() {
title="Auth not enabled"
message="To use authentication features, please enable it in the settings."
/>
<div className="flex flex-col flex-grow p-3 gap-3">
<div className="grid xs:grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-5">
<div className="flex flex-col md:flex-row flex-1 p-3 gap-3">
<div className="flex flex-col border border-primary/20 self-stretch md:self-start">
<div className="flex flex-row gap-3 py-3 px-4 border-b border-b-muted font-medium bg-muted items-center justify-between">
Getting started
<TbFingerprint className="size-5" />
</div>
<Item title="Enable authentication" done={enabled} to={routes.auth.settings()} />
<Item title="Create Roles" done={rolesTotal > 0} to={rolesLink} />
<Item title="Create an user" done={usersTotal > 0} to={usersLink} />
<Item
title="Enable a second strategy"
done={strategiesTotal > 1}
to={strategiesLink}
/>
</div>
<div className="grid xs:grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-5 flex-grow">
<KpiCard
title="Users registered"
value={!enabled ? 0 : usersTotal}
actions={[
{
label: "View all",
href: usersLink
href: usersLink,
},
{ label: "Add new", variant: "default", href: usersLink }
{ label: "Add new", variant: "default", href: usersLink },
]}
/>
<KpiCard
@@ -50,7 +67,7 @@ export function AuthIndex() {
value={!enabled ? 0 : rolesTotal}
actions={[
{ label: "View all", href: rolesLink },
{ label: "Add new", variant: "default", href: rolesLink }
{ label: "Manage", variant: "default", href: rolesLink },
]}
/>
<KpiCard
@@ -58,7 +75,7 @@ export function AuthIndex() {
value={!enabled ? 0 : strategiesTotal}
actions={[
{ label: "View all", href: strategiesLink },
{ label: "Add new", variant: "default", href: strategiesLink }
{ label: "Manage", variant: "default", href: strategiesLink },
]}
/>
</div>
@@ -75,7 +92,7 @@ type KpiCardProps = {
};
const KpiCard: React.FC<KpiCardProps> = ({ title, value, actions }) => (
<div className="flex flex-col border border-muted">
<div className="flex flex-col border border-muted h-auto self-start">
<div className="flex flex-col gap-2 px-5 pt-3.5 pb-4 border-b border-b-muted">
<div>
<span className="opacity-50">{title}</span>
@@ -92,3 +109,25 @@ const KpiCard: React.FC<KpiCardProps> = ({ title, value, actions }) => (
</div>
</div>
);
const Item = ({ title, done = false, to }: { title: string; done?: boolean; to?: string }) => {
const [navigate] = useNavigate();
return (
<div className="flex border-b border-b-muted">
<div className={clsx("flex flex-1 flex-row gap-3 py-3 px-4", done && "opacity-50")}>
<div className="flex flex-row flex-1 gap-3 items-center">
{done ? <TbCircleCheckFilled className="size-5" /> : <TbCircle className="size-5" />}
<p
className={clsx(
"font-medium text-primary/80 leading-none",
done ? "line-through" : "",
)}
>
{title}
</p>
</div>
{to && <IconButton Icon={TbArrowRight} onClick={() => navigate(to)} />}
</div>
</div>
);
};

View File

@@ -54,14 +54,14 @@ function AuthRolesEditInternal({ params }) {
label: "Advanced Settings",
onClick: () =>
navigate(routes.settings.path(["auth", "roles", roleName]), {
absolute: true
})
absolute: true,
}),
},
{
label: "Delete",
onClick: handleDelete,
destructive: true
}
destructive: true,
},
]}
position="bottom-end"
>
@@ -77,7 +77,7 @@ function AuthRolesEditInternal({ params }) {
<Breadcrumbs2
path={[
{ label: "Roles & Permissions", href: routes.auth.roles.list() },
{ label: roleName }
{ label: roleName },
]}
/>
</AppShell.SectionHeader>

View File

@@ -16,8 +16,8 @@ export function AuthRolesList() {
role: name,
permissions: role.permissions,
is_default: role.is_default ?? false,
implicit_allow: role.implicit_allow ?? false
}))
implicit_allow: role.implicit_allow ?? false,
})),
);
function handleClick(row) {
@@ -31,14 +31,14 @@ export function AuthRolesList() {
schema: {
type: "object",
properties: {
name: StringIdentifier
name: StringIdentifier,
},
required: ["name"]
required: ["name"],
},
uiSchema: {
name: {
"ui:title": "Role name"
}
"ui:title": "Role name",
},
},
onSubmit: async (data) => {
if (data.name.length > 0) {
@@ -46,11 +46,11 @@ export function AuthRolesList() {
navigate(routes.auth.roles.edit(data.name));
}
}
}
},
},
{
title: "New Role"
}
title: "New Role",
},
);
}

View File

@@ -1,111 +1,210 @@
import { cloneDeep, omit } from "lodash-es";
import { useEffect, useRef } from "react";
import { useBknd } from "ui/client/bknd";
import clsx from "clsx";
import { isDebug } from "core";
import { TbAlertCircle, TbChevronDown, TbChevronUp } from "react-icons/tb";
import { useBknd } from "ui/client/BkndProvider";
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
import { Button } from "ui/components/buttons/Button";
import { Alert } from "ui/components/display/Alert";
import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
import { Icon } from "ui/components/display/Icon";
import { Message } from "ui/components/display/Message";
import {
Field,
type FieldProps,
Form,
FormDebug,
Subscribe,
} from "ui/components/form/json-schema-form";
import { useBrowserTitle } from "ui/hooks/use-browser-title";
import * as AppShell from "ui/layouts/AppShell/AppShell";
import { useNavigate } from "ui/lib/routes";
import { extractSchema } from "../settings/utils/schema";
import { create } from "zustand";
import { combine } from "zustand/middleware";
// @todo: improve the inline editing expierence, for now redirect to settings
export function AuthSettingsList() {
const { app } = useBknd();
const [navigate] = useNavigate();
useEffect(() => {
navigate(app.getSettingsPath(["auth"]));
}, []);
const useAuthSettingsStore = create(
combine(
{
advanced: [] as string[],
},
(set) => ({
toggleAdvanced: (which: string) =>
set((state) => ({
advanced: state.advanced.includes(which)
? state.advanced.filter((w) => w !== which)
: [...state.advanced, which],
})),
}),
),
);
return null;
export function AuthSettings(props) {
useBrowserTitle(["Auth", "Settings"]);
/*useBknd({ withSecrets: true });
return <AuthSettingsListInternal />;*/
const { hasSecrets } = useBknd({ withSecrets: true });
if (!hasSecrets) {
return <Message.MissingPermission what="Auth Settings" />;
}
return <AuthSettingsInternal {...props} />;
}
const uiSchema = {
jwt: {
fields: {
"ui:options": {
orderable: false
}
}
}
const formConfig = {
ignoreKeys: ["roles", "strategies"],
options: { keepEmpty: true, debug: isDebug() },
};
function AuthSettingsListInternal() {
const $auth = useBkndAuth();
const { entities } = useBkndData();
const formRef = useRef<JsonSchemaFormRef>(null);
const config = $auth.config;
const schema = cloneDeep(omit($auth.schema, ["title"]));
const [generalSchema, generalConfig, extracted] = extractSchema(schema as any, config, [
"jwt",
"roles",
"guard",
"strategies"
]);
try {
const user_entity = config.entity_name ?? "users";
const user_fields = Object.entries(entities[user_entity]?.fields ?? {})
.map(([name, field]) => (!field.config?.virtual ? name : undefined))
.filter(Boolean);
function AuthSettingsInternal() {
const { config, schema: _schema, actions, $auth } = useBkndAuth();
const schema = JSON.parse(JSON.stringify(_schema));
if (user_fields) {
console.log("user_fields", user_fields);
extracted.jwt.schema.properties.fields.items.enum = user_fields;
extracted.jwt.schema.properties.fields.uniqueItems = true;
uiSchema.jwt.fields["ui:widget"] = "checkboxes";
} else {
uiSchema.jwt.fields["ui:widget"] = "hidden";
}
} catch (e) {
console.error(e);
}
schema.properties.jwt.required = ["alg"];
async function handleSubmit() {
console.log(formRef.current?.validateForm(), formRef.current?.formData());
async function onSubmit(data: any) {
await actions.config.set(data);
}
return (
<>
<AppShell.SectionHeader
right={
<Button variant="primary" onClick={handleSubmit}>
Update
</Button>
}
<Form schema={schema} initialValues={config as any} onSubmit={onSubmit} {...formConfig}>
<Subscribe
selector={(state) => ({
dirty: state.dirty,
errors: state.errors.length > 0,
submitting: state.submitting,
})}
>
Settings
</AppShell.SectionHeader>
{({ dirty, errors, submitting }) => (
<AppShell.SectionHeader
className="pl-4"
right={
<Button
variant="primary"
type="submit"
disabled={!dirty || errors || submitting}
>
Update
</Button>
}
>
Settings
</AppShell.SectionHeader>
)}
</Subscribe>
<AppShell.Scrollable>
<Alert.Warning
visible={!config.enabled}
title="Auth not enabled"
message="Enable it by toggling the switch below. Please also make sure set a secure secret to sign JWT tokens."
/>
<div className="flex flex-col flex-grow px-5 py-4 gap-8">
<div>
<JsonSchemaForm
schema={generalSchema}
className="legacy hide-required-mark fieldset-alternative mute-root"
<Section className="pt-4 pl-0 pb-0">
<div className="pl-4">
<AuthField
name="enabled"
label="Authentication Enabled"
description="Only after enabling authentication, all settings below will take effect."
descriptionPlacement="top"
/>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-bold">JWT Settings</h3>
<JsonSchemaForm
ref={formRef}
schema={extracted.jwt.schema}
uiSchema={uiSchema.jwt}
className="legacy hide-required-mark fieldset-alternative mute-root"
<div className="flex flex-col gap-6 relative pl-4 pb-2">
<Overlay />
<AuthField
name="guard.enabled"
label={
<div className="flex flex-row gap-2 items-center">
<span>Guard Enabled</span>
{!$auth.roles.has_admin && (
<Icon.Warning title="No admin roles defined. Enabling the guard will likely block all requests." />
)}
</div>
}
disabled={$auth.roles.none}
description="When enabled, enforces permissions on all routes. Make sure to create roles first."
descriptionPlacement="top"
/>
<AuthField
name="allow_register"
label="Allow User Registration"
description="When enabled, allows users to register autonomously. New users use the default role."
descriptionPlacement="top"
/>
</div>
</Section>
<div className="flex flex-col gap-3 relative mt-3 pb-4">
<Overlay />
<AppShell.Separator />
<Section title="JWT">
<AuthField name="jwt.issuer" />
<AuthField
name="jwt.secret"
description="The secret used to sign the JWT token. If not set, a random key will be generated after enabling authentication."
advanced="jwt"
/>
<AuthField name="jwt.alg" advanced="jwt" />
<AuthField name="jwt.expires" advanced="jwt" />
<ToggleAdvanced which="jwt" />
</Section>
<AppShell.Separator />
<Section title="Cookie">
<AuthField name="cookie.path" advanced="cookie" />
<AuthField name="cookie.sameSite" advanced="cookie" />
<AuthField name="cookie.secure" advanced="cookie" />
<AuthField name="cookie.expires" advanced="cookie" />
<AuthField
name="cookie.renew"
label="Renew Cookie"
description="Automatically renew users cookie on every request."
descriptionPlacement="top"
/>
<AuthField name="cookie.pathSuccess" advanced="cookie" />
<AuthField name="cookie.pathLoggedOut" />
<ToggleAdvanced which="cookie" />
</Section>
</div>
<FormDebug />
</AppShell.Scrollable>
</Form>
);
}
const ToggleAdvanced = ({ which }: { which: string }) => {
const { advanced, toggleAdvanced } = useAuthSettingsStore();
const show = advanced.includes(which);
return (
<Button
IconLeft={show ? TbChevronUp : TbChevronDown}
onClick={() => toggleAdvanced(which)}
variant={show ? "default" : "ghost"}
className="self-start"
size="small"
>
{show ? "Hide advanced settings" : "Show advanced settings"}
</Button>
);
};
//const Overlay = () => null;
const Overlay = () => (
<Subscribe selector={(state) => ({ enabled: state.data.enabled })}>
{({ enabled }) =>
!enabled && (
<div className="absolute w-full h-full z-50 inset-0 bg-background opacity-90" />
)
}
</Subscribe>
);
function Section(props: {
children: React.ReactNode;
className?: string;
title?: string;
first?: boolean;
}) {
const { children, title, className } = props;
return (
<>
<div className={clsx("flex flex-col gap-6 px-4", title && "pt-0", className)}>
{title && <h3 className="text-lg font-bold">{title}</h3>}
{children}
</div>
</>
);
}
function AuthJwtSettings() {}
function AuthField(props: FieldProps & { advanced?: string }) {
const { advanced, ...rest } = props;
const showAdvanced = useAuthSettingsStore((state) => state.advanced);
if (advanced && !showAdvanced.includes(advanced)) return null;
return <Field {...rest} />;
}

View File

@@ -1,55 +1,247 @@
import { cloneDeep, omit } from "lodash-es";
import { isDebug } from "core";
import { autoFormatString } from "core/utils";
import { type ChangeEvent, useState } from "react";
import {
TbAt,
TbBrandAppleFilled,
TbBrandDiscordFilled,
TbBrandFacebookFilled,
TbBrandGithubFilled,
TbBrandGoogleFilled,
TbBrandInstagram,
TbBrandOauth,
TbBrandX,
TbSettings,
} from "react-icons/tb";
import { twMerge } from "tailwind-merge";
import { useBknd } from "ui/client/bknd";
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
import { Button } from "ui/components/buttons/Button";
import { IconButton } from "ui/components/buttons/IconButton";
import { Message } from "ui/components/display/Message";
import {
Field,
Form,
FormContextOverride,
FormDebug,
ObjectField,
Subscribe,
useDerivedFieldContext,
useFormError,
useFormValue,
} from "ui/components/form/json-schema-form";
import { useBrowserTitle } from "ui/hooks/use-browser-title";
import * as AppShell from "../../layouts/AppShell/AppShell";
export function AuthStrategiesList() {
useBknd({ withSecrets: true });
return <AuthStrategiesListInternal />;
export function AuthStrategiesList(props) {
useBrowserTitle(["Auth", "Strategies"]);
const { hasSecrets } = useBknd({ withSecrets: true });
if (!hasSecrets) {
return <Message.MissingPermission what="Auth Strategies" />;
}
return <AuthStrategiesListInternal {...props} />;
}
const uiSchema = {
jwt: {
fields: {
"ui:options": {
orderable: false
}
}
}
const formOptions = {
keepEmpty: true,
debug: isDebug(),
};
function AuthStrategiesListInternal() {
const s = useBknd();
const config = s.config.auth.strategies;
const schema = cloneDeep(omit(s.schema.auth.properties.strategies, ["title"]));
const $auth = useBkndAuth();
const config = $auth.config.strategies;
const schema = $auth.schema.properties.strategies;
const schemas = Object.fromEntries(
// @ts-ignore
$auth.schema.properties.strategies.additionalProperties.anyOf.map((s) => [
s.properties.type.const,
s,
]),
);
console.log("strategies", { config, schema });
async function handleSubmit(data: any) {
console.log("submit", { strategies: data });
await $auth.actions.config.set({ strategies: data });
}
return (
<>
<AppShell.SectionHeader right={<Button variant="primary">Update</Button>}>
Strategies
</AppShell.SectionHeader>
<Form schema={schema} initialValues={config} onSubmit={handleSubmit} options={formOptions}>
<Subscribe
selector={(state) => ({
dirty: state.dirty,
errors: state.errors.length > 0,
submitting: state.submitting,
})}
>
{({ dirty, errors, submitting }) => (
<AppShell.SectionHeader
className="pl-4"
right={
<Button
variant="primary"
type="submit"
disabled={!dirty || errors || submitting}
>
Update
</Button>
}
>
Strategies
</AppShell.SectionHeader>
)}
</Subscribe>
<AppShell.Scrollable>
strat
{/*<div className="flex flex-col flex-grow px-5 py-4 gap-8">
<div>
<JsonSchemaForm
schema={generalSchema}
className="legacy hide-required-mark fieldset-alternative mute-root"
/>
<div className="flex flex-col p-4 gap-4">
<p className="opacity-70">
Allow users to sign in or sign up using different strategies.
</p>
<div className="flex flex-col gap-2 max-w-4xl">
<Strategy type="password" name="password" />
<Strategy type="oauth" name="google" />
<Strategy type="oauth" name="github" />
<Strategy type="oauth" name="facebook" unavailable />
<Strategy type="oauth" name="x" unavailable />
<Strategy type="oauth" name="instagram" unavailable />
<Strategy type="oauth" name="apple" unavailable />
<Strategy type="oauth" name="discord" unavailable />
</div>
<div className="flex flex-col gap-3">
<h3 className="font-bold">JWT Settings</h3>
<JsonSchemaForm
schema={extracted.jwt.schema}
uiSchema={uiSchema.jwt}
className="legacy hide-required-mark fieldset-alternative mute-root"
/>
</div>
</div>*/}
</div>
<FormDebug />
</AppShell.Scrollable>
</>
</Form>
);
}
type StrategyProps = {
type: "password" | "oauth" | "custom_oauth";
name: string;
unavailable?: boolean;
};
const Strategy = ({ type, name, unavailable }: StrategyProps) => {
const errors = useFormError(name, { strict: true });
const $auth = useBkndAuth();
const schemas = Object.fromEntries(
// @ts-ignore
$auth.schema.properties.strategies.additionalProperties.anyOf.map((s) => [
s.properties.type.const,
s,
]),
);
const schema = schemas[type];
const [open, setOpen] = useState(false);
if (!schema) return null;
return (
<FormContextOverride schema={schema} prefix={name}>
<div
className={twMerge(
"flex flex-col border border-muted rounded bg-background",
unavailable && "opacity-20 pointer-events-none cursor-not-allowed",
errors.length > 0 && "border-red-500",
)}
>
<div className="flex flex-row justify-between p-3 gap-3 items-center">
<div className="flex flex-row items-center p-2 bg-primary/5 rounded">
<StrategyIcon type={type} provider={name} />
</div>
<div className="font-mono flex-grow flex flex-row gap-3">
<span className="leading-none">{autoFormatString(name)}</span>
</div>
<div className="flex flex-row gap-4 items-center">
<StrategyToggle />
<IconButton
Icon={TbSettings}
size="lg"
iconProps={{ strokeWidth: 1.5 }}
variant={open ? "primary" : "ghost"}
onClick={() => setOpen((o) => !o)}
/>
</div>
</div>
{open && (
<div
className={twMerge(
"flex flex-col border-t border-t-muted px-4 pt-3 pb-4 bg-lightest/50 gap-4",
)}
>
<StrategyForm type={type} />
</div>
)}
</div>
</FormContextOverride>
);
};
const StrategyToggle = () => {
const ctx = useDerivedFieldContext("");
const { value } = useFormValue("");
function handleToggleChange(e: ChangeEvent<HTMLInputElement>) {
const checked = e.target.value;
const value_keys = Object.keys(value ?? {});
const can_remove =
value_keys.length === 0 || (value_keys.length === 1 && value_keys[0] === "enabled");
if (!checked && can_remove) {
ctx.deleteValue(ctx.path);
} else {
ctx.setValue([ctx.path, "enabled"].join("."), checked);
}
}
return <Field name="enabled" label={false} required onChange={handleToggleChange} />;
};
const StrategyIcon = ({ type, provider }: { type: StrategyProps["type"]; provider?: string }) => {
if (type === "password") {
return <TbAt className="size-5" />;
}
if (provider && provider in OAUTH_BRANDS) {
const BrandIcon = OAUTH_BRANDS[provider];
return <BrandIcon className="size-5" />;
}
return <TbBrandOauth className="size-5" />;
};
const OAUTH_BRANDS = {
google: TbBrandGoogleFilled,
github: TbBrandGithubFilled,
facebook: TbBrandFacebookFilled,
x: TbBrandX,
instagram: TbBrandInstagram,
apple: TbBrandAppleFilled,
discord: TbBrandDiscordFilled,
};
const StrategyForm = ({ type }: Pick<StrategyProps, "type">) => {
let Component = () => <ObjectField path="" wrapperProps={{ wrapper: "group", label: false }} />;
switch (type) {
case "password":
Component = StrategyPasswordForm;
break;
case "oauth":
Component = StrategyOAuthForm;
break;
}
return <Component />;
};
const StrategyPasswordForm = () => {
return <ObjectField path="config" wrapperProps={{ wrapper: "group", label: false }} />;
};
const StrategyOAuthForm = () => {
return (
<>
<Field name="config.client.client_id" required />
<Field name="config.client.client_secret" required />
</>
);
};

View File

@@ -31,16 +31,16 @@ export const AuthRoleForm = forwardRef<
watch,
control,
reset,
getValues
getValues,
} = useForm({
resolver: typeboxResolver(schema),
defaultValues: role
defaultValues: role,
});
useImperativeHandle(ref, () => ({
reset,
getData: () => getValues(),
isValid: () => isValid
isValid: () => isValid,
}));
return (
@@ -82,14 +82,14 @@ export const AuthRoleForm = forwardRef<
const Permissions = ({
control,
permissions
permissions,
}: Omit<UseControllerProps, "name"> & { permissions: string[] }) => {
const {
field: { value, onChange: fieldOnChange, ...field },
fieldState
fieldState,
} = useController<Static<typeof schema>, "permissions">({
name: "permissions",
control
control,
});
const data = value ?? [];
@@ -108,7 +108,7 @@ const Permissions = ({
acc[group].push(permission);
return acc;
},
{} as Record<string, string[]>
{} as Record<string, string[]>,
);
console.log("grouped", grouped);

View File

@@ -3,7 +3,7 @@ import { AuthRoot } from "./_auth.root";
import { AuthIndex } from "./auth.index";
import { AuthRolesList } from "./auth.roles";
import { AuthRolesEdit } from "./auth.roles.edit.$role";
import { AuthSettingsList } from "./auth.settings";
import { AuthSettings } from "./auth.settings";
import { AuthStrategiesList } from "./auth.strategies";
import { AuthUsersList } from "./auth.users";
@@ -15,7 +15,7 @@ export default function AuthRoutes() {
<Route path="/roles" component={AuthRolesList} />
<Route path="/roles/edit/:role" component={AuthRolesEdit} />
<Route path="/strategies" component={AuthStrategiesList} />
<Route path="/settings" component={AuthSettingsList} />
<Route path="/settings" component={AuthSettings} />
</AuthRoot>
);
}

View File

@@ -6,19 +6,21 @@ import {
IconExternalLink,
IconPhoto,
IconPlus,
IconSettings
IconSettings,
IconSwitchHorizontal,
} from "@tabler/icons-react";
import type { Entity, TEntityType } from "data";
import { TbDatabasePlus } from "react-icons/tb";
import { twMerge } from "tailwind-merge";
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
import { Button } from "ui/components/buttons/Button";
import { IconButton } from "ui/components/buttons/IconButton";
import { Empty } from "ui/components/display/Empty";
import { Dropdown, type DropdownClickableChild } from "ui/components/overlay/Dropdown";
import { Link } from "ui/components/wouter/Link";
import { Link, isLinkActive } from "ui/components/wouter/Link";
import { useBrowserTitle } from "ui/hooks/use-browser-title";
import * as AppShell from "ui/layouts/AppShell/AppShell";
import { routes, useNavigate } from "ui/lib/routes";
import { routes, useNavigate, useRouteNavigate } from "ui/lib/routes";
export function DataRoot({ children }) {
// @todo: settings routes should be centralized
@@ -26,7 +28,7 @@ export function DataRoot({ children }) {
const entityList: Record<TEntityType, Entity[]> = {
regular: [],
generated: [],
system: []
system: [],
} as const;
const [navigate] = useNavigate();
const context = window.location.href.match(/\/schema/) ? "schema" : "data";
@@ -65,7 +67,7 @@ export function DataRoot({ children }) {
<SegmentedControl
data={[
{ value: "data", label: "Data" },
{ value: "schema", label: "Schema" }
{ value: "schema", label: "Schema" },
]}
value={context}
onChange={handleSegmentChange}
@@ -103,9 +105,10 @@ const EntityLinkList = ({
entities,
title,
context,
suggestCreate = false
suggestCreate = false,
}: { entities: Entity[]; title?: string; context: "data" | "schema"; suggestCreate?: boolean }) => {
const { $data } = useBkndData();
const navigate = useRouteNavigate();
if (entities.length === 0) {
return suggestCreate ? (
<Empty
@@ -113,17 +116,33 @@ const EntityLinkList = ({
description="Create your first entity to get started."
secondary={{
children: "Create entity",
onClick: () => $data.modals.createEntity()
onClick: () => $data.modals.createEntity(),
}}
/>
) : null;
}
function handleClick(entity: Entity) {
return (e) => {
e.stopPropagation();
e.preventDefault();
switch (context) {
case "schema":
navigate((r) => r.data.entity.list(entity.name));
break;
case "data":
navigate((r) => r.data.schema.entity(entity.name));
break;
}
};
}
return (
<nav
className={twMerge(
"flex flex-col flex-1 gap-1 px-3",
title && "border-t border-primary/10 pt-2"
title && "border-t border-primary/10 pt-2",
)}
>
{title && <div className="text-sm text-primary/50 ml-3.5 mb-1">{title}</div>}
@@ -135,8 +154,22 @@ const EntityLinkList = ({
: routes.data.schema.entity(entity.name);
return (
<EntityContextMenu key={entity.name} entity={entity}>
<AppShell.SidebarLink as={Link} href={href}>
<AppShell.SidebarLink
as={Link}
href={href}
className="justify-between items-center"
>
{entity.label}
{isLinkActive(href, 1) && (
<Button
IconLeft={IconSwitchHorizontal}
size="small"
onClick={handleClick(entity)}
>
{context === "schema" ? "Data" : "Fields"}
</Button>
)}
</AppShell.SidebarLink>
</EntityContextMenu>
);
@@ -148,7 +181,7 @@ const EntityLinkList = ({
const EntityContextMenu = ({
entity,
children,
enabled = true
enabled = true,
}: { entity: Entity; children: DropdownClickableChild; enabled?: boolean }) => {
if (!enabled) return children;
const [navigate] = useNavigate();
@@ -162,41 +195,41 @@ const EntityContextMenu = ({
<Dropdown
className="flex flex-col w-full"
dropdownWrapperProps={{
className: "min-w-fit"
className: "min-w-fit",
}}
title={entity.label + " Actions"}
items={[
href && {
icon: IconExternalLink,
label: "Open in tab",
onClick: () => navigate(href, { target: "_blank" })
onClick: () => navigate(href, { target: "_blank" }),
},
separator,
!$data.system(entity.name).any && {
icon: IconPlus,
label: "Create new",
onClick: () => navigate(routes.data.entity.create(entity.name))
onClick: () => navigate(routes.data.entity.create(entity.name)),
},
{
icon: IconDatabase,
label: "List entries",
onClick: () => navigate(routes.data.entity.list(entity.name))
onClick: () => navigate(routes.data.entity.list(entity.name)),
},
separator,
{
icon: IconAlignJustified,
label: "Manage fields",
onClick: () => navigate(routes.data.schema.entity(entity.name))
onClick: () => navigate(routes.data.schema.entity(entity.name)),
},
{
icon: IconCirclesRelation,
label: "Add relation",
onClick: () => $data.modals.createRelation(entity.name)
onClick: () => $data.modals.createRelation(entity.name),
},
!$data.system(entity.name).media && {
icon: IconPhoto,
label: "Add media",
onClick: () => $data.modals.createMedia(entity.name)
onClick: () => $data.modals.createMedia(entity.name),
},
separator,
{
@@ -204,9 +237,9 @@ const EntityContextMenu = ({
label: "Advanced",
onClick: () =>
navigate(routes.settings.path(["data", "entities", entity.name]), {
absolute: true
})
}
absolute: true,
}),
},
]}
openEvent="onContextMenu"
position="bottom-start"
@@ -232,11 +265,11 @@ export function DataEmpty() {
description="Please select an entity from the left sidebar or create a new one to continue."
secondary={{
children: "Go to schema",
onClick: handleButtonClick
onClick: handleButtonClick,
}}
primary={{
children: "Create entity",
onClick: $data.modals.createEntity
onClick: $data.modals.createEntity,
}}
/>
);

View File

@@ -1,5 +1,5 @@
import { ucFirst } from "core/utils";
import type { Entity, EntityData, EntityRelation, RepoQuery } from "data";
import type { Entity, EntityData, EntityRelation } from "data";
import { Fragment, useState } from "react";
import { TbDots } from "react-icons/tb";
import { useApiQuery, useEntityQuery } from "ui/client";
@@ -38,13 +38,13 @@ export function DataEntityUpdate({ params }) {
entity.name,
entityId,
{
with: local_relation_refs
with: local_relation_refs,
},
{
keepPreviousData: false,
revalidateOnFocus: false,
shouldRetryOnError: false
}
shouldRetryOnError: false,
},
);
function goBack() {
@@ -85,7 +85,7 @@ export function DataEntityUpdate({ params }) {
action: "update",
entity,
initialData: $q.data?.toJSON(),
onSubmitted
onSubmitted,
});
if (!data && !$q.isLoading) {
@@ -117,24 +117,24 @@ export function DataEntityUpdate({ params }) {
entity: entity.toJSON(),
schema: entity.toSchema({ clean: true }),
form: Form.state.values,
state: Form.state
}
state: Form.state,
},
});
}
},
},
{
label: "Settings",
onClick: () =>
navigate(routes.settings.path(["data", "entities", entity.name]), {
absolute: true
})
absolute: true,
}),
},
{
label: "Delete",
onClick: handleDelete,
destructive: true,
disabled: fieldsDisabled
}
disabled: fieldsDisabled,
},
]}
>
<IconButton Icon={TbDots} />
@@ -160,7 +160,7 @@ export function DataEntityUpdate({ params }) {
<Breadcrumbs2
path={[
{ label: entity.label, href: routes.data.entity.list(entity.name) },
{ label: `Edit #${entityId}` }
{ label: `Edit #${entityId}` },
]}
/>
</AppShell.SectionHeader>
@@ -191,7 +191,7 @@ export function DataEntityUpdate({ params }) {
function EntityDetailRelations({
id,
entity,
relations
relations,
}: {
id: number;
entity: Entity;
@@ -199,7 +199,7 @@ function EntityDetailRelations({
}) {
const [selected, setSelected] = useState<EntityRelation>(
// @ts-ignore
relations.length > 0 ? relations[0] : undefined
relations.length > 0 ? relations[0] : undefined,
);
function handleClick(relation: EntityRelation) {
@@ -225,7 +225,7 @@ function EntityDetailRelations({
label: ucFirst(other.reference),
onClick: () => handleClick(relation),
active: selected?.other(entity).reference === other.reference,
badge: relation.type()
badge: relation.type(),
};
})}
/>
@@ -239,7 +239,7 @@ function EntityDetailRelations({
function EntityDetailInner({
id,
entity,
relation
relation,
}: {
id: number;
entity: Entity;
@@ -251,11 +251,11 @@ function EntityDetailInner({
const search = {
select: other.entity.getSelect(undefined, "table"),
limit: 10,
offset: 0
offset: 0,
};
// @todo: add custom key for invalidation
const $q = useApiQuery((api) =>
api.data.readManyByReference(entity.name, id, other.reference, search)
api.data.readManyByReference(entity.name, id, other.reference, search),
);
function handleClickRow(row: Record<string, any>) {
@@ -268,7 +268,7 @@ function EntityDetailInner({
const ref = relation.getReferenceQuery(other.entity, id, other.reference);
handleClickNew = () => {
navigate(routes.data.entity.create(other.entity.name), {
query: ref.where
query: ref.where,
});
//navigate(routes.data.entity.create(other.entity.name) + `?${query}`);
};

View File

@@ -52,7 +52,7 @@ export function DataEntityCreate({ params }) {
action: "create",
entity: entity,
initialData: search.value,
onSubmitted
onSubmitted,
});
const fieldsDisabled = $q.isLoading || $q.isValidating || Form.state.isSubmitting;
@@ -83,7 +83,7 @@ export function DataEntityCreate({ params }) {
<Breadcrumbs2
path={[
{ label: entity.label, href: routes.data.entity.list(entity.name) },
{ label: "Create" }
{ label: "Create" },
]}
/>
</AppShell.SectionHeader>

View File

@@ -2,7 +2,7 @@ import { Type } from "core/utils";
import { type Entity, querySchema } from "data";
import { Fragment } from "react";
import { TbDots } from "react-icons/tb";
import { useApi, useApiQuery } from "ui/client";
import { useApiQuery } from "ui/client";
import { useBknd } from "ui/client/bknd";
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
import { Button } from "ui/components/buttons/Button";
@@ -22,10 +22,10 @@ const searchSchema = Type.Composite(
Type.Pick(querySchema, ["select", "where", "sort"]),
Type.Object({
page: Type.Optional(Type.Number({ default: 1 })),
perPage: Type.Optional(Type.Number({ default: 10 }))
})
perPage: Type.Optional(Type.Number({ default: 10 })),
}),
],
{ additionalProperties: false }
{ additionalProperties: false },
);
const PER_PAGE_OPTIONS = [5, 10, 25];
@@ -41,7 +41,7 @@ export function DataEntityList({ params }) {
const [navigate] = useNavigate();
const search = useSearch(searchSchema, {
select: entity.getSelect(undefined, "table"),
sort: entity.getDefaultSort()
sort: entity.getDefaultSort(),
});
const $q = useApiQuery(
@@ -50,13 +50,13 @@ export function DataEntityList({ params }) {
select: search.value.select,
limit: search.value.perPage,
offset: (search.value.page - 1) * search.value.perPage,
sort: `${search.value.sort.dir === "asc" ? "" : "-"}${search.value.sort.by}`
sort: `${search.value.sort.dir === "asc" ? "" : "-"}${search.value.sort.by}`,
}),
{
enabled: !!entity,
revalidateOnFocus: true,
keepPreviousData: true
}
keepPreviousData: true,
},
);
const data = $q.data?.data;
const meta = $q.data?.body.meta;
@@ -83,7 +83,7 @@ export function DataEntityList({ params }) {
search.set("perPage", perPage);
}
const isUpdating = $q.isLoading && $q.isValidating;
const isUpdating = $q.isLoading || $q.isValidating;
return (
<Fragment key={entity.name}>
@@ -94,19 +94,19 @@ export function DataEntityList({ params }) {
items={[
{
label: "Settings",
onClick: () => navigate(routes.data.schema.entity(entity.name))
onClick: () => navigate(routes.data.schema.entity(entity.name)),
},
{
label: "Data Schema",
onClick: () => navigate(routes.data.schema.root())
onClick: () => navigate(routes.data.schema.root()),
},
{
label: "Advanced Settings",
onClick: () =>
navigate(routes.settings.path(["data", "entities", entity.name]), {
absolute: true
})
}
absolute: true,
}),
},
]}
position="bottom-end"
>
@@ -158,7 +158,7 @@ function EntityCreateButton({ entity }: { entity: Entity }) {
if (entity.type === "system") {
const system = {
users: b.app.config.auth.entity_name,
media: b.app.config.media.entity_name
media: b.app.config.media.entity_name,
};
if (system.users === entity.name) {
return (

View File

@@ -2,7 +2,7 @@ import {
IconAlignJustified,
IconBolt,
IconCirclesRelation,
IconSettings
IconSettings,
} from "@tabler/icons-react";
import { isDebug } from "core";
import type { Entity } from "data";
@@ -14,7 +14,7 @@ import {
TbDots,
TbPhoto,
TbPlus,
TbSitemap
TbSitemap,
} from "react-icons/tb";
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
import { Button } from "ui/components/buttons/Button";
@@ -56,16 +56,16 @@ export function DataSchemaEntity({ params }) {
label: "Data",
onClick: () =>
navigate(routes.data.root() + routes.data.entity.list(entity.name), {
absolute: true
})
absolute: true,
}),
},
{
label: "Advanced Settings",
onClick: () =>
navigate(routes.settings.path(["data", "entities", entity.name]), {
absolute: true
})
}
absolute: true,
}),
},
]}
position="bottom-end"
>
@@ -76,19 +76,19 @@ export function DataSchemaEntity({ params }) {
{
icon: TbCirclesRelation,
label: "Add relation",
onClick: () => $data.modals.createRelation(entity.name)
onClick: () => $data.modals.createRelation(entity.name),
},
{
icon: TbPhoto,
label: "Add media",
onClick: () => $data.modals.createMedia(entity.name)
onClick: () => $data.modals.createMedia(entity.name),
},
() => <div className="h-px my-1 w-full bg-primary/5" />,
{
icon: TbDatabasePlus,
label: "Create Entity",
onClick: () => $data.modals.createEntity()
}
onClick: () => $data.modals.createEntity(),
},
]}
position="bottom-end"
>
@@ -124,7 +124,7 @@ export function DataSchemaEntity({ params }) {
primary={{
children: "Advanced Settings",
onClick: () =>
navigate(routes.settings.path(["data", "relations"]), { absolute: true })
navigate(routes.settings.path(["data", "relations"]), { absolute: true }),
}}
/>
</AppShell.SectionHeaderAccordionItem>
@@ -141,8 +141,8 @@ export function DataSchemaEntity({ params }) {
children: "Advanced Settings",
onClick: () =>
navigate(routes.settings.path(["data", "indices"]), {
absolute: true
})
absolute: true,
}),
}}
/>
</AppShell.SectionHeaderAccordionItem>
@@ -154,7 +154,7 @@ export function DataSchemaEntity({ params }) {
const Fields = ({
entity,
open,
toggle
toggle,
}: { entity: Entity; open: boolean; toggle: () => void }) => {
const [submitting, setSubmitting] = useState(false);
const [updates, setUpdates] = useState(0);
@@ -209,7 +209,7 @@ const Fields = ({
$data.modals.createMedia(entity.name);
break;
}
}
},
}))}
/>
@@ -225,6 +225,9 @@ const Fields = ({
<Button size="small" onClick={() => setRes(ref.current?.getData())}>
data
</Button>
<Button size="small" onClick={() => setRes(ref.current?.getErrors())}>
errors
</Button>
<Button size="small" onClick={handleUpdate}>
update
</Button>
@@ -241,7 +244,7 @@ const Fields = ({
const BasicSettings = ({
entity,
open,
toggle
toggle,
}: { entity: Entity; open: boolean; toggle: () => void }) => {
const d = useBkndData();
const config = d.entities?.[entity.name]?.config;
@@ -249,7 +252,7 @@ const BasicSettings = ({
const schema = cloneDeep(
// @ts-ignore
d.schema.properties.entities.additionalProperties?.properties?.config
d.schema.properties.entities.additionalProperties?.properties?.config,
);
const [_schema, _config] = extractSchema(schema as any, config, ["fields"]);

View File

@@ -5,8 +5,8 @@ import * as AppShell from "ui/layouts/AppShell/AppShell";
const DataSchemaCanvas = lazy(() =>
import("ui/modules/data/components/canvas/DataSchemaCanvas").then((m) => ({
default: m.DataSchemaCanvas
}))
default: m.DataSchemaCanvas,
})),
);
export function DataSchemaIndex() {

View File

@@ -1,17 +1,17 @@
import { typeboxResolver } from "@hookform/resolvers/typebox";
import { Tabs, TextInput, Textarea, Tooltip } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { Type } from "@sinclair/typebox";
import {
Default,
type Static,
StringIdentifier,
Type,
objectCleanEmpty,
ucFirstAllSnakeToPascalWithSpaces
ucFirstAllSnakeToPascalWithSpaces,
} from "core/utils";
import { Entity } from "data";
import {
type TAppDataEntityFields,
fieldsSchemaObject as originalFieldsSchemaObject
fieldsSchemaObject as originalFieldsSchemaObject,
} from "data/data-schema";
import { omit } from "lodash-es";
import { forwardRef, memo, useEffect, useImperativeHandle } from "react";
@@ -31,15 +31,20 @@ import { dataFieldsUiSchema } from "../../settings/routes/data.settings";
const fieldsSchemaObject = originalFieldsSchemaObject;
const fieldsSchema = Type.Union(Object.values(fieldsSchemaObject));
const fieldSchema = Type.Object({
name: StringIdentifier,
new: Type.Optional(Type.Boolean({ const: true })),
field: fieldsSchema
});
const fieldSchema = Type.Object(
{
name: StringIdentifier,
new: Type.Optional(Type.Boolean({ const: true })),
field: fieldsSchema,
},
{
additionalProperties: false,
},
);
type TFieldSchema = Static<typeof fieldSchema>;
const schema = Type.Object({
fields: Type.Array(fieldSchema)
fields: Type.Array(fieldSchema),
});
type TFieldsFormSchema = Static<typeof schema>;
@@ -63,6 +68,7 @@ export type EntityFieldsFormRef = {
getValues: () => TFieldsFormSchema;
getData: () => TAppDataEntityFields;
isValid: () => boolean;
getErrors: () => any;
reset: () => void;
};
@@ -70,7 +76,7 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
function EntityFieldsForm({ fields: _fields, sortable, additionalFieldTypes, ...props }, ref) {
const entityFields = Object.entries(_fields).map(([name, field]) => ({
name,
field
field,
}));
const {
@@ -81,30 +87,28 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
register,
setValue,
setError,
reset
reset,
} = useForm({
mode: "all",
resolver: typeboxResolver(schema),
defaultValues: {
fields: entityFields
} as TFieldsFormSchema
fields: entityFields,
} as TFieldsFormSchema,
});
const { fields, append, remove, move } = useFieldArray({
control,
name: "fields"
name: "fields",
});
function toCleanValues(formData: TFieldsFormSchema): TAppDataEntityFields {
return Object.fromEntries(
formData.fields.map((field) => [field.name, objectCleanEmpty(field.field)])
formData.fields.map((field) => [field.name, objectCleanEmpty(field.field)]),
);
}
useEffect(() => {
if (props?.onChange) {
console.log("----set");
watch((data: any) => {
console.log("---calling");
props?.onChange?.(toCleanValues(data));
});
}
@@ -116,7 +120,8 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
getData: () => {
return toCleanValues(getValues());
},
isValid: () => isValid
isValid: () => isValid,
getErrors: () => errors,
}));
function handleAppend(_type: keyof typeof fieldsSchemaObject) {
@@ -125,8 +130,8 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
new: true,
field: {
type: _type,
config: {}
}
config: Default(fieldsSchemaObject[_type]?.properties.config, {}) as any,
},
};
append(newField);
}
@@ -137,7 +142,7 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
setValue,
getValues,
control,
setError
setError,
};
return (
<>
@@ -197,20 +202,20 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
</div>
</>
);
}
},
);
const SelectType = ({
onSelect,
additionalFieldTypes = [],
onSelected
onSelected,
}: {
onSelect: (type: string) => void;
additionalFieldTypes?: (TFieldSpec & { onClick?: () => void })[];
onSelected?: () => void;
}) => {
const types: (TFieldSpec & { onClick?: () => void })[] = fieldSpecs.filter(
(s) => s.addable !== false
(s) => s.addable !== false,
);
if (additionalFieldTypes) {
@@ -266,7 +271,7 @@ function EntityField({
form: { watch, register, setValue, getValues, control, setError },
remove,
errors,
dnd
dnd,
}: {
field: FieldArrayWithId<TFieldsFormSchema, "fields", "id">;
index: number;
@@ -307,7 +312,7 @@ function EntityField({
className={twMerge(
"flex flex-col border border-muted rounded bg-background mb-2",
opened && "mb-6",
hasErrors && "border-red-500 "
hasErrors && "border-red-500 ",
)}
{...dndProps}
>
@@ -331,7 +336,7 @@ function EntityField({
classNames={{
root: "w-full h-full",
wrapper: "font-mono h-full",
input: "pt-px !h-full"
input: "pt-px !h-full",
}}
{...register(`fields.${index}.name`)}
disabled={!field.new}
@@ -416,7 +421,7 @@ function EntityField({
onChange={(value) => {
setValue(`${prefix}.config`, {
...getValues([`fields.${index}.config`])[0],
...value
...value,
});
}}
/>

View File

@@ -7,7 +7,7 @@ import {
StringEnum,
StringIdentifier,
Type,
registerCustomTypeboxKinds
registerCustomTypeboxKinds,
} from "core/utils";
import { TRIGGERS } from "flows/flows-schema";
import { forwardRef, useState } from "react";
@@ -19,7 +19,7 @@ import {
type Modal2Ref,
ModalBody,
ModalFooter,
ModalTitle
ModalTitle,
} from "../../../components/modal/Modal2";
import { Step, Steps, useStepContext } from "../../../components/steps/Steps";
@@ -31,7 +31,7 @@ const triggerNames = Object.keys(TRIGGERS) as unknown as (keyof typeof TRIGGERS)
const schema = Type.Object({
name: StringIdentifier,
trigger: StringEnum(triggerNames),
mode: StringEnum(["async", "sync"])
mode: StringEnum(["async", "sync"]),
});
export const FlowCreateModal = forwardRef<Modal2Ref>(function FlowCreateModal(props, ref) {
@@ -63,15 +63,15 @@ export function StepCreate() {
watch,
control,
register,
formState: { isValid, errors }
formState: { isValid, errors },
} = useForm({
resolver: typeboxResolver(schema),
defaultValues: {
name: "",
trigger: "manual",
mode: "async"
},
mode: "onSubmit"
mode: "async",
} as Static<typeof schema>,
mode: "onSubmit",
});
async function onSubmit(data: Static<typeof schema>) {
@@ -80,9 +80,9 @@ export function StepCreate() {
trigger: {
type: data.trigger,
config: {
mode: data.mode
}
}
mode: data.mode,
},
},
});
}
console.log("errors", errors);
@@ -106,7 +106,7 @@ export function StepCreate() {
data={[
{ label: "Manual", value: "manual" },
{ label: "HTTP", value: "http" },
{ label: "Event", value: "event" }
{ label: "Event", value: "event" },
]}
control={control}
/>
@@ -115,7 +115,7 @@ export function StepCreate() {
name="mode"
data={[
{ label: "Async", value: "async" },
{ label: "Sync", value: "sync" }
{ label: "Sync", value: "sync" },
]}
control={control}
/>
@@ -125,7 +125,7 @@ export function StepCreate() {
<ModalFooter
next={{
type: "submit",
disabled: !isValid
disabled: !isValid,
}}
nextLabel="Create"
prev={{ onClick: stepBack }}

View File

@@ -16,7 +16,7 @@ import {
flowToNodes,
useFlowCanvas,
useFlowCanvasState,
useFlowSelector
useFlowSelector,
} from "ui/modules/flows/hooks/use-flow";
import { JsonViewer } from "../../components/code/JsonViewer";
import { routes, useGoBack, useNavigate } from "../../lib/routes";
@@ -59,7 +59,7 @@ function FlowsEditInner() {
const viewport = {
zoom: 1,
x: rect?.width ? rect.width * 0.1 : 0,
y: rect?.height ? rect.height / 2 - triggerHeight / 2 - offset : 0
y: rect?.height ? rect.height / 2 - triggerHeight / 2 - offset : 0,
};
return (
@@ -79,18 +79,18 @@ function FlowsEditInner() {
onDropNewNode={(node) => ({
...node,
type: "select",
data: { label: "" }
data: { label: "" },
})}
onDropNewEdge={(edge) => ({
...edge,
style: {
strokeWidth: 2
strokeWidth: 2,
},
markerEnd: {
type: MarkerType.ArrowClosed,
width: 10,
height: 10
}
height: 10,
},
})}
>
<FlowPanels />
@@ -202,7 +202,7 @@ const Debugger = () => {
title="Context"
json={{
name: $flow.name,
...$flow.data
...$flow.data,
}}
expand={expand}
/>
@@ -211,7 +211,7 @@ const Debugger = () => {
title="State"
json={{
name: state.name,
...state.flow
...state.flow,
}}
expand={expand}
/>
@@ -225,9 +225,9 @@ const Debugger = () => {
(n) =>
_setState((prev) => ({
...prev,
store: { ...prev.store, expand: n }
store: { ...prev.store, expand: n },
})),
250
250,
)}
/>
</Tabs.Panel>

View File

@@ -19,7 +19,7 @@ export function FlowsList() {
trigger: flow.trigger.type,
mode: flow.trigger.config.mode,
tasks: Object.keys(flow.tasks).length,
start_task: flow.startTask?.name
start_task: flow.startTask?.name,
}));
function handleClick(row) {

View File

@@ -57,7 +57,7 @@ export function FlowsEmpty() {
to continue."
primary={{
children: "Create Flow",
onClick: () => navigate(app.getSettingsPath(["flows"]))
onClick: () => navigate(app.getSettingsPath(["flows"])),
}}
/>
</main>

View File

@@ -7,7 +7,7 @@ import {
TbChevronUp,
TbDots,
TbPlayerPlayFilled,
TbSettings
TbSettings,
} from "react-icons/tb";
import { twMerge } from "tailwind-merge";
import FlowCanvas from "ui/modules/flows/components/FlowCanvas";
@@ -48,7 +48,7 @@ export function FlowEdit({ params }) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
},
"sync"
"sync",
);
execution.subscribe(async (event) => {
@@ -67,7 +67,7 @@ export function FlowEdit({ params }) {
onChange: ({ nodes, edges }) => {
setSelectedNodes(nodes);
setSelectedEdges(edges);
}
},
});
return (
@@ -94,8 +94,8 @@ export function FlowEdit({ params }) {
items={[
{
label: "Settings",
onClick: () => navigate(`${prefix}/flows/flows/${flow.name}`)
}
onClick: () => navigate(`${prefix}/flows/flows/${flow.name}`),
},
]}
position="bottom-end"
>
@@ -145,7 +145,7 @@ function Sidebar({ nodes, edges, flow }: { flow: Flow; nodes: Node[]; edges: Edg
<div
className={twMerge(
"flex flex-row pl-5 pr-3 py-3 border-muted border-b cursor-pointer justify-between items-center font-bold",
opened && "bg-primary/5"
opened && "bg-primary/5",
)}
onClick={onClick}
>

View File

@@ -2,6 +2,7 @@ import { IconAlertHexagon } from "@tabler/icons-react";
import { TbSettings } from "react-icons/tb";
import { useBknd } from "ui/client/BkndProvider";
import { IconButton } from "ui/components/buttons/IconButton";
import { Icon } from "ui/components/display/Icon";
import { Link } from "ui/components/wouter/Link";
import { Media } from "ui/elements";
import { useBrowserTitle } from "ui/hooks/use-browser-title";
@@ -32,7 +33,10 @@ export function MediaRoot({ children }) {
href={"/"}
className="flex flex-row justify-between"
>
Main Bucket {mediaDisabled && <IconAlertHexagon className="size-5" />}
Main Bucket{" "}
{mediaDisabled && (
<Icon.Warning title="Media not enabled." className="size-5" />
)}
</AppShell.SidebarLink>
<AppShell.SidebarLink as={Link} href={"/settings"}>
Settings

View File

@@ -19,7 +19,7 @@ export function MediaIndex() {
description="Please enable media in the settings to continue."
primary={{
children: "Manage Settings",
onClick: () => navigate("/settings")
onClick: () => navigate("/settings"),
}}
/>
);

View File

@@ -16,9 +16,8 @@ import {
FormDebug,
ObjectField,
Subscribe,
useFormError
useFormError,
} from "ui/components/form/json-schema-form";
import { Media } from "ui/elements";
import { useBrowserTitle } from "ui/hooks/use-browser-title";
import * as AppShell from "ui/layouts/AppShell/AppShell";
@@ -35,7 +34,7 @@ export function MediaSettings(props) {
const formConfig = {
ignoreKeys: ["entity_name", "basepath"],
options: { debug: isDebug(), keepEmpty: true }
options: { debug: isDebug(), keepEmpty: true },
};
function MediaSettingsInternal() {
@@ -58,7 +57,7 @@ function MediaSettingsInternal() {
selector={(state) => ({
dirty: state.dirty,
errors: state.errors.length > 0,
submitting: state.submitting
submitting: state.submitting,
})}
>
{({ dirty, errors, submitting }) => (
@@ -87,7 +86,7 @@ function MediaSettingsInternal() {
</div>
</div>
<AppShell.Separator />
<div className="flex flex-col gap-3 p-3">
<div className="flex flex-col gap-3 p-3 relative">
<Overlay />
<AnyOf.Root path="adapter">
<Adapters />
@@ -117,7 +116,7 @@ const Icons = {
s3: IconBrandAws,
cloudinary: IconCloud,
local: IconServer,
r2: IconBrandCloudflare
r2: IconBrandCloudflare,
};
const AdapterIcon = ({ type }: { type: string }) => {
@@ -143,7 +142,7 @@ function Adapters() {
variant={ctx.selected === i ? "primary" : "outline"}
className={twMerge(
"flex flex-row items-center justify-center gap-3 border",
ctx.selected === i && "border-primary"
ctx.selected === i && "border-primary",
)}
>
<div>
@@ -177,7 +176,7 @@ const Overlay = () => (
<Subscribe selector={(state) => ({ enabled: state.data.enabled })}>
{({ enabled }) =>
!enabled && (
<div className="absolute w-full h-full z-50 bg-background opacity-70 pointer-events-none" />
<div className="absolute w-full h-full z-50 inset-0 bg-background opacity-90" />
)
}
</Subscribe>

View File

@@ -21,7 +21,7 @@ import { SettingSchemaModal, type SettingsSchemaModalRef } from "./SettingSchema
export type SettingProps<
Schema extends TObject = TObject,
Props = Schema extends TObject<infer TProperties> ? TProperties : any
Props = Schema extends TObject<infer TProperties> ? TProperties : any,
> = {
schema: Schema;
config: any;
@@ -51,7 +51,7 @@ export function Setting<Schema extends TObject = any>({
prefix = "/",
options,
path = [],
properties
properties,
}: SettingProps<Schema>) {
const [submitting, setSubmitting] = useState(false);
const { actions } = useBknd();
@@ -67,7 +67,7 @@ export function Setting<Schema extends TObject = any>({
e.preventDefault();
onSave();
return false;
}
},
],
[
"e",
@@ -75,7 +75,7 @@ export function Setting<Schema extends TObject = any>({
if (!editing) {
onToggleEdit();
}
}
},
],
[
"Escape",
@@ -83,8 +83,8 @@ export function Setting<Schema extends TObject = any>({
if (editing) {
onToggleEdit();
}
}
]
},
],
]);
const exclude = useMemo(
@@ -94,7 +94,7 @@ export function Setting<Schema extends TObject = any>({
.filter(([, value]) => value.hide || value.extract)
.map(([key]) => key)
: [],
[properties]
[properties],
);
const goBack = useEvent((replace?: boolean) => {
@@ -178,7 +178,7 @@ export function Setting<Schema extends TObject = any>({
description={`Configuration at path ${path.join(".")} doesn't exist.`}
primary={{
children: "Go back",
onClick: () => goBack()
onClick: () => goBack(),
}}
/>
);
@@ -197,19 +197,19 @@ export function Setting<Schema extends TObject = any>({
label: "Inspect local schema",
onClick: () => {
schemaLocalModalRef.current?.open();
}
},
},
{
label: "Inspect schema",
onClick: () => {
schemaModalRef.current?.open();
}
},
},
deleteAllowed && {
label: "Delete",
destructive: true,
onClick: handleDelete
}
onClick: handleDelete,
},
]}
position="bottom-end"
>
@@ -255,7 +255,7 @@ export function Setting<Schema extends TObject = any>({
label: ucFirst(sub),
href: `${prefix}/${sub}`.replace(/\/+/g, "/"),
active: selectedSubKey === sub,
badge: Object.keys(extracted[sub]?.config ?? {}).length
badge: Object.keys(extracted[sub]?.config ?? {}).length,
}))}
/>
<div className="flex flex-grow flex-col gap-3 p-3">
@@ -271,7 +271,7 @@ export function Setting<Schema extends TObject = any>({
if (!value || typeof value !== "object") {
return {
key,
value
value,
};
}
@@ -281,7 +281,7 @@ export function Setting<Schema extends TObject = any>({
return {
key,
[fistValueKey]: firstValueKeyValue,
value: _value
value: _value,
};
});
const newSetting = properties?.[key]?.new;
@@ -331,12 +331,12 @@ export function Setting<Schema extends TObject = any>({
tabs={[
{
title: "Schema",
json: reducedSchema
json: reducedSchema,
},
{
title: "Config",
json: reducedConfig
}
json: reducedConfig,
},
]}
/>
<SettingSchemaModal
@@ -345,12 +345,12 @@ export function Setting<Schema extends TObject = any>({
tabs={[
{
title: "Schema",
json: schema
json: schema,
},
{
title: "Config",
json: config
}
json: config,
},
]}
/>
</>

View File

@@ -26,7 +26,7 @@ export const SettingNewModal = ({
anyOfValues,
path,
prefixPath,
generateKey
generateKey,
}: SettingsNewModalProps) => {
const [location, navigate] = useLocation();
const [formSchema, setFormSchema] = useState(schema);
@@ -44,8 +44,8 @@ export const SettingNewModal = ({
if (generateKey && typeof generateKey === "function") {
handleKeyNameChange({
target: {
value: generateKey(data)
}
value: generateKey(data),
},
});
}
console.log("form change", data);
@@ -69,7 +69,7 @@ export const SettingNewModal = ({
if (await actions.add(module as any, addPath, data)) {
setTimeout(() => {
navigate(prefixPath + newKey, {
replace: true
replace: true,
});
}, 500);
} else {
@@ -92,7 +92,7 @@ export const SettingNewModal = ({
onClick: () => {
setFormSchema(item);
open();
}
},
};
})
: [];

View File

@@ -26,7 +26,7 @@ export const SettingSchemaModal = forwardRef<SettingsSchemaModalRef, SettingSche
useImperativeHandle(ref, () => ({
open,
close,
isOpen: opened
isOpen: opened,
}));
if (!tab) return null;
@@ -69,5 +69,5 @@ export const SettingSchemaModal = forwardRef<SettingsSchemaModalRef, SettingSche
</div>
</Modal>
);
}
},
);

View File

@@ -1,4 +1,4 @@
import { IconSettings } from "@tabler/icons-react";
import { IconRefresh, IconSettings } from "@tabler/icons-react";
import { ucFirst } from "core/utils";
import { useBknd } from "ui/client/bknd";
import { Empty } from "ui/components/display/Empty";
@@ -12,21 +12,33 @@ import { AuthSettings } from "./routes/auth.settings";
import { DataSettings } from "./routes/data.settings";
import { FlowsSettings } from "./routes/flows.settings";
import { ServerSettings } from "./routes/server.settings";
import { IconButton } from "ui/components/buttons/IconButton";
function SettingsSidebar() {
const { version, schema } = useBknd();
const { version, schema, actions } = useBknd();
useBrowserTitle(["Settings"]);
async function handleRefresh() {
await actions.reload();
}
const modules = Object.keys(schema).map((key) => {
return {
title: schema[key].title ?? ucFirst(key),
key
key,
};
});
return (
<AppShell.Sidebar>
<AppShell.SectionHeader right={<span className="font-mono">v{version}</span>}>
<AppShell.SectionHeader
right={
<div className="flex items-center gap-2">
<span className="font-mono leading-none">v{version}</span>
<IconButton Icon={IconRefresh} onClick={handleRefresh} />
</div>
}
>
Settings
</AppShell.SectionHeader>
<AppShell.Scrollable initialOffset={96}>
@@ -86,25 +98,25 @@ const uiSchema = {
server: {
cors: {
allow_methods: {
"ui:widget": "checkboxes"
"ui:widget": "checkboxes",
},
allow_headers: {
"ui:options": {
orderable: false
}
}
}
orderable: false,
},
},
},
},
media: {
adapter: {
"ui:options": {
label: false
}
label: false,
},
/*type: {
"ui:widget": "hidden"
}*/
}
}
},
},
};
const SettingRoutesRoutes = () => {
@@ -112,7 +124,7 @@ const SettingRoutesRoutes = () => {
console.log("flows", {
schema: schema.flows,
config: config.flows
config: config.flows,
});
return (

View File

@@ -8,37 +8,37 @@ const uiSchema = {
jwt: {
basepath: {
"ui:options": {
label: false
}
label: false,
},
},
fields: {
"ui:options": {
orderable: false
}
}
orderable: false,
},
},
},
strategies: {
additionalProperties: {
"ui:widget": "select",
type: {
"ui:widget": "hidden"
}
"ui:widget": "hidden",
},
},
type: {
"ui:widget": "hidden"
}
"ui:widget": "hidden",
},
},
roles: {
"ui:options": {
orderable: false
orderable: false,
},
permissions: {
items: {
"ui:widget": "checkboxes"
"ui:widget": "checkboxes",
},
"ui:widget": "checkboxes"
}
}
"ui:widget": "checkboxes",
},
},
};
export const AuthSettings = ({ schema: _unsafe_copy, config }) => {
@@ -122,7 +122,7 @@ export const AuthSettings = ({ schema: _unsafe_copy, config }) => {
}
return;
},
reloadOnSave: true
reloadOnSave: true,
}}
properties={{
strategies: {
@@ -131,8 +131,8 @@ export const AuthSettings = ({ schema: _unsafe_copy, config }) => {
return Object.values(
transformObject(strategies, (s, name) => ({
key: name,
type: s.type
}))
type: s.type,
})),
);
},
new: {
@@ -142,16 +142,16 @@ export const AuthSettings = ({ schema: _unsafe_copy, config }) => {
return data.type === "password"
? "password"
: data.config.name?.toLowerCase() || "";
}
}
},
},
},
roles: {
extract: true,
new: {
schema: roleSchema,
uiSchema: uiSchema.roles
}
}
uiSchema: uiSchema.roles,
},
},
}}
prefix={`${prefix}/auth`}
path={["auth"]}

View File

@@ -9,33 +9,33 @@ export const dataFieldsUiSchema = {
config: {
fillable: {
"ui:options": {
wrap: true
wrap: true,
},
anyOf: [
{},
{
"ui:widget": "checkboxes"
}
]
"ui:widget": "checkboxes",
},
],
},
hidden: {
"ui:options": {
wrap: true
wrap: true,
},
anyOf: [
{},
{
"ui:widget": "checkboxes"
}
]
"ui:widget": "checkboxes",
},
],
},
schema: {
"ui:field": "JsonField"
"ui:field": "JsonField",
},
ui_schema: {
"ui:field": "JsonField"
}
}
"ui:field": "JsonField",
},
},
};
const fieldsAnyOfValues = fieldSpecs
@@ -43,29 +43,29 @@ const fieldsAnyOfValues = fieldSpecs
.reduce((acc, s) => {
acc[s.type] = {
label: s.label,
icon: s.icon
icon: s.icon,
};
return acc;
}, {});
const relationAnyOfValues = {
"1:1": {
label: "One-to-One"
label: "One-to-One",
},
"n:1": {
label: "Many-to-One"
label: "Many-to-One",
},
"m:n": {
label: "Many-to-Many"
label: "Many-to-Many",
},
poly: {
label: "Polymorphic"
}
label: "Polymorphic",
},
};
export const DataSettings = ({
schema,
config
config,
}: { schema: ModuleSchemas["data"]; config: ModuleConfigs["data"] }) => {
const { app } = useBknd();
const basepath = app.getAdminConfig().basepath;
@@ -110,7 +110,7 @@ export const DataSettings = ({
return "Modifying the primary field may result in strange behaviors.";
}
return;
}
},
}}
uiSchema={dataFieldsUiSchema}
/>
@@ -128,8 +128,8 @@ export const DataSettings = ({
const fieldsSchema = {
anyOf: editSchema.properties.fields.additionalProperties.anyOf.filter(
(s) => s.properties.type.const !== "primary"
)
(s) => s.properties.type.const !== "primary",
),
} as any;
return (
@@ -142,7 +142,7 @@ export const DataSettings = ({
return "Modifying the system entities may result in strange behaviors.";
}
return;
}
},
}}
properties={{
fields: {
@@ -154,17 +154,17 @@ export const DataSettings = ({
acc.push({
property: key,
type: value.type,
required: value.config?.required ? "Yes" : "No"
required: value.config?.required ? "Yes" : "No",
});
},
[] as any[]
[] as any[],
),
new: {
schema: fieldsSchema,
uiSchema: dataFieldsUiSchema,
anyOfValues: fieldsAnyOfValues
}
}
anyOfValues: fieldsAnyOfValues,
},
},
}}
path={["data", "entities", entity]}
prefix={`${prefix}/data/entities/${entity}`}
@@ -247,22 +247,22 @@ export const DataSettings = ({
acc.push({
name: key,
type: value.type,
fields: Object.keys(value.fields ?? {}).length
fields: Object.keys(value.fields ?? {}).length,
});
},
[] as any[]
[] as any[],
),
new: {
schema: schema.properties.entities.additionalProperties as any,
uiSchema: {
fields: {
"ui:widget": "hidden"
"ui:widget": "hidden",
},
type: {
"ui:widget": "hidden"
}
}
}
"ui:widget": "hidden",
},
},
},
},
relations: {
extract: true,
@@ -276,12 +276,12 @@ export const DataSettings = ({
data.config?.mappedBy,
data.config?.inversedBy,
data.config?.connectionTable,
data.config?.connectionTableMappedName
data.config?.connectionTableMappedName,
].filter(Boolean);
return [...new Set(parts)].join("_");
},
anyOfValues: relationAnyOfValues
anyOfValues: relationAnyOfValues,
},
tableValues: (config: any) =>
transform(
@@ -291,11 +291,11 @@ export const DataSettings = ({
name: key,
type: value.type,
source: value.source,
target: value.target
target: value.target,
});
},
[] as any[]
)
[] as any[],
),
},
indices: {
extract: true,
@@ -307,32 +307,32 @@ export const DataSettings = ({
name: key,
entity: value.entity,
fields: value.fields.join(", "),
unique: value.unique ? "Yes" : "No"
unique: value.unique ? "Yes" : "No",
});
},
[] as any[]
[] as any[],
),
new: {
schema: newIndex,
uiSchema: {
fields: {
"ui:options": {
orderable: false
}
}
orderable: false,
},
},
},
generateKey: (data: any) => {
const parts = [
"idx",
data.entity,
data.unique && "unique",
...data.fields.filter(Boolean)
...data.fields.filter(Boolean),
].filter(Boolean);
return parts.join("_");
}
}
}
},
},
},
}}
prefix={`${prefix}/data`}
path={["data"]}

View File

@@ -7,26 +7,26 @@ const uiSchema = {
jwt: {
basepath: {
"ui:options": {
label: false
}
label: false,
},
},
fields: {
"ui:options": {
orderable: false
}
}
orderable: false,
},
},
},
strategies: {
additionalProperties: {
"ui:widget": "select",
type: {
"ui:widget": "hidden"
}
"ui:widget": "hidden",
},
},
type: {
"ui:widget": "hidden"
}
}
"ui:widget": "hidden",
},
},
};
export const FlowsSettings = ({ schema, config }) => {
@@ -91,9 +91,9 @@ export const FlowsSettings = ({ schema, config }) => {
uiSchema={{
params: {
render: {
"ui:field": "LiquidJsField"
}
}
"ui:field": "LiquidJsField",
},
},
}}
/>
);
@@ -124,7 +124,7 @@ export const FlowsSettings = ({ schema, config }) => {
tasks: {
extract: true,
new: {
schema: newTask
schema: newTask,
},
tableValues: (config: any) =>
transform(
@@ -132,17 +132,17 @@ export const FlowsSettings = ({ schema, config }) => {
(acc, value, key) => {
acc.push({
name: key,
type: value.type
type: value.type,
});
},
[] as any[]
)
[] as any[],
),
},
connections: {
extract: true,
new: {
schema: newConnection,
generateKey: crypto.randomUUID() as string
generateKey: crypto.randomUUID() as string,
},
tableValues: (config: any) =>
transform(
@@ -152,12 +152,12 @@ export const FlowsSettings = ({ schema, config }) => {
id: key,
source: value.source,
target: value.target,
condition: value.config.condition?.type
condition: value.config.condition?.type,
});
},
[] as any[]
)
}
[] as any[],
),
},
}}
/>
);
@@ -176,7 +176,7 @@ export const FlowsSettings = ({ schema, config }) => {
flows: {
extract: true,
new: {
schema: schema.properties.flows.additionalProperties as any
schema: schema.properties.flows.additionalProperties as any,
/*uiSchema: {
fields: {
@@ -193,12 +193,12 @@ export const FlowsSettings = ({ schema, config }) => {
trigger: value.trigger.type,
mode: value.trigger.config.mode,
start_task: value.start_task,
responding_task: value.responding_task
responding_task: value.responding_task,
});
},
[] as any[]
)
}
[] as any[],
),
},
}}
prefix={`${prefix}/flows`}
path={["flows"]}

View File

@@ -6,14 +6,14 @@ import { Route } from "wouter";
const uiSchema = {
cors: {
allow_methods: {
"ui:widget": "checkboxes"
"ui:widget": "checkboxes",
},
allow_headers: {
"ui:options": {
orderable: false
}
}
}
orderable: false,
},
},
},
};
export const ServerSettings = ({ schema: _unsafe_copy, config }) => {
@@ -39,7 +39,7 @@ export const ServerSettings = ({ schema: _unsafe_copy, config }) => {
return "The admin settings are read-only as they are overriden. Remaining server configuration can be edited.";
}
return;
}
},
}}
schema={schema}
uiSchema={uiSchema}

View File

@@ -5,11 +5,11 @@ import { cloneDeep, omit, pick } from "lodash-es";
export function extractSchema<
Schema extends TObject,
Keys extends keyof Schema["properties"],
Config extends Static<Schema>
Config extends Static<Schema>,
>(
schema: Schema,
config: Config,
keys: Keys[]
keys: Keys[],
): [
JSONSchema7,
Partial<Config>,
@@ -19,7 +19,7 @@ export function extractSchema<
config: Config[K];
schema: Schema["properties"][K];
};
}
},
] {
if (!schema.properties) {
return [{ ...schema }, config, {} as any];
@@ -28,7 +28,7 @@ export function extractSchema<
const newSchema = cloneDeep(schema);
const updated = {
...newSchema,
properties: omit(newSchema.properties, keys)
properties: omit(newSchema.properties, keys),
};
if (updated.required) {
updated.required = updated.required.filter((key) => !keys.includes(key as any));
@@ -40,7 +40,7 @@ export function extractSchema<
// @ts-ignore
config: config[key],
// @ts-ignore
schema: newSchema.properties[key]
schema: newSchema.properties[key],
};
}

View File

@@ -16,7 +16,6 @@ import ModalTest from "../../routes/test/tests/modal-test";
import QueryJsonFormTest from "../../routes/test/tests/query-jsonform";
import DropdownTest from "./tests/dropdown-test";
import DropzoneElementTest from "./tests/dropzone-element-test";
import EntityFieldsForm from "./tests/entity-fields-form";
import FlowsTest from "./tests/flows-test";
import JsonSchemaForm3 from "./tests/json-schema-form3";
import JsonFormTest from "./tests/jsonform-test";
@@ -27,9 +26,11 @@ import ReactFlowTest from "./tests/reactflow-test";
import SchemaTest from "./tests/schema-test";
import SortableTest from "./tests/sortable-test";
import { SqlAiTest } from "./tests/sql-ai-test";
import Themes from "./tests/themes";
const tests = {
DropdownTest,
Themes,
ModalTest,
JsonFormTest,
FlowFormTest,
@@ -42,7 +43,6 @@ const tests = {
SqlAiTest,
SortableTest,
ReactHookErrors,
EntityFieldsForm,
FlowsTest,
AppShellAccordionsTest,
SwaggerTest,
@@ -52,7 +52,7 @@ const tests = {
JsonSchemaFormReactTest,
JsonSchemaForm3,
FormyTest,
HtmlFormTest
HtmlFormTest,
} as const;
export default function TestRoutes() {

View File

@@ -5,7 +5,7 @@ import {
IconChevronUp,
IconCirclesRelation,
IconSettings,
IconUser
IconUser,
} from "@tabler/icons-react";
import { useState } from "react";
import { TbDots } from "react-icons/tb";
@@ -23,7 +23,7 @@ const Item = ({
toggle,
ActiveIcon = IconChevronUp,
children,
renderHeaderRight
renderHeaderRight,
}: {
title: string;
open: boolean;
@@ -38,12 +38,12 @@ const Item = ({
"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 border-muted border-b h-14 py-4 pr-4 pl-2 items-center gap-2"
"flex flex-row border-muted border-b h-14 py-4 pr-4 pl-2 items-center gap-2",
)}
onClick={toggle}
>
@@ -55,7 +55,7 @@ const Item = ({
<div
className={twMerge(
"overflow-y-scroll transition-all",
open ? " flex-grow" : "h-0 opacity-0"
open ? " flex-grow" : "h-0 opacity-0",
)}
>
<div className="flex flex-col gap-5 p-4 ">{children}</div>
@@ -77,8 +77,8 @@ export default function AppShellAccordionsTest() {
<Dropdown
items={[
{
label: "Settings"
}
label: "Settings",
},
]}
position="bottom-end"
>

View File

@@ -6,6 +6,9 @@ export default function DropzoneElementTest() {
<Scrollable>
<div className="flex flex-col w-full h-full p-4 gap-4">
<div>
<b>Dropzone no auto avif only</b>
<Media.Dropzone autoUpload={false} allowedMimeTypes={["image/avif"]} />
<b>Dropzone User Avatar 1 (fully customized)</b>
<Media.Dropzone
entity={{ name: "users", id: 1, field: "avatar" }}
@@ -54,7 +57,7 @@ function CustomUserAvatarDropzone() {
wrapperRef,
inputProps,
state: { files, isOver, isOverAccepted, showPlaceholder },
actions: { openFileInput }
actions: { openFileInput },
} = Media.useDropzone();
const file = files[0];

View File

@@ -1,296 +0,0 @@
import { typeboxResolver } from "@hookform/resolvers/typebox";
import { Select, Switch, Tabs, TextInput, Textarea, Tooltip } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { Type } from "@sinclair/typebox";
import { StringEnum, StringIdentifier, transformObject } from "core/utils";
import { FieldClassMap } from "data";
import { omit } from "lodash-es";
import {
type FieldArrayWithId,
type FieldValues,
type UseControllerProps,
type UseFormReturn,
useController,
useFieldArray,
useForm
} from "react-hook-form";
import { TbChevronDown, TbChevronUp, TbGripVertical, TbTrash } from "react-icons/tb";
import { Button } from "../../../components/buttons/Button";
import { IconButton } from "../../../components/buttons/IconButton";
import { MantineSelect } from "../../../components/form/hook-form-mantine/MantineSelect";
const fieldSchemas = transformObject(omit(FieldClassMap, ["primary"]), (value) => value.schema);
const fieldSchema = Type.Union(
Object.entries(fieldSchemas).map(([type, schema]) =>
Type.Object(
{
type: Type.Const(type),
name: StringIdentifier,
config: Type.Optional(schema)
},
{
additionalProperties: false
}
)
)
);
const schema = Type.Object({
fields: Type.Array(fieldSchema)
});
const fieldSchema2 = Type.Object({
type: StringEnum(Object.keys(fieldSchemas)),
name: StringIdentifier
});
function specificFieldSchema(type: keyof typeof fieldSchemas) {
return Type.Omit(fieldSchemas[type], [
"label",
"description",
"required",
"fillable",
"hidden",
"virtual"
]);
}
export default function EntityFieldsForm() {
const {
control,
formState: { isValid, errors },
getValues,
handleSubmit,
watch,
register,
setValue
} = useForm({
mode: "onTouched",
resolver: typeboxResolver(schema),
defaultValues: {
fields: [{ type: "text", name: "", config: {} }],
sort: { by: "-1", dir: "asc" }
}
});
const defaultType = Object.keys(fieldSchemas)[0];
const { fields, append, prepend, remove, swap, move, insert, update } = useFieldArray({
control, // control props comes from useForm (optional: if you are using FormProvider)
name: "fields" // unique name for your Field Array
});
function handleAppend() {
append({ type: "text", name: "", config: {} });
}
return (
<div className="flex flex-col gap-1 p-8">
{/*{fields.map((field, index) => (
<EntityField
key={field.id}
field={field}
index={index}
form={{ watch, register, setValue, getValues, control }}
defaultType={defaultType}
remove={remove}
/>
))}*/}
{fields.map((field, index) => (
<EntityFieldForm key={field.id} value={field} index={index} update={update} />
))}
<Button className="justify-center" onClick={handleAppend}>
Add Field
</Button>
<div>
<pre>{JSON.stringify(watch(), null, 2)}</pre>
</div>
</div>
);
}
function EntityFieldForm({ update, index, value }) {
const {
register,
handleSubmit,
control,
formState: { errors }
} = useForm({
mode: "onBlur",
resolver: typeboxResolver(
Type.Object({
type: StringEnum(Object.keys(fieldSchemas)),
name: Type.String({ minLength: 1, maxLength: 3 })
})
),
defaultValues: value
});
function handleUpdate({ id, ...data }) {
console.log("data", data);
update(index, data);
}
return (
<form>
<MantineSelect
control={control}
name="type"
data={[...Object.keys(fieldSchemas), "test"]}
/>
<TextInput
label="Name"
placeholder="name"
classNames={{ root: "w-full" }}
{...register("name")}
error={errors.name?.message as any}
/>
</form>
);
}
function EntityFieldController({
name,
control,
defaultValue,
rules,
shouldUnregister
}: UseControllerProps & {
index: number;
}) {
const {
field: { value, onChange: fieldOnChange, ...field },
fieldState
} = useController({
name,
control,
defaultValue,
rules,
shouldUnregister
});
return <div>field</div>;
}
function EntityField({
field,
index,
form: { watch, register, setValue, getValues, control },
remove,
defaultType
}: {
field: FieldArrayWithId;
index: number;
form: Pick<UseFormReturn<any>, "watch" | "register" | "setValue" | "getValues" | "control">;
remove: (index: number) => void;
defaultType: string;
}) {
const [opened, handlers] = useDisclosure(false);
const prefix = `fields.${index}` as const;
const name = watch(`${prefix}.name`);
const enabled = name?.length > 0;
const type = watch(`${prefix}.type`);
//const config = watch(`${prefix}.config`);
const selectFieldRegister = register(`${prefix}.type`);
//console.log("type", type, specificFieldSchema(type as any));
function handleDelete(index: number) {
return () => {
if (name.length === 0) {
remove(index);
return;
}
window.confirm(`Sure to delete "${name}"?`) && remove(index);
};
}
return (
<div key={field.id} className="flex flex-col border border-muted rounded">
<div className="flex flex-row gap-2 px-2 pt-1 pb-2">
<div className="flex items-center">
<IconButton Icon={TbGripVertical} className="mt-1" />
</div>
<div className="flex flex-row flex-grow gap-1">
<div>
<Select
label="Type"
data={[...Object.keys(fieldSchemas), "test"]}
defaultValue={defaultType}
onBlur={selectFieldRegister.onBlur}
onChange={(value) => {
setValue(`${prefix}.type`, value as any);
setValue(`${prefix}.config`, {} as any);
}}
/>
</div>
<TextInput
label="Name"
placeholder="name"
classNames={{ root: "w-full" }}
{...register(`fields.${index}.name`)}
/>
</div>
<div className="flex items-end ">
<div className="flex flex-row gap-1">
<Tooltip label="Specify a property name to see options." disabled={enabled}>
<Button
IconRight={opened ? TbChevronUp : TbChevronDown}
onClick={handlers.toggle}
variant={opened ? "default" : "ghost"}
disabled={!enabled}
>
Options
</Button>
</Tooltip>
<IconButton Icon={TbTrash} onClick={handleDelete(index)} />
</div>
</div>
</div>
{/*{enabled && opened && (
<div className="flex flex-col border-t border-t-muted px-3 py-2">
<Tabs defaultValue="general">
<Tabs.List>
<Tabs.Tab value="general">General</Tabs.Tab>
<Tabs.Tab value="specific">Specific</Tabs.Tab>
<Tabs.Tab value="visibility">Visiblity</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="general">
<div className="flex flex-col gap-2 pt-3 pb-1" key={`${prefix}_${type}`}>
<Switch
label="Required"
{...register(`${prefix}.config.required` as any)}
/>
<TextInput
label="Label"
placeholder="Label"
{...register(`${prefix}.config.label` as any)}
/>
<Textarea
label="Description"
placeholder="Description"
{...register(`${prefix}.config.description` as any)}
/>
<Switch label="Virtual" {...register(`${prefix}.config.virtual` as any)} />
</div>
</Tabs.Panel>
<Tabs.Panel value="specific">
<div className="flex flex-col gap-2 pt-3 pb-1">
<JsonSchemaForm
key={type}
schema={specificFieldSchema(type as any)}
uiSchema={dataFieldsUiSchema.config}
className="legacy hide-required-mark fieldset-alternative mute-root"
onChange={(value) => {
setValue(`${prefix}.config`, {
...getValues([`fields.${index}.config`])[0],
...value
});
}}
/>
</div>
</Tabs.Panel>
</Tabs>
</div>
)}*/}
</div>
);
}

View File

@@ -6,7 +6,7 @@ export default function FlowFormTest() {
const [data, setData] = useState(null);
const task = new FetchTask("Fetch Something", {
url: "https://jsonplaceholder.typicode.com/todos/1",
method: "{{ input.mode }}"
method: "{{ input.mode }}",
});
return (

View File

@@ -9,7 +9,7 @@ import {
IconPlayerPlay,
IconPlus,
IconTrash,
IconWorld
IconWorld,
} from "@tabler/icons-react";
import { useState } from "react";
import { TbPlayerPlayFilled } from "react-icons/tb";
@@ -30,9 +30,9 @@ const TRIGGERS = {
mode: "sync",
method: "GET",
response_type: "json",
path: "/trigger_http"
}
}
path: "/trigger_http",
},
},
};
const TASKS = {
@@ -41,9 +41,9 @@ const TASKS = {
params: {
method: "GET",
headers: [],
url: "https://jsonplaceholder.typicode.com/todos/1"
}
}
url: "https://jsonplaceholder.typicode.com/todos/1",
},
},
};
export default function FlowsTest() {
@@ -63,7 +63,7 @@ const NodeHeader = ({
iconProps,
rightSection,
initialValue,
onChange
onChange,
}: {
Icon: React.FC<any>;
iconProps?: ElementProps<"svg">;
@@ -100,7 +100,7 @@ const TriggerComponent = () => {
data={[
{ label: "Manual", value: "manual" },
{ label: "HTTP", value: "http" },
{ label: "Event", value: "event", disabled: true }
{ label: "Event", value: "event", disabled: true },
]}
/>
<SegmentedControl
@@ -108,7 +108,7 @@ const TriggerComponent = () => {
defaultValue="async"
data={[
{ label: "Async", value: "async" },
{ label: "Sync", value: "sync" }
{ label: "Sync", value: "sync" },
]}
/>
</div>
@@ -134,7 +134,7 @@ const TriggerComponent = () => {
data={[
{ label: "JSON", value: "json" },
{ label: "HTML", value: "html" },
{ label: "Text", value: "text" }
{ label: "Text", value: "text" },
]}
/>
</div>

View File

@@ -13,7 +13,7 @@ const validator = new TypeboxValidator();
const schema = Type.Object({
name: Type.String(),
age: Type.Optional(Type.Number())
age: Type.Optional(Type.Number()),
});
export default function JsonSchemaFormReactTest() {

View File

@@ -9,7 +9,7 @@ import {
FormContextOverride,
FormDebug,
ObjectField,
useFormError
useFormError,
} from "ui/components/form/json-schema-form";
import { Scrollable } from "ui/layouts/AppShell/AppShell";
@@ -20,18 +20,45 @@ const schema2 = {
age: { type: "number" },
gender: {
type: "string",
enum: ["male", "female", "uni"]
enum: ["male", "female", "uni"],
},
deep: {
type: "object",
properties: {
nested: { type: "string" }
}
}
nested: { type: "string" },
},
},
},
required: ["age"]
required: ["age"],
} as const satisfies JSONSchema;
const authSchema = {
type: "object",
properties: {
what: {
type: "array",
items: {
type: "string",
},
},
jwt: {
type: "object",
properties: {
fields: {
type: "array",
items: {
type: "string",
},
},
},
},
},
} as const satisfies JSONSchema;
const formOptions = {
debug: true,
};
export default function JsonSchemaForm3() {
const { schema: _schema, config } = useBknd();
const schema = JSON.parse(JSON.stringify(_schema));
@@ -46,6 +73,8 @@ export default function JsonSchemaForm3() {
return (
<Scrollable>
<div className="flex flex-col p-3">
<Form schema={_schema.auth} options={formOptions} />
{/*<Form
onChange={(data) => console.log("change", data)}
onSubmit={(data) => console.log("submit", data)}
@@ -249,13 +278,13 @@ export default function JsonSchemaForm3() {
</Form>*/}
{/*<CustomMediaForm />*/}
<Form
{/*<Form
schema={schema.media}
initialValues={config.media as any}
onSubmit={console.log}
options={{ debug: true }}
/*validateOn="change"*/
/>
/>*/}
{/*<Form
schema={removeKeyRecursively(schema.media, "pattern") as any}
@@ -285,7 +314,7 @@ const ss = {
interested: { type: "boolean" },
bla: {
type: "string",
enum: ["small", "medium", "large"]
enum: ["small", "medium", "large"],
},
password: { type: "string", format: "password" },
birthdate: { type: "string", format: "date" },
@@ -294,18 +323,18 @@ const ss = {
tags: {
type: "array",
items: {
type: "string"
}
type: "string",
},
},
config: {
type: "object",
properties: {
min: { type: "number" }
}
}
min: { type: "number" },
},
},
},
required: ["name"],
additionalProperties: false
additionalProperties: false,
} as const satisfies JSONSchema;
function CustomMediaForm() {
@@ -333,7 +362,7 @@ function CustomMediaForm() {
<AnyOf.Root path="adapter">
<CustomMediaFormAdapter />
</AnyOf.Root>
{/*<FormDebug force />*/}
<FormDebug force />
</Form>
);
}

View File

@@ -16,12 +16,12 @@ export default function JsonFormTest() {
name: {
type: "string",
title: "Name",
minLength: 3
minLength: 3,
},
variants: {
anyOf: [{ type: "string" }, { type: "number" }]
}
}
anyOf: [{ type: "string" }, { type: "number" }],
},
},
};
const ref = useRef<JsonSchemaFormRef>(null);

View File

@@ -1,5 +1,6 @@
import { Button, Modal, Switch, Tooltip, useMantineColorScheme } from "@mantine/core";
import { useColorScheme, useDisclosure } from "@mantine/hooks";
import { notifications } from "@mantine/notifications";
import { Button as AppButton } from "../../../components/buttons/Button";
export default function MantineTest() {
@@ -10,6 +11,19 @@ export default function MantineTest() {
<Button color="blue">Mantine</Button>
<AppButton>Button</AppButton>
<AppButton variant="primary">Button</AppButton>
<Button
onClick={() => {
notifications.show({
title: "Notification",
message: "This is a notification",
autoClose: false,
color: "blue",
});
}}
>
Notification
</Button>
</div>
<MantineModal />
<MantineTooltip />

View File

@@ -9,52 +9,52 @@ const schema: Schema = {
anyOf: [
{
title: "String",
type: "string"
type: "string",
},
{
title: "Number",
type: "number"
type: "number",
},
{
title: "Boolean",
type: "boolean"
}
]
type: "boolean",
},
],
},
numeric: {
anyOf: [
{
title: "Number",
type: "number"
type: "number",
},
{
title: "Datetime",
type: "string",
format: "date-time"
format: "date-time",
},
{
title: "Date",
type: "string",
format: "date"
format: "date",
},
{
title: "Time",
type: "string",
format: "time"
}
]
format: "time",
},
],
},
boolean: {
title: "Boolean",
type: "boolean"
}
type: "boolean",
},
},
type: "object",
properties: {
operand: {
enum: ["$and", "$or"],
default: "$and",
type: "string"
type: "string",
},
conditions: {
type: "array",
@@ -64,10 +64,10 @@ const schema: Schema = {
operand: {
enum: ["$and", "$or"],
default: "$and",
type: "string"
type: "string",
},
key: {
type: "string"
type: "string",
},
operator: {
type: "array",
@@ -78,30 +78,30 @@ const schema: Schema = {
type: "object",
properties: {
$eq: {
$ref: "#/definitions/primitive"
}
$ref: "#/definitions/primitive",
},
},
required: ["$eq"]
required: ["$eq"],
},
{
title: "Lower than",
type: "object",
properties: {
$lt: {
$ref: "#/definitions/numeric"
}
$ref: "#/definitions/numeric",
},
},
required: ["$lt"]
required: ["$lt"],
},
{
title: "Greather than",
type: "object",
properties: {
$gt: {
$ref: "#/definitions/numeric"
}
$ref: "#/definitions/numeric",
},
},
required: ["$gt"]
required: ["$gt"],
},
{
title: "Between",
@@ -110,13 +110,13 @@ const schema: Schema = {
$between: {
type: "array",
items: {
$ref: "#/definitions/numeric"
$ref: "#/definitions/numeric",
},
minItems: 2,
maxItems: 2
}
maxItems: 2,
},
},
required: ["$between"]
required: ["$between"],
},
{
title: "In",
@@ -125,23 +125,23 @@ const schema: Schema = {
$in: {
type: "array",
items: {
$ref: "#/definitions/primitive"
$ref: "#/definitions/primitive",
},
minItems: 1
}
}
}
]
minItems: 1,
},
},
},
],
},
minItems: 1
}
minItems: 1,
},
},
required: ["key", "operator"]
required: ["key", "operator"],
},
minItems: 1
}
minItems: 1,
},
},
required: ["operand", "conditions"]
required: ["operand", "conditions"],
};
export default function QueryJsonFormTest() {

View File

@@ -5,7 +5,7 @@ import { useForm } from "react-hook-form";
const schema = Type.Object({
example: Type.Optional(Type.String()),
exampleRequired: Type.String({ minLength: 2 })
exampleRequired: Type.String({ minLength: 2 }),
});
export default function ReactHookErrors() {
@@ -13,9 +13,9 @@ export default function ReactHookErrors() {
register,
handleSubmit,
watch,
formState: { errors }
formState: { errors },
} = useForm({
resolver: typeboxResolver(schema)
resolver: typeboxResolver(schema),
});
const onSubmit = (data) => console.log(data);

View File

@@ -2,7 +2,7 @@ import { ReactFlow } from "@xyflow/react";
const initialNodes = [
{ id: "1", position: { x: 0, y: 0 }, data: { label: "1" } },
{ id: "2", position: { x: 0, y: 100 }, data: { label: "2" } }
{ id: "2", position: { x: 0, y: 100 }, data: { label: "2" } },
];
const initialEdges = [{ id: "e1-2", source: "1", target: "2" }];

View File

@@ -23,50 +23,50 @@ const uiSchema = {
jwt: {
basepath: {
"ui:options": {
label: false
}
label: false,
},
},
fields: {
"ui:options": {
label: false,
orderable: false
}
}
orderable: false,
},
},
},
strategies: {
additionalProperties: {
"ui:widget": "select",
type: {
"ui:widget": "hidden"
}
"ui:widget": "hidden",
},
},
type: {
"ui:widget": "hidden"
}
}
"ui:widget": "hidden",
},
},
},
server: {
cors: {
allow_methods: {
"ui:widget": "checkboxes"
"ui:widget": "checkboxes",
},
allow_headers: {
"ui:options": {
orderable: false
}
}
}
orderable: false,
},
},
},
},
media: {
adapter: {
"ui:options": {
label: false
label: false,
},
type: {
"ui:widget": "hidden"
}
}
}
"ui:widget": "hidden",
},
},
},
};
export default function SchemaTest() {
@@ -81,7 +81,7 @@ export default function SchemaTest() {
key: tab,
schema: schema[tab],
uiSchema: uiSchema[tab] || {},
config: app.config[tab]
config: app.config[tab],
};
console.log("current", current);

View File

@@ -5,7 +5,7 @@ import {
type DraggableRubric,
type DraggableStateSnapshot,
Droppable,
type DroppableProps
type DroppableProps,
} from "@hello-pangea/dnd";
import { useListState } from "@mantine/hooks";
import { IconGripVertical } from "@tabler/icons-react";
@@ -24,7 +24,7 @@ export default function SortableTest() {
{ id: "N", name: "Nitrogen" },
{ id: "Y", name: "Yttrium" },
{ id: "Ba", name: "Barium" },
{ id: "Ce", name: "Cerium" }
{ id: "Ce", name: "Cerium" },
]}
onReordered={(...args) => console.log("reordered", args)}
onChange={(data) => console.log("changed", data)}
@@ -54,7 +54,7 @@ export function SortableList({
renderItem,
dndProps = { droppableId: "sortable-list", direction: "vertical" },
onReordered,
onChange
onChange,
}: SortableListProps) {
const [state, handlers] = useListState(data);

View File

@@ -16,7 +16,7 @@ function SwaggerUI() {
// @ts-ignore
window.ui = window.SwaggerUIBundle({
url: "http://localhost:28623/api/system/openapi.json",
dom_id: "#swagger-ui"
dom_id: "#swagger-ui",
});
}
};

View File

@@ -19,7 +19,7 @@ export default function SWRAndAPI() {
const [text, setText] = useState("");
const { data, ...r } = useApiQuery((api) => api.data.readOne("comments", 1), {
refine: (data) => data.data,
revalidateOnFocus: true
revalidateOnFocus: true,
});
const comment = data ? data : null;

View File

@@ -16,7 +16,7 @@ export default function SwrAndDataApi() {
function QueryMutateDataApi() {
const { mutate } = useEntityMutate("comments");
const { data, ...r } = useEntityQuery("comments", undefined, {
limit: 2
limit: 2,
});
return (
@@ -48,7 +48,7 @@ function QueryMutateDataApi() {
function QueryDataApi() {
const { data, update, ...r } = useEntityQuery("comments", undefined, {
sort: { by: "id", dir: "asc" },
limit: 3
limit: 3,
});
return (

View File

@@ -0,0 +1,48 @@
import { Button } from "ui/components/buttons/Button";
import { Alert } from "ui/components/display/Alert";
import * as Formy from "ui/components/form/Formy";
export default function Themes() {
return (
<div className="flex flex-col p-3 gap-4">
<div className="flex flex-row gap-2 items-center">
<Button size="small">Small</Button>
<Button>Default</Button>
<Button variant="primary">Primary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="outline">Outline</Button>
<Button variant="red">Red</Button>
<Button variant="subtlered">Subtlered</Button>
<Button size="large">Large</Button>
</div>
<div className="flex flex-row gap-1">
<Alert.Exception title="Exception">Alert.Exception</Alert.Exception>
<Alert.Info title="Info">Alert.Info</Alert.Info>
<Alert.Success title="Success">Alert.Success</Alert.Success>
<Alert.Warning title="Warning">Alert.Warning</Alert.Warning>
</div>
<div className="flex flex-row gap-3 items-start">
<Formy.Group>
<Formy.Label>Input</Formy.Label>
<Formy.Input placeholder="Input" />
</Formy.Group>
<Formy.Group>
<Formy.Label>Checkbox</Formy.Label>
<Formy.BooleanInput />
</Formy.Group>
<Formy.Group>
<Formy.Label>Switch</Formy.Label>
<Formy.Switch />
</Formy.Group>
<Formy.Group>
<Formy.Label>Select</Formy.Label>
<Formy.Select>
<option value="" />
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</Formy.Select>
</Formy.Group>
</div>
</div>
);
}