admin: fix useSearch

This commit is contained in:
dswbx
2025-06-10 08:38:10 +02:00
parent 12c955155d
commit 88419548c7
4 changed files with 64 additions and 31 deletions

View File

@@ -104,15 +104,17 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
<button <button
type="button" type="button"
className={twMerge( className={twMerge(
"link hover:bg-primary/5 py-1.5 rounded-md inline-flex flex-row justify-start items-center gap-1", "py-1.5 rounded-md inline-flex flex-row justify-start items-center gap-1",
onClickSort ? "pl-2.5 pr-1" : "px-2.5", onClickSort
? "link hover:bg-primary/5 pl-2.5 pr-1"
: "px-2.5",
)} )}
onClick={() => onClickSort?.(property)} onClick={() => onClickSort?.(property)}
> >
<span className="text-left text-nowrap whitespace-nowrap"> <span className="text-left text-nowrap whitespace-nowrap">
{label} {label}
</span> </span>
{onClickSort && ( {(onClickSort || (sort && sort.by === property)) && (
<SortIndicator sort={sort} field={property} /> <SortIndicator sort={sort} field={property} />
)} )}
</button> </button>

View File

@@ -1,32 +1,41 @@
import { decodeSearch, encodeSearch, mergeObject, parseDecode } from "core/utils"; import { decodeSearch, encodeSearch, mergeObject } from "core/utils";
import { isEqual, transform } from "lodash-es"; import { isEqual, transform } from "lodash-es";
import { useLocation, useSearch as useWouterSearch } from "wouter"; import { useLocation, useSearch as useWouterSearch } from "wouter";
import { type s, parse, cloneSchema } from "core/object/schema"; import { type s, parse } from "core/object/schema";
import { useEffect, useState } from "react";
export type UseSearchOptions<Schema extends s.TAnySchema = s.TAnySchema> = {
defaultValue?: Partial<s.StaticCoerced<Schema>>;
beforeEncode?: (search: Partial<s.StaticCoerced<Schema>>) => object;
};
// @todo: migrate to Typebox
export function useSearch<Schema extends s.TAnySchema = s.TAnySchema>( export function useSearch<Schema extends s.TAnySchema = s.TAnySchema>(
_schema: Schema, schema: Schema,
defaultValue?: Partial<s.StaticCoerced<Schema>>, options?: UseSearchOptions<Schema>,
) { ) {
const schema = cloneSchema(_schema as any) as s.TSchema;
const searchString = useWouterSearch(); const searchString = useWouterSearch();
const [location, navigate] = useLocation(); const [location, navigate] = useLocation();
const initial = searchString.length > 0 ? decodeSearch(searchString) : (defaultValue ?? {}); const [value, setValue] = useState<s.StaticCoerced<Schema>>(
const value = parse(schema, initial, { options?.defaultValue ?? ({} as any),
withDefaults: true, );
clone: true, const _defaults = mergeObject(
}) as s.StaticCoerced<Schema>; // @ts-ignore
schema.template({ withOptional: true }),
options?.defaultValue ?? {},
);
// @ts-ignore useEffect(() => {
const _defaults = mergeObject(schema.template({ withOptional: true }), defaultValue ?? {}); const initial =
searchString.length > 0 ? decodeSearch(searchString) : (options?.defaultValue ?? {});
const v = parse(schema, Object.assign({}, _defaults, initial)) as any;
setValue(v);
}, [searchString, JSON.stringify(options?.defaultValue), location]);
function set<Update extends Partial<s.StaticCoerced<Schema>>>(update: Update): void { function set<Update extends Partial<s.StaticCoerced<Schema>>>(update: Update): void {
// @ts-ignore const search = getWithoutDefaults(Object.assign({}, value, update), _defaults);
if (schema.validate(update).valid) { const prepared = options?.beforeEncode?.(search) ?? search;
const search = getWithoutDefaults(mergeObject(value, update), _defaults); const encoded = encodeSearch(prepared, { encode: false });
const encoded = encodeSearch(search, { encode: false }); navigate(location + (encoded.length > 0 ? "?" + encoded : ""));
navigate(location + (encoded.length > 0 ? "?" + encoded : ""));
}
} }
return { return {

View File

@@ -257,15 +257,20 @@ function EntityDetailInner({
}) { }) {
const other = relation.other(entity); const other = relation.other(entity);
const [navigate] = useNavigate(); const [navigate] = useNavigate();
const [search, setSearch] = useState({
const search = {
select: other.entity.getSelect(undefined, "table"), select: other.entity.getSelect(undefined, "table"),
sort: other.entity.getDefaultSort(),
limit: 10, limit: 10,
offset: 0, offset: 0,
}; });
// @todo: add custom key for invalidation // @todo: add custom key for invalidation
const $q = useApiQuery((api) => const $q = useApiQuery(
api.data.readManyByReference(entity.name, id, other.reference, search), (api) => api.data.readManyByReference(entity.name, id, other.reference, search),
{
keepPreviousData: true,
revalidateOnFocus: true,
},
); );
function handleClickRow(row: Record<string, any>) { function handleClickRow(row: Record<string, any>) {
@@ -300,11 +305,17 @@ function EntityDetailInner({
select={search.select} select={search.select}
data={$q.data ?? null} data={$q.data ?? null}
entity={other.entity} entity={other.entity}
sort={search.sort}
onClickRow={handleClickRow} onClickRow={handleClickRow}
onClickNew={handleClickNew} onClickNew={handleClickNew}
page={1} page={Math.floor(search.offset / search.limit) + 1}
total={$q.data?.body?.meta?.count ?? 1} total={$q.data?.body?.meta?.count ?? 1}
/*onClickPage={handleClickPage}*/ onClickPage={(page) => {
setSearch((s) => ({
...s,
offset: (page - 1) * s.limit,
}));
}}
/> />
</div> </div>
); );

View File

@@ -35,8 +35,19 @@ export function DataEntityList({ params }) {
useBrowserTitle(["Data", entity?.label ?? params.entity]); useBrowserTitle(["Data", entity?.label ?? params.entity]);
const [navigate] = useNavigate(); const [navigate] = useNavigate();
const search = useSearch(searchSchema, { const search = useSearch(searchSchema, {
select: entity.getSelect(undefined, "table"), defaultValue: {
sort: entity.getDefaultSort(), select: entity.getSelect(undefined, "table"),
sort: entity.getDefaultSort(),
},
beforeEncode: (v) => {
if ("sort" in v && v.sort) {
return {
...v,
sort: `${v.sort.dir === "asc" ? "" : "-"}${v.sort.by}`,
};
}
return v;
},
}); });
const $q = useApiQuery( const $q = useApiQuery(