mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +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",
|
||||
"@rjsf/core": "^5.22.2",
|
||||
"@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/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { SafeUser } from "auth";
|
||||
import { AuthApi } from "auth/api/AuthApi";
|
||||
import { DataApi } from "data/api/DataApi";
|
||||
import { decode } from "hono/jwt";
|
||||
@@ -5,7 +6,7 @@ import { omit } from "lodash-es";
|
||||
import { MediaApi } from "media/api/MediaApi";
|
||||
import { SystemApi } from "modules/SystemApi";
|
||||
|
||||
export type TApiUser = object;
|
||||
export type TApiUser = SafeUser;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -24,6 +25,12 @@ export type ApiOptions = {
|
||||
localStorage?: boolean;
|
||||
};
|
||||
|
||||
export type AuthState = {
|
||||
token?: string;
|
||||
user?: TApiUser;
|
||||
verified: boolean;
|
||||
};
|
||||
|
||||
export class Api {
|
||||
private token?: string;
|
||||
private user?: TApiUser;
|
||||
@@ -50,6 +57,10 @@ export class Api {
|
||||
this.buildApis();
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
return this.options.host;
|
||||
}
|
||||
|
||||
get tokenKey() {
|
||||
return this.options.key ?? "auth";
|
||||
}
|
||||
@@ -85,7 +96,11 @@ export class Api {
|
||||
|
||||
updateToken(token?: string, rebuild?: boolean) {
|
||||
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) {
|
||||
const key = this.tokenKey;
|
||||
@@ -105,7 +120,7 @@ export class Api {
|
||||
return this;
|
||||
}
|
||||
|
||||
getAuthState() {
|
||||
getAuthState(): AuthState {
|
||||
return {
|
||||
token: this.token,
|
||||
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 {
|
||||
return this.user || null;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import type { ModuleConfigs, ModuleSchemas } from "modules";
|
||||
import { getDefaultConfig, getDefaultSchema } from "modules/ModuleManager";
|
||||
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 { useClient } from "./ClientProvider";
|
||||
import { useApi } from "ui/client";
|
||||
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
||||
import { AppReduced } from "./utils/AppReduced";
|
||||
|
||||
@@ -38,7 +33,7 @@ export function BkndProvider({
|
||||
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions">>();
|
||||
const [fetched, setFetched] = useState(false);
|
||||
const errorShown = useRef<boolean>();
|
||||
const client = useClient();
|
||||
const api = useApi();
|
||||
|
||||
async function reloadSchema() {
|
||||
await fetchSchema(includeSecrets, true);
|
||||
@@ -46,7 +41,7 @@ export function BkndProvider({
|
||||
|
||||
async function fetchSchema(_includeSecrets: boolean = false, force?: boolean) {
|
||||
if (withSecrets && !force) return;
|
||||
const res = await client.api.system.readSchema({
|
||||
const res = await api.system.readSchema({
|
||||
config: true,
|
||||
secrets: _includeSecrets
|
||||
});
|
||||
@@ -100,7 +95,7 @@ export function BkndProvider({
|
||||
|
||||
if (!fetched || !schema) return fallback;
|
||||
const app = new AppReduced(schema?.config as any);
|
||||
const actions = getSchemaActions({ client, setSchema, reloadSchema });
|
||||
const actions = getSchemaActions({ api, setSchema, reloadSchema });
|
||||
|
||||
return (
|
||||
<BkndContext.Provider value={{ ...schema, actions, requireSecrets, app, adminOverride }}>
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { TApiUser } from "Api";
|
||||
import { Api, type ApiOptions, type TApiUser } from "Api";
|
||||
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
|
||||
} as any);
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type ClientProviderProps = {
|
||||
children?: any;
|
||||
baseUrl?: string;
|
||||
@@ -49,47 +37,34 @@ export const ClientProvider = ({ children, baseUrl, user }: ClientProviderProps)
|
||||
return null; // or a loader/spinner if desired
|
||||
}
|
||||
|
||||
//console.log("client provider11 with", { baseUrl, fallback: actualBaseUrl, user });
|
||||
const client = createClient(actualBaseUrl, user ?? winCtx.user);
|
||||
const api = new Api({ host: actualBaseUrl, user: user ?? winCtx.user });
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ClientContext.Provider value={{ baseUrl: actualBaseUrl, client }}>
|
||||
{children}
|
||||
</ClientContext.Provider>
|
||||
</QueryClientProvider>
|
||||
<ClientContext.Provider value={{ baseUrl: actualBaseUrl, api }}>
|
||||
{children}
|
||||
</ClientContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function createClient(baseUrl: string, user?: object) {
|
||||
return new AppQueryClient(baseUrl, user);
|
||||
}
|
||||
|
||||
export function createOrUseClient(baseUrl: string) {
|
||||
export const useApi = (host?: ApiOptions["host"]) => {
|
||||
const context = useContext(ClientContext);
|
||||
if (!context) {
|
||||
console.warn("createOrUseClient returned a new client");
|
||||
return createClient(baseUrl);
|
||||
if (host && host !== context.baseUrl) {
|
||||
return new Api({ host });
|
||||
}
|
||||
|
||||
return context.client;
|
||||
}
|
||||
|
||||
export const useClient = () => {
|
||||
const context = useContext(ClientContext);
|
||||
if (!context) {
|
||||
throw new Error("useClient must be used within a ClientProvider");
|
||||
}
|
||||
return context.client;
|
||||
return context.api;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated use useApi().baseUrl instead
|
||||
*/
|
||||
export const useBaseUrl = () => {
|
||||
const context = useContext(ClientContext);
|
||||
return context.baseUrl;
|
||||
};
|
||||
|
||||
type BkndWindowContext = {
|
||||
user?: object;
|
||||
user?: TApiUser;
|
||||
logout_route: string;
|
||||
};
|
||||
export function useBkndWindowContext(): BkndWindowContext {
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import type { Api } from "Api";
|
||||
import type { FetchPromise, ResponseObject } from "modules/ModuleApi";
|
||||
import useSWR, { type SWRConfiguration } from "swr";
|
||||
import { useClient } from "ui/client/ClientProvider";
|
||||
|
||||
export const useApi = () => {
|
||||
const client = useClient();
|
||||
return client.api;
|
||||
};
|
||||
import useSWR, { type SWRConfiguration, useSWRConfig } from "swr";
|
||||
import { useApi } from "ui/client";
|
||||
|
||||
export const useApiQuery = <
|
||||
Data,
|
||||
@@ -21,7 +16,7 @@ export const useApiQuery = <
|
||||
const fetcher = () => promise.execute().then(refine);
|
||||
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);
|
||||
return {
|
||||
@@ -31,3 +26,13 @@ export const useApiQuery = <
|
||||
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 {
|
||||
...swr,
|
||||
...mapped,
|
||||
api,
|
||||
key
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ export {
|
||||
ClientProvider,
|
||||
useBkndWindowContext,
|
||||
type ClientProviderProps,
|
||||
useClient,
|
||||
useApi,
|
||||
useBaseUrl
|
||||
} from "./ClientProvider";
|
||||
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { type NotificationData, notifications } from "@mantine/notifications";
|
||||
import type { Api } from "Api";
|
||||
import { ucFirst } from "core/utils";
|
||||
import type { ModuleConfigs } from "modules";
|
||||
import type { ResponseObject } from "modules/ModuleApi";
|
||||
import type { ConfigUpdateResponse } from "modules/server/SystemController";
|
||||
import type { AppQueryClient } from "../utils/AppQueryClient";
|
||||
|
||||
export type SchemaActionsProps = {
|
||||
client: AppQueryClient;
|
||||
api: Api;
|
||||
setSchema: React.Dispatch<React.SetStateAction<any>>;
|
||||
reloadSchema: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type TSchemaActions = ReturnType<typeof getSchemaActions>;
|
||||
|
||||
export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActionsProps) {
|
||||
const api = client.api;
|
||||
|
||||
export function getSchemaActions({ api, setSchema, reloadSchema }: SchemaActionsProps) {
|
||||
async function handleConfigUpdate<Module extends keyof ModuleConfigs>(
|
||||
action: string,
|
||||
module: Module,
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import { Api } from "Api";
|
||||
import { Api, type AuthState } from "Api";
|
||||
import type { AuthResponse } from "auth";
|
||||
import type { AppAuthSchema } from "auth/auth-schema";
|
||||
import type { ApiResponse } from "modules/ModuleApi";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
createClient,
|
||||
createOrUseClient,
|
||||
queryClient,
|
||||
useBaseUrl,
|
||||
useClient
|
||||
} from "../../ClientProvider";
|
||||
import { useApi, useInvalidate } from "ui/client";
|
||||
|
||||
type LoginData = {
|
||||
email: string;
|
||||
@@ -18,55 +11,54 @@ type LoginData = {
|
||||
};
|
||||
|
||||
type UseAuth = {
|
||||
data: (AuthResponse & { verified: boolean }) | undefined;
|
||||
user: AuthResponse["user"] | undefined;
|
||||
token: AuthResponse["token"] | undefined;
|
||||
data: AuthState | undefined;
|
||||
user: AuthState["user"] | undefined;
|
||||
token: AuthState["token"] | undefined;
|
||||
verified: boolean;
|
||||
login: (data: LoginData) => Promise<ApiResponse<AuthResponse>>;
|
||||
register: (data: LoginData) => Promise<ApiResponse<AuthResponse>>;
|
||||
login: (data: LoginData) => Promise<AuthResponse>;
|
||||
register: (data: LoginData) => Promise<AuthResponse>;
|
||||
logout: () => void;
|
||||
verify: () => void;
|
||||
setToken: (token: string) => void;
|
||||
};
|
||||
|
||||
// @todo: needs to use a specific auth endpoint to get strategy information
|
||||
export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
|
||||
const ctxBaseUrl = useBaseUrl();
|
||||
//const client = useClient();
|
||||
const client = createOrUseClient(options?.baseUrl ? options?.baseUrl : ctxBaseUrl);
|
||||
const authState = client.auth().state();
|
||||
const api = useApi(options?.baseUrl);
|
||||
const invalidate = useInvalidate();
|
||||
const authState = api.getAuthState();
|
||||
const [authData, setAuthData] = useState<UseAuth["data"]>(authState);
|
||||
const verified = authState?.verified ?? false;
|
||||
|
||||
function updateAuthState() {
|
||||
setAuthData(api.getAuthState());
|
||||
}
|
||||
|
||||
async function login(input: LoginData) {
|
||||
const res = await client.auth().login(input);
|
||||
if (res.res.ok && res.data && "user" in res.data) {
|
||||
setAuthData(res.data);
|
||||
}
|
||||
return res;
|
||||
const res = await api.auth.loginWithPassword(input);
|
||||
updateAuthState();
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function register(input: LoginData) {
|
||||
const res = await client.auth().register(input);
|
||||
if (res.res.ok && res.data && "user" in res.data) {
|
||||
setAuthData(res.data);
|
||||
}
|
||||
return res;
|
||||
const res = await api.auth.registerWithPassword(input);
|
||||
updateAuthState();
|
||||
return res.data;
|
||||
}
|
||||
|
||||
function setToken(token: string) {
|
||||
setAuthData(client.auth().setToken(token) as any);
|
||||
api.updateToken(token);
|
||||
updateAuthState();
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await client.auth().logout();
|
||||
await api.updateToken(undefined);
|
||||
setAuthData(undefined);
|
||||
queryClient.clear();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
async function verify() {
|
||||
await client.auth().verify();
|
||||
setAuthData(client.auth().state());
|
||||
await api.verifyAuth();
|
||||
updateAuthState();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -87,10 +79,7 @@ export const useAuthStrategies = (options?: { baseUrl?: string }): Partial<AuthS
|
||||
loading: boolean;
|
||||
} => {
|
||||
const [data, setData] = useState<AuthStrategyData>();
|
||||
const ctxBaseUrl = useBaseUrl();
|
||||
const api = new Api({
|
||||
host: options?.baseUrl ? options?.baseUrl : ctxBaseUrl
|
||||
});
|
||||
const api = useApi(options?.baseUrl);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
|
||||
export function useBkndAuth() {
|
||||
//const client = useClient();
|
||||
const { config, app, schema, actions: bkndActions } = useBknd();
|
||||
const { config, schema, actions: bkndActions } = useBknd();
|
||||
|
||||
const actions = {
|
||||
roles: {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { type Static, parse } from "core/utils";
|
||||
import { type TAppFlowSchema, flowSchema } from "flows/flows-schema";
|
||||
import { useBknd } from "../../BkndProvider";
|
||||
import { useClient } from "../../ClientProvider";
|
||||
|
||||
export function useFlows() {
|
||||
const client = useClient();
|
||||
const { config, app, actions: bkndActions } = useBknd();
|
||||
|
||||
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 {
|
||||
EntitiesContainer,
|
||||
useEntities,
|
||||
type EntitiesContainerProps
|
||||
} from "./container/EntitiesContainer";
|
||||
export {
|
||||
EntityContainer,
|
||||
useEntity,
|
||||
type EntityContainerProps
|
||||
} from "./container/EntityContainer";
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "data";
|
||||
import { MediaField } from "media/MediaField";
|
||||
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 * as Formy from "ui/components/form/Formy";
|
||||
import { FieldLabel } from "ui/components/form/Formy";
|
||||
@@ -215,7 +215,9 @@ function EntityMediaFormField({
|
||||
}) {
|
||||
if (!entityId) return;
|
||||
|
||||
const client = useClient();
|
||||
const api = useApi();
|
||||
const baseUrl = useBaseUrl();
|
||||
const invalidate = useInvalidate();
|
||||
const value = formApi.useStore((state) => {
|
||||
const val = state.values[field.name];
|
||||
if (!val || typeof val === "undefined") return [];
|
||||
@@ -227,22 +229,21 @@ function EntityMediaFormField({
|
||||
value.length === 0
|
||||
? []
|
||||
: mediaItemsToFileStates(value, {
|
||||
baseUrl: client.baseUrl,
|
||||
baseUrl: api.baseUrl,
|
||||
overrides: { state: "uploaded" }
|
||||
});
|
||||
|
||||
const getUploadInfo = useEvent(() => {
|
||||
const api = client.media().api();
|
||||
return {
|
||||
url: api.getEntityUploadUrl(entity.name, entityId, field.name),
|
||||
headers: api.getUploadHeaders(),
|
||||
url: api.media.getEntityUploadUrl(entity.name, entityId, field.name),
|
||||
headers: api.media.getUploadHeaders(),
|
||||
method: "POST"
|
||||
};
|
||||
});
|
||||
|
||||
const handleDelete = useEvent(async (file) => {
|
||||
client.__invalidate(entity.name, entityId);
|
||||
return await client.media().deleteFile(file);
|
||||
const handleDelete = useEvent(async (file: FileState) => {
|
||||
invalidate((api) => api.data.readOne(entity.name, entityId));
|
||||
return api.media.deleteFile(file.path);
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,12 +4,11 @@ import { ucFirst } from "core/utils";
|
||||
import type { EntityData, RelationField } from "data";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { TbEye } from "react-icons/tb";
|
||||
import { useClient, useEntityQuery } from "ui/client";
|
||||
import { useEntityQuery } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import * as Formy from "ui/components/form/Formy";
|
||||
import { Popover } from "ui/components/overlay/Popover";
|
||||
import { useEntities } from "ui/container";
|
||||
import { routes } from "ui/lib/routes";
|
||||
import { useLocation } from "wouter";
|
||||
import { EntityTable } from "../EntityTable";
|
||||
@@ -33,7 +32,6 @@ export function EntityRelationalFormField({
|
||||
const [query, setQuery] = useState<any>({ limit: 10, page: 1, perPage: 10 });
|
||||
const [, navigate] = useLocation();
|
||||
const ref = useRef<any>(null);
|
||||
const client = useClient();
|
||||
const $q = useEntityQuery(field.target(), undefined, {
|
||||
limit: query.limit,
|
||||
offset: (query.page - 1) * query.limit
|
||||
@@ -53,7 +51,7 @@ export function EntityRelationalFormField({
|
||||
const rel_value = field.target();
|
||||
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) {
|
||||
_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 { 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 * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
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() {
|
||||
const client = useClient();
|
||||
const { app } = useBknd();
|
||||
const {
|
||||
config: { roles, strategies, entity_name, enabled }
|
||||
} = useBkndAuth();
|
||||
const users_entity = entity_name;
|
||||
const query = client.query().data.entity("users").count();
|
||||
const usersTotal = query.data?.body.count ?? 0;
|
||||
const $q = useApiQuery((api) => api.data.count(users_entity));
|
||||
const usersTotal = $q.data?.count ?? 0;
|
||||
const rolesTotal = Object.keys(roles ?? {}).length ?? 0;
|
||||
const strategiesTotal = Object.keys(strategies ?? {}).length ?? 0;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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 { 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 { Button } from "ui/components/buttons/Button";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
@@ -232,18 +232,17 @@ function EntityDetailInner({
|
||||
relation: EntityRelation;
|
||||
}) {
|
||||
const other = relation.other(entity);
|
||||
const client = useClient();
|
||||
const [navigate] = useNavigate();
|
||||
|
||||
const search = {
|
||||
const search: Partial<RepoQuery> = {
|
||||
select: other.entity.getSelect(undefined, "table"),
|
||||
limit: 10,
|
||||
offset: 0
|
||||
};
|
||||
const query = client
|
||||
.query()
|
||||
.data.entity(entity.name)
|
||||
.readManyByReference(id, other.reference, other.entity.name, search);
|
||||
// @todo: add custom key for invalidation
|
||||
const $q = useApiQuery((api) =>
|
||||
api.data.readManyByReference(entity.name, id, other.reference, search)
|
||||
);
|
||||
|
||||
function handleClickRow(row: Record<string, any>) {
|
||||
navigate(routes.data.entity.edit(other.entity.name, row.id));
|
||||
@@ -262,12 +261,11 @@ function EntityDetailInner({
|
||||
}
|
||||
}
|
||||
|
||||
if (query.isPending) {
|
||||
if (!$q.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isUpdating = query.isInitialLoading || query.isFetching;
|
||||
//console.log("query", query, search.select);
|
||||
const isUpdating = $q.isValidating || $q.isLoading;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -276,13 +274,12 @@ function EntityDetailInner({
|
||||
>
|
||||
<EntityTable2
|
||||
select={search.select}
|
||||
data={query.data?.data ?? []}
|
||||
data={$q.data ?? null}
|
||||
entity={other.entity}
|
||||
onClickRow={handleClickRow}
|
||||
onClickNew={handleClickNew}
|
||||
page={1}
|
||||
/* @ts-ignore */
|
||||
total={query.data?.body?.meta?.count ?? 1}
|
||||
total={$q.data?.body?.meta?.count ?? 1}
|
||||
/*onClickPage={handleClickPage}*/
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Type } from "core/utils";
|
||||
import type { EntityData } from "data";
|
||||
import { useState } from "react";
|
||||
import { useEntityMutate, useEntityQuery } from "ui/client";
|
||||
import { useEntityMutate } 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";
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { IconPhoto } from "@tabler/icons-react";
|
||||
import type { MediaFieldSchema } from "modules";
|
||||
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 { 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 }) {
|
||||
const { app, config } = useBknd();
|
||||
@@ -62,32 +63,30 @@ export function MediaRoot({ children }) {
|
||||
// @todo: add infinite load
|
||||
export function MediaEmpty() {
|
||||
useBrowserTitle(["Media"]);
|
||||
const client = useClient();
|
||||
const query = client.media().list({ limit: 50 });
|
||||
const baseUrl = useBaseUrl();
|
||||
const api = useApi();
|
||||
const $q = useEntityQuery("media", undefined, { limit: 50 });
|
||||
|
||||
const getUploadInfo = useEvent((file) => {
|
||||
const api = client.media().api();
|
||||
return {
|
||||
url: api.getFileUploadUrl(file),
|
||||
headers: api.getUploadHeaders(),
|
||||
url: api.media.getFileUploadUrl(file),
|
||||
headers: api.media.getUploadHeaders(),
|
||||
method: "POST"
|
||||
};
|
||||
});
|
||||
|
||||
const handleDelete = useEvent(async (file) => {
|
||||
return await client.media().deleteFile(file);
|
||||
const handleDelete = useEvent(async (file: FileState) => {
|
||||
return api.media.deleteFile(file.path);
|
||||
});
|
||||
|
||||
const media = query.data?.data || [];
|
||||
const initialItems = mediaItemsToFileStates(media, { baseUrl: client.baseUrl });
|
||||
|
||||
console.log("initialItems", initialItems);
|
||||
const media = ($q.data || []) as MediaFieldSchema[];
|
||||
const initialItems = mediaItemsToFileStates(media, { baseUrl });
|
||||
|
||||
return (
|
||||
<AppShell.Scrollable>
|
||||
<div className="flex flex-1 p-3">
|
||||
<Dropzone
|
||||
key={query.isSuccess ? "loaded" : "initial"}
|
||||
key={$q.isLoading ? "loaded" : "initial"}
|
||||
getUploadInfo={getUploadInfo}
|
||||
handleDelete={handleDelete}
|
||||
autoUpload
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import { IconHome } from "@tabler/icons-react";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useAuth } from "ui/client";
|
||||
import { Empty } from "../components/display/Empty";
|
||||
import { useBrowserTitle } from "../hooks/use-browser-title";
|
||||
import * as AppShell from "../layouts/AppShell/AppShell";
|
||||
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 }) => {
|
||||
const { verify } = useAuth();
|
||||
|
||||
@@ -26,10 +17,6 @@ export const Root = ({ children }) => {
|
||||
<AppShell.Root>
|
||||
<AppShell.Header />
|
||||
<AppShell.Content>{children}</AppShell.Content>
|
||||
|
||||
<Suspense>
|
||||
<ReactQueryDevtools buttonPosition="bottom-left" />
|
||||
</Suspense>
|
||||
</AppShell.Root>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user