public commit

This commit is contained in:
dswbx
2024-11-16 12:01:47 +01:00
commit 90f80c4280
582 changed files with 49291 additions and 0 deletions
+211
View File
@@ -0,0 +1,211 @@
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) {
this.api = new Api({
host: baseUrl,
tokenStorage: "localStorage"
});
}
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 }): Promise<
ApiResponse<AuthResponse>
> => {
return await this.api.auth.loginWithPassword(data);
},
register: async (data: any): Promise<ApiResponse<AuthResponse>> => {
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 () => {
console.log("verifiying");
const res = await this.api.auth.me();
console.log("verifying result", res);
if (!res.res.ok) {
this.api.markAuthVerified(false);
this.api.updateToken(undefined);
} else {
this.api.markAuthVerified(true);
}
}
};
};
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.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] });
}
});
}
};
}
}
};
}
+83
View File
@@ -0,0 +1,83 @@
import type { App } from "App";
import type { Entity, EntityRelation } from "data";
import { AppData } from "data/AppData";
import { RelationAccessor } from "data/relations/RelationAccessor";
import { Flow, TaskMap } from "flows";
export type AppType = ReturnType<App["toJSON"]>;
/**
* Reduced version of the App class for frontend use
*/
export class AppReduced {
// @todo: change to record
private _entities: Entity[] = [];
private _relations: EntityRelation[] = [];
private _flows: Flow[] = [];
constructor(protected appJson: AppType) {
console.log("received appjson", appJson);
this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => {
return AppData.constructEntity(name, entity);
});
this._relations = Object.entries(this.appJson.data.relations ?? {}).map(([, relation]) => {
return AppData.constructRelation(relation, this.entity.bind(this));
});
for (const [name, obj] of Object.entries(this.appJson.flows.flows ?? {})) {
// @ts-ignore
// @todo: fix constructing flow
const flow = Flow.fromObject(name, obj, TaskMap);
this._flows.push(flow);
}
}
get entities(): Entity[] {
return this._entities;
}
// @todo: change to record
entity(_entity: Entity | string): Entity {
const name = typeof _entity === "string" ? _entity : _entity.name;
const entity = this._entities.find((entity) => entity.name === name);
if (!entity) {
throw new Error(`Entity "${name}" not found`);
}
return entity;
}
get relations(): RelationAccessor {
return new RelationAccessor(this._relations);
}
get flows(): Flow[] {
return this._flows;
}
get config() {
return this.appJson;
}
getAdminConfig() {
return this.appJson.server.admin;
}
getSettingsPath(path: string[] = []): string {
const { basepath } = this.getAdminConfig();
const base = `~/${basepath}/settings`.replace(/\/+/g, "/");
return [base, ...path].join("/");
}
getAbsolutePath(path?: string): string {
const { basepath } = this.getAdminConfig();
return (path ? `~/${basepath}/${path}` : `~/${basepath}`).replace(/\/+/g, "/");
}
getAuthConfig() {
return this.appJson.auth;
}
}
+28
View File
@@ -0,0 +1,28 @@
import { useState } from "react";
export type AppTheme = "light" | "dark" | string;
export function useSetTheme(initialTheme: AppTheme = "light") {
const [theme, _setTheme] = useState(initialTheme);
const $html = document.querySelector("#bknd-admin")!;
function setTheme(newTheme: AppTheme) {
$html?.classList.remove("dark", "light");
$html?.classList.add(newTheme);
_setTheme(newTheme);
// @todo: just a quick switcher config update test
fetch("/api/system/config/patch/server/admin", {
method: "PATCH",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ color_scheme: newTheme })
})
.then((res) => res.json())
.then((data) => {
console.log("theme updated", data);
});
}
return { theme, setTheme };
}