mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
updated admin to use swr hooks instead of react-query
This commit is contained in:
@@ -15,7 +15,7 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
|
|||||||
|
|
||||||
async loginWithPassword(input: any) {
|
async loginWithPassword(input: any) {
|
||||||
const res = await this.post<AuthResponse>(["password", "login"], input);
|
const res = await this.post<AuthResponse>(["password", "login"], input);
|
||||||
if (res.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;
|
||||||
@@ -23,7 +23,7 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
|
|||||||
|
|
||||||
async registerWithPassword(input: any) {
|
async registerWithPassword(input: any) {
|
||||||
const res = await this.post<AuthResponse>(["password", "register"], input);
|
const res = await this.post<AuthResponse>(["password", "register"], input);
|
||||||
if (res.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;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export function getChangeSet(
|
|||||||
data: EntityData,
|
data: EntityData,
|
||||||
fields: Field[]
|
fields: Field[]
|
||||||
): EntityData {
|
): EntityData {
|
||||||
|
//console.log("getChangeSet", formData, data);
|
||||||
return transform(
|
return transform(
|
||||||
formData,
|
formData,
|
||||||
(acc, _value, key) => {
|
(acc, _value, key) => {
|
||||||
@@ -26,11 +27,12 @@ export function getChangeSet(
|
|||||||
if (!field || field.isVirtual()) return;
|
if (!field || field.isVirtual()) return;
|
||||||
const value = _value === "" ? null : _value;
|
const value = _value === "" ? null : _value;
|
||||||
|
|
||||||
const newValue = field.getValue(value, "submit");
|
// normalize to null if undefined
|
||||||
|
const newValue = field.getValue(value, "submit") || null;
|
||||||
// @todo: add typing for "action"
|
// @todo: add typing for "action"
|
||||||
if (action === "create" || newValue !== data[key]) {
|
if (action === "create" || newValue !== data[key]) {
|
||||||
acc[key] = newValue;
|
acc[key] = newValue;
|
||||||
console.log("changed", {
|
/*console.log("changed", {
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
valueType: typeof value,
|
valueType: typeof value,
|
||||||
@@ -38,7 +40,7 @@ export function getChangeSet(
|
|||||||
newValue,
|
newValue,
|
||||||
new: value,
|
new: value,
|
||||||
sent: acc[key]
|
sent: acc[key]
|
||||||
});
|
});*/
|
||||||
} else {
|
} else {
|
||||||
//console.log("no change", key, value, data[key]);
|
//console.log("no change", key, value, data[key]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { PrimaryFieldType } from "core";
|
import { type PrimaryFieldType, isDebug } from "core";
|
||||||
import { encodeSearch } from "core/utils";
|
import { encodeSearch } from "core/utils";
|
||||||
|
|
||||||
export type { PrimaryFieldType };
|
export type { PrimaryFieldType };
|
||||||
@@ -10,6 +10,7 @@ export type BaseModuleApiOptions = {
|
|||||||
token_transport?: "header" | "cookie" | "none";
|
token_transport?: "header" | "cookie" | "none";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
export type ApiResponse<Data = any> = {
|
export type ApiResponse<Data = any> = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
status: number;
|
status: number;
|
||||||
@@ -47,7 +48,7 @@ export abstract class ModuleApi<Options extends BaseModuleApiOptions = BaseModul
|
|||||||
_input: TInput,
|
_input: TInput,
|
||||||
_query?: Record<string, any> | URLSearchParams,
|
_query?: Record<string, any> | URLSearchParams,
|
||||||
_init?: RequestInit
|
_init?: RequestInit
|
||||||
): FetchPromise<ApiResponse<Data>> {
|
): FetchPromise<ResponseObject<Data>> {
|
||||||
const method = _init?.method ?? "GET";
|
const method = _init?.method ?? "GET";
|
||||||
const input = Array.isArray(_input) ? _input.join("/") : _input;
|
const input = Array.isArray(_input) ? _input.join("/") : _input;
|
||||||
let url = this.getUrl(input);
|
let url = this.getUrl(input);
|
||||||
@@ -138,6 +139,58 @@ export abstract class ModuleApi<Options extends BaseModuleApiOptions = BaseModul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ResponseObject<Body = any, Data = Body extends { data: infer R } ? R : Body> = Data & {
|
||||||
|
raw: Response;
|
||||||
|
res: Response;
|
||||||
|
data: Data;
|
||||||
|
body: Body;
|
||||||
|
ok: boolean;
|
||||||
|
status: number;
|
||||||
|
toJSON(): Data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createResponseProxy<Body = any, Data = any>(
|
||||||
|
raw: Response,
|
||||||
|
body: Body,
|
||||||
|
data?: Data
|
||||||
|
): ResponseObject<Body, Data> {
|
||||||
|
const actualData = data ?? (body as unknown as Data);
|
||||||
|
const _props = ["raw", "body", "ok", "status", "res", "data", "toJSON"];
|
||||||
|
|
||||||
|
return new Proxy(actualData as any, {
|
||||||
|
get(target, prop, receiver) {
|
||||||
|
if (prop === "raw" || prop === "res") return raw;
|
||||||
|
if (prop === "body") return body;
|
||||||
|
if (prop === "data") return data;
|
||||||
|
if (prop === "ok") return raw.ok;
|
||||||
|
if (prop === "status") return raw.status;
|
||||||
|
if (prop === "toJSON") {
|
||||||
|
return () => target;
|
||||||
|
}
|
||||||
|
return Reflect.get(target, prop, receiver);
|
||||||
|
},
|
||||||
|
has(target, prop) {
|
||||||
|
if (_props.includes(prop as string)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Reflect.has(target, prop);
|
||||||
|
},
|
||||||
|
ownKeys(target) {
|
||||||
|
return Array.from(new Set([...Reflect.ownKeys(target), ..._props]));
|
||||||
|
},
|
||||||
|
getOwnPropertyDescriptor(target, prop) {
|
||||||
|
if (_props.includes(prop as string)) {
|
||||||
|
return {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
value: Reflect.get({ raw, body, ok: raw.ok, status: raw.status }, prop)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return Reflect.getOwnPropertyDescriptor(target, prop);
|
||||||
|
}
|
||||||
|
}) as ResponseObject<Body, Data>;
|
||||||
|
}
|
||||||
|
|
||||||
export class FetchPromise<T = ApiResponse<any>> implements Promise<T> {
|
export class FetchPromise<T = ApiResponse<any>> implements Promise<T> {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
[Symbol.toStringTag]: "FetchPromise";
|
[Symbol.toStringTag]: "FetchPromise";
|
||||||
@@ -149,7 +202,10 @@ export class FetchPromise<T = ApiResponse<any>> implements Promise<T> {
|
|||||||
}
|
}
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(): Promise<T> {
|
async execute(): Promise<ResponseObject<T>> {
|
||||||
|
// delay in dev environment
|
||||||
|
isDebug() && (await new Promise((resolve) => setTimeout(resolve, 200)));
|
||||||
|
|
||||||
const fetcher = this.options?.fetcher ?? fetch;
|
const fetcher = this.options?.fetcher ?? fetch;
|
||||||
const res = await fetcher(this.request);
|
const res = await fetcher(this.request);
|
||||||
let resBody: any;
|
let resBody: any;
|
||||||
@@ -165,13 +221,7 @@ export class FetchPromise<T = ApiResponse<any>> implements Promise<T> {
|
|||||||
resBody = await res.text();
|
resBody = await res.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return createResponseProxy<T>(res, resBody, resData);
|
||||||
success: res.ok,
|
|
||||||
status: res.status,
|
|
||||||
body: resBody,
|
|
||||||
data: resData,
|
|
||||||
res
|
|
||||||
} as T;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noThenProperty: it's a promise :)
|
// biome-ignore lint/suspicious/noThenProperty: it's a promise :)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ConfigUpdateResponse } from "modules/server/SystemController";
|
||||||
import { ModuleApi } from "./ModuleApi";
|
import { ModuleApi } from "./ModuleApi";
|
||||||
import type { ModuleConfigs, ModuleKey, ModuleSchemas } from "./ModuleManager";
|
import type { ModuleConfigs, ModuleKey, ModuleSchemas } from "./ModuleManager";
|
||||||
|
|
||||||
@@ -15,37 +16,37 @@ export class SystemApi extends ModuleApi<any> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async readSchema(options?: { config?: boolean; secrets?: boolean }) {
|
readSchema(options?: { config?: boolean; secrets?: boolean }) {
|
||||||
return await this.get<ApiSchemaResponse>("schema", {
|
return this.get<ApiSchemaResponse>("schema", {
|
||||||
config: options?.config ? 1 : 0,
|
config: options?.config ? 1 : 0,
|
||||||
secrets: options?.secrets ? 1 : 0
|
secrets: options?.secrets ? 1 : 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setConfig<Module extends ModuleKey>(
|
setConfig<Module extends ModuleKey>(
|
||||||
module: Module,
|
module: Module,
|
||||||
value: ModuleConfigs[Module],
|
value: ModuleConfigs[Module],
|
||||||
force?: boolean
|
force?: boolean
|
||||||
) {
|
) {
|
||||||
return await this.post<any>(
|
return this.post<ConfigUpdateResponse>(
|
||||||
["config", "set", module].join("/") + `?force=${force ? 1 : 0}`,
|
["config", "set", module].join("/") + `?force=${force ? 1 : 0}`,
|
||||||
value
|
value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addConfig<Module extends ModuleKey>(module: Module, path: string, value: any) {
|
addConfig<Module extends ModuleKey>(module: Module, path: string, value: any) {
|
||||||
return await this.post<any>(["config", "add", module, path], value);
|
return this.post<ConfigUpdateResponse>(["config", "add", module, path], value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async patchConfig<Module extends ModuleKey>(module: Module, path: string, value: any) {
|
patchConfig<Module extends ModuleKey>(module: Module, path: string, value: any) {
|
||||||
return await this.patch<any>(["config", "patch", module, path], value);
|
return this.patch<ConfigUpdateResponse>(["config", "patch", module, path], value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async overwriteConfig<Module extends ModuleKey>(module: Module, path: string, value: any) {
|
overwriteConfig<Module extends ModuleKey>(module: Module, path: string, value: any) {
|
||||||
return await this.put<any>(["config", "overwrite", module, path], value);
|
return this.put<ConfigUpdateResponse>(["config", "overwrite", module, path], value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeConfig<Module extends ModuleKey>(module: Module, path: string) {
|
removeConfig<Module extends ModuleKey>(module: Module, path: string) {
|
||||||
return await this.delete<any>(["config", "remove", module, path]);
|
return this.delete<ConfigUpdateResponse>(["config", "remove", module, path]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,32 @@
|
|||||||
/// <reference types="@cloudflare/workers-types" />
|
/// <reference types="@cloudflare/workers-types" />
|
||||||
|
|
||||||
|
import type { App } from "App";
|
||||||
import type { ClassController } from "core";
|
import type { ClassController } from "core";
|
||||||
import { tbValidator as tb } from "core";
|
import { tbValidator as tb } from "core";
|
||||||
import { StringEnum, Type, TypeInvalidError } from "core/utils";
|
import { StringEnum, Type, TypeInvalidError } from "core/utils";
|
||||||
import { type Context, Hono } from "hono";
|
import { type Context, Hono } from "hono";
|
||||||
import { MODULE_NAMES, type ModuleKey, getDefaultConfig } from "modules/ModuleManager";
|
import {
|
||||||
|
MODULE_NAMES,
|
||||||
|
type ModuleConfigs,
|
||||||
|
type ModuleKey,
|
||||||
|
getDefaultConfig
|
||||||
|
} from "modules/ModuleManager";
|
||||||
import * as SystemPermissions from "modules/permissions";
|
import * as SystemPermissions from "modules/permissions";
|
||||||
import { generateOpenAPI } from "modules/server/openapi";
|
import { generateOpenAPI } from "modules/server/openapi";
|
||||||
import type { App } from "../../App";
|
|
||||||
|
|
||||||
const booleanLike = Type.Transform(Type.String())
|
const booleanLike = Type.Transform(Type.String())
|
||||||
.Decode((v) => v === "1")
|
.Decode((v) => v === "1")
|
||||||
.Encode((v) => (v ? "1" : "0"));
|
.Encode((v) => (v ? "1" : "0"));
|
||||||
|
|
||||||
|
export type ConfigUpdate<Key extends ModuleKey = ModuleKey> = {
|
||||||
|
success: true;
|
||||||
|
module: Key;
|
||||||
|
config: ModuleConfigs[Key];
|
||||||
|
};
|
||||||
|
export type ConfigUpdateResponse<Key extends ModuleKey = ModuleKey> =
|
||||||
|
| ConfigUpdate<Key>
|
||||||
|
| { success: false; type: "type-invalid" | "error" | "unknown"; error?: any; errors?: any };
|
||||||
|
|
||||||
export class SystemController implements ClassController {
|
export class SystemController implements ClassController {
|
||||||
constructor(private readonly app: App) {}
|
constructor(private readonly app: App) {}
|
||||||
|
|
||||||
@@ -60,7 +74,7 @@ export class SystemController implements ClassController {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
async function handleConfigUpdateResponse(c: Context<any>, cb: () => Promise<object>) {
|
async function handleConfigUpdateResponse(c: Context<any>, cb: () => Promise<ConfigUpdate>) {
|
||||||
try {
|
try {
|
||||||
return c.json(await cb(), { status: 202 });
|
return c.json(await cb(), { status: 202 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { Notifications } from "@mantine/notifications";
|
|||||||
import type { ModuleConfigs } from "modules";
|
import type { ModuleConfigs } from "modules";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { BkndProvider, useBknd } from "ui/client/bknd";
|
import { BkndProvider, useBknd } from "ui/client/bknd";
|
||||||
|
import { Logo } from "ui/components/display/Logo";
|
||||||
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
import { FlashMessage } from "ui/modules/server/FlashMessage";
|
import { FlashMessage } from "ui/modules/server/FlashMessage";
|
||||||
import { ClientProvider, type ClientProviderProps } from "./client";
|
import { ClientProvider, type ClientProviderProps } from "./client";
|
||||||
import { createMantineTheme } from "./lib/mantine/theme";
|
import { createMantineTheme } from "./lib/mantine/theme";
|
||||||
@@ -21,7 +23,7 @@ export default function Admin({
|
|||||||
config
|
config
|
||||||
}: BkndAdminProps) {
|
}: BkndAdminProps) {
|
||||||
const Component = (
|
const Component = (
|
||||||
<BkndProvider adminOverride={config}>
|
<BkndProvider adminOverride={config} fallback={<Skeleton theme={config?.color_scheme} />}>
|
||||||
<AdminInternal />
|
<AdminInternal />
|
||||||
</BkndProvider>
|
</BkndProvider>
|
||||||
);
|
);
|
||||||
@@ -51,3 +53,41 @@ function AdminInternal() {
|
|||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Skeleton = ({ theme = "light" }: { theme?: string }) => {
|
||||||
|
return (
|
||||||
|
<div id="bknd-admin" className={(theme ?? "light") + " antialiased"}>
|
||||||
|
<AppShell.Root>
|
||||||
|
<header
|
||||||
|
data-shell="header"
|
||||||
|
className="flex flex-row w-full h-16 gap-2.5 border-muted border-b justify-start bg-muted/10"
|
||||||
|
>
|
||||||
|
<div className="max-h-full flex hover:bg-primary/5 link p-2.5 w-[134px] outline-none">
|
||||||
|
<Logo />
|
||||||
|
</div>
|
||||||
|
<nav className="hidden md:flex flex-row gap-2.5 pl-0 p-2.5 items-center">
|
||||||
|
{[...new Array(5)].map((item, key) => (
|
||||||
|
<AppShell.NavLink key={key} as="span" className="active h-full opacity-50">
|
||||||
|
<div className="w-10 h-3" />
|
||||||
|
</AppShell.NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
<nav className="flex md:hidden flex-row items-center">
|
||||||
|
<AppShell.NavLink as="span" className="active h-full opacity-50">
|
||||||
|
<div className="w-10 h-3" />
|
||||||
|
</AppShell.NavLink>
|
||||||
|
</nav>
|
||||||
|
<div className="flex flex-grow" />
|
||||||
|
<div className="hidden lg:flex flex-row items-center px-4 gap-2 opacity-50">
|
||||||
|
<div className="size-11 rounded-full bg-primary/10" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<AppShell.Content>
|
||||||
|
<div className="flex flex-col w-full h-full justify-center items-center">
|
||||||
|
<span className="font-mono opacity-30">Loading</span>
|
||||||
|
</div>
|
||||||
|
</AppShell.Content>
|
||||||
|
</AppShell.Root>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { getDefaultConfig, getDefaultSchema } from "modules/ModuleManager";
|
import { getDefaultConfig, getDefaultSchema } from "modules/ModuleManager";
|
||||||
import { createContext, startTransition, useContext, useEffect, useRef, useState } from "react";
|
import { createContext, startTransition, useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import { Logo } from "ui/components/display/Logo";
|
||||||
|
import { Link } from "ui/components/wouter/Link";
|
||||||
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
|
import { HeaderNavigation } from "ui/layouts/AppShell/Header";
|
||||||
|
import { Root } from "ui/routes/root";
|
||||||
import type { ModuleConfigs, ModuleSchemas } from "../../modules";
|
import type { ModuleConfigs, ModuleSchemas } from "../../modules";
|
||||||
import { useClient } from "./ClientProvider";
|
import { useClient } from "./ClientProvider";
|
||||||
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
||||||
@@ -22,8 +27,12 @@ export type { TSchemaActions };
|
|||||||
export function BkndProvider({
|
export function BkndProvider({
|
||||||
includeSecrets = false,
|
includeSecrets = false,
|
||||||
adminOverride,
|
adminOverride,
|
||||||
children
|
children,
|
||||||
}: { includeSecrets?: boolean; children: any } & Pick<BkndContext, "adminOverride">) {
|
fallback = null
|
||||||
|
}: { includeSecrets?: boolean; children: any; fallback?: React.ReactNode } & Pick<
|
||||||
|
BkndContext,
|
||||||
|
"adminOverride"
|
||||||
|
>) {
|
||||||
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
|
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
|
||||||
const [schema, setSchema] =
|
const [schema, setSchema] =
|
||||||
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions">>();
|
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions">>();
|
||||||
@@ -37,7 +46,7 @@ export function BkndProvider({
|
|||||||
|
|
||||||
async function fetchSchema(_includeSecrets: boolean = false, force?: boolean) {
|
async function fetchSchema(_includeSecrets: boolean = false, force?: boolean) {
|
||||||
if (withSecrets && !force) return;
|
if (withSecrets && !force) return;
|
||||||
const { body, res } = await client.api.system.readSchema({
|
const res = await client.api.system.readSchema({
|
||||||
config: true,
|
config: true,
|
||||||
secrets: _includeSecrets
|
secrets: _includeSecrets
|
||||||
});
|
});
|
||||||
@@ -57,7 +66,7 @@ export function BkndProvider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const schema = res.ok
|
const schema = res.ok
|
||||||
? body
|
? res.body
|
||||||
: ({
|
: ({
|
||||||
version: 0,
|
version: 0,
|
||||||
schema: getDefaultSchema(),
|
schema: getDefaultSchema(),
|
||||||
@@ -89,7 +98,7 @@ export function BkndProvider({
|
|||||||
fetchSchema(includeSecrets);
|
fetchSchema(includeSecrets);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!fetched || !schema) return null;
|
if (!fetched || !schema) return fallback;
|
||||||
const app = new AppReduced(schema?.config as any);
|
const app = new AppReduced(schema?.config as any);
|
||||||
const actions = getSchemaActions({ client, setSchema, reloadSchema });
|
const actions = getSchemaActions({ client, setSchema, reloadSchema });
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Api } from "Api";
|
import type { Api } from "Api";
|
||||||
import type { FetchPromise } from "modules/ModuleApi";
|
import type { FetchPromise, ResponseObject } from "modules/ModuleApi";
|
||||||
import useSWR, { type SWRConfiguration, useSWRConfig } from "swr";
|
import useSWR, { type SWRConfiguration } from "swr";
|
||||||
import { useClient } from "ui/client/ClientProvider";
|
import { useClient } from "ui/client/ClientProvider";
|
||||||
|
|
||||||
export const useApi = () => {
|
export const useApi = () => {
|
||||||
@@ -8,13 +8,16 @@ export const useApi = () => {
|
|||||||
return client.api;
|
return client.api;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useApiQuery = <Data, RefineFn extends (data: Data) => any = (data: Data) => Data>(
|
export const useApiQuery = <
|
||||||
|
Data,
|
||||||
|
RefineFn extends (data: ResponseObject<Data>) => any = (data: ResponseObject<Data>) => Data
|
||||||
|
>(
|
||||||
fn: (api: Api) => FetchPromise<Data>,
|
fn: (api: Api) => FetchPromise<Data>,
|
||||||
options?: SWRConfiguration & { enabled?: boolean; refine?: RefineFn }
|
options?: SWRConfiguration & { enabled?: boolean; refine?: RefineFn }
|
||||||
) => {
|
) => {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const promise = fn(api);
|
const promise = fn(api);
|
||||||
const refine = options?.refine ?? ((data: Data) => data);
|
const refine = options?.refine ?? ((data: ResponseObject<Data>) => data);
|
||||||
const fetcher = () => promise.execute().then(refine);
|
const fetcher = () => promise.execute().then(refine);
|
||||||
const key = promise.key();
|
const key = promise.key();
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
import type { PrimaryFieldType } from "core";
|
import type { PrimaryFieldType } from "core";
|
||||||
import { objectTransform } from "core/utils";
|
import { objectTransform } from "core/utils";
|
||||||
import type { EntityData, RepoQuery } from "data";
|
import type { EntityData, RepoQuery } from "data";
|
||||||
|
import type { ResponseObject } from "modules/ModuleApi";
|
||||||
import useSWR, { type SWRConfiguration } from "swr";
|
import useSWR, { type SWRConfiguration } from "swr";
|
||||||
import { useApi } from "ui/client";
|
import { useApi } from "ui/client";
|
||||||
|
|
||||||
|
export class UseEntityApiError<Payload = any> extends Error {
|
||||||
|
constructor(
|
||||||
|
public payload: Payload,
|
||||||
|
public response: Response,
|
||||||
|
message?: string
|
||||||
|
) {
|
||||||
|
super(message ?? "UseEntityApiError");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const useEntity = <
|
export const useEntity = <
|
||||||
Entity extends string,
|
Entity extends string,
|
||||||
Id extends PrimaryFieldType | undefined = undefined
|
Id extends PrimaryFieldType | undefined = undefined
|
||||||
@@ -16,18 +27,27 @@ export const useEntity = <
|
|||||||
return {
|
return {
|
||||||
create: async (input: EntityData) => {
|
create: async (input: EntityData) => {
|
||||||
const res = await api.createOne(entity, input);
|
const res = await api.createOne(entity, input);
|
||||||
return res.data;
|
if (!res.ok) {
|
||||||
|
throw new UseEntityApiError(res.data, res.res, "Failed to create entity");
|
||||||
|
}
|
||||||
|
return res;
|
||||||
},
|
},
|
||||||
read: async (query: Partial<RepoQuery> = {}) => {
|
read: async (query: Partial<RepoQuery> = {}) => {
|
||||||
const res = id ? await api.readOne(entity, id!, query) : await api.readMany(entity, query);
|
const res = id ? await api.readOne(entity, id!, query) : await api.readMany(entity, query);
|
||||||
return res.data;
|
if (!res.ok) {
|
||||||
|
throw new UseEntityApiError(res.data, res.res, "Failed to read entity");
|
||||||
|
}
|
||||||
|
return res;
|
||||||
},
|
},
|
||||||
update: async (input: Partial<EntityData>, _id: PrimaryFieldType | undefined = id) => {
|
update: async (input: Partial<EntityData>, _id: PrimaryFieldType | undefined = id) => {
|
||||||
if (!_id) {
|
if (!_id) {
|
||||||
throw new Error("id is required");
|
throw new Error("id is required");
|
||||||
}
|
}
|
||||||
const res = await api.updateOne(entity, _id, input);
|
const res = await api.updateOne(entity, _id, input);
|
||||||
return res.data;
|
if (!res.ok) {
|
||||||
|
throw new UseEntityApiError(res.data, res.res, "Failed to update entity");
|
||||||
|
}
|
||||||
|
return res;
|
||||||
},
|
},
|
||||||
_delete: async (_id: PrimaryFieldType | undefined = id) => {
|
_delete: async (_id: PrimaryFieldType | undefined = id) => {
|
||||||
if (!_id) {
|
if (!_id) {
|
||||||
@@ -35,7 +55,10 @@ export const useEntity = <
|
|||||||
}
|
}
|
||||||
|
|
||||||
const res = await api.deleteOne(entity, _id);
|
const res = await api.deleteOne(entity, _id);
|
||||||
return res.data;
|
if (!res.ok) {
|
||||||
|
throw new UseEntityApiError(res.data, res.res, "Failed to delete entity");
|
||||||
|
}
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -47,24 +70,30 @@ export const useEntityQuery = <
|
|||||||
entity: Entity,
|
entity: Entity,
|
||||||
id?: Id,
|
id?: Id,
|
||||||
query?: Partial<RepoQuery>,
|
query?: Partial<RepoQuery>,
|
||||||
options?: SWRConfiguration
|
options?: SWRConfiguration & { enabled?: boolean }
|
||||||
) => {
|
) => {
|
||||||
const api = useApi().data;
|
const api = useApi().data;
|
||||||
const key = [...(api.options?.basepath?.split("/") ?? []), entity, ...(id ? [id] : [])].filter(
|
const key =
|
||||||
Boolean
|
options?.enabled !== false
|
||||||
);
|
? [...(api.options?.basepath?.split("/") ?? []), entity, ...(id ? [id] : [])].filter(
|
||||||
|
Boolean
|
||||||
|
)
|
||||||
|
: null;
|
||||||
const { read, ...actions } = useEntity(entity, id) as any;
|
const { read, ...actions } = useEntity(entity, id) as any;
|
||||||
const fetcher = id ? () => read(query) : () => null;
|
const fetcher = () => read(query);
|
||||||
const swr = useSWR<EntityData>(id ? key : null, fetcher, options);
|
|
||||||
|
type T = Awaited<ReturnType<(typeof api)[Id extends undefined ? "readMany" : "readOne"]>>;
|
||||||
|
const swr = useSWR<T>(key, fetcher, {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
keepPreviousData: false,
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
|
||||||
const mapped = objectTransform(actions, (action) => {
|
const mapped = objectTransform(actions, (action) => {
|
||||||
if (action === "read") return;
|
if (action === "read") return;
|
||||||
|
|
||||||
return async (...args) => {
|
return async (...args) => {
|
||||||
return swr.mutate(async () => {
|
return swr.mutate(action(...args)) as any;
|
||||||
const res = await action(...args);
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}) as Omit<ReturnType<typeof useEntity<Entity, Id>>, "read">;
|
}) as Omit<ReturnType<typeof useEntity<Entity, Id>>, "read">;
|
||||||
|
|
||||||
@@ -74,3 +103,18 @@ export const useEntityQuery = <
|
|||||||
key
|
key
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useEntityMutate = <
|
||||||
|
Entity extends string,
|
||||||
|
Id extends PrimaryFieldType | undefined = undefined
|
||||||
|
>(
|
||||||
|
entity: Entity,
|
||||||
|
id?: Id,
|
||||||
|
options?: SWRConfiguration
|
||||||
|
) => {
|
||||||
|
const { data, ...$q } = useEntityQuery(entity, id, undefined, {
|
||||||
|
...options,
|
||||||
|
enabled: false
|
||||||
|
});
|
||||||
|
return $q;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { type NotificationData, notifications } from "@mantine/notifications";
|
import { type NotificationData, notifications } from "@mantine/notifications";
|
||||||
import { ucFirst } from "core/utils";
|
import { ucFirst } from "core/utils";
|
||||||
import type { ApiResponse, ModuleConfigs } from "../../../modules";
|
import type { ModuleConfigs } from "modules";
|
||||||
|
import type { ResponseObject } from "modules/ModuleApi";
|
||||||
|
import type { ConfigUpdateResponse } from "modules/server/SystemController";
|
||||||
import type { AppQueryClient } from "../utils/AppQueryClient";
|
import type { AppQueryClient } from "../utils/AppQueryClient";
|
||||||
|
|
||||||
export type SchemaActionsProps = {
|
export type SchemaActionsProps = {
|
||||||
@@ -14,10 +16,10 @@ export type TSchemaActions = ReturnType<typeof getSchemaActions>;
|
|||||||
export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActionsProps) {
|
export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActionsProps) {
|
||||||
const api = client.api;
|
const api = client.api;
|
||||||
|
|
||||||
async function handleConfigUpdate(
|
async function handleConfigUpdate<Module extends keyof ModuleConfigs>(
|
||||||
action: string,
|
action: string,
|
||||||
module: string,
|
module: Module,
|
||||||
res: ApiResponse,
|
res: ResponseObject<ConfigUpdateResponse<Module>>,
|
||||||
path?: string
|
path?: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const base: Partial<NotificationData> = {
|
const base: Partial<NotificationData> = {
|
||||||
@@ -26,7 +28,7 @@ export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActi
|
|||||||
autoClose: 3000
|
autoClose: 3000
|
||||||
};
|
};
|
||||||
|
|
||||||
if (res.res.ok && res.body.success) {
|
if (res.success === true) {
|
||||||
console.log("update config", action, module, path, res.body);
|
console.log("update config", action, module, path, res.body);
|
||||||
if (res.body.success) {
|
if (res.body.success) {
|
||||||
setSchema((prev) => {
|
setSchema((prev) => {
|
||||||
@@ -35,7 +37,7 @@ export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActi
|
|||||||
...prev,
|
...prev,
|
||||||
config: {
|
config: {
|
||||||
...prev.config,
|
...prev.config,
|
||||||
[module]: res.body.config
|
[module]: res.config
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -47,18 +49,18 @@ export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActi
|
|||||||
color: "blue",
|
color: "blue",
|
||||||
message: `Operation ${action.toUpperCase()} at ${module}${path ? "." + path : ""}`
|
message: `Operation ${action.toUpperCase()} at ${module}${path ? "." + path : ""}`
|
||||||
});
|
});
|
||||||
return true;
|
} else {
|
||||||
|
notifications.show({
|
||||||
|
...base,
|
||||||
|
title: `Config Update failed: ${ucFirst(module)}${path ? "." + path : ""}`,
|
||||||
|
color: "red",
|
||||||
|
withCloseButton: true,
|
||||||
|
autoClose: false,
|
||||||
|
message: res.error ?? "Failed to complete config update"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.show({
|
return res.success;
|
||||||
...base,
|
|
||||||
title: `Config Update failed: ${ucFirst(module)}${path ? "." + path : ""}`,
|
|
||||||
color: "red",
|
|
||||||
withCloseButton: true,
|
|
||||||
autoClose: false,
|
|
||||||
message: res.body.error ?? "Failed to complete config update"
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -72,7 +74,7 @@ export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActi
|
|||||||
return await handleConfigUpdate("set", module, res);
|
return await handleConfigUpdate("set", module, res);
|
||||||
},
|
},
|
||||||
patch: async <Module extends keyof ModuleConfigs>(
|
patch: async <Module extends keyof ModuleConfigs>(
|
||||||
module: keyof ModuleConfigs,
|
module: Module,
|
||||||
path: string,
|
path: string,
|
||||||
value: any
|
value: any
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
@@ -80,25 +82,18 @@ export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActi
|
|||||||
return await handleConfigUpdate("patch", module, res, path);
|
return await handleConfigUpdate("patch", module, res, path);
|
||||||
},
|
},
|
||||||
overwrite: async <Module extends keyof ModuleConfigs>(
|
overwrite: async <Module extends keyof ModuleConfigs>(
|
||||||
module: keyof ModuleConfigs,
|
module: Module,
|
||||||
path: string,
|
path: string,
|
||||||
value: any
|
value: any
|
||||||
) => {
|
) => {
|
||||||
const res = await api.system.overwriteConfig(module, path, value);
|
const res = await api.system.overwriteConfig(module, path, value);
|
||||||
return await handleConfigUpdate("overwrite", module, res, path);
|
return await handleConfigUpdate("overwrite", module, res, path);
|
||||||
},
|
},
|
||||||
add: async <Module extends keyof ModuleConfigs>(
|
add: async <Module extends keyof ModuleConfigs>(module: Module, path: string, value: any) => {
|
||||||
module: keyof ModuleConfigs,
|
|
||||||
path: string,
|
|
||||||
value: any
|
|
||||||
) => {
|
|
||||||
const res = await api.system.addConfig(module, path, value);
|
const res = await api.system.addConfig(module, path, value);
|
||||||
return await handleConfigUpdate("add", module, res, path);
|
return await handleConfigUpdate("add", module, res, path);
|
||||||
},
|
},
|
||||||
remove: async <Module extends keyof ModuleConfigs>(
|
remove: async <Module extends keyof ModuleConfigs>(module: Module, path: string) => {
|
||||||
module: keyof ModuleConfigs,
|
|
||||||
path: string
|
|
||||||
) => {
|
|
||||||
const res = await api.system.removeConfig(module, path);
|
const res = await api.system.removeConfig(module, path);
|
||||||
return await handleConfigUpdate("remove", module, res, path);
|
return await handleConfigUpdate("remove", module, res, path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,12 +36,10 @@ export class AppQueryClient {
|
|||||||
state: (): (AuthResponse & { verified: boolean }) | undefined => {
|
state: (): (AuthResponse & { verified: boolean }) | undefined => {
|
||||||
return this.api.getAuthState() as any;
|
return this.api.getAuthState() as any;
|
||||||
},
|
},
|
||||||
login: async (data: { email: string; password: string }): Promise<
|
login: async (data: { email: string; password: string }) => {
|
||||||
ApiResponse<AuthResponse>
|
|
||||||
> => {
|
|
||||||
return await this.api.auth.loginWithPassword(data);
|
return await this.api.auth.loginWithPassword(data);
|
||||||
},
|
},
|
||||||
register: async (data: any): Promise<ApiResponse<AuthResponse>> => {
|
register: async (data: any) => {
|
||||||
return await this.api.auth.registerWithPassword(data);
|
return await this.api.auth.registerWithPassword(data);
|
||||||
},
|
},
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
@@ -57,7 +55,7 @@ export class AppQueryClient {
|
|||||||
//console.log("verifiying");
|
//console.log("verifiying");
|
||||||
const res = await this.api.auth.me();
|
const res = await this.api.auth.me();
|
||||||
//console.log("verifying result", res);
|
//console.log("verifying result", res);
|
||||||
if (!res.res.ok || !res.body.user) {
|
if (!res.ok || !res.body.user) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +88,7 @@ export class AppQueryClient {
|
|||||||
typeof filename === "string" ? filename : filename.path
|
typeof filename === "string" ? filename : filename.path
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.res.ok) {
|
if (res.ok) {
|
||||||
queryClient.invalidateQueries({ queryKey: ["data", "entity", "media"] });
|
queryClient.invalidateQueries({ queryKey: ["data", "entity", "media"] });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { useBknd } from "../../client/BkndProvider";
|
import { useBknd } from "ui/client/bknd";
|
||||||
|
|
||||||
export function Logo({ scale = 0.2, fill }: { scale?: number; fill?: string }) {
|
export function Logo({
|
||||||
const { app } = useBknd();
|
scale = 0.2,
|
||||||
const theme = app.getAdminConfig().color_scheme;
|
fill,
|
||||||
const svgFill = fill ? fill : theme === "light" ? "black" : "white";
|
theme = "light"
|
||||||
|
}: { scale?: number; fill?: string; theme?: string }) {
|
||||||
|
const $bknd = useBknd();
|
||||||
|
const _theme = theme ?? $bknd?.app?.getAdminConfig().color_scheme ?? "light";
|
||||||
|
const svgFill = fill ? fill : _theme === "light" ? "black" : "white";
|
||||||
|
|
||||||
const dim = {
|
const dim = {
|
||||||
width: Math.round(578 * scale),
|
width: Math.round(578 * scale),
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const Check = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DataTableProps<Data> = {
|
export type DataTableProps<Data> = {
|
||||||
data: Data[];
|
data: Data[] | null; // "null" implies loading
|
||||||
columns?: string[];
|
columns?: string[];
|
||||||
checkable?: boolean;
|
checkable?: boolean;
|
||||||
onClickRow?: (row: Data) => void;
|
onClickRow?: (row: Data) => void;
|
||||||
@@ -71,10 +71,10 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
|
|||||||
renderValue,
|
renderValue,
|
||||||
onClickNew
|
onClickNew
|
||||||
}: DataTableProps<Data>) {
|
}: DataTableProps<Data>) {
|
||||||
total = total || data.length;
|
total = total || data?.length || 0;
|
||||||
page = page || 1;
|
page = page || 1;
|
||||||
|
|
||||||
const select = columns && columns.length > 0 ? columns : Object.keys(data[0] || {});
|
const select = columns && columns.length > 0 ? columns : Object.keys(data?.[0] || {});
|
||||||
const pages = Math.max(Math.ceil(total / perPage), 1);
|
const pages = Math.max(Math.ceil(total / perPage), 1);
|
||||||
const CellRender = renderValue || CellValue;
|
const CellRender = renderValue || CellValue;
|
||||||
|
|
||||||
@@ -129,7 +129,9 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
|
|||||||
<tr>
|
<tr>
|
||||||
<td colSpan={select.length + (checkable ? 1 : 0)}>
|
<td colSpan={select.length + (checkable ? 1 : 0)}>
|
||||||
<div className="flex flex-col gap-2 p-8 justify-center items-center border-t border-muted">
|
<div className="flex flex-col gap-2 p-8 justify-center items-center border-t border-muted">
|
||||||
<i className="opacity-50">No data to show</i>
|
<i className="opacity-50">
|
||||||
|
{Array.isArray(data) ? "No data to show" : "Loading..."}
|
||||||
|
</i>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -188,7 +190,12 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center justify-between">
|
<div className="flex flex-row items-center justify-between">
|
||||||
<div className="hidden md:flex text-primary/40">
|
<div className="hidden md:flex text-primary/40">
|
||||||
<TableDisplay perPage={perPage} page={page} items={data.length} total={total} />
|
<TableDisplay
|
||||||
|
perPage={perPage}
|
||||||
|
page={page}
|
||||||
|
items={data?.length || 0}
|
||||||
|
total={total}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-2 md:gap-10 items-center">
|
<div className="flex flex-row gap-2 md:gap-10 items-center">
|
||||||
{perPageOptions && (
|
{perPageOptions && (
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { useClickOutside, useDisclosure, useHotkeys, useViewportSize } from "@mantine/hooks";
|
import { useClickOutside, useHotkeys } from "@mantine/hooks";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
|
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
|
||||||
import { ucFirst } from "core/utils";
|
|
||||||
import { throttle } from "lodash-es";
|
import { throttle } from "lodash-es";
|
||||||
import { type ComponentProps, createContext, useContext, useEffect, useRef, useState } from "react";
|
import { type ComponentProps, useEffect, useRef, useState } from "react";
|
||||||
import type { IconType } from "react-icons";
|
import type { IconType } from "react-icons";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
import { AppShellProvider, useAppShell } from "ui/layouts/AppShell/use-appshell";
|
import { AppShellProvider, useAppShell } from "ui/layouts/AppShell/use-appshell";
|
||||||
import { Link } from "wouter";
|
|
||||||
import { useEvent } from "../../hooks/use-event";
|
import { useEvent } from "../../hooks/use-event";
|
||||||
|
|
||||||
export function Root({ children }) {
|
export function Root({ children }) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { useNavigate } from "ui/lib/routes";
|
|||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { NavLink } from "./AppShell";
|
import { NavLink } from "./AppShell";
|
||||||
|
|
||||||
function HeaderNavigation() {
|
export function HeaderNavigation() {
|
||||||
const [location, navigate] = useLocation();
|
const [location, navigate] = useLocation();
|
||||||
|
|
||||||
const items: {
|
const items: {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ucFirst } from "core/utils";
|
|||||||
import type { EntityData, RelationField } from "data";
|
import type { EntityData, RelationField } from "data";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { TbEye } from "react-icons/tb";
|
import { TbEye } from "react-icons/tb";
|
||||||
import { useClient } from "ui/client";
|
import { useClient, useEntityQuery } from "ui/client";
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import * as Formy from "ui/components/form/Formy";
|
import * as Formy from "ui/components/form/Formy";
|
||||||
@@ -31,25 +31,21 @@ export function EntityRelationalFormField({
|
|||||||
const { app } = useBknd();
|
const { app } = useBknd();
|
||||||
const entity = app.entity(field.target())!;
|
const entity = app.entity(field.target())!;
|
||||||
const [query, setQuery] = useState<any>({ limit: 10, page: 1, perPage: 10 });
|
const [query, setQuery] = useState<any>({ limit: 10, page: 1, perPage: 10 });
|
||||||
const [location, navigate] = useLocation();
|
const [, navigate] = useLocation();
|
||||||
const ref = useRef<any>(null);
|
const ref = useRef<any>(null);
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const container = useEntities(
|
const $q = useEntityQuery(field.target(), undefined, {
|
||||||
field.target(),
|
limit: query.limit,
|
||||||
{
|
offset: (query.page - 1) * query.limit
|
||||||
limit: query.limit,
|
});
|
||||||
offset: (query.page - 1) * query.limit
|
|
||||||
//select: entity.getSelect(undefined, "form")
|
|
||||||
},
|
|
||||||
{ enabled: true }
|
|
||||||
);
|
|
||||||
const [_value, _setValue] = useState<{ id: number | undefined; [key: string]: any }>();
|
const [_value, _setValue] = useState<{ id: number | undefined; [key: string]: any }>();
|
||||||
|
|
||||||
const referenceField = data?.[field.reference()];
|
const referenceField = data?.[field.reference()];
|
||||||
const relationalField = data?.[field.name];
|
const relationalField = data?.[field.name];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
_setValue(data?.[field.reference()]);
|
const value = data?.[field.reference()];
|
||||||
|
_setValue(value);
|
||||||
}, [referenceField]);
|
}, [referenceField]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -57,62 +53,40 @@ export function EntityRelationalFormField({
|
|||||||
const rel_value = field.target();
|
const rel_value = field.target();
|
||||||
if (!rel_value || !relationalField) return;
|
if (!rel_value || !relationalField) return;
|
||||||
|
|
||||||
console.log("-- need to fetch", field.target(), relationalField);
|
|
||||||
const fetched = await client.api.data.readOne(field.target(), relationalField);
|
const fetched = await client.api.data.readOne(field.target(), relationalField);
|
||||||
if (fetched.res.ok && fetched.data) {
|
if (fetched.ok && fetched.data) {
|
||||||
_setValue(fetched.data as any);
|
_setValue(fetched.data as any);
|
||||||
}
|
}
|
||||||
console.log("-- fetched", fetched);
|
|
||||||
|
|
||||||
console.log("relation", {
|
|
||||||
referenceField,
|
|
||||||
relationalField,
|
|
||||||
data,
|
|
||||||
field,
|
|
||||||
entity
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
}, [relationalField]);
|
}, [relationalField]);
|
||||||
|
|
||||||
/*const initialValue: { id: number | undefined; [key: string]: any } = data?.[
|
|
||||||
field.reference()
|
|
||||||
] ?? {
|
|
||||||
id: data?.[field.name],
|
|
||||||
};*/
|
|
||||||
|
|
||||||
function handleViewItem(e: React.MouseEvent<HTMLButtonElement>) {
|
function handleViewItem(e: React.MouseEvent<HTMLButtonElement>) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
console.log("yo");
|
|
||||||
if (_value) {
|
if (_value) {
|
||||||
navigate(routes.data.entity.edit(entity.name, _value.id as any));
|
navigate(routes.data.entity.edit(entity.name, _value.id as any));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*console.log(
|
|
||||||
"relationfield:data",
|
|
||||||
{ _value, initialValue },
|
|
||||||
data,
|
|
||||||
field.reference(),
|
|
||||||
entity,
|
|
||||||
//container.entity,
|
|
||||||
//data[field.reference()],
|
|
||||||
data?.[field.name],
|
|
||||||
field,
|
|
||||||
);*/
|
|
||||||
|
|
||||||
// fix missing value on fields that are required
|
// fix missing value on fields that are required
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (field.isRequired() && !fieldApi.state.value) {
|
if (field.isRequired() && !fieldApi.state.value) {
|
||||||
fieldApi.setValue(container.data?.[0]?.id);
|
const firstValue = $q.data?.[0];
|
||||||
|
if (!firstValue) return;
|
||||||
|
|
||||||
|
console.warn("setting first value because field is required", field.name, firstValue.id);
|
||||||
|
fieldApi.setValue(firstValue.id);
|
||||||
|
_setValue(firstValue as any);
|
||||||
}
|
}
|
||||||
}, [container.data]);
|
}, [$q.data]);
|
||||||
|
|
||||||
|
const fetching = $q.isLoading || $q.isValidating;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formy.Group>
|
<Formy.Group>
|
||||||
<Formy.Label htmlFor={fieldApi.name}>{field.getLabel()}</Formy.Label>
|
<Formy.Label htmlFor={fieldApi.name}>{field.getLabel()}</Formy.Label>
|
||||||
<div
|
<div
|
||||||
data-disabled={!Array.isArray(container.data) || disabled ? 1 : undefined}
|
data-disabled={fetching || disabled ? 1 : undefined}
|
||||||
className="data-[disabled]:opacity-70 data-[disabled]:pointer-events-none"
|
className="data-[disabled]:opacity-70 data-[disabled]:pointer-events-none"
|
||||||
>
|
>
|
||||||
<Popover
|
<Popover
|
||||||
@@ -120,7 +94,7 @@ export function EntityRelationalFormField({
|
|||||||
className=""
|
className=""
|
||||||
target={({ toggle }) => (
|
target={({ toggle }) => (
|
||||||
<PopoverTable
|
<PopoverTable
|
||||||
container={container}
|
container={$q.data}
|
||||||
entity={entity}
|
entity={entity}
|
||||||
query={query}
|
query={query}
|
||||||
toggle={toggle}
|
toggle={toggle}
|
||||||
@@ -198,28 +172,6 @@ export function EntityRelationalFormField({
|
|||||||
onChange={console.log}
|
onChange={console.log}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
{/*<Formy.Select
|
|
||||||
ref={ref}
|
|
||||||
name={fieldApi.name}
|
|
||||||
id={fieldApi.name}
|
|
||||||
value={fieldApi.state.value}
|
|
||||||
data-value={fieldApi.state.value}
|
|
||||||
onBlur={fieldApi.handleBlur}
|
|
||||||
onChange={handleUpdate}
|
|
||||||
disabled={!Array.isArray(container.data)}
|
|
||||||
>
|
|
||||||
{container.data ? (
|
|
||||||
<>
|
|
||||||
{emptyOption}
|
|
||||||
{!field.isRequired() && emptyOption}
|
|
||||||
{container.data?.map(renderRow)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<option value={undefined} disabled>
|
|
||||||
Loading...
|
|
||||||
</option>
|
|
||||||
)}
|
|
||||||
</Formy.Select>*/}
|
|
||||||
</Formy.Group>
|
</Formy.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,18 +19,16 @@ export function useEntityForm({
|
|||||||
// @todo: check if virtual must be filtered
|
// @todo: check if virtual must be filtered
|
||||||
const fields = entity.getFillableFields(action, true);
|
const fields = entity.getFillableFields(action, true);
|
||||||
|
|
||||||
console.log("useEntityForm:data", data);
|
|
||||||
|
|
||||||
// filter defaultValues to only contain fillable fields
|
// filter defaultValues to only contain fillable fields
|
||||||
const defaultValues = getDefaultValues(fields, data);
|
const defaultValues = getDefaultValues(fields, data);
|
||||||
console.log("useEntityForm:defaultValues", data);
|
//console.log("useEntityForm", { data, defaultValues });
|
||||||
|
|
||||||
const Form = useForm({
|
const Form = useForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
validators: {
|
validators: {
|
||||||
onSubmitAsync: async ({ value }): Promise<any> => {
|
onSubmitAsync: async ({ value }): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
console.log("validating", value, entity.isValidData(value, action));
|
//console.log("validating", value, entity.isValidData(value, action));
|
||||||
entity.isValidData(value, action, true);
|
entity.isValidData(value, action, true);
|
||||||
return undefined;
|
return undefined;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -40,7 +38,7 @@ export function useEntityForm({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSubmit: async ({ value, formApi }) => {
|
onSubmit: async ({ value, formApi }) => {
|
||||||
console.log("onSubmit", value);
|
//console.log("onSubmit", value);
|
||||||
if (!entity.isValidData(value, action)) {
|
if (!entity.isValidData(value, action)) {
|
||||||
console.error("invalid data", value);
|
console.error("invalid data", value);
|
||||||
return;
|
return;
|
||||||
@@ -49,7 +47,7 @@ export function useEntityForm({
|
|||||||
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
const changeSet = getChangeSet(action, value, data, fields);
|
const changeSet = getChangeSet(action, value, data, fields);
|
||||||
console.log("changesSet", action, changeSet);
|
//console.log("changesSet", action, changeSet, { data });
|
||||||
|
|
||||||
// only submit change set if there were changes
|
// only submit change set if there were changes
|
||||||
await onSubmitted?.(Object.keys(changeSet).length === 0 ? undefined : changeSet);
|
await onSubmitted?.(Object.keys(changeSet).length === 0 ? undefined : changeSet);
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ import { ucFirst } from "core/utils";
|
|||||||
import type { Entity, EntityData, EntityRelation } from "data";
|
import type { Entity, EntityData, EntityRelation } from "data";
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { TbDots } from "react-icons/tb";
|
import { TbDots } from "react-icons/tb";
|
||||||
import { useClient } from "ui/client";
|
import { useClient, 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 { Dropdown } from "ui/components/overlay/Dropdown";
|
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||||
import { useEntity } from "ui/container";
|
|
||||||
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";
|
||||||
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
|
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
|
||||||
@@ -25,22 +24,23 @@ export function DataEntityUpdate({ params }) {
|
|||||||
const [navigate] = useNavigate();
|
const [navigate] = useNavigate();
|
||||||
useBrowserTitle(["Data", entity.label, `#${entityId}`]);
|
useBrowserTitle(["Data", entity.label, `#${entityId}`]);
|
||||||
const targetRelations = relations.listableRelationsOf(entity);
|
const targetRelations = relations.listableRelationsOf(entity);
|
||||||
//console.log("targetRelations", targetRelations, relations.relationsOf(entity));
|
|
||||||
// filter out polymorphic for now
|
|
||||||
//.filter((r) => r.type() !== "poly");
|
|
||||||
const local_relation_refs = relations
|
const local_relation_refs = relations
|
||||||
.sourceRelationsOf(entity)
|
.sourceRelationsOf(entity)
|
||||||
?.map((r) => r.other(entity).reference);
|
?.map((r) => r.other(entity).reference);
|
||||||
|
|
||||||
const container = useEntity(entity.name, entityId, {
|
const $q = useEntityQuery(
|
||||||
fetch: {
|
entity.name,
|
||||||
query: {
|
entityId,
|
||||||
with: local_relation_refs
|
{
|
||||||
}
|
with: local_relation_refs
|
||||||
|
},
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
function goBack(state?: Record<string, any>) {
|
function goBack() {
|
||||||
window.history.go(-1);
|
window.history.go(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,43 +52,39 @@ export function DataEntityUpdate({ params }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await container.actions.update(changeSet);
|
try {
|
||||||
console.log("update:res", res);
|
await $q.update(changeSet);
|
||||||
if (res.data?.error) {
|
if (error) setError(null);
|
||||||
setError(res.data.error);
|
|
||||||
} else {
|
|
||||||
error && setError(null);
|
|
||||||
goBack();
|
goBack();
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : "Failed to update");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
if (confirm("Are you sure to delete?")) {
|
if (confirm("Are you sure to delete?")) {
|
||||||
const res = await container.actions.remove();
|
try {
|
||||||
if (res.error) {
|
await $q._delete();
|
||||||
setError(res.error);
|
if (error) setError(null);
|
||||||
} else {
|
|
||||||
error && setError(null);
|
|
||||||
goBack();
|
goBack();
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : "Failed to delete");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = $q.data;
|
||||||
const { Form, handleSubmit } = useEntityForm({
|
const { Form, handleSubmit } = useEntityForm({
|
||||||
action: "update",
|
action: "update",
|
||||||
entity,
|
entity,
|
||||||
initialData: container.data,
|
initialData: $q.data?.toJSON(),
|
||||||
onSubmitted
|
onSubmitted
|
||||||
});
|
});
|
||||||
//console.log("form.data", Form.state.values, container.data);
|
|
||||||
|
|
||||||
const makeKey = (key: string | number = "") =>
|
const makeKey = (key: string | number = "") =>
|
||||||
`${params.entity.name}_${entityId}_${String(key)}`;
|
`${params.entity.name}_${entityId}_${String(key)}`;
|
||||||
|
|
||||||
const fieldsDisabled =
|
const fieldsDisabled = $q.isLoading || $q.isValidating || Form.state.isSubmitting;
|
||||||
container.raw.fetch?.isLoading ||
|
|
||||||
container.status.fetch.isUpdating ||
|
|
||||||
Form.state.isSubmitting;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={makeKey()}>
|
<Fragment key={makeKey()}>
|
||||||
@@ -103,7 +99,7 @@ export function DataEntityUpdate({ params }) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
bkndModals.open("debug", {
|
bkndModals.open("debug", {
|
||||||
data: {
|
data: {
|
||||||
data: container.data as any,
|
data: data as any,
|
||||||
entity: entity.toJSON(),
|
entity: entity.toJSON(),
|
||||||
schema: entity.toSchema(true),
|
schema: entity.toSchema(true),
|
||||||
form: Form.state.values,
|
form: Form.state.values,
|
||||||
@@ -165,7 +161,7 @@ export function DataEntityUpdate({ params }) {
|
|||||||
entityId={entityId}
|
entityId={entityId}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
fieldsDisabled={fieldsDisabled}
|
fieldsDisabled={fieldsDisabled}
|
||||||
data={container.data ?? undefined}
|
data={data ?? undefined}
|
||||||
Form={Form}
|
Form={Form}
|
||||||
action="update"
|
action="update"
|
||||||
className="flex flex-grow flex-col gap-3 p-3"
|
className="flex flex-grow flex-col gap-3 p-3"
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { Type } from "core/utils";
|
import { Type } from "core/utils";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useEntityMutate, useEntityQuery } from "ui/client";
|
||||||
|
import { useBknd } from "ui/client/BkndProvider";
|
||||||
|
import { Button } from "ui/components/buttons/Button";
|
||||||
|
import { type EntityData, useEntity } from "ui/container";
|
||||||
|
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||||
|
import { useSearch } from "ui/hooks/use-search";
|
||||||
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
|
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
|
||||||
|
import { routes } from "ui/lib/routes";
|
||||||
import { EntityForm } from "ui/modules/data/components/EntityForm";
|
import { EntityForm } from "ui/modules/data/components/EntityForm";
|
||||||
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
|
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
|
||||||
import { useBknd } from "../../client/BkndProvider";
|
|
||||||
import { Button } from "../../components/buttons/Button";
|
|
||||||
import { type EntityData, useEntity } from "../../container";
|
|
||||||
import { useBrowserTitle } from "../../hooks/use-browser-title";
|
|
||||||
import { useSearch } from "../../hooks/use-search";
|
|
||||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
|
||||||
import { Breadcrumbs2 } from "../../layouts/AppShell/Breadcrumbs2";
|
|
||||||
import { routes } from "../../lib/routes";
|
|
||||||
|
|
||||||
export function DataEntityCreate({ params }) {
|
export function DataEntityCreate({ params }) {
|
||||||
const { app } = useBknd();
|
const { app } = useBknd();
|
||||||
@@ -17,40 +18,37 @@ export function DataEntityCreate({ params }) {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
useBrowserTitle(["Data", entity.label, "Create"]);
|
useBrowserTitle(["Data", entity.label, "Create"]);
|
||||||
|
|
||||||
const container = useEntity(entity.name);
|
const $q = useEntityMutate(entity.name);
|
||||||
|
|
||||||
// @todo: use entity schema for prefilling
|
// @todo: use entity schema for prefilling
|
||||||
const search = useSearch(Type.Object({}), {});
|
const search = useSearch(Type.Object({}), {});
|
||||||
console.log("search", search.value);
|
|
||||||
|
|
||||||
function goBack(state?: Record<string, any>) {
|
function goBack() {
|
||||||
window.history.go(-1);
|
window.history.go(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmitted(changeSet?: EntityData) {
|
async function onSubmitted(changeSet?: EntityData) {
|
||||||
console.log("create:changeSet", changeSet);
|
console.log("create:changeSet", changeSet);
|
||||||
//return;
|
if (!changeSet) return;
|
||||||
const res = await container.actions.create(changeSet);
|
|
||||||
console.log("create:res", res);
|
try {
|
||||||
if (res.data?.error) {
|
await $q.create(changeSet);
|
||||||
setError(res.data.error);
|
if (error) setError(null);
|
||||||
} else {
|
|
||||||
error && setError(null);
|
|
||||||
// @todo: navigate to created?
|
// @todo: navigate to created?
|
||||||
goBack();
|
goBack();
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : "Failed to create");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { Form, handleSubmit, values } = useEntityForm({
|
const { Form, handleSubmit } = useEntityForm({
|
||||||
action: "create",
|
action: "create",
|
||||||
entity,
|
entity,
|
||||||
initialData: search.value,
|
initialData: search.value,
|
||||||
onSubmitted
|
onSubmitted
|
||||||
});
|
});
|
||||||
|
|
||||||
const fieldsDisabled =
|
const fieldsDisabled = $q.isLoading || $q.isValidating || Form.state.isSubmitting;
|
||||||
container.raw.fetch?.isLoading ||
|
|
||||||
container.status.fetch.isUpdating ||
|
|
||||||
Form.state.isSubmitting;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Type } from "core/utils";
|
import { Type } from "core/utils";
|
||||||
import { querySchema } from "data";
|
import { querySchema } from "data";
|
||||||
import { TbDots } from "react-icons/tb";
|
import { TbDots } from "react-icons/tb";
|
||||||
|
import { useApiQuery } 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 { Message } from "ui/components/display/Message";
|
||||||
import { Dropdown } from "ui/components/overlay/Dropdown";
|
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||||
import { EntitiesContainer } from "ui/container";
|
|
||||||
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";
|
||||||
@@ -25,19 +25,33 @@ const searchSchema = Type.Composite(
|
|||||||
{ additionalProperties: false }
|
{ additionalProperties: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const PER_PAGE_OPTIONS = [5, 10, 25];
|
||||||
|
|
||||||
export function DataEntityList({ params }) {
|
export function DataEntityList({ params }) {
|
||||||
const { $data, relations } = useBkndData();
|
const { $data } = useBkndData();
|
||||||
const entity = $data.entity(params.entity as string);
|
const entity = $data.entity(params.entity as string)!;
|
||||||
|
useBrowserTitle(["Data", entity?.label ?? params.entity]);
|
||||||
const [navigate] = useNavigate();
|
const [navigate] = useNavigate();
|
||||||
const search = useSearch(searchSchema, {
|
const search = useSearch(searchSchema, {
|
||||||
select: entity?.getSelect(undefined, "table") ?? [],
|
select: entity?.getSelect(undefined, "table") ?? [],
|
||||||
sort: entity?.getDefaultSort()
|
sort: entity?.getDefaultSort()
|
||||||
});
|
});
|
||||||
console.log("search", search.value);
|
|
||||||
useBrowserTitle(["Data", entity?.label ?? params.entity]);
|
|
||||||
const PER_PAGE_OPTIONS = [5, 10, 25];
|
|
||||||
|
|
||||||
//console.log("search", search.value);
|
const $q = useApiQuery(
|
||||||
|
(api) =>
|
||||||
|
api.data.readMany(entity.name, {
|
||||||
|
select: search.value.select,
|
||||||
|
limit: search.value.perPage,
|
||||||
|
offset: (search.value.page - 1) * search.value.perPage,
|
||||||
|
sort: search.value.sort
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
revalidateOnFocus: true,
|
||||||
|
keepPreviousData: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const data = $q.data?.data;
|
||||||
|
const meta = $q.data?.body.meta;
|
||||||
|
|
||||||
function handleClickRow(row: Record<string, any>) {
|
function handleClickRow(row: Record<string, any>) {
|
||||||
if (entity) navigate(routes.data.entity.edit(entity.name, row.id));
|
if (entity) navigate(routes.data.entity.edit(entity.name, row.id));
|
||||||
@@ -65,6 +79,8 @@ export function DataEntityList({ params }) {
|
|||||||
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
|
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isUpdating = $q.isLoading && $q.isValidating;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppShell.SectionHeader
|
<AppShell.SectionHeader
|
||||||
@@ -103,45 +119,25 @@ export function DataEntityList({ params }) {
|
|||||||
<SearchInput placeholder={`Filter ${entity.label}`} />
|
<SearchInput placeholder={`Filter ${entity.label}`} />
|
||||||
</div>*/}
|
</div>*/}
|
||||||
|
|
||||||
<EntitiesContainer
|
<div
|
||||||
entity={entity.name}
|
data-updating={isUpdating ? 1 : undefined}
|
||||||
query={{
|
className="data-[updating]:opacity-50 transition-opacity pb-10"
|
||||||
select: search.value.select,
|
|
||||||
limit: search.value.perPage,
|
|
||||||
offset: (search.value.page - 1) * search.value.perPage,
|
|
||||||
sort: search.value.sort
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{(params) => {
|
<EntityTable2
|
||||||
if (params.status.fetch.isLoading) {
|
data={data ?? null}
|
||||||
return null;
|
entity={entity}
|
||||||
}
|
/*select={search.value.select}*/
|
||||||
|
onClickRow={handleClickRow}
|
||||||
const isUpdating = params.status.fetch.isUpdating;
|
page={search.value.page}
|
||||||
|
sort={search.value.sort}
|
||||||
return (
|
onClickSort={handleSortClick}
|
||||||
<div
|
perPage={search.value.perPage}
|
||||||
data-updating={isUpdating ? 1 : undefined}
|
perPageOptions={PER_PAGE_OPTIONS}
|
||||||
className="data-[updating]:opacity-50 transition-opacity pb-10"
|
total={meta?.count}
|
||||||
>
|
onClickPage={handleClickPage}
|
||||||
<EntityTable2
|
onClickPerPage={handleClickPerPage}
|
||||||
data={params.data ?? []}
|
/>
|
||||||
entity={entity}
|
</div>
|
||||||
select={search.value.select}
|
|
||||||
onClickRow={handleClickRow}
|
|
||||||
page={search.value.page}
|
|
||||||
sort={search.value.sort}
|
|
||||||
onClickSort={handleSortClick}
|
|
||||||
perPage={search.value.perPage}
|
|
||||||
perPageOptions={PER_PAGE_OPTIONS}
|
|
||||||
total={params.meta?.count}
|
|
||||||
onClickPage={handleClickPage}
|
|
||||||
onClickPerPage={handleClickPerPage}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</EntitiesContainer>
|
|
||||||
</div>
|
</div>
|
||||||
</AppShell.Scrollable>
|
</AppShell.Scrollable>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user