mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +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:
@@ -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" />;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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}`);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -57,7 +57,7 @@ export function FlowsEmpty() {
|
||||
to continue."
|
||||
primary={{
|
||||
children: "Create Flow",
|
||||
onClick: () => navigate(app.getSettingsPath(["flows"]))
|
||||
onClick: () => navigate(app.getSettingsPath(["flows"])),
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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" }];
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
48
app/src/ui/routes/test/tests/themes.tsx
Normal file
48
app/src/ui/routes/test/tests/themes.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user