replaced all react-query usages with new hooks + removed react-query

This commit is contained in:
dswbx
2024-12-13 17:36:09 +01:00
parent 8c91dff94d
commit 1631bbb754
23 changed files with 148 additions and 644 deletions

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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 }}>

View File

@@ -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 {

View File

@@ -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());
};
};

View File

@@ -100,6 +100,7 @@ export const useEntityQuery = <
return {
...swr,
...mapped,
api,
key
};
};

View File

@@ -2,7 +2,7 @@ export {
ClientProvider,
useBkndWindowContext,
type ClientProviderProps,
useClient,
useApi,
useBaseUrl
} from "./ClientProvider";

View File

@@ -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,

View File

@@ -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 () => {

View File

@@ -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: {

View File

@@ -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 = {

View File

@@ -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] });
}
});
}
};
}
}
};
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,2 +0,0 @@
export * from "./EntitiesContainer";
export * from "./EntityContainer";

View File

@@ -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";

View File

@@ -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 (

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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";

View File

@@ -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

View File

@@ -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>
);
};