From 1631bbb7544ca452385e0e5c2fb6111cb2b9f757 Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 13 Dec 2024 17:36:09 +0100 Subject: [PATCH] replaced all react-query usages with new hooks + removed react-query --- app/package.json | 2 - app/src/Api.ts | 35 ++- app/src/ui/client/BkndProvider.tsx | 15 +- app/src/ui/client/ClientProvider.tsx | 53 ++--- app/src/ui/client/api/use-api.ts | 21 +- app/src/ui/client/api/use-entity.ts | 1 + app/src/ui/client/index.ts | 2 +- app/src/ui/client/schema/actions.ts | 8 +- app/src/ui/client/schema/auth/use-auth.ts | 65 +++--- .../ui/client/schema/auth/use-bknd-auth.ts | 3 +- app/src/ui/client/schema/flows/use-flows.ts | 2 - app/src/ui/client/utils/AppQueryClient.ts | 216 ------------------ app/src/ui/container/EntitiesContainer.tsx | 95 -------- app/src/ui/container/EntityContainer.tsx | 136 ----------- app/src/ui/container/index.ts | 2 - app/src/ui/index.ts | 10 - .../ui/modules/data/components/EntityForm.tsx | 19 +- .../fields/EntityRelationalFormField.tsx | 6 +- app/src/ui/routes/auth/auth.index.tsx | 16 +- app/src/ui/routes/data/data.$entity.$id.tsx | 25 +- .../ui/routes/data/data.$entity.create.tsx | 4 +- app/src/ui/routes/media/_media.root.tsx | 41 ++-- app/src/ui/routes/root.tsx | 15 +- 23 files changed, 148 insertions(+), 644 deletions(-) delete mode 100644 app/src/ui/client/utils/AppQueryClient.ts delete mode 100644 app/src/ui/container/EntitiesContainer.tsx delete mode 100644 app/src/ui/container/EntityContainer.tsx delete mode 100644 app/src/ui/container/index.ts diff --git a/app/package.json b/app/package.json index 46054e4..34a5e12 100644 --- a/app/package.json +++ b/app/package.json @@ -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", diff --git a/app/src/Api.ts b/app/src/Api.ts index d94aff9..5196622 100644 --- a/app/src/Api.ts +++ b/app/src/Api.ts @@ -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; } diff --git a/app/src/ui/client/BkndProvider.tsx b/app/src/ui/client/BkndProvider.tsx index 3b26d26..4f5293f 100644 --- a/app/src/ui/client/BkndProvider.tsx +++ b/app/src/ui/client/BkndProvider.tsx @@ -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>(); const [fetched, setFetched] = useState(false); const errorShown = useRef(); - 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 ( diff --git a/app/src/ui/client/ClientProvider.tsx b/app/src/ui/client/ClientProvider.tsx index 1101b21..6e13bfb 100644 --- a/app/src/ui/client/ClientProvider.tsx +++ b/app/src/ui/client/ClientProvider.tsx @@ -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 ( - - - {children} - - + + {children} + ); }; -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 { diff --git a/app/src/ui/client/api/use-api.ts b/app/src/ui/client/api/use-api.ts index 754ef7d..6d75f82 100644 --- a/app/src/ui/client/api/use-api.ts +++ b/app/src/ui/client/api/use-api.ts @@ -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) => infer R ? R : Data; const swr = useSWR(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)) => { + if (!arg) return async () => mutate(""); + return mutate(typeof arg === "string" ? arg : arg(api).key()); + }; +}; diff --git a/app/src/ui/client/api/use-entity.ts b/app/src/ui/client/api/use-entity.ts index 4482c12..23be395 100644 --- a/app/src/ui/client/api/use-entity.ts +++ b/app/src/ui/client/api/use-entity.ts @@ -100,6 +100,7 @@ export const useEntityQuery = < return { ...swr, ...mapped, + api, key }; }; diff --git a/app/src/ui/client/index.ts b/app/src/ui/client/index.ts index 9bbdd65..792f884 100644 --- a/app/src/ui/client/index.ts +++ b/app/src/ui/client/index.ts @@ -2,7 +2,7 @@ export { ClientProvider, useBkndWindowContext, type ClientProviderProps, - useClient, + useApi, useBaseUrl } from "./ClientProvider"; diff --git a/app/src/ui/client/schema/actions.ts b/app/src/ui/client/schema/actions.ts index 9bed2c4..cebc137 100644 --- a/app/src/ui/client/schema/actions.ts +++ b/app/src/ui/client/schema/actions.ts @@ -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>; reloadSchema: () => Promise; }; export type TSchemaActions = ReturnType; -export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActionsProps) { - const api = client.api; - +export function getSchemaActions({ api, setSchema, reloadSchema }: SchemaActionsProps) { async function handleConfigUpdate( action: string, module: Module, diff --git a/app/src/ui/client/schema/auth/use-auth.ts b/app/src/ui/client/schema/auth/use-auth.ts index 405e140..fd2ec84 100644 --- a/app/src/ui/client/schema/auth/use-auth.ts +++ b/app/src/ui/client/schema/auth/use-auth.ts @@ -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>; - register: (data: LoginData) => Promise>; + login: (data: LoginData) => Promise; + register: (data: LoginData) => Promise; 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(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 { const [data, setData] = useState(); - const ctxBaseUrl = useBaseUrl(); - const api = new Api({ - host: options?.baseUrl ? options?.baseUrl : ctxBaseUrl - }); + const api = useApi(options?.baseUrl); useEffect(() => { (async () => { diff --git a/app/src/ui/client/schema/auth/use-bknd-auth.ts b/app/src/ui/client/schema/auth/use-bknd-auth.ts index 26b8720..a3bb003 100644 --- a/app/src/ui/client/schema/auth/use-bknd-auth.ts +++ b/app/src/ui/client/schema/auth/use-bknd-auth.ts @@ -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: { diff --git a/app/src/ui/client/schema/flows/use-flows.ts b/app/src/ui/client/schema/flows/use-flows.ts index cdaa220..554a028 100644 --- a/app/src/ui/client/schema/flows/use-flows.ts +++ b/app/src/ui/client/schema/flows/use-flows.ts @@ -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 = { diff --git a/app/src/ui/client/utils/AppQueryClient.ts b/app/src/ui/client/utils/AppQueryClient.ts deleted file mode 100644 index 239bd93..0000000 --- a/app/src/ui/client/utils/AppQueryClient.ts +++ /dev/null @@ -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): Partial { - 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) => { - const queryOptions = this.queryOptions(options); - return { - api: () => { - return this.api.media; - }, - list: (query: Partial = { limit: 10 }): UseQueryResult => { - 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) => { - const queryOptions = this.queryOptions(options); - return { - data: { - entity: (name: string) => { - return { - readOne: ( - id: number, - query: Partial> = {} - ): any => { - return useQuery({ - ...queryOptions, - queryKey: ["data", "entity", name, id, { query }], - queryFn: async () => { - return await this.api.data.readOne(name, id, query); - } - }); - }, - readMany: ( - query: Partial = { limit: 10, offset: 0 } - ): UseQueryResult => { - 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 = { limit: 10, offset: 0 } - ): UseQueryResult> => { - 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> => { - 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] }); - } - }); - } - }; - } - } - }; -} diff --git a/app/src/ui/container/EntitiesContainer.tsx b/app/src/ui/container/EntitiesContainer.tsx deleted file mode 100644 index 3814c10..0000000 --- a/app/src/ui/container/EntitiesContainer.tsx +++ /dev/null @@ -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: 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; - queryOptions?: Partial; -}; - -export function useEntities( - entity: string, - query?: Partial, - queryOptions?: Partial -): 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: - 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: - 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({ - entity, - query, - queryOptions, - children -}: EntitiesContainerProps & { - children(params: RenderParams): any; -}) { - const params = useEntities(entity, query, queryOptions); - return children(params as any); -} - -export const Entities = EntitiesContainer; diff --git a/app/src/ui/container/EntityContainer.tsx b/app/src/ui/container/EntityContainer.tsx deleted file mode 100644 index da27a71..0000000 --- a/app/src/ui/container/EntityContainer.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import type { UseQueryResult } from "@tanstack/react-query"; -import type { RepoQuery } from "data"; -import { useClient } from "../client"; - -export type EntityData = Record; - -export type EntityContainerRenderParams = { - data: Data | null; - client: ReturnType; - 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>; -}; - -// @todo: add option to disable fetches (for form updates) -// @todo: must return a way to indicate error! -export function useEntity( - entity: string, - id?: number, - options?: { fetch?: FetchOptions } -): EntityContainerRenderParams { - 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: - return new Promise(async (resolve, reject) => { - await createMutation?.mutate(obj, { - onSuccess: resolve, - onError: reject - }); - }); - } - - function update(obj: any) { - // biome-ignore lint/suspicious/noAsyncPromiseExecutor: - 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: - 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; diff --git a/app/src/ui/container/index.ts b/app/src/ui/container/index.ts deleted file mode 100644 index a427a24..0000000 --- a/app/src/ui/container/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./EntitiesContainer"; -export * from "./EntityContainer"; diff --git a/app/src/ui/index.ts b/app/src/ui/index.ts index 7b3c676..92a9c97 100644 --- a/app/src/ui/index.ts +++ b/app/src/ui/index.ts @@ -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"; diff --git a/app/src/ui/modules/data/components/EntityForm.tsx b/app/src/ui/modules/data/components/EntityForm.tsx index 40147b0..131bd61 100644 --- a/app/src/ui/modules/data/components/EntityForm.tsx +++ b/app/src/ui/modules/data/components/EntityForm.tsx @@ -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 ( diff --git a/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx b/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx index cb8c16e..861bc24 100644 --- a/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx +++ b/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx @@ -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({ limit: 10, page: 1, perPage: 10 }); const [, navigate] = useLocation(); const ref = useRef(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); } diff --git a/app/src/ui/routes/auth/auth.index.tsx b/app/src/ui/routes/auth/auth.index.tsx index ae7b9f0..21eace5 100644 --- a/app/src/ui/routes/auth/auth.index.tsx +++ b/app/src/ui/routes/auth/auth.index.tsx @@ -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; diff --git a/app/src/ui/routes/data/data.$entity.$id.tsx b/app/src/ui/routes/data/data.$entity.$id.tsx index 7b6fcf0..09cd38a 100644 --- a/app/src/ui/routes/data/data.$entity.$id.tsx +++ b/app/src/ui/routes/data/data.$entity.$id.tsx @@ -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 = { 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) { 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 (
diff --git a/app/src/ui/routes/data/data.$entity.create.tsx b/app/src/ui/routes/data/data.$entity.create.tsx index 3df30db..5b16b64 100644 --- a/app/src/ui/routes/data/data.$entity.create.tsx +++ b/app/src/ui/routes/data/data.$entity.create.tsx @@ -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"; diff --git a/app/src/ui/routes/media/_media.root.tsx b/app/src/ui/routes/media/_media.root.tsx index 2b23bb6..dcb70d8 100644 --- a/app/src/ui/routes/media/_media.root.tsx +++ b/app/src/ui/routes/media/_media.root.tsx @@ -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 (
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 }) => { {children} - - - - ); };