mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
Merge pull request #50 from bknd-io/feat/admin-user-create
Create users in Admin UI
This commit is contained in:
@@ -1,11 +1,16 @@
|
|||||||
import { type AuthAction, Authenticator, type ProfileExchange, Role, type Strategy } from "auth";
|
import {
|
||||||
|
type AuthAction,
|
||||||
|
AuthPermissions,
|
||||||
|
Authenticator,
|
||||||
|
type ProfileExchange,
|
||||||
|
Role,
|
||||||
|
type Strategy
|
||||||
|
} from "auth";
|
||||||
import type { PasswordStrategy } from "auth/authenticate/strategies";
|
import type { PasswordStrategy } from "auth/authenticate/strategies";
|
||||||
import { auth } from "auth/middlewares";
|
|
||||||
import { type DB, Exception, type PrimaryFieldType } from "core";
|
import { type DB, Exception, type PrimaryFieldType } from "core";
|
||||||
import { type Static, secureRandomString, transformObject } from "core/utils";
|
import { type Static, secureRandomString, transformObject } from "core/utils";
|
||||||
import { type Entity, EntityIndex, type EntityManager } from "data";
|
import type { Entity, EntityManager } from "data";
|
||||||
import { type FieldSchema, em, entity, enumm, make, text } from "data/prototype";
|
import { type FieldSchema, em, entity, enumm, make, text } from "data/prototype";
|
||||||
import type { Hono } from "hono";
|
|
||||||
import { pick } from "lodash-es";
|
import { pick } from "lodash-es";
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
import { AuthController } from "./api/AuthController";
|
import { AuthController } from "./api/AuthController";
|
||||||
@@ -79,8 +84,8 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
super.setBuilt();
|
super.setBuilt();
|
||||||
|
|
||||||
this._controller = new AuthController(this);
|
this._controller = new AuthController(this);
|
||||||
//this.ctx.server.use(controller.getMiddleware);
|
|
||||||
this.ctx.server.route(this.config.basepath, this._controller.getController());
|
this.ctx.server.route(this.config.basepath, this._controller.getController());
|
||||||
|
this.ctx.guard.registerPermissions(Object.values(AuthPermissions));
|
||||||
}
|
}
|
||||||
|
|
||||||
get controller(): AuthController {
|
get controller(): AuthController {
|
||||||
@@ -260,14 +265,12 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const roles = Object.keys(this.config.roles ?? {});
|
const roles = Object.keys(this.config.roles ?? {});
|
||||||
const field = make("role", enumm({ enum: roles }));
|
this.replaceEntityField(users, "role", enumm({ enum: roles }));
|
||||||
users.__replaceField("role", field);
|
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const strategies = Object.keys(this.config.strategies ?? {});
|
const strategies = Object.keys(this.config.strategies ?? {});
|
||||||
const field = make("strategy", enumm({ enum: strategies }));
|
this.replaceEntityField(users, "strategy", enumm({ enum: strategies }));
|
||||||
users.__replaceField("strategy", field);
|
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { AuthActionResponse } from "auth/api/AuthController";
|
||||||
import type { AppAuthSchema, AppAuthStrategies } from "auth/auth-schema";
|
import type { AppAuthSchema, AppAuthStrategies } from "auth/auth-schema";
|
||||||
import type { AuthResponse, SafeUser, Strategy } from "auth/authenticate/Authenticator";
|
import type { AuthResponse, SafeUser, Strategy } from "auth/authenticate/Authenticator";
|
||||||
import { type BaseModuleApiOptions, ModuleApi } from "modules/ModuleApi";
|
import { type BaseModuleApiOptions, ModuleApi } from "modules/ModuleApi";
|
||||||
@@ -13,22 +14,46 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async loginWithPassword(input: any) {
|
async login(strategy: string, input: any) {
|
||||||
const res = await this.post<AuthResponse>(["password", "login"], input);
|
const res = await this.post<AuthResponse>([strategy, "login"], input);
|
||||||
if (res.ok && res.body.token) {
|
if (res.ok && res.body.token) {
|
||||||
await this.options.onTokenUpdate?.(res.body.token);
|
await this.options.onTokenUpdate?.(res.body.token);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerWithPassword(input: any) {
|
async register(strategy: string, input: any) {
|
||||||
const res = await this.post<AuthResponse>(["password", "register"], input);
|
const res = await this.post<AuthResponse>([strategy, "register"], input);
|
||||||
if (res.ok && res.body.token) {
|
if (res.ok && res.body.token) {
|
||||||
await this.options.onTokenUpdate?.(res.body.token);
|
await this.options.onTokenUpdate?.(res.body.token);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async actionSchema(strategy: string, action: string) {
|
||||||
|
return this.get<Strategy>([strategy, "actions", action, "schema.json"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async action(strategy: string, action: string, input: any) {
|
||||||
|
return this.post<AuthActionResponse>([strategy, "actions", action], input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use login("password", ...) instead
|
||||||
|
* @param input
|
||||||
|
*/
|
||||||
|
async loginWithPassword(input: any) {
|
||||||
|
return this.login("password", input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use register("password", ...) instead
|
||||||
|
* @param input
|
||||||
|
*/
|
||||||
|
async registerWithPassword(input: any) {
|
||||||
|
return this.register("password", input);
|
||||||
|
}
|
||||||
|
|
||||||
me() {
|
me() {
|
||||||
return this.get<{ user: SafeUser | null }>(["me"]);
|
return this.get<{ user: SafeUser | null }>(["me"]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import type { AppAuth } from "auth";
|
import { type AppAuth, AuthPermissions, type SafeUser, type Strategy } from "auth";
|
||||||
|
import { TypeInvalidError, parse } from "core/utils";
|
||||||
|
import { DataPermissions } from "data";
|
||||||
|
import type { Hono } from "hono";
|
||||||
import { Controller } from "modules/Controller";
|
import { Controller } from "modules/Controller";
|
||||||
|
import type { ServerEnv } from "modules/Module";
|
||||||
|
|
||||||
|
export type AuthActionResponse = {
|
||||||
|
success: boolean;
|
||||||
|
action: string;
|
||||||
|
data?: SafeUser;
|
||||||
|
errors?: any;
|
||||||
|
};
|
||||||
|
|
||||||
export class AuthController extends Controller {
|
export class AuthController extends Controller {
|
||||||
constructor(private auth: AppAuth) {
|
constructor(private auth: AppAuth) {
|
||||||
@@ -10,6 +21,70 @@ export class AuthController extends Controller {
|
|||||||
return this.auth.ctx.guard;
|
return this.auth.ctx.guard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private registerStrategyActions(strategy: Strategy, mainHono: Hono<ServerEnv>) {
|
||||||
|
const actions = strategy.getActions?.();
|
||||||
|
if (!actions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { auth, permission } = this.middlewares;
|
||||||
|
const hono = this.create().use(auth());
|
||||||
|
|
||||||
|
const name = strategy.getName();
|
||||||
|
const { create, change } = actions;
|
||||||
|
const em = this.auth.em;
|
||||||
|
const mutator = em.mutator(this.auth.config.entity_name as "users");
|
||||||
|
|
||||||
|
if (create) {
|
||||||
|
hono.post(
|
||||||
|
"/create",
|
||||||
|
permission([AuthPermissions.createUser, DataPermissions.entityCreate]),
|
||||||
|
async (c) => {
|
||||||
|
try {
|
||||||
|
const body = await this.auth.authenticator.getBody(c);
|
||||||
|
const valid = parse(create.schema, body, {
|
||||||
|
skipMark: true
|
||||||
|
});
|
||||||
|
const processed = (await create.preprocess?.(valid)) ?? valid;
|
||||||
|
console.log("processed", processed);
|
||||||
|
|
||||||
|
// @todo: check processed for "role" and check permissions
|
||||||
|
|
||||||
|
mutator.__unstable_toggleSystemEntityCreation(false);
|
||||||
|
const { data: created } = await mutator.insertOne({
|
||||||
|
...processed,
|
||||||
|
strategy: name
|
||||||
|
});
|
||||||
|
mutator.__unstable_toggleSystemEntityCreation(true);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
action: "create",
|
||||||
|
strategy: name,
|
||||||
|
data: created as unknown as SafeUser
|
||||||
|
} as AuthActionResponse);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof TypeInvalidError) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
errors: e.errors
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
hono.get("create/schema.json", async (c) => {
|
||||||
|
return c.json(create.schema);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mainHono.route(`/${name}/actions`, hono);
|
||||||
|
}
|
||||||
|
|
||||||
override getController() {
|
override getController() {
|
||||||
const { auth } = this.middlewares;
|
const { auth } = this.middlewares;
|
||||||
const hono = this.create();
|
const hono = this.create();
|
||||||
@@ -18,6 +93,7 @@ export class AuthController extends Controller {
|
|||||||
for (const [name, strategy] of Object.entries(strategies)) {
|
for (const [name, strategy] of Object.entries(strategies)) {
|
||||||
//console.log("registering", name, "at", `/${name}`);
|
//console.log("registering", name, "at", `/${name}`);
|
||||||
hono.route(`/${name}`, strategy.getController(this.auth.authenticator));
|
hono.route(`/${name}`, strategy.getController(this.auth.authenticator));
|
||||||
|
this.registerStrategyActions(strategy, hono);
|
||||||
}
|
}
|
||||||
|
|
||||||
hono.get("/me", auth(), async (c) => {
|
hono.get("/me", auth(), async (c) => {
|
||||||
|
|||||||
4
app/src/auth/auth-permissions.ts
Normal file
4
app/src/auth/auth-permissions.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Permission } from "core";
|
||||||
|
|
||||||
|
export const createUser = new Permission("auth.user.create");
|
||||||
|
//export const updateUser = new Permission("auth.user.update");
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
import { Exception } from "core";
|
import { type DB, Exception } from "core";
|
||||||
import { addFlashMessage } from "core/server/flash";
|
import { addFlashMessage } from "core/server/flash";
|
||||||
import { type Static, StringEnum, Type, parse, runtimeSupports, transformObject } from "core/utils";
|
import {
|
||||||
|
type Static,
|
||||||
|
StringEnum,
|
||||||
|
type TObject,
|
||||||
|
Type,
|
||||||
|
parse,
|
||||||
|
runtimeSupports,
|
||||||
|
transformObject
|
||||||
|
} from "core/utils";
|
||||||
import type { Context, Hono } from "hono";
|
import type { Context, Hono } from "hono";
|
||||||
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
|
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
|
||||||
import { sign, verify } from "hono/jwt";
|
import { sign, verify } from "hono/jwt";
|
||||||
@@ -10,6 +18,14 @@ import type { ServerEnv } from "modules/Module";
|
|||||||
type Input = any; // workaround
|
type Input = any; // workaround
|
||||||
export type JWTPayload = Parameters<typeof sign>[0];
|
export type JWTPayload = Parameters<typeof sign>[0];
|
||||||
|
|
||||||
|
export const strategyActions = ["create", "change"] as const;
|
||||||
|
export type StrategyActionName = (typeof strategyActions)[number];
|
||||||
|
export type StrategyAction<S extends TObject = TObject> = {
|
||||||
|
schema: S;
|
||||||
|
preprocess: (input: unknown) => Promise<Omit<DB["users"], "id" | "strategy">>;
|
||||||
|
};
|
||||||
|
export type StrategyActions = Partial<Record<StrategyActionName, StrategyAction>>;
|
||||||
|
|
||||||
// @todo: add schema to interface to ensure proper inference
|
// @todo: add schema to interface to ensure proper inference
|
||||||
export interface Strategy {
|
export interface Strategy {
|
||||||
getController: (auth: Authenticator) => Hono<any>;
|
getController: (auth: Authenticator) => Hono<any>;
|
||||||
@@ -17,6 +33,7 @@ export interface Strategy {
|
|||||||
getMode: () => "form" | "external";
|
getMode: () => "form" | "external";
|
||||||
getName: () => string;
|
getName: () => string;
|
||||||
toJSON: (secrets?: boolean) => any;
|
toJSON: (secrets?: boolean) => any;
|
||||||
|
getActions?: () => StrategyActions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
@@ -274,6 +291,14 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
return c.req.header("Content-Type") === "application/json";
|
return c.req.header("Content-Type") === "application/json";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getBody(c: Context) {
|
||||||
|
if (this.isJsonRequest(c)) {
|
||||||
|
return await c.req.json();
|
||||||
|
} else {
|
||||||
|
return Object.fromEntries((await c.req.formData()).entries());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getSuccessPath(c: Context) {
|
private getSuccessPath(c: Context) {
|
||||||
const p = (this.config.cookie.pathSuccess ?? "/").replace(/\/+$/, "/");
|
const p = (this.config.cookie.pathSuccess ?? "/").replace(/\/+$/, "/");
|
||||||
|
|
||||||
@@ -338,3 +363,13 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createStrategyAction<S extends TObject>(
|
||||||
|
schema: S,
|
||||||
|
preprocess: (input: Static<S>) => Promise<Partial<DB["users"]>>
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
schema,
|
||||||
|
preprocess
|
||||||
|
} as StrategyAction<S>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Authenticator, Strategy } from "auth";
|
|||||||
import { type Static, StringEnum, Type, parse } from "core/utils";
|
import { type Static, StringEnum, Type, parse } from "core/utils";
|
||||||
import { hash } from "core/utils";
|
import { hash } from "core/utils";
|
||||||
import { type Context, Hono } from "hono";
|
import { type Context, Hono } from "hono";
|
||||||
|
import { type StrategyAction, type StrategyActions, createStrategyAction } from "../Authenticator";
|
||||||
|
|
||||||
type LoginSchema = { username: string; password: string } | { email: string; password: string };
|
type LoginSchema = { username: string; password: string } | { email: string; password: string };
|
||||||
type RegisterSchema = { email: string; password: string; [key: string]: any };
|
type RegisterSchema = { email: string; password: string; [key: string]: any };
|
||||||
@@ -54,17 +55,9 @@ export class PasswordStrategy implements Strategy {
|
|||||||
getController(authenticator: Authenticator): Hono<any> {
|
getController(authenticator: Authenticator): Hono<any> {
|
||||||
const hono = new Hono();
|
const hono = new Hono();
|
||||||
|
|
||||||
async function getBody(c: Context) {
|
|
||||||
if (authenticator.isJsonRequest(c)) {
|
|
||||||
return await c.req.json();
|
|
||||||
} else {
|
|
||||||
return Object.fromEntries((await c.req.formData()).entries());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hono
|
return hono
|
||||||
.post("/login", async (c) => {
|
.post("/login", async (c) => {
|
||||||
const body = await getBody(c);
|
const body = await authenticator.getBody(c);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = await this.login(body);
|
const payload = await this.login(body);
|
||||||
@@ -76,7 +69,7 @@ export class PasswordStrategy implements Strategy {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.post("/register", async (c) => {
|
.post("/register", async (c) => {
|
||||||
const body = await getBody(c);
|
const body = await authenticator.getBody(c);
|
||||||
|
|
||||||
const payload = await this.register(body);
|
const payload = await this.register(body);
|
||||||
const data = await authenticator.resolve("register", this, payload.password, payload);
|
const data = await authenticator.resolve("register", this, payload.password, payload);
|
||||||
@@ -85,6 +78,27 @@ export class PasswordStrategy implements Strategy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getActions(): StrategyActions {
|
||||||
|
return {
|
||||||
|
create: createStrategyAction(
|
||||||
|
Type.Object({
|
||||||
|
email: Type.String({
|
||||||
|
pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"
|
||||||
|
}),
|
||||||
|
password: Type.String({
|
||||||
|
minLength: 8 // @todo: this should be configurable
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
async ({ password, ...input }) => {
|
||||||
|
return {
|
||||||
|
...input,
|
||||||
|
strategy_value: await this.hash(password)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,3 +19,5 @@ export { AppAuth, type UserFieldSchema } from "./AppAuth";
|
|||||||
|
|
||||||
export { Guard, type GuardUserContext, type GuardConfig } from "./authorize/Guard";
|
export { Guard, type GuardUserContext, type GuardConfig } from "./authorize/Guard";
|
||||||
export { Role } from "./authorize/Role";
|
export { Role } from "./authorize/Role";
|
||||||
|
|
||||||
|
export * as AuthPermissions from "./auth-permissions";
|
||||||
|
|||||||
@@ -3,7 +3,15 @@ import type { Guard } from "auth";
|
|||||||
import { SchemaObject } from "core";
|
import { SchemaObject } from "core";
|
||||||
import type { EventManager } from "core/events";
|
import type { EventManager } from "core/events";
|
||||||
import type { Static, TSchema } from "core/utils";
|
import type { Static, TSchema } from "core/utils";
|
||||||
import type { Connection, EntityIndex, EntityManager, em as prototypeEm } from "data";
|
import {
|
||||||
|
type Connection,
|
||||||
|
type EntityIndex,
|
||||||
|
type EntityManager,
|
||||||
|
type Field,
|
||||||
|
FieldPrototype,
|
||||||
|
make,
|
||||||
|
type em as prototypeEm
|
||||||
|
} from "data";
|
||||||
import { Entity } from "data";
|
import { Entity } from "data";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
|
|
||||||
@@ -184,4 +192,16 @@ export abstract class Module<Schema extends TSchema = TSchema, ConfigSchema = St
|
|||||||
|
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected replaceEntityField(
|
||||||
|
_entity: string | Entity,
|
||||||
|
field: Field | string,
|
||||||
|
_newField: Field | FieldPrototype
|
||||||
|
) {
|
||||||
|
const entity = this.ctx.em.entity(_entity);
|
||||||
|
const name = typeof field === "string" ? field : field.name;
|
||||||
|
const newField =
|
||||||
|
_newField instanceof FieldPrototype ? make(name, _newField as any) : _newField;
|
||||||
|
entity.__replaceField(name, newField);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ export abstract class ModuleApi<Options extends BaseModuleApiOptions = BaseModul
|
|||||||
} as Options;
|
} as Options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* used for SWR invalidation of basepath
|
||||||
|
*/
|
||||||
|
key(): string {
|
||||||
|
return this.options.basepath ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
protected getUrl(path: string) {
|
protected getUrl(path: string) {
|
||||||
const basepath = this.options.basepath ?? "";
|
const basepath = this.options.basepath ?? "";
|
||||||
return this.options.host + (basepath + "/" + path).replace(/\/{2,}/g, "/").replace(/\/$/, "");
|
return this.options.host + (basepath + "/" + path).replace(/\/{2,}/g, "/").replace(/\/$/, "");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Api } from "Api";
|
import type { Api } from "Api";
|
||||||
import type { FetchPromise, ResponseObject } from "modules/ModuleApi";
|
import type { FetchPromise, ModuleApi, ResponseObject } from "modules/ModuleApi";
|
||||||
import useSWR, { type SWRConfiguration, useSWRConfig } from "swr";
|
import useSWR, { type SWRConfiguration, useSWRConfig } from "swr";
|
||||||
import { useApi } from "ui/client";
|
import { useApi } from "ui/client";
|
||||||
|
|
||||||
@@ -27,12 +27,19 @@ export const useApiQuery = <
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useInvalidate = () => {
|
export const useInvalidate = (options?: { exact?: boolean }) => {
|
||||||
const mutate = useSWRConfig().mutate;
|
const mutate = useSWRConfig().mutate;
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
||||||
return async (arg?: string | ((api: Api) => FetchPromise<any>)) => {
|
return async (arg?: string | ((api: Api) => FetchPromise<any> | ModuleApi<any>)) => {
|
||||||
if (!arg) return async () => mutate("");
|
let key = "";
|
||||||
return mutate(typeof arg === "string" ? arg : arg(api).key());
|
if (typeof arg === "string") {
|
||||||
|
key = arg;
|
||||||
|
} else if (typeof arg === "function") {
|
||||||
|
key = arg(api).key();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.exact) return mutate(key);
|
||||||
|
return mutate((k) => typeof k === "string" && k.startsWith(key));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,15 +22,6 @@ export class UseEntityApiError<Payload = any> extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Test() {
|
|
||||||
const { read } = useEntity("users");
|
|
||||||
async () => {
|
|
||||||
const data = await read();
|
|
||||||
};
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useEntity = <
|
export const useEntity = <
|
||||||
Entity extends keyof DB | string,
|
Entity extends keyof DB | string,
|
||||||
Id extends PrimaryFieldType | undefined = undefined,
|
Id extends PrimaryFieldType | undefined = undefined,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Empty, type EmptyProps } from "./Empty";
|
import { Empty, type EmptyProps } from "./Empty";
|
||||||
|
|
||||||
const NotFound = (props: Partial<EmptyProps>) => <Empty title="Not Found" {...props} />;
|
const NotFound = (props: Partial<EmptyProps>) => <Empty title="Not Found" {...props} />;
|
||||||
|
const NotAllowed = (props: Partial<EmptyProps>) => <Empty title="Not Allowed" {...props} />;
|
||||||
|
|
||||||
export const Message = {
|
export const Message = {
|
||||||
NotFound
|
NotFound,
|
||||||
|
NotAllowed
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,12 +15,13 @@ export type JsonSchemaFormProps = any & {
|
|||||||
schema: RJSFSchema | Schema;
|
schema: RJSFSchema | Schema;
|
||||||
uiSchema?: any;
|
uiSchema?: any;
|
||||||
direction?: "horizontal" | "vertical";
|
direction?: "horizontal" | "vertical";
|
||||||
onChange?: (value: any) => void;
|
onChange?: (value: any, isValid: () => boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type JsonSchemaFormRef = {
|
export type JsonSchemaFormRef = {
|
||||||
formData: () => any;
|
formData: () => any;
|
||||||
validateForm: () => boolean;
|
validateForm: () => boolean;
|
||||||
|
silentValidate: () => boolean;
|
||||||
cancel: () => void;
|
cancel: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,15 +53,18 @@ export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>
|
|||||||
const handleChange = ({ formData }: any, e) => {
|
const handleChange = ({ formData }: any, e) => {
|
||||||
const clean = JSON.parse(JSON.stringify(formData));
|
const clean = JSON.parse(JSON.stringify(formData));
|
||||||
//console.log("Data changed: ", clean, JSON.stringify(formData, null, 2));
|
//console.log("Data changed: ", clean, JSON.stringify(formData, null, 2));
|
||||||
onChange?.(clean);
|
|
||||||
setValue(clean);
|
setValue(clean);
|
||||||
|
onChange?.(clean, () => isValid(clean));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isValid = (data: any) => validator.validateFormData(data, schema).errors.length === 0;
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
ref,
|
ref,
|
||||||
() => ({
|
() => ({
|
||||||
formData: () => value,
|
formData: () => value,
|
||||||
validateForm: () => formRef.current!.validateForm(),
|
validateForm: () => formRef.current!.validateForm(),
|
||||||
|
silentValidate: () => isValid(value),
|
||||||
cancel: () => formRef.current!.reset()
|
cancel: () => formRef.current!.reset()
|
||||||
}),
|
}),
|
||||||
[value]
|
[value]
|
||||||
|
|||||||
22
app/src/ui/modals/debug/OverlayModal.tsx
Normal file
22
app/src/ui/modals/debug/OverlayModal.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { ContextModalProps } from "@mantine/modals";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
export function OverlayModal({
|
||||||
|
context,
|
||||||
|
id,
|
||||||
|
innerProps: { content }
|
||||||
|
}: ContextModalProps<{ content?: ReactNode }>) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
OverlayModal.defaultTitle = undefined;
|
||||||
|
OverlayModal.modalProps = {
|
||||||
|
withCloseButton: false,
|
||||||
|
classNames: {
|
||||||
|
size: "md",
|
||||||
|
root: "bknd-admin",
|
||||||
|
content: "text-center justify-center",
|
||||||
|
title: "font-bold !text-md",
|
||||||
|
body: "py-3 px-5 gap-4 flex flex-col"
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -7,21 +7,31 @@ import {
|
|||||||
} from "ui/components/form/json-schema";
|
} from "ui/components/form/json-schema";
|
||||||
|
|
||||||
import type { ContextModalProps } from "@mantine/modals";
|
import type { ContextModalProps } from "@mantine/modals";
|
||||||
|
import { Alert } from "ui/components/display/Alert";
|
||||||
|
|
||||||
type Props = JsonSchemaFormProps & {
|
type Props = JsonSchemaFormProps & {
|
||||||
onSubmit?: (data: any) => void | Promise<void>;
|
autoCloseAfterSubmit?: boolean;
|
||||||
|
onSubmit?: (
|
||||||
|
data: any,
|
||||||
|
context: {
|
||||||
|
close: () => void;
|
||||||
|
}
|
||||||
|
) => void | Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SchemaFormModal({
|
export function SchemaFormModal({
|
||||||
context,
|
context,
|
||||||
id,
|
id,
|
||||||
innerProps: { schema, uiSchema, onSubmit }
|
innerProps: { schema, uiSchema, onSubmit, autoCloseAfterSubmit }
|
||||||
}: ContextModalProps<Props>) {
|
}: ContextModalProps<Props>) {
|
||||||
const [valid, setValid] = useState(false);
|
const [valid, setValid] = useState(false);
|
||||||
const formRef = useRef<JsonSchemaFormRef>(null);
|
const formRef = useRef<JsonSchemaFormRef>(null);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const was_submitted = useRef(false);
|
||||||
|
const [error, setError] = useState<string>();
|
||||||
|
|
||||||
function handleChange(data) {
|
function handleChange(data, isValid) {
|
||||||
const valid = formRef.current?.validateForm() ?? false;
|
const valid = isValid();
|
||||||
console.log("Data changed", data, valid);
|
console.log("Data changed", data, valid);
|
||||||
setValid(valid);
|
setValid(valid);
|
||||||
}
|
}
|
||||||
@@ -30,29 +40,45 @@ export function SchemaFormModal({
|
|||||||
context.closeModal(id);
|
context.closeModal(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClickAdd() {
|
async function handleSubmit() {
|
||||||
await onSubmit?.(formRef.current?.formData());
|
was_submitted.current = true;
|
||||||
handleClose();
|
if (!formRef.current?.validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
await onSubmit?.(formRef.current?.formData(), {
|
||||||
|
close: handleClose,
|
||||||
|
setError
|
||||||
|
});
|
||||||
|
setSubmitting(false);
|
||||||
|
|
||||||
|
if (autoCloseAfterSubmit !== false) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pt-3 pb-3 px-3 gap-4 flex flex-col">
|
<>
|
||||||
<JsonSchemaForm
|
{error && <Alert.Exception message={error} />}
|
||||||
tagName="form"
|
<div className="pt-3 pb-3 px-3 gap-4 flex flex-col">
|
||||||
ref={formRef}
|
<JsonSchemaForm
|
||||||
schema={schema}
|
tagName="form"
|
||||||
uiSchema={uiSchema}
|
ref={formRef}
|
||||||
className="legacy hide-required-mark fieldset-alternative mute-root"
|
schema={schema}
|
||||||
onChange={handleChange}
|
uiSchema={uiSchema}
|
||||||
onSubmit={handleClickAdd}
|
className="legacy hide-required-mark fieldset-alternative mute-root"
|
||||||
/>
|
onChange={handleChange}
|
||||||
<div className="flex flex-row justify-end gap-2">
|
onSubmit={handleSubmit}
|
||||||
<Button onClick={handleClose}>Cancel</Button>
|
/>
|
||||||
<Button variant="primary" onClick={handleClickAdd} disabled={!valid}>
|
<div className="flex flex-row justify-end gap-2">
|
||||||
Create
|
<Button onClick={handleClose}>Cancel</Button>
|
||||||
</Button>
|
<Button variant="primary" onClick={handleSubmit} disabled={!valid || submitting}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +89,7 @@ SchemaFormModal.modalProps = {
|
|||||||
root: "bknd-admin",
|
root: "bknd-admin",
|
||||||
header: "!bg-primary/5 border-b border-b-muted !py-3 px-5 !h-auto !min-h-px",
|
header: "!bg-primary/5 border-b border-b-muted !py-3 px-5 !h-auto !min-h-px",
|
||||||
content: "rounded-lg select-none",
|
content: "rounded-lg select-none",
|
||||||
title: "font-bold !text-md",
|
title: "!font-bold !text-md",
|
||||||
body: "!p-0"
|
body: "!p-0"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { ModalProps } from "@mantine/core";
|
import type { ModalProps } from "@mantine/core";
|
||||||
import { ModalsProvider, modals as mantineModals } from "@mantine/modals";
|
import { modals as $modals, ModalsProvider, closeModal, openContextModal } from "@mantine/modals";
|
||||||
import { transformObject } from "core/utils";
|
import { transformObject } from "core/utils";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import { OverlayModal } from "ui/modals/debug/OverlayModal";
|
||||||
import { DebugModal } from "./debug/DebugModal";
|
import { DebugModal } from "./debug/DebugModal";
|
||||||
import { SchemaFormModal } from "./debug/SchemaFormModal";
|
import { SchemaFormModal } from "./debug/SchemaFormModal";
|
||||||
import { TestModal } from "./debug/TestModal";
|
import { TestModal } from "./debug/TestModal";
|
||||||
@@ -9,7 +10,8 @@ import { TestModal } from "./debug/TestModal";
|
|||||||
const modals = {
|
const modals = {
|
||||||
test: TestModal,
|
test: TestModal,
|
||||||
debug: DebugModal,
|
debug: DebugModal,
|
||||||
form: SchemaFormModal
|
form: SchemaFormModal,
|
||||||
|
overlay: OverlayModal
|
||||||
};
|
};
|
||||||
|
|
||||||
declare module "@mantine/modals" {
|
declare module "@mantine/modals" {
|
||||||
@@ -33,17 +35,22 @@ function open<Modal extends keyof typeof modals>(
|
|||||||
) {
|
) {
|
||||||
const title = _title ?? modals[modal].defaultTitle ?? undefined;
|
const title = _title ?? modals[modal].defaultTitle ?? undefined;
|
||||||
const cmpModalProps = modals[modal].modalProps ?? {};
|
const cmpModalProps = modals[modal].modalProps ?? {};
|
||||||
return mantineModals.openContextModal({
|
const props = {
|
||||||
title,
|
title,
|
||||||
...modalProps,
|
...modalProps,
|
||||||
...cmpModalProps,
|
...cmpModalProps,
|
||||||
modal,
|
modal,
|
||||||
innerProps
|
innerProps
|
||||||
});
|
};
|
||||||
|
openContextModal(props);
|
||||||
|
return {
|
||||||
|
close: () => close(modal),
|
||||||
|
closeAll: $modals.closeAll
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function close<Modal extends keyof typeof modals>(modal: Modal) {
|
function close<Modal extends keyof typeof modals>(modal: Modal) {
|
||||||
return mantineModals.close(modal);
|
return closeModal(modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bkndModals = {
|
export const bkndModals = {
|
||||||
@@ -53,5 +60,5 @@ export const bkndModals = {
|
|||||||
>,
|
>,
|
||||||
open,
|
open,
|
||||||
close,
|
close,
|
||||||
closeAll: mantineModals.closeAll
|
closeAll: $modals.closeAll
|
||||||
};
|
};
|
||||||
|
|||||||
53
app/src/ui/modules/auth/hooks/use-create-user-modal.ts
Normal file
53
app/src/ui/modules/auth/hooks/use-create-user-modal.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { useApi, useInvalidate } from "ui/client";
|
||||||
|
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
||||||
|
import { routes, useNavigate } from "ui/lib/routes";
|
||||||
|
import { bkndModals } from "ui/modals";
|
||||||
|
|
||||||
|
export function useCreateUserModal() {
|
||||||
|
const api = useApi();
|
||||||
|
const { config } = useBkndAuth();
|
||||||
|
const invalidate = useInvalidate();
|
||||||
|
const [navigate] = useNavigate();
|
||||||
|
|
||||||
|
const open = async () => {
|
||||||
|
const loading = bkndModals.open("overlay", {
|
||||||
|
content: "Loading..."
|
||||||
|
});
|
||||||
|
|
||||||
|
const schema = await api.auth.actionSchema("password", "create");
|
||||||
|
loading.closeAll(); // currently can't close by id...
|
||||||
|
|
||||||
|
bkndModals.open(
|
||||||
|
"form",
|
||||||
|
{
|
||||||
|
schema,
|
||||||
|
uiSchema: {
|
||||||
|
password: {
|
||||||
|
"ui:widget": "password"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
autoCloseAfterSubmit: false,
|
||||||
|
onSubmit: async (data, ctx) => {
|
||||||
|
console.log("submitted:", data, ctx);
|
||||||
|
const res = await api.auth.action("password", "create", data);
|
||||||
|
console.log(res);
|
||||||
|
if (res.ok) {
|
||||||
|
// invalidate all data
|
||||||
|
invalidate();
|
||||||
|
navigate(routes.data.entity.edit(config.entity_name, res.data.id));
|
||||||
|
ctx.close();
|
||||||
|
} else if ("error" in res) {
|
||||||
|
ctx.setError(res.error);
|
||||||
|
} else {
|
||||||
|
ctx.setError("Unknown error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Create User"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { open };
|
||||||
|
}
|
||||||
@@ -34,7 +34,11 @@ export function EntityTable2({ entity, select, ...props }: EntityTableProps) {
|
|||||||
const field = getField(property)!;
|
const field = getField(property)!;
|
||||||
_value = field.getValue(value, "table");
|
_value = field.getValue(value, "table");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Couldn't render value", { value, property, entity, select, ...props }, e);
|
console.warn(
|
||||||
|
"Couldn't render value",
|
||||||
|
{ value, property, entity, select, columns, ...props },
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <CellValue value={_value} property={property} />;
|
return <CellValue value={_value} property={property} />;
|
||||||
|
|||||||
@@ -28,14 +28,9 @@ function AuthRolesEditInternal({ params }) {
|
|||||||
if (!formRef.current?.isValid()) return;
|
if (!formRef.current?.isValid()) return;
|
||||||
const data = formRef.current?.getData();
|
const data = formRef.current?.getData();
|
||||||
const success = await actions.roles.patch(roleName, data);
|
const success = await actions.roles.patch(roleName, data);
|
||||||
|
if (success) {
|
||||||
/*notifications.show({
|
navigate(routes.auth.roles.list());
|
||||||
id: `role-${roleName}-update`,
|
}
|
||||||
position: "top-right",
|
|
||||||
title: success ? "Update success" : "Update failed",
|
|
||||||
message: success ? "Role updated successfully" : "Failed to update role",
|
|
||||||
color: !success ? "red" : undefined
|
|
||||||
});*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
|
|||||||
@@ -90,9 +90,16 @@ const renderValue = ({ value, property }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (property === "permissions") {
|
if (property === "permissions") {
|
||||||
|
const max = 3;
|
||||||
|
let permissions = value || [];
|
||||||
|
const count = permissions.length;
|
||||||
|
if (count > max) {
|
||||||
|
permissions = [...permissions.slice(0, max), `+${count - max}`];
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
{[...(value || [])].map((p, i) => (
|
{permissions.map((p, i) => (
|
||||||
<span
|
<span
|
||||||
key={i}
|
key={i}
|
||||||
className="inline-block px-2 py-1.5 text-sm bg-primary/5 rounded font-mono leading-none"
|
className="inline-block px-2 py-1.5 text-sm bg-primary/5 rounded font-mono leading-none"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useApiQuery, useEntityQuery } from "ui/client";
|
|||||||
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
|
import { Message } from "ui/components/display/Message";
|
||||||
import { Dropdown } from "ui/components/overlay/Dropdown";
|
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
@@ -18,7 +19,11 @@ import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
|
|||||||
|
|
||||||
export function DataEntityUpdate({ params }) {
|
export function DataEntityUpdate({ params }) {
|
||||||
const { $data, relations } = useBkndData();
|
const { $data, relations } = useBkndData();
|
||||||
const entity = $data.entity(params.entity as string)!;
|
const entity = $data.entity(params.entity as string);
|
||||||
|
if (!entity) {
|
||||||
|
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
|
||||||
|
}
|
||||||
|
|
||||||
const entityId = Number.parseInt(params.id as string);
|
const entityId = Number.parseInt(params.id as string);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [navigate] = useNavigate();
|
const [navigate] = useNavigate();
|
||||||
@@ -36,7 +41,8 @@ export function DataEntityUpdate({ params }) {
|
|||||||
with: local_relation_refs
|
with: local_relation_refs
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
revalidateOnFocus: false
|
revalidateOnFocus: false,
|
||||||
|
shouldRetryOnError: false
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -81,6 +87,14 @@ export function DataEntityUpdate({ params }) {
|
|||||||
onSubmitted
|
onSubmitted
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!data && !$q.isLoading) {
|
||||||
|
return (
|
||||||
|
<Message.NotFound
|
||||||
|
description={`Entity "${params.entity}" with ID "${entityId}" doesn't exist.`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const makeKey = (key: string | number = "") =>
|
const makeKey = (key: string | number = "") =>
|
||||||
`${params.entity.name}_${entityId}_${String(key)}`;
|
`${params.entity.name}_${entityId}_${String(key)}`;
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { Type } from "core/utils";
|
|||||||
import type { EntityData } from "data";
|
import type { EntityData } from "data";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useEntityMutate } from "ui/client";
|
import { useEntityMutate } from "ui/client";
|
||||||
import { useBknd } from "ui/client/BkndProvider";
|
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
|
import { Message } from "ui/components/display/Message";
|
||||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||||
import { useSearch } from "ui/hooks/use-search";
|
import { useSearch } from "ui/hooks/use-search";
|
||||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
@@ -13,8 +14,14 @@ import { EntityForm } from "ui/modules/data/components/EntityForm";
|
|||||||
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
|
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
|
||||||
|
|
||||||
export function DataEntityCreate({ params }) {
|
export function DataEntityCreate({ params }) {
|
||||||
const { app } = useBknd();
|
const { $data } = useBkndData();
|
||||||
const entity = app.entity(params.entity as string)!;
|
const entity = $data.entity(params.entity as string);
|
||||||
|
if (!entity) {
|
||||||
|
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
|
||||||
|
} else if (entity.type !== "regular") {
|
||||||
|
return <Message.NotAllowed description={`Entity "${params.entity}" cannot be created.`} />;
|
||||||
|
}
|
||||||
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
useBrowserTitle(["Data", entity.label, "Create"]);
|
useBrowserTitle(["Data", entity.label, "Create"]);
|
||||||
|
|
||||||
@@ -43,7 +50,7 @@ export function DataEntityCreate({ params }) {
|
|||||||
|
|
||||||
const { Form, handleSubmit } = useEntityForm({
|
const { Form, handleSubmit } = useEntityForm({
|
||||||
action: "create",
|
action: "create",
|
||||||
entity,
|
entity: entity,
|
||||||
initialData: search.value,
|
initialData: search.value,
|
||||||
onSubmitted
|
onSubmitted
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Type } from "core/utils";
|
import { Type } from "core/utils";
|
||||||
import { querySchema } from "data";
|
import { type Entity, querySchema } from "data";
|
||||||
|
import { Fragment } from "react";
|
||||||
import { TbDots } from "react-icons/tb";
|
import { TbDots } from "react-icons/tb";
|
||||||
import { useApiQuery } from "ui/client";
|
import { useApi, useApiQuery } from "ui/client";
|
||||||
|
import { useBknd } from "ui/client/bknd";
|
||||||
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
@@ -11,6 +13,7 @@ import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
|||||||
import { useSearch } from "ui/hooks/use-search";
|
import { useSearch } from "ui/hooks/use-search";
|
||||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
import { routes, useNavigate } from "ui/lib/routes";
|
import { routes, useNavigate } from "ui/lib/routes";
|
||||||
|
import { useCreateUserModal } from "ui/modules/auth/hooks/use-create-user-modal";
|
||||||
import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
|
import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
|
||||||
|
|
||||||
// @todo: migrate to Typebox
|
// @todo: migrate to Typebox
|
||||||
@@ -29,7 +32,11 @@ const PER_PAGE_OPTIONS = [5, 10, 25];
|
|||||||
|
|
||||||
export function DataEntityList({ params }) {
|
export function DataEntityList({ params }) {
|
||||||
const { $data } = useBkndData();
|
const { $data } = useBkndData();
|
||||||
const entity = $data.entity(params.entity as string)!;
|
const entity = $data.entity(params.entity as string);
|
||||||
|
if (!entity) {
|
||||||
|
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
|
||||||
|
}
|
||||||
|
|
||||||
useBrowserTitle(["Data", entity?.label ?? params.entity]);
|
useBrowserTitle(["Data", entity?.label ?? params.entity]);
|
||||||
const [navigate] = useNavigate();
|
const [navigate] = useNavigate();
|
||||||
const search = useSearch(searchSchema, {
|
const search = useSearch(searchSchema, {
|
||||||
@@ -39,13 +46,14 @@ export function DataEntityList({ params }) {
|
|||||||
|
|
||||||
const $q = useApiQuery(
|
const $q = useApiQuery(
|
||||||
(api) =>
|
(api) =>
|
||||||
api.data.readMany(entity.name, {
|
api.data.readMany(entity?.name as any, {
|
||||||
select: search.value.select,
|
select: search.value.select,
|
||||||
limit: search.value.perPage,
|
limit: search.value.perPage,
|
||||||
offset: (search.value.page - 1) * search.value.perPage,
|
offset: (search.value.page - 1) * search.value.perPage,
|
||||||
sort: search.value.sort
|
sort: search.value.sort
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
enabled: !!entity,
|
||||||
revalidateOnFocus: true,
|
revalidateOnFocus: true,
|
||||||
keepPreviousData: true
|
keepPreviousData: true
|
||||||
}
|
}
|
||||||
@@ -75,14 +83,10 @@ export function DataEntityList({ params }) {
|
|||||||
search.set("perPage", perPage);
|
search.set("perPage", perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!entity) {
|
|
||||||
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUpdating = $q.isLoading && $q.isValidating;
|
const isUpdating = $q.isLoading && $q.isValidating;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment key={entity.name}>
|
||||||
<AppShell.SectionHeader
|
<AppShell.SectionHeader
|
||||||
right={
|
right={
|
||||||
<>
|
<>
|
||||||
@@ -100,14 +104,7 @@ export function DataEntityList({ params }) {
|
|||||||
>
|
>
|
||||||
<IconButton Icon={TbDots} />
|
<IconButton Icon={TbDots} />
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Button
|
<EntityCreateButton entity={entity} />
|
||||||
onClick={() => {
|
|
||||||
navigate(routes.data.entity.create(entity.name));
|
|
||||||
}}
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Create new
|
|
||||||
</Button>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -140,6 +137,40 @@ export function DataEntityList({ params }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppShell.Scrollable>
|
</AppShell.Scrollable>
|
||||||
</>
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EntityCreateButton({ entity }: { entity: Entity }) {
|
||||||
|
const b = useBknd();
|
||||||
|
const createUserModal = useCreateUserModal();
|
||||||
|
|
||||||
|
const [navigate] = useNavigate();
|
||||||
|
if (!entity) return null;
|
||||||
|
if (entity.type !== "regular") {
|
||||||
|
const system = {
|
||||||
|
users: b.app.config.auth.entity_name,
|
||||||
|
media: b.app.config.media.entity_name
|
||||||
|
};
|
||||||
|
if (system.users === entity.name) {
|
||||||
|
return (
|
||||||
|
<Button onClick={createUserModal.open} variant="primary">
|
||||||
|
New User
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
navigate(routes.data.entity.create(entity.name));
|
||||||
|
}}
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
Create new
|
||||||
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user