mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
replaced all react-query usages with new hooks + removed react-query
This commit is contained in:
@@ -55,8 +55,6 @@
|
|||||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||||
"@rjsf/core": "^5.22.2",
|
"@rjsf/core": "^5.22.2",
|
||||||
"@tabler/icons-react": "3.18.0",
|
"@tabler/icons-react": "3.18.0",
|
||||||
"@tanstack/react-query": "^5.59.16",
|
|
||||||
"@tanstack/react-query-devtools": "^5.59.16",
|
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { SafeUser } from "auth";
|
||||||
import { AuthApi } from "auth/api/AuthApi";
|
import { AuthApi } from "auth/api/AuthApi";
|
||||||
import { DataApi } from "data/api/DataApi";
|
import { DataApi } from "data/api/DataApi";
|
||||||
import { decode } from "hono/jwt";
|
import { decode } from "hono/jwt";
|
||||||
@@ -5,7 +6,7 @@ import { omit } from "lodash-es";
|
|||||||
import { MediaApi } from "media/api/MediaApi";
|
import { MediaApi } from "media/api/MediaApi";
|
||||||
import { SystemApi } from "modules/SystemApi";
|
import { SystemApi } from "modules/SystemApi";
|
||||||
|
|
||||||
export type TApiUser = object;
|
export type TApiUser = SafeUser;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -24,6 +25,12 @@ export type ApiOptions = {
|
|||||||
localStorage?: boolean;
|
localStorage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AuthState = {
|
||||||
|
token?: string;
|
||||||
|
user?: TApiUser;
|
||||||
|
verified: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export class Api {
|
export class Api {
|
||||||
private token?: string;
|
private token?: string;
|
||||||
private user?: TApiUser;
|
private user?: TApiUser;
|
||||||
@@ -50,6 +57,10 @@ export class Api {
|
|||||||
this.buildApis();
|
this.buildApis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get baseUrl() {
|
||||||
|
return this.options.host;
|
||||||
|
}
|
||||||
|
|
||||||
get tokenKey() {
|
get tokenKey() {
|
||||||
return this.options.key ?? "auth";
|
return this.options.key ?? "auth";
|
||||||
}
|
}
|
||||||
@@ -85,7 +96,11 @@ export class Api {
|
|||||||
|
|
||||||
updateToken(token?: string, rebuild?: boolean) {
|
updateToken(token?: string, rebuild?: boolean) {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.user = token ? omit(decode(token).payload as any, ["iat", "iss", "exp"]) : undefined;
|
if (token) {
|
||||||
|
this.user = omit(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
|
||||||
|
} else {
|
||||||
|
this.user = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.options.localStorage) {
|
if (this.options.localStorage) {
|
||||||
const key = this.tokenKey;
|
const key = this.tokenKey;
|
||||||
@@ -105,7 +120,7 @@ export class Api {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthState() {
|
getAuthState(): AuthState {
|
||||||
return {
|
return {
|
||||||
token: this.token,
|
token: this.token,
|
||||||
user: this.user,
|
user: this.user,
|
||||||
@@ -113,6 +128,20 @@ export class Api {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async verifyAuth() {
|
||||||
|
try {
|
||||||
|
const res = await this.auth.me();
|
||||||
|
if (!res.ok || !res.body.user) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.markAuthVerified(true);
|
||||||
|
} catch (e) {
|
||||||
|
this.markAuthVerified(false);
|
||||||
|
this.updateToken(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getUser(): TApiUser | null {
|
getUser(): TApiUser | null {
|
||||||
return this.user || null;
|
return this.user || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
|
import type { ModuleConfigs, ModuleSchemas } from "modules";
|
||||||
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 { useApi } from "ui/client";
|
||||||
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 { useClient } from "./ClientProvider";
|
|
||||||
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
||||||
import { AppReduced } from "./utils/AppReduced";
|
import { AppReduced } from "./utils/AppReduced";
|
||||||
|
|
||||||
@@ -38,7 +33,7 @@ export function BkndProvider({
|
|||||||
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions">>();
|
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions">>();
|
||||||
const [fetched, setFetched] = useState(false);
|
const [fetched, setFetched] = useState(false);
|
||||||
const errorShown = useRef<boolean>();
|
const errorShown = useRef<boolean>();
|
||||||
const client = useClient();
|
const api = useApi();
|
||||||
|
|
||||||
async function reloadSchema() {
|
async function reloadSchema() {
|
||||||
await fetchSchema(includeSecrets, true);
|
await fetchSchema(includeSecrets, true);
|
||||||
@@ -46,7 +41,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 res = await client.api.system.readSchema({
|
const res = await api.system.readSchema({
|
||||||
config: true,
|
config: true,
|
||||||
secrets: _includeSecrets
|
secrets: _includeSecrets
|
||||||
});
|
});
|
||||||
@@ -100,7 +95,7 @@ export function BkndProvider({
|
|||||||
|
|
||||||
if (!fetched || !schema) return fallback;
|
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({ api, setSchema, reloadSchema });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BkndContext.Provider value={{ ...schema, actions, requireSecrets, app, adminOverride }}>
|
<BkndContext.Provider value={{ ...schema, actions, requireSecrets, app, adminOverride }}>
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { Api, type ApiOptions, type TApiUser } from "Api";
|
||||||
import type { TApiUser } from "Api";
|
|
||||||
import { createContext, useContext, useEffect, useState } from "react";
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
//import { useBkndWindowContext } from "ui/client/BkndProvider";
|
|
||||||
import { AppQueryClient } from "./utils/AppQueryClient";
|
|
||||||
|
|
||||||
const ClientContext = createContext<{ baseUrl: string; client: AppQueryClient }>({
|
const ClientContext = createContext<{ baseUrl: string; api: Api }>({
|
||||||
baseUrl: undefined
|
baseUrl: undefined
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
|
||||||
defaultOptions: {
|
|
||||||
queries: {
|
|
||||||
retry: false,
|
|
||||||
refetchOnWindowFocus: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ClientProviderProps = {
|
export type ClientProviderProps = {
|
||||||
children?: any;
|
children?: any;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
@@ -49,47 +37,34 @@ export const ClientProvider = ({ children, baseUrl, user }: ClientProviderProps)
|
|||||||
return null; // or a loader/spinner if desired
|
return null; // or a loader/spinner if desired
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log("client provider11 with", { baseUrl, fallback: actualBaseUrl, user });
|
const api = new Api({ host: actualBaseUrl, user: user ?? winCtx.user });
|
||||||
const client = createClient(actualBaseUrl, user ?? winCtx.user);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<ClientContext.Provider value={{ baseUrl: actualBaseUrl, api }}>
|
||||||
<ClientContext.Provider value={{ baseUrl: actualBaseUrl, client }}>
|
{children}
|
||||||
{children}
|
</ClientContext.Provider>
|
||||||
</ClientContext.Provider>
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createClient(baseUrl: string, user?: object) {
|
export const useApi = (host?: ApiOptions["host"]) => {
|
||||||
return new AppQueryClient(baseUrl, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createOrUseClient(baseUrl: string) {
|
|
||||||
const context = useContext(ClientContext);
|
const context = useContext(ClientContext);
|
||||||
if (!context) {
|
if (host && host !== context.baseUrl) {
|
||||||
console.warn("createOrUseClient returned a new client");
|
return new Api({ host });
|
||||||
return createClient(baseUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return context.client;
|
return context.api;
|
||||||
}
|
|
||||||
|
|
||||||
export const useClient = () => {
|
|
||||||
const context = useContext(ClientContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useClient must be used within a ClientProvider");
|
|
||||||
}
|
|
||||||
return context.client;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use useApi().baseUrl instead
|
||||||
|
*/
|
||||||
export const useBaseUrl = () => {
|
export const useBaseUrl = () => {
|
||||||
const context = useContext(ClientContext);
|
const context = useContext(ClientContext);
|
||||||
return context.baseUrl;
|
return context.baseUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BkndWindowContext = {
|
type BkndWindowContext = {
|
||||||
user?: object;
|
user?: TApiUser;
|
||||||
logout_route: string;
|
logout_route: string;
|
||||||
};
|
};
|
||||||
export function useBkndWindowContext(): BkndWindowContext {
|
export function useBkndWindowContext(): BkndWindowContext {
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import type { Api } from "Api";
|
import type { Api } from "Api";
|
||||||
import type { FetchPromise, ResponseObject } from "modules/ModuleApi";
|
import type { FetchPromise, ResponseObject } from "modules/ModuleApi";
|
||||||
import useSWR, { type SWRConfiguration } from "swr";
|
import useSWR, { type SWRConfiguration, useSWRConfig } from "swr";
|
||||||
import { useClient } from "ui/client/ClientProvider";
|
import { useApi } from "ui/client";
|
||||||
|
|
||||||
export const useApi = () => {
|
|
||||||
const client = useClient();
|
|
||||||
return client.api;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useApiQuery = <
|
export const useApiQuery = <
|
||||||
Data,
|
Data,
|
||||||
@@ -21,7 +16,7 @@ export const useApiQuery = <
|
|||||||
const fetcher = () => promise.execute().then(refine);
|
const fetcher = () => promise.execute().then(refine);
|
||||||
const key = promise.key();
|
const key = promise.key();
|
||||||
|
|
||||||
type RefinedData = RefineFn extends (data: Data) => infer R ? R : Data;
|
type RefinedData = RefineFn extends (data: ResponseObject<Data>) => infer R ? R : Data;
|
||||||
|
|
||||||
const swr = useSWR<RefinedData>(options?.enabled === false ? null : key, fetcher, options);
|
const swr = useSWR<RefinedData>(options?.enabled === false ? null : key, fetcher, options);
|
||||||
return {
|
return {
|
||||||
@@ -31,3 +26,13 @@ export const useApiQuery = <
|
|||||||
api
|
api
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useInvalidate = () => {
|
||||||
|
const mutate = useSWRConfig().mutate;
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
return async (arg?: string | ((api: Api) => FetchPromise<any>)) => {
|
||||||
|
if (!arg) return async () => mutate("");
|
||||||
|
return mutate(typeof arg === "string" ? arg : arg(api).key());
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export const useEntityQuery = <
|
|||||||
return {
|
return {
|
||||||
...swr,
|
...swr,
|
||||||
...mapped,
|
...mapped,
|
||||||
|
api,
|
||||||
key
|
key
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export {
|
|||||||
ClientProvider,
|
ClientProvider,
|
||||||
useBkndWindowContext,
|
useBkndWindowContext,
|
||||||
type ClientProviderProps,
|
type ClientProviderProps,
|
||||||
useClient,
|
useApi,
|
||||||
useBaseUrl
|
useBaseUrl
|
||||||
} from "./ClientProvider";
|
} from "./ClientProvider";
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import { type NotificationData, notifications } from "@mantine/notifications";
|
import { type NotificationData, notifications } from "@mantine/notifications";
|
||||||
|
import type { Api } from "Api";
|
||||||
import { ucFirst } from "core/utils";
|
import { ucFirst } from "core/utils";
|
||||||
import type { ModuleConfigs } from "modules";
|
import type { ModuleConfigs } from "modules";
|
||||||
import type { ResponseObject } from "modules/ModuleApi";
|
import type { ResponseObject } from "modules/ModuleApi";
|
||||||
import type { ConfigUpdateResponse } from "modules/server/SystemController";
|
import type { ConfigUpdateResponse } from "modules/server/SystemController";
|
||||||
import type { AppQueryClient } from "../utils/AppQueryClient";
|
|
||||||
|
|
||||||
export type SchemaActionsProps = {
|
export type SchemaActionsProps = {
|
||||||
client: AppQueryClient;
|
api: Api;
|
||||||
setSchema: React.Dispatch<React.SetStateAction<any>>;
|
setSchema: React.Dispatch<React.SetStateAction<any>>;
|
||||||
reloadSchema: () => Promise<void>;
|
reloadSchema: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSchemaActions = ReturnType<typeof getSchemaActions>;
|
export type TSchemaActions = ReturnType<typeof getSchemaActions>;
|
||||||
|
|
||||||
export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActionsProps) {
|
export function getSchemaActions({ api, setSchema, reloadSchema }: SchemaActionsProps) {
|
||||||
const api = client.api;
|
|
||||||
|
|
||||||
async function handleConfigUpdate<Module extends keyof ModuleConfigs>(
|
async function handleConfigUpdate<Module extends keyof ModuleConfigs>(
|
||||||
action: string,
|
action: string,
|
||||||
module: Module,
|
module: Module,
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import { Api } from "Api";
|
import { Api, type AuthState } from "Api";
|
||||||
import type { AuthResponse } from "auth";
|
import type { AuthResponse } from "auth";
|
||||||
import type { AppAuthSchema } from "auth/auth-schema";
|
import type { AppAuthSchema } from "auth/auth-schema";
|
||||||
import type { ApiResponse } from "modules/ModuleApi";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import { useApi, useInvalidate } from "ui/client";
|
||||||
createClient,
|
|
||||||
createOrUseClient,
|
|
||||||
queryClient,
|
|
||||||
useBaseUrl,
|
|
||||||
useClient
|
|
||||||
} from "../../ClientProvider";
|
|
||||||
|
|
||||||
type LoginData = {
|
type LoginData = {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -18,55 +11,54 @@ type LoginData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UseAuth = {
|
type UseAuth = {
|
||||||
data: (AuthResponse & { verified: boolean }) | undefined;
|
data: AuthState | undefined;
|
||||||
user: AuthResponse["user"] | undefined;
|
user: AuthState["user"] | undefined;
|
||||||
token: AuthResponse["token"] | undefined;
|
token: AuthState["token"] | undefined;
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
login: (data: LoginData) => Promise<ApiResponse<AuthResponse>>;
|
login: (data: LoginData) => Promise<AuthResponse>;
|
||||||
register: (data: LoginData) => Promise<ApiResponse<AuthResponse>>;
|
register: (data: LoginData) => Promise<AuthResponse>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
verify: () => void;
|
verify: () => void;
|
||||||
setToken: (token: string) => void;
|
setToken: (token: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @todo: needs to use a specific auth endpoint to get strategy information
|
|
||||||
export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
|
export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
|
||||||
const ctxBaseUrl = useBaseUrl();
|
const api = useApi(options?.baseUrl);
|
||||||
//const client = useClient();
|
const invalidate = useInvalidate();
|
||||||
const client = createOrUseClient(options?.baseUrl ? options?.baseUrl : ctxBaseUrl);
|
const authState = api.getAuthState();
|
||||||
const authState = client.auth().state();
|
|
||||||
const [authData, setAuthData] = useState<UseAuth["data"]>(authState);
|
const [authData, setAuthData] = useState<UseAuth["data"]>(authState);
|
||||||
const verified = authState?.verified ?? false;
|
const verified = authState?.verified ?? false;
|
||||||
|
|
||||||
|
function updateAuthState() {
|
||||||
|
setAuthData(api.getAuthState());
|
||||||
|
}
|
||||||
|
|
||||||
async function login(input: LoginData) {
|
async function login(input: LoginData) {
|
||||||
const res = await client.auth().login(input);
|
const res = await api.auth.loginWithPassword(input);
|
||||||
if (res.res.ok && res.data && "user" in res.data) {
|
updateAuthState();
|
||||||
setAuthData(res.data);
|
return res.data;
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function register(input: LoginData) {
|
async function register(input: LoginData) {
|
||||||
const res = await client.auth().register(input);
|
const res = await api.auth.registerWithPassword(input);
|
||||||
if (res.res.ok && res.data && "user" in res.data) {
|
updateAuthState();
|
||||||
setAuthData(res.data);
|
return res.data;
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setToken(token: string) {
|
function setToken(token: string) {
|
||||||
setAuthData(client.auth().setToken(token) as any);
|
api.updateToken(token);
|
||||||
|
updateAuthState();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await client.auth().logout();
|
await api.updateToken(undefined);
|
||||||
setAuthData(undefined);
|
setAuthData(undefined);
|
||||||
queryClient.clear();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verify() {
|
async function verify() {
|
||||||
await client.auth().verify();
|
await api.verifyAuth();
|
||||||
setAuthData(client.auth().state());
|
updateAuthState();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -87,10 +79,7 @@ export const useAuthStrategies = (options?: { baseUrl?: string }): Partial<AuthS
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
} => {
|
} => {
|
||||||
const [data, setData] = useState<AuthStrategyData>();
|
const [data, setData] = useState<AuthStrategyData>();
|
||||||
const ctxBaseUrl = useBaseUrl();
|
const api = useApi(options?.baseUrl);
|
||||||
const api = new Api({
|
|
||||||
host: options?.baseUrl ? options?.baseUrl : ctxBaseUrl
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
|
|
||||||
export function useBkndAuth() {
|
export function useBkndAuth() {
|
||||||
//const client = useClient();
|
const { config, schema, actions: bkndActions } = useBknd();
|
||||||
const { config, app, schema, actions: bkndActions } = useBknd();
|
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
roles: {
|
roles: {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { type Static, parse } from "core/utils";
|
import { type Static, parse } from "core/utils";
|
||||||
import { type TAppFlowSchema, flowSchema } from "flows/flows-schema";
|
import { type TAppFlowSchema, flowSchema } from "flows/flows-schema";
|
||||||
import { useBknd } from "../../BkndProvider";
|
import { useBknd } from "../../BkndProvider";
|
||||||
import { useClient } from "../../ClientProvider";
|
|
||||||
|
|
||||||
export function useFlows() {
|
export function useFlows() {
|
||||||
const client = useClient();
|
|
||||||
const { config, app, actions: bkndActions } = useBknd();
|
const { config, app, actions: bkndActions } = useBknd();
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
|||||||
@@ -1,216 +0,0 @@
|
|||||||
import {
|
|
||||||
type QueryObserverOptions,
|
|
||||||
type UseQueryResult,
|
|
||||||
keepPreviousData,
|
|
||||||
useMutation,
|
|
||||||
useQuery
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
import type { AuthResponse } from "auth";
|
|
||||||
import type { EntityData, RepoQuery, RepositoryResponse } from "data";
|
|
||||||
import { Api } from "../../../Api";
|
|
||||||
import type { ApiResponse } from "../../../modules/ModuleApi";
|
|
||||||
import { queryClient } from "../ClientProvider";
|
|
||||||
|
|
||||||
export class AppQueryClient {
|
|
||||||
api: Api;
|
|
||||||
constructor(
|
|
||||||
public baseUrl: string,
|
|
||||||
user?: object
|
|
||||||
) {
|
|
||||||
this.api = new Api({
|
|
||||||
host: baseUrl,
|
|
||||||
user
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
queryOptions(options?: Partial<QueryObserverOptions>): Partial<QueryObserverOptions> {
|
|
||||||
return {
|
|
||||||
staleTime: 1000 * 60 * 5,
|
|
||||||
placeholderData: keepPreviousData,
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
auth = () => {
|
|
||||||
return {
|
|
||||||
state: (): (AuthResponse & { verified: boolean }) | undefined => {
|
|
||||||
return this.api.getAuthState() as any;
|
|
||||||
},
|
|
||||||
login: async (data: { email: string; password: string }) => {
|
|
||||||
return await this.api.auth.loginWithPassword(data);
|
|
||||||
},
|
|
||||||
register: async (data: any) => {
|
|
||||||
return await this.api.auth.registerWithPassword(data);
|
|
||||||
},
|
|
||||||
logout: async () => {
|
|
||||||
this.api.updateToken(undefined);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
setToken: (token) => {
|
|
||||||
this.api.updateToken(token);
|
|
||||||
return this.api.getAuthState();
|
|
||||||
},
|
|
||||||
verify: async () => {
|
|
||||||
try {
|
|
||||||
//console.log("verifiying");
|
|
||||||
const res = await this.api.auth.me();
|
|
||||||
//console.log("verifying result", res);
|
|
||||||
if (!res.ok || !res.body.user) {
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.api.markAuthVerified(true);
|
|
||||||
} catch (e) {
|
|
||||||
this.api.markAuthVerified(false);
|
|
||||||
this.api.updateToken(undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
media = (options?: Partial<QueryObserverOptions>) => {
|
|
||||||
const queryOptions = this.queryOptions(options);
|
|
||||||
return {
|
|
||||||
api: () => {
|
|
||||||
return this.api.media;
|
|
||||||
},
|
|
||||||
list: (query: Partial<RepoQuery> = { limit: 10 }): UseQueryResult<ApiResponse> => {
|
|
||||||
return useQuery({
|
|
||||||
...(queryOptions as any), // @todo: fix typing
|
|
||||||
queryKey: ["data", "entity", "media", { query }],
|
|
||||||
queryFn: async () => {
|
|
||||||
return await this.api.data.readMany("media", query);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
deleteFile: async (filename: string | { path: string }) => {
|
|
||||||
const res = await this.api.media.deleteFile(
|
|
||||||
typeof filename === "string" ? filename : filename.path
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["data", "entity", "media"] });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
query = (options?: Partial<QueryObserverOptions>) => {
|
|
||||||
const queryOptions = this.queryOptions(options);
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
entity: (name: string) => {
|
|
||||||
return {
|
|
||||||
readOne: (
|
|
||||||
id: number,
|
|
||||||
query: Partial<Omit<RepoQuery, "where" | "limit" | "offset">> = {}
|
|
||||||
): any => {
|
|
||||||
return useQuery({
|
|
||||||
...queryOptions,
|
|
||||||
queryKey: ["data", "entity", name, id, { query }],
|
|
||||||
queryFn: async () => {
|
|
||||||
return await this.api.data.readOne(name, id, query);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
readMany: (
|
|
||||||
query: Partial<RepoQuery> = { limit: 10, offset: 0 }
|
|
||||||
): UseQueryResult<ApiResponse> => {
|
|
||||||
return useQuery({
|
|
||||||
...(queryOptions as any), // @todo: fix typing
|
|
||||||
queryKey: ["data", "entity", name, { query }],
|
|
||||||
queryFn: async () => {
|
|
||||||
return await this.api.data.readMany(name, query);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
readManyByReference: (
|
|
||||||
id: number,
|
|
||||||
reference: string,
|
|
||||||
referenced_entity?: string, // required for query invalidation
|
|
||||||
query: Partial<RepoQuery> = { limit: 10, offset: 0 }
|
|
||||||
): UseQueryResult<Pick<RepositoryResponse, "meta" | "data">> => {
|
|
||||||
return useQuery({
|
|
||||||
...(queryOptions as any), // @todo: fix typing
|
|
||||||
queryKey: [
|
|
||||||
"data",
|
|
||||||
"entity",
|
|
||||||
referenced_entity ?? reference,
|
|
||||||
{ name, id, reference, query }
|
|
||||||
],
|
|
||||||
queryFn: async () => {
|
|
||||||
return await this.api.data.readManyByReference(
|
|
||||||
name,
|
|
||||||
id,
|
|
||||||
reference,
|
|
||||||
query
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
count: (
|
|
||||||
where: RepoQuery["where"] = {}
|
|
||||||
): UseQueryResult<ApiResponse<{ entity: string; count: number }>> => {
|
|
||||||
return useQuery({
|
|
||||||
...(queryOptions as any), // @todo: fix typing
|
|
||||||
queryKey: ["data", "entity", name, "fn", "count", { where }],
|
|
||||||
queryFn: async () => {
|
|
||||||
return await this.api.data.count(name, where);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// @todo: centralize, improve
|
|
||||||
__invalidate = (...args: any[]) => {
|
|
||||||
console.log("___invalidate", ["data", "entity", ...args]);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["data", "entity", ...args] });
|
|
||||||
};
|
|
||||||
|
|
||||||
// @todo: must return response... why?
|
|
||||||
mutation = {
|
|
||||||
data: {
|
|
||||||
entity: (name: string) => {
|
|
||||||
return {
|
|
||||||
update: (id: number): any => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (input: EntityData) => {
|
|
||||||
return await this.api.data.updateOne(name, id, input);
|
|
||||||
},
|
|
||||||
onSuccess: async () => {
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["data", "entity", name] });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
create: (): any => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (input: EntityData) => {
|
|
||||||
return await this.api.data.createOne(name, input);
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["data", "entity", name] });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
delete: (id: number): any => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
return await this.api.data.deleteOne(name, id);
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["data", "entity", name] });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import type { UseQueryOptions, UseQueryResult } from "@tanstack/react-query";
|
|
||||||
import type { RepositoryResponse } from "data";
|
|
||||||
import type { RepoQuery } from "data";
|
|
||||||
import { useClient } from "../client";
|
|
||||||
import { type EntityData, type QueryStatus, getStatus } from "./EntityContainer";
|
|
||||||
|
|
||||||
export type RenderParams<Data extends EntityData = EntityData> = {
|
|
||||||
data: Data[] | undefined;
|
|
||||||
meta: RepositoryResponse["meta"] | undefined;
|
|
||||||
status: {
|
|
||||||
fetch: QueryStatus;
|
|
||||||
};
|
|
||||||
raw: {
|
|
||||||
fetch: UseQueryResult;
|
|
||||||
};
|
|
||||||
actions: {
|
|
||||||
create(obj: any): any;
|
|
||||||
update(id: number, obj: any): any;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EntitiesContainerProps = {
|
|
||||||
entity: string;
|
|
||||||
query?: Partial<RepoQuery>;
|
|
||||||
queryOptions?: Partial<UseQueryOptions>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useEntities(
|
|
||||||
entity: string,
|
|
||||||
query?: Partial<RepoQuery>,
|
|
||||||
queryOptions?: Partial<UseQueryOptions>
|
|
||||||
): RenderParams {
|
|
||||||
const client = useClient();
|
|
||||||
let data: any = null;
|
|
||||||
let meta: any = null;
|
|
||||||
|
|
||||||
const fetchQuery = client.query(queryOptions).data.entity(entity).readMany(query);
|
|
||||||
const createMutation = client.mutation.data.entity(entity).create();
|
|
||||||
const updateMutation = (id: number) => client.mutation.data.entity(entity).update(id);
|
|
||||||
|
|
||||||
if (fetchQuery?.isSuccess) {
|
|
||||||
meta = fetchQuery.data?.body.meta;
|
|
||||||
data = fetchQuery.data?.body.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
function create(obj: any) {
|
|
||||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: <explanation>
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
await createMutation?.mutate(obj, {
|
|
||||||
onSuccess: resolve,
|
|
||||||
onError: reject
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(id: number, obj: any) {
|
|
||||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: <explanation>
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
await updateMutation(id).mutate(obj, {
|
|
||||||
onSuccess: resolve,
|
|
||||||
onError: reject
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
meta,
|
|
||||||
actions: {
|
|
||||||
create,
|
|
||||||
update
|
|
||||||
// remove
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
fetch: getStatus(fetchQuery)
|
|
||||||
},
|
|
||||||
raw: {
|
|
||||||
fetch: fetchQuery
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EntitiesContainer<Data extends EntityData = EntityData>({
|
|
||||||
entity,
|
|
||||||
query,
|
|
||||||
queryOptions,
|
|
||||||
children
|
|
||||||
}: EntitiesContainerProps & {
|
|
||||||
children(params: RenderParams<Data>): any;
|
|
||||||
}) {
|
|
||||||
const params = useEntities(entity, query, queryOptions);
|
|
||||||
return children(params as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Entities = EntitiesContainer;
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
import type { UseQueryResult } from "@tanstack/react-query";
|
|
||||||
import type { RepoQuery } from "data";
|
|
||||||
import { useClient } from "../client";
|
|
||||||
|
|
||||||
export type EntityData = Record<string, any>;
|
|
||||||
|
|
||||||
export type EntityContainerRenderParams<Data extends EntityData = EntityData> = {
|
|
||||||
data: Data | null;
|
|
||||||
client: ReturnType<typeof useClient>;
|
|
||||||
initialValues: object;
|
|
||||||
raw: {
|
|
||||||
fetch?: UseQueryResult;
|
|
||||||
};
|
|
||||||
status: {
|
|
||||||
fetch: QueryStatus;
|
|
||||||
};
|
|
||||||
actions: {
|
|
||||||
create(obj: any): any;
|
|
||||||
update(obj: any): any;
|
|
||||||
remove(): any;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MutationStatus = {
|
|
||||||
isLoading: boolean;
|
|
||||||
isSuccess: boolean;
|
|
||||||
isError: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QueryStatus = MutationStatus & {
|
|
||||||
isUpdating: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getStatus(query?: UseQueryResult): QueryStatus {
|
|
||||||
return {
|
|
||||||
isLoading: query ? query.isPending : false,
|
|
||||||
isUpdating: query ? !query.isInitialLoading && query.isFetching : false,
|
|
||||||
isSuccess: query ? query.isSuccess : false,
|
|
||||||
isError: query ? query.isError : false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EntityContainerProps = {
|
|
||||||
entity: string;
|
|
||||||
id?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FetchOptions = {
|
|
||||||
disabled?: boolean;
|
|
||||||
query?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @todo: add option to disable fetches (for form updates)
|
|
||||||
// @todo: must return a way to indicate error!
|
|
||||||
export function useEntity<Data extends EntityData = EntityData>(
|
|
||||||
entity: string,
|
|
||||||
id?: number,
|
|
||||||
options?: { fetch?: FetchOptions }
|
|
||||||
): EntityContainerRenderParams<Data> {
|
|
||||||
const client = useClient();
|
|
||||||
let data: any = null;
|
|
||||||
|
|
||||||
const fetchQuery = id
|
|
||||||
? client.query().data.entity(entity).readOne(id, options?.fetch?.query)
|
|
||||||
: undefined;
|
|
||||||
const createMutation = id ? null : client.mutation.data.entity(entity).create();
|
|
||||||
const updateMutation = id ? client.mutation.data.entity(entity).update(id) : undefined;
|
|
||||||
const deleteMutation = id ? client.mutation.data.entity(entity).delete(id) : undefined;
|
|
||||||
|
|
||||||
if (fetchQuery?.isSuccess) {
|
|
||||||
data = fetchQuery.data?.body.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialValues = { one: 1 };
|
|
||||||
|
|
||||||
function create(obj: any) {
|
|
||||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: <explanation>
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
await createMutation?.mutate(obj, {
|
|
||||||
onSuccess: resolve,
|
|
||||||
onError: reject
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(obj: any) {
|
|
||||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: <explanation>
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
//await new Promise((r) => setTimeout(r, 4000));
|
|
||||||
await updateMutation?.mutate(obj, {
|
|
||||||
onSuccess: resolve,
|
|
||||||
onError: reject
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove() {
|
|
||||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: <explanation>
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
//await new Promise((r) => setTimeout(r, 4000));
|
|
||||||
await deleteMutation?.mutate(undefined, {
|
|
||||||
onSuccess: resolve,
|
|
||||||
onError: reject
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
client,
|
|
||||||
initialValues,
|
|
||||||
actions: {
|
|
||||||
create,
|
|
||||||
update,
|
|
||||||
remove
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
fetch: getStatus(fetchQuery)
|
|
||||||
//update: getMutationStatus(updateMutation),
|
|
||||||
},
|
|
||||||
raw: {
|
|
||||||
fetch: fetchQuery
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EntityContainer({
|
|
||||||
entity,
|
|
||||||
id,
|
|
||||||
children
|
|
||||||
}: EntityContainerProps & { children(params: EntityContainerRenderParams): any }) {
|
|
||||||
const params = useEntity(entity, id);
|
|
||||||
return children(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Entity = EntityContainer;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./EntitiesContainer";
|
|
||||||
export * from "./EntityContainer";
|
|
||||||
@@ -1,11 +1 @@
|
|||||||
export { default as Admin, type BkndAdminProps } from "./Admin";
|
export { default as Admin, type BkndAdminProps } from "./Admin";
|
||||||
export {
|
|
||||||
EntitiesContainer,
|
|
||||||
useEntities,
|
|
||||||
type EntitiesContainerProps
|
|
||||||
} from "./container/EntitiesContainer";
|
|
||||||
export {
|
|
||||||
EntityContainer,
|
|
||||||
useEntity,
|
|
||||||
type EntityContainerProps
|
|
||||||
} from "./container/EntityContainer";
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "data";
|
} from "data";
|
||||||
import { MediaField } from "media/MediaField";
|
import { MediaField } from "media/MediaField";
|
||||||
import { type ComponentProps, Suspense } from "react";
|
import { type ComponentProps, Suspense } from "react";
|
||||||
import { useClient } from "ui/client";
|
import { useApi, useBaseUrl, useInvalidate } from "ui/client";
|
||||||
import { JsonEditor } from "ui/components/code/JsonEditor";
|
import { JsonEditor } from "ui/components/code/JsonEditor";
|
||||||
import * as Formy from "ui/components/form/Formy";
|
import * as Formy from "ui/components/form/Formy";
|
||||||
import { FieldLabel } from "ui/components/form/Formy";
|
import { FieldLabel } from "ui/components/form/Formy";
|
||||||
@@ -215,7 +215,9 @@ function EntityMediaFormField({
|
|||||||
}) {
|
}) {
|
||||||
if (!entityId) return;
|
if (!entityId) return;
|
||||||
|
|
||||||
const client = useClient();
|
const api = useApi();
|
||||||
|
const baseUrl = useBaseUrl();
|
||||||
|
const invalidate = useInvalidate();
|
||||||
const value = formApi.useStore((state) => {
|
const value = formApi.useStore((state) => {
|
||||||
const val = state.values[field.name];
|
const val = state.values[field.name];
|
||||||
if (!val || typeof val === "undefined") return [];
|
if (!val || typeof val === "undefined") return [];
|
||||||
@@ -227,22 +229,21 @@ function EntityMediaFormField({
|
|||||||
value.length === 0
|
value.length === 0
|
||||||
? []
|
? []
|
||||||
: mediaItemsToFileStates(value, {
|
: mediaItemsToFileStates(value, {
|
||||||
baseUrl: client.baseUrl,
|
baseUrl: api.baseUrl,
|
||||||
overrides: { state: "uploaded" }
|
overrides: { state: "uploaded" }
|
||||||
});
|
});
|
||||||
|
|
||||||
const getUploadInfo = useEvent(() => {
|
const getUploadInfo = useEvent(() => {
|
||||||
const api = client.media().api();
|
|
||||||
return {
|
return {
|
||||||
url: api.getEntityUploadUrl(entity.name, entityId, field.name),
|
url: api.media.getEntityUploadUrl(entity.name, entityId, field.name),
|
||||||
headers: api.getUploadHeaders(),
|
headers: api.media.getUploadHeaders(),
|
||||||
method: "POST"
|
method: "POST"
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDelete = useEvent(async (file) => {
|
const handleDelete = useEvent(async (file: FileState) => {
|
||||||
client.__invalidate(entity.name, entityId);
|
invalidate((api) => api.data.readOne(entity.name, entityId));
|
||||||
return await client.media().deleteFile(file);
|
return api.media.deleteFile(file.path);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ 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, useEntityQuery } from "ui/client";
|
import { 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";
|
||||||
import { Popover } from "ui/components/overlay/Popover";
|
import { Popover } from "ui/components/overlay/Popover";
|
||||||
import { useEntities } from "ui/container";
|
|
||||||
import { routes } from "ui/lib/routes";
|
import { routes } from "ui/lib/routes";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { EntityTable } from "../EntityTable";
|
import { EntityTable } from "../EntityTable";
|
||||||
@@ -33,7 +32,6 @@ export function EntityRelationalFormField({
|
|||||||
const [query, setQuery] = useState<any>({ limit: 10, page: 1, perPage: 10 });
|
const [query, setQuery] = useState<any>({ limit: 10, page: 1, perPage: 10 });
|
||||||
const [, navigate] = useLocation();
|
const [, navigate] = useLocation();
|
||||||
const ref = useRef<any>(null);
|
const ref = useRef<any>(null);
|
||||||
const client = useClient();
|
|
||||||
const $q = useEntityQuery(field.target(), undefined, {
|
const $q = useEntityQuery(field.target(), undefined, {
|
||||||
limit: query.limit,
|
limit: query.limit,
|
||||||
offset: (query.page - 1) * query.limit
|
offset: (query.page - 1) * query.limit
|
||||||
@@ -53,7 +51,7 @@ export function EntityRelationalFormField({
|
|||||||
const rel_value = field.target();
|
const rel_value = field.target();
|
||||||
if (!rel_value || !relationalField) return;
|
if (!rel_value || !relationalField) return;
|
||||||
|
|
||||||
const fetched = await client.api.data.readOne(field.target(), relationalField);
|
const fetched = await $q.api.readOne(field.target(), relationalField);
|
||||||
if (fetched.ok && fetched.data) {
|
if (fetched.ok && fetched.data) {
|
||||||
_setValue(fetched.data as any);
|
_setValue(fetched.data as any);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,19 @@
|
|||||||
import { useClient } from "ui/client";
|
import { useApiQuery } from "ui/client";
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
||||||
|
import { ButtonLink, type ButtonLinkProps } from "ui/components/buttons/Button";
|
||||||
import { Alert } from "ui/components/display/Alert";
|
import { Alert } from "ui/components/display/Alert";
|
||||||
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
import { routes } from "ui/lib/routes";
|
import { routes } from "ui/lib/routes";
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
ButtonLink,
|
|
||||||
type ButtonLinkProps,
|
|
||||||
type ButtonProps
|
|
||||||
} from "../../components/buttons/Button";
|
|
||||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
|
||||||
|
|
||||||
export function AuthIndex() {
|
export function AuthIndex() {
|
||||||
const client = useClient();
|
|
||||||
const { app } = useBknd();
|
const { app } = useBknd();
|
||||||
const {
|
const {
|
||||||
config: { roles, strategies, entity_name, enabled }
|
config: { roles, strategies, entity_name, enabled }
|
||||||
} = useBkndAuth();
|
} = useBkndAuth();
|
||||||
const users_entity = entity_name;
|
const users_entity = entity_name;
|
||||||
const query = client.query().data.entity("users").count();
|
const $q = useApiQuery((api) => api.data.count(users_entity));
|
||||||
const usersTotal = query.data?.body.count ?? 0;
|
const usersTotal = $q.data?.count ?? 0;
|
||||||
const rolesTotal = Object.keys(roles ?? {}).length ?? 0;
|
const rolesTotal = Object.keys(roles ?? {}).length ?? 0;
|
||||||
const strategiesTotal = Object.keys(strategies ?? {}).length ?? 0;
|
const strategiesTotal = Object.keys(strategies ?? {}).length ?? 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ucFirst } from "core/utils";
|
import { ucFirst } from "core/utils";
|
||||||
import type { Entity, EntityData, EntityRelation } from "data";
|
import type { Entity, EntityData, EntityRelation, RepoQuery } 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, useEntityQuery } from "ui/client";
|
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";
|
||||||
@@ -232,18 +232,17 @@ function EntityDetailInner({
|
|||||||
relation: EntityRelation;
|
relation: EntityRelation;
|
||||||
}) {
|
}) {
|
||||||
const other = relation.other(entity);
|
const other = relation.other(entity);
|
||||||
const client = useClient();
|
|
||||||
const [navigate] = useNavigate();
|
const [navigate] = useNavigate();
|
||||||
|
|
||||||
const search = {
|
const search: Partial<RepoQuery> = {
|
||||||
select: other.entity.getSelect(undefined, "table"),
|
select: other.entity.getSelect(undefined, "table"),
|
||||||
limit: 10,
|
limit: 10,
|
||||||
offset: 0
|
offset: 0
|
||||||
};
|
};
|
||||||
const query = client
|
// @todo: add custom key for invalidation
|
||||||
.query()
|
const $q = useApiQuery((api) =>
|
||||||
.data.entity(entity.name)
|
api.data.readManyByReference(entity.name, id, other.reference, search)
|
||||||
.readManyByReference(id, other.reference, other.entity.name, search);
|
);
|
||||||
|
|
||||||
function handleClickRow(row: Record<string, any>) {
|
function handleClickRow(row: Record<string, any>) {
|
||||||
navigate(routes.data.entity.edit(other.entity.name, row.id));
|
navigate(routes.data.entity.edit(other.entity.name, row.id));
|
||||||
@@ -262,12 +261,11 @@ function EntityDetailInner({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.isPending) {
|
if (!$q.data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUpdating = query.isInitialLoading || query.isFetching;
|
const isUpdating = $q.isValidating || $q.isLoading;
|
||||||
//console.log("query", query, search.select);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -276,13 +274,12 @@ function EntityDetailInner({
|
|||||||
>
|
>
|
||||||
<EntityTable2
|
<EntityTable2
|
||||||
select={search.select}
|
select={search.select}
|
||||||
data={query.data?.data ?? []}
|
data={$q.data ?? null}
|
||||||
entity={other.entity}
|
entity={other.entity}
|
||||||
onClickRow={handleClickRow}
|
onClickRow={handleClickRow}
|
||||||
onClickNew={handleClickNew}
|
onClickNew={handleClickNew}
|
||||||
page={1}
|
page={1}
|
||||||
/* @ts-ignore */
|
total={$q.data?.body?.meta?.count ?? 1}
|
||||||
total={query.data?.body?.meta?.count ?? 1}
|
|
||||||
/*onClickPage={handleClickPage}*/
|
/*onClickPage={handleClickPage}*/
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Type } from "core/utils";
|
import { Type } from "core/utils";
|
||||||
|
import type { EntityData } from "data";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useEntityMutate, useEntityQuery } from "ui/client";
|
import { useEntityMutate } from "ui/client";
|
||||||
import { useBknd } from "ui/client/BkndProvider";
|
import { useBknd } from "ui/client/BkndProvider";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { type EntityData, useEntity } 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";
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { IconPhoto } from "@tabler/icons-react";
|
import { IconPhoto } from "@tabler/icons-react";
|
||||||
|
import type { MediaFieldSchema } from "modules";
|
||||||
import { TbSettings } from "react-icons/tb";
|
import { TbSettings } from "react-icons/tb";
|
||||||
import { Dropzone } from "ui/modules/media/components/dropzone/Dropzone";
|
import { useApi, useBaseUrl, useEntityQuery } from "ui/client";
|
||||||
|
import { useBknd } from "ui/client/BkndProvider";
|
||||||
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
|
import { Empty } from "ui/components/display/Empty";
|
||||||
|
import { Link } from "ui/components/wouter/Link";
|
||||||
|
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||||
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
|
import { Dropzone, type FileState } from "ui/modules/media/components/dropzone/Dropzone";
|
||||||
import { mediaItemsToFileStates } from "ui/modules/media/helper";
|
import { mediaItemsToFileStates } from "ui/modules/media/helper";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { useClient } from "../../client";
|
|
||||||
import { useBknd } from "../../client/BkndProvider";
|
|
||||||
import { IconButton } from "../../components/buttons/IconButton";
|
|
||||||
import { Empty } from "../../components/display/Empty";
|
|
||||||
import { Link } from "../../components/wouter/Link";
|
|
||||||
import { useBrowserTitle } from "../../hooks/use-browser-title";
|
|
||||||
import { useEvent } from "../../hooks/use-event";
|
|
||||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
|
||||||
|
|
||||||
export function MediaRoot({ children }) {
|
export function MediaRoot({ children }) {
|
||||||
const { app, config } = useBknd();
|
const { app, config } = useBknd();
|
||||||
@@ -62,32 +63,30 @@ export function MediaRoot({ children }) {
|
|||||||
// @todo: add infinite load
|
// @todo: add infinite load
|
||||||
export function MediaEmpty() {
|
export function MediaEmpty() {
|
||||||
useBrowserTitle(["Media"]);
|
useBrowserTitle(["Media"]);
|
||||||
const client = useClient();
|
const baseUrl = useBaseUrl();
|
||||||
const query = client.media().list({ limit: 50 });
|
const api = useApi();
|
||||||
|
const $q = useEntityQuery("media", undefined, { limit: 50 });
|
||||||
|
|
||||||
const getUploadInfo = useEvent((file) => {
|
const getUploadInfo = useEvent((file) => {
|
||||||
const api = client.media().api();
|
|
||||||
return {
|
return {
|
||||||
url: api.getFileUploadUrl(file),
|
url: api.media.getFileUploadUrl(file),
|
||||||
headers: api.getUploadHeaders(),
|
headers: api.media.getUploadHeaders(),
|
||||||
method: "POST"
|
method: "POST"
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDelete = useEvent(async (file) => {
|
const handleDelete = useEvent(async (file: FileState) => {
|
||||||
return await client.media().deleteFile(file);
|
return api.media.deleteFile(file.path);
|
||||||
});
|
});
|
||||||
|
|
||||||
const media = query.data?.data || [];
|
const media = ($q.data || []) as MediaFieldSchema[];
|
||||||
const initialItems = mediaItemsToFileStates(media, { baseUrl: client.baseUrl });
|
const initialItems = mediaItemsToFileStates(media, { baseUrl });
|
||||||
|
|
||||||
console.log("initialItems", initialItems);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell.Scrollable>
|
<AppShell.Scrollable>
|
||||||
<div className="flex flex-1 p-3">
|
<div className="flex flex-1 p-3">
|
||||||
<Dropzone
|
<Dropzone
|
||||||
key={query.isSuccess ? "loaded" : "initial"}
|
key={$q.isLoading ? "loaded" : "initial"}
|
||||||
getUploadInfo={getUploadInfo}
|
getUploadInfo={getUploadInfo}
|
||||||
handleDelete={handleDelete}
|
handleDelete={handleDelete}
|
||||||
autoUpload
|
autoUpload
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import { IconHome } from "@tabler/icons-react";
|
import { IconHome } from "@tabler/icons-react";
|
||||||
import { Suspense, useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useAuth } from "ui/client";
|
import { useAuth } from "ui/client";
|
||||||
import { Empty } from "../components/display/Empty";
|
import { Empty } from "../components/display/Empty";
|
||||||
import { useBrowserTitle } from "../hooks/use-browser-title";
|
import { useBrowserTitle } from "../hooks/use-browser-title";
|
||||||
import * as AppShell from "../layouts/AppShell/AppShell";
|
import * as AppShell from "../layouts/AppShell/AppShell";
|
||||||
import { useNavigate } from "../lib/routes";
|
import { useNavigate } from "../lib/routes";
|
||||||
|
|
||||||
// @todo: package is still required somehow
|
|
||||||
const ReactQueryDevtools = (p: any) => null; /*!isDebug()
|
|
||||||
? () => null // Render nothing in production
|
|
||||||
: lazy(() =>
|
|
||||||
import("@tanstack/react-query-devtools").then((res) => ({
|
|
||||||
default: res.ReactQueryDevtools,
|
|
||||||
})),
|
|
||||||
);*/
|
|
||||||
|
|
||||||
export const Root = ({ children }) => {
|
export const Root = ({ children }) => {
|
||||||
const { verify } = useAuth();
|
const { verify } = useAuth();
|
||||||
|
|
||||||
@@ -26,10 +17,6 @@ export const Root = ({ children }) => {
|
|||||||
<AppShell.Root>
|
<AppShell.Root>
|
||||||
<AppShell.Header />
|
<AppShell.Header />
|
||||||
<AppShell.Content>{children}</AppShell.Content>
|
<AppShell.Content>{children}</AppShell.Content>
|
||||||
|
|
||||||
<Suspense>
|
|
||||||
<ReactQueryDevtools buttonPosition="bottom-left" />
|
|
||||||
</Suspense>
|
|
||||||
</AppShell.Root>
|
</AppShell.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user