akanjs 3.0.0-alpha.60 → 3.0.0-alpha.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,20 @@
1
1
  /**
2
- * The one line a human scans for, derived from what the model already declared: the `text: "title"` search role
3
- * names exactly that field, so an agent-facing label costs no new declaration. Falls back to the conventional
4
- * `title`/`name` keys; the id is the caller's floor.
2
+ * The one line a human scans for. A model that writes its own `Light<Model>.label()` owns the answer, since
3
+ * display logic is the Light class's job; otherwise it is derived from what the model already declared — the
4
+ * `text: "title"` search role names exactly that field, so an agent-facing label costs no new declaration.
5
+ * Falls back to the conventional `title`/`name` keys; the id is the caller's floor.
5
6
  */
6
7
  export const labelOf = (model: unknown, value: unknown): string | undefined => {
7
8
  if (!value || typeof value !== "object") return undefined;
8
9
  const source = value as Record<string, unknown>;
10
+ const written = source.label;
11
+ if (typeof written === "function") {
12
+ try {
13
+ const label = (written as () => unknown).call(source);
14
+ if (typeof label === "string" && label) return label;
15
+ } catch {
16
+ }
17
+ }
9
18
  const paths = (model as { text?: { title?: Iterable<string> } } | null)?.text?.title;
10
19
  const titlePath = [...(paths ?? [])].find((path) => !path.includes(".") && !path.includes("["));
11
20
  for (const key of [titlePath, "title", "name"]) {
@@ -275,7 +275,6 @@ interface ArgProps<Value = unknown> {
275
275
  nullable?: boolean;
276
276
  ref?: string;
277
277
  default?: Value;
278
- renderOption?: (arg: never) => string;
279
278
  }
280
279
  export class FilterInfo<ArgNames extends string[] = any, Args extends any[] = any, Model = any> {
281
280
  readonly argNames: ArgNames = [] as unknown as ArgNames;
package/document/types.ts CHANGED
@@ -21,7 +21,6 @@ export interface FilterArgProps {
21
21
  nullable?: boolean;
22
22
  ref?: string;
23
23
  default?: string | number | boolean | object | null | (() => string | number | boolean | object | null);
24
- renderOption?: (value: never) => string;
25
24
  enum?: EnumInstance;
26
25
  }
27
26
 
@@ -115,8 +115,11 @@ export class HttpClient {
115
115
  const argValue = argMap.get(arg.name);
116
116
  if (argValue === null || argValue === undefined) return;
117
117
 
118
- if (arg.refName === "Any") searchParams.set(arg.name, JSON.stringify(argValue));
119
- else if (arg.arrDepth && Array.isArray(argValue))
118
+ if (arg.refName === "Any") {
119
+
120
+ const encoded = JSON.stringify(argValue);
121
+ if (encoded !== undefined) searchParams.set(arg.name, encoded);
122
+ } else if (arg.arrDepth && Array.isArray(argValue))
120
123
  argValue.forEach((value) => {
121
124
  searchParams.append(arg.name, String(value));
122
125
  });
@@ -11,7 +11,11 @@ export type SliceMeta = {
11
11
  /** What the root slice takes: one of the model's declared filter queries, and the args that filter asks for. */
12
12
  export interface QuerySetting {
13
13
  queryKey: string;
14
- args?: unknown[];
14
+ /**
15
+ * Read when the query is applied rather than when the setting is written, so a thunk keeps an arg relative to
16
+ * now — `() => [dayjs().subtract(1, "hour")]` — current at the moment the user asks for it.
17
+ */
18
+ args?: unknown[] | (() => unknown[]);
15
19
  }
16
20
 
17
21
  export type ServerInit<
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.60",
3
+ "version": "3.0.0-alpha.62",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -216,7 +216,6 @@
216
216
  "postgres": "^3.4.9",
217
217
  "protobufjs": "^8.4.0",
218
218
  "react": "19.2.7",
219
- "react-datepicker": "^9.1.0",
220
219
  "react-dom": "19.2.7",
221
220
  "react-icons": "^5.6.0",
222
221
  "react-refresh": "^0.18.0",
@@ -324,9 +323,6 @@
324
323
  "react": {
325
324
  "optional": true
326
325
  },
327
- "react-datepicker": {
328
- "optional": true
329
- },
330
326
  "react-dom": {
331
327
  "optional": true
332
328
  },
@@ -1,6 +1,7 @@
1
1
  /**
2
- * The one line a human scans for, derived from what the model already declared: the `text: "title"` search role
3
- * names exactly that field, so an agent-facing label costs no new declaration. Falls back to the conventional
4
- * `title`/`name` keys; the id is the caller's floor.
2
+ * The one line a human scans for. A model that writes its own `Light<Model>.label()` owns the answer, since
3
+ * display logic is the Light class's job; otherwise it is derived from what the model already declared — the
4
+ * `text: "title"` search role names exactly that field, so an agent-facing label costs no new declaration.
5
+ * Falls back to the conventional `title`/`name` keys; the id is the caller's floor.
5
6
  */
6
7
  export declare const labelOf: (model: unknown, value: unknown) => string | undefined;
@@ -117,7 +117,6 @@ interface ArgProps<Value = unknown> {
117
117
  nullable?: boolean;
118
118
  ref?: string;
119
119
  default?: Value;
120
- renderOption?: (arg: never) => string;
121
120
  }
122
121
  export declare class FilterInfo<ArgNames extends string[] = any, Args extends any[] = any, Model = any> {
123
122
  readonly argNames: ArgNames;
@@ -17,7 +17,6 @@ export interface FilterArgProps {
17
17
  nullable?: boolean;
18
18
  ref?: string;
19
19
  default?: string | number | boolean | object | null | (() => string | number | boolean | object | null);
20
- renderOption?: (value: never) => string;
21
20
  enum?: EnumInstance;
22
21
  }
23
22
  export type DocumentProjection<T> = Partial<Record<keyof T, boolean>>;
@@ -9,7 +9,11 @@ export type SliceMeta = {
9
9
  /** What the root slice takes: one of the model's declared filter queries, and the args that filter asks for. */
10
10
  export interface QuerySetting {
11
11
  queryKey: string;
12
- args?: unknown[];
12
+ /**
13
+ * Read when the query is applied rather than when the setting is written, so a thunk keeps an arg relative to
14
+ * now — `() => [dayjs().subtract(1, "hour")]` — current at the moment the user asks for it.
15
+ */
16
+ args?: unknown[] | (() => unknown[]);
13
17
  }
14
18
  export type ServerInit<RefName extends string, Light, Insight = any, QueryArgs = any, Filter extends FilterInstance = any, _CapitalizedRefName extends string = Capitalize<RefName>, _LightObj = GetStateObject<Light>, _InsightObj = GetStateObject<Insight>, _Sort = ExtractSort<Filter>> = SliceMeta & {
15
19
  [K in `${RefName}ObjList`]: _LightObj[];
@@ -2,13 +2,18 @@ import type { QuerySetting, SliceMeta } from "akanjs/fetch";
2
2
  export interface DashboardProps<T extends string, State> {
3
3
  className?: string;
4
4
  summary: Record<string, unknown>;
5
+ /** The listing these tiles belong to. Kept so a caller names its target; the labels are app-level keys. */
5
6
  slice: SliceMeta;
6
7
  /** Columns that narrow the listing when clicked. A column absent from the map renders as a plain tile. */
7
8
  queryMap?: {
8
9
  [column: string]: QuerySetting;
9
10
  };
11
+ /** Applies one column's filter. Without it a mapped column still renders, but as a plain tile. */
12
+ onSelect?: (setting: QuerySetting, column: string) => void;
13
+ /** The filter key the listing is showing, so a tile stops looking active once the toolbar moves off it. */
14
+ queryKey?: string;
10
15
  columns?: string[];
11
16
  presents?: string[];
12
17
  hidePresents?: boolean;
13
18
  }
14
- export default function Dashboard<T extends string, State>({ className, summary, slice, queryMap, columns, presents, hidePresents, }: DashboardProps<T, State>): import("react/jsx-runtime").JSX.Element | null;
19
+ export default function Dashboard<T extends string, State>({ className, summary, queryMap, onSelect, queryKey, columns, presents, hidePresents, }: DashboardProps<T, State>): import("react/jsx-runtime").JSX.Element | null;
@@ -34,9 +34,13 @@ export interface ListContainerProps<T extends string, State, Input, Full extends
34
34
  tools?: DataTool[] | ((modelList: Light[]) => DataTool[]);
35
35
  /** Per-row actions or action factory. */
36
36
  actions?: DataAction[] | ((item: Light, idx: number) => DataAction[]);
37
- renderDashboard?: ({ summary, hidePresents, }: {
37
+ renderDashboard?: ({ summary, hidePresents, onSelect, queryKey, }: {
38
38
  summary: Record<string, unknown>;
39
39
  hidePresents?: boolean;
40
+ /** Applies one summary column's filter to this listing, in place. */
41
+ onSelect: (setting: QuerySetting) => void;
42
+ /** The filter key the listing is showing right now. */
43
+ queryKey: string;
40
44
  }) => ReactNode;
41
45
  renderItem?: (props: ModelProps<any, any>) => ReactNode;
42
46
  renderTemplate?: (props: any) => ReactNode | null;
@@ -1,8 +1,50 @@
1
1
  import type { QuerySetting, SliceMeta } from "akanjs/fetch";
2
+ import type { SerializedArg } from "akanjs/signal";
2
3
  interface QueryMakerProps {
3
4
  className?: string;
4
5
  slice: SliceMeta;
5
6
  query?: QuerySetting;
7
+ /** Where a completed filter goes. Defaults to the slice's own store, which is where a listing reads it. */
8
+ onApply?: (setting: ResolvedQuerySetting) => void;
6
9
  }
7
- export default function QueryMaker({ className, slice, query }: QueryMakerProps): import("react/jsx-runtime").JSX.Element | null;
10
+ export interface QueryMakerState {
11
+ queryKeys: string[];
12
+ args: SerializedArg[];
13
+ setting: ResolvedQuerySetting;
14
+ selectKey: (queryKey: string) => void;
15
+ setArg: (idx: number, value: unknown) => void;
16
+ /** Applies a whole filter at once, for a control that stands for one — a dashboard tile, a saved view. */
17
+ applySetting: (setting: QuerySetting) => void;
18
+ }
19
+ /** A setting whose thunk has been read, which is the only form the store and the wire accept. */
20
+ export interface ResolvedQuerySetting {
21
+ queryKey: string;
22
+ args: unknown[];
23
+ }
24
+ interface QueryMakerKeyProps {
25
+ className?: string;
26
+ selectClassName?: string;
27
+ slice: SliceMeta;
28
+ state: QueryMakerState;
29
+ }
30
+ interface QueryMakerArgsProps {
31
+ className?: string;
32
+ slice: SliceMeta;
33
+ state: QueryMakerState;
34
+ }
35
+ /** A filter's args may be written as a thunk, so that an arg relative to now is read when the filter is applied. */
36
+ export declare const resolveQuerySetting: (setting: QuerySetting) => ResolvedQuerySetting;
37
+ /** A filter the server will accept: every arg it declared as required has been given a value. */
38
+ export declare const isReadyQuery: (args: SerializedArg[], values: unknown[]) => boolean;
39
+ /**
40
+ * The filter a listing is showing, and the one write that applies it. Held in a hook rather than a component
41
+ * because the key select rides the toolbar while the args that key takes render under it — two places in the
42
+ * tree, one selection.
43
+ */
44
+ export declare const useQueryMaker: ({ slice, query, onApply }: QueryMakerProps) => QueryMakerState;
45
+ /** The filter picker. Sized for a toolbar, beside the sort and page-size selects. */
46
+ export declare const QueryMakerKey: ({ className, selectClassName, slice, state }: QueryMakerKeyProps) => import("react/jsx-runtime").JSX.Element | null;
47
+ /** What the picked filter asks for. Renders nothing for a filter that takes no arguments. */
48
+ export declare const QueryMakerArgs: ({ className, slice, state }: QueryMakerArgsProps) => import("react/jsx-runtime").JSX.Element | null;
49
+ export default function QueryMaker({ className, slice, query, onApply }: QueryMakerProps): import("react/jsx-runtime").JSX.Element;
8
50
  export {};
@@ -0,0 +1,16 @@
1
+ interface RefPickerProps {
2
+ className?: string;
3
+ /** The model this id points at, named by the filter arg's `ref`. */
4
+ refName: string;
5
+ value: string | null;
6
+ onChange: (id: string | null) => void;
7
+ }
8
+ /**
9
+ * Picks one row of a referenced model instead of asking for its hex id. Rows are held here rather than in the
10
+ * ref model's store: that store is a singleton, so a picker loading into it would overwrite whatever listing of
11
+ * the same model is already on the screen — and a picker opened from inside a picker would overwrite itself.
12
+ *
13
+ * The search is the ref model's own root slice, which is Admin-guarded, so this belongs to admin surfaces.
14
+ */
15
+ export default function RefPicker({ className, refName, value, onChange }: RefPickerProps): import("react/jsx-runtime").JSX.Element;
16
+ export {};
@@ -6,5 +6,6 @@ export declare const Data: {
6
6
  ListContainer: typeof import("./ListContainer.d.ts").default;
7
7
  Pagination: typeof import("./Pagination.d.ts").default;
8
8
  QueryMaker: typeof import("./QueryMaker.d.ts").default;
9
+ RefPicker: typeof import("./RefPicker.d.ts").default;
9
10
  TableList: typeof import("./TableList.d.ts").default;
10
11
  };
@@ -6,3 +6,4 @@ export declare const ListContainer: typeof import("./ListContainer.d.ts").defaul
6
6
  export declare const Pagination: typeof import("./Pagination.d.ts").default;
7
7
  export declare const TableList: typeof import("./TableList.d.ts").default;
8
8
  export declare const QueryMaker: typeof import("./QueryMaker.d.ts").default;
9
+ export declare const RefPicker: typeof import("./RefPicker.d.ts").default;
@@ -1,37 +1,41 @@
1
1
  import { type Dayjs } from "akanjs/base";
2
2
  export interface DatePickerProps {
3
+ className?: string;
3
4
  value?: Dayjs | null;
4
5
  onChange: (value: Dayjs | null) => void;
5
6
  showTime?: boolean;
6
- format?: string;
7
- timeIntervals?: number;
7
+ /** Earliest selectable value. The browser enforces it. */
8
+ min?: Dayjs | null;
9
+ /** Latest selectable value. The browser enforces it. */
10
+ max?: Dayjs | null;
11
+ /** Rejected on selection rather than greyed out — a native field constrains only through `min` / `max`. */
8
12
  disabledDate?: (date: Dayjs) => boolean | null | undefined;
9
- className?: string;
10
- placement?: "top" | "bottom" | "left" | "right";
11
13
  defaultValue?: Dayjs;
12
14
  }
13
15
  export interface RangePickerProps {
16
+ className?: string;
14
17
  value: [Dayjs | null, Dayjs | null];
15
18
  onChange: (value: [Dayjs | null, Dayjs | null]) => void;
16
- format?: string;
17
19
  showTime?: boolean;
18
- timeIntervals?: number;
20
+ /** Rejected on selection rather than greyed out — a native field constrains only through `min` / `max`. */
19
21
  disabledDate?: (date: Dayjs) => boolean | null | undefined;
20
- className?: string;
21
22
  }
22
23
  export interface TimePickerProps {
24
+ className?: string;
23
25
  value: Dayjs | null;
24
26
  onChange: (value: Dayjs) => void;
25
- format?: string;
26
- timeIntervals?: number;
27
- disabledDate?: (date: Dayjs) => boolean | null | undefined;
28
- className?: string;
29
27
  disabled?: boolean;
28
+ /** Rejected on selection rather than greyed out — a native field constrains only through `min` / `max`. */
29
+ disabledDate?: (date: Dayjs) => boolean | null | undefined;
30
30
  }
31
31
  /**
32
32
  * Date picker. `DatePicker`, `DatePicker.RangePicker`, and `DatePicker.TimePicker` each resolve to a
33
33
  * route-scoped override when a `page/**\/_overrides.tsx` in the route's ancestry declares one (slots
34
34
  * `DatePicker`, `DatePickerRangePicker`, `DatePickerTimePicker`).
35
+ *
36
+ * Each renders one native `<input type="date" | "datetime-local" | "time">`, so the calendar is the browser's
37
+ * own: an OS wheel on mobile, keyboard entry everywhere, nothing shipped in the bundle. What that costs is a
38
+ * themed popup, a chosen display format, and per-day disabling — an app needing any of those overrides the slot.
35
39
  */
36
40
  export declare const DatePicker: import("react").ComponentType<DatePickerProps> & {
37
41
  RangePicker: import("react").ComponentType<RangePickerProps>;
@@ -1,5 +1,6 @@
1
1
  import { type Dayjs, type DefaultPrimitiveName } from "akanjs/base";
2
2
  import type { SerializedArg } from "akanjs/signal";
3
+ import type { ReactNode } from "react";
3
4
  interface ArgProps {
4
5
  argType: DefaultPrimitiveName;
5
6
  value: any;
@@ -9,7 +10,7 @@ declare function Arg({ argType, value, onChange }: ArgProps): import("react/jsx-
9
10
  declare namespace Arg {
10
11
  var Table: ({ refName, endpointKey, args }: ArgTableProps) => import("react/jsx-runtime").JSX.Element;
11
12
  var Param: ({ endpointKey, arg, value, onChange }: ArgParamProps) => import("react/jsx-runtime").JSX.Element;
12
- var Query: ({ endpointKey, arg, label, value, onChange }: ArgQueryProps) => import("react/jsx-runtime").JSX.Element;
13
+ var Query: ({ endpointKey, arg, label, value, onChange, renderScalar }: ArgQueryProps) => import("react/jsx-runtime").JSX.Element;
13
14
  var FormData: ({ endpointKey, arg, value, onChange }: ArgFormDataProps) => import("react/jsx-runtime").JSX.Element;
14
15
  var ID: ({ value, onChange }: ArgIDProps) => import("react/jsx-runtime").JSX.Element;
15
16
  var Int: ({ value, onChange }: ArgIntProps) => import("react/jsx-runtime").JSX.Element;
@@ -38,6 +39,8 @@ interface ArgQueryProps {
38
39
  label?: string;
39
40
  value: any;
40
41
  onChange: (value: any) => void;
42
+ /** Replaces the scalar input this arg would otherwise get, once per element for an array arg. */
43
+ renderScalar?: (value: any, onChange: (value: any) => void) => ReactNode;
41
44
  }
42
45
  interface ArgFormDataProps {
43
46
  endpointKey: string;
@@ -2,16 +2,21 @@
2
2
  import { cn, usePage } from "akanjs/client";
3
3
  import type { QuerySetting, SliceMeta } from "akanjs/fetch";
4
4
  import { st } from "akanjs/store";
5
+ import { useState } from "react";
5
6
 
6
- import { Link } from "../Link";
7
7
  import { dictLabel, formatStat } from "./dataText";
8
8
 
9
9
  export interface DashboardProps<T extends string, State> {
10
10
  className?: string;
11
11
  summary: Record<string, unknown>;
12
+ /** The listing these tiles belong to. Kept so a caller names its target; the labels are app-level keys. */
12
13
  slice: SliceMeta;
13
14
  /** Columns that narrow the listing when clicked. A column absent from the map renders as a plain tile. */
14
15
  queryMap?: { [column: string]: QuerySetting };
16
+ /** Applies one column's filter. Without it a mapped column still renders, but as a plain tile. */
17
+ onSelect?: (setting: QuerySetting, column: string) => void;
18
+ /** The filter key the listing is showing, so a tile stops looking active once the toolbar moves off it. */
19
+ queryKey?: string;
15
20
  columns?: string[];
16
21
  presents?: string[];
17
22
  hidePresents?: boolean;
@@ -22,42 +27,55 @@ const tileClassName = "flex min-w-40 flex-1 flex-col gap-1 rounded-box border px
22
27
  export default function Dashboard<T extends string, State>({
23
28
  className,
24
29
  summary,
25
- slice,
26
30
  queryMap,
31
+ onSelect,
32
+ queryKey,
27
33
  columns,
28
34
  presents,
29
35
  hidePresents,
30
36
  }: DashboardProps<T, State>) {
31
- const { refName } = slice;
32
37
  const { l } = usePage();
33
38
  const searchParams = st.use.searchParams({ agent: false });
34
39
  const filter = Array.isArray(searchParams.filter) ? searchParams.filter[0] : searchParams.filter;
40
+
41
+ const [selected, setSelected] = useState(typeof filter === "string" ? filter : undefined);
35
42
  const shownColumns = (columns ?? []).filter((column) => summary[column] !== undefined);
36
43
  const shownPresents = hidePresents ? [] : (presents ?? []).filter((column) => summary[column] !== undefined);
37
44
  if (!shownColumns.length && !shownPresents.length) return null;
45
+ const activeColumn = selected && queryMap?.[selected]?.queryKey === queryKey ? selected : undefined;
38
46
  return (
39
47
  <div className={cn("mb-4 flex flex-wrap gap-2", className)}>
40
48
  {[...shownColumns, ...shownPresents].map((column) => {
41
- const linkable = queryMap?.[column] !== undefined;
42
- return (
43
- <Link
44
- key={column}
45
- disabled={!linkable}
46
- href={`/admin?topMenu=data&subMenu=${refName}&filter=${column}`}
49
+ const setting = queryMap?.[column];
50
+ const label = dictLabel(l._, `summary.${column}`, column);
51
+ const body = (
52
+ <>
53
+ <span className="truncate text-muted-foreground text-xs">{label}</span>
54
+ <span className="truncate font-semibold text-2xl text-primary tabular-nums">
55
+ {formatStat(summary[column])}
56
+ </span>
57
+ </>
58
+ );
59
+ return setting && onSelect ? (
60
+ <button
47
61
  className={cn(
48
62
  tileClassName,
49
- "bg-card",
50
- linkable && "transition hover:border-primary/50",
51
- filter === column && linkable ? "border-primary" : "border-border",
63
+ "bg-card transition hover:border-primary/50",
64
+ activeColumn === column ? "border-primary" : "border-border",
52
65
  )}
66
+ key={column}
67
+ onClick={() => {
68
+ setSelected(column);
69
+ onSelect(setting, column);
70
+ }}
71
+ type="button"
53
72
  >
54
- <span className="truncate text-muted-foreground text-xs">
55
- {dictLabel(l._, `summary.${column}`, column)}
56
- </span>
57
- <span className="truncate font-semibold text-2xl text-primary tabular-nums">
58
- {formatStat(summary[column])}
59
- </span>
60
- </Link>
73
+ {body}
74
+ </button>
75
+ ) : (
76
+ <div className={cn(tileClassName, "border-border bg-card")} key={column}>
77
+ {body}
78
+ </div>
61
79
  );
62
80
  })}
63
81
  </div>
@@ -36,7 +36,7 @@ import { Select } from "../Select";
36
36
  import DataCardList from "./CardList";
37
37
  import { columnKey, downloadBlob, toCsvBlob, toJsonBlob } from "./dataExport";
38
38
  import { dictLabel } from "./dataText";
39
- import DataQueryMaker from "./QueryMaker";
39
+ import { QueryMakerArgs, QueryMakerKey, resolveQuerySetting, useQueryMaker } from "./QueryMaker";
40
40
  import DataTableList from "./TableList";
41
41
 
42
42
  const controlClassName = "h-9";
@@ -79,9 +79,15 @@ export interface ListContainerProps<
79
79
  renderDashboard?: ({
80
80
  summary,
81
81
  hidePresents,
82
+ onSelect,
83
+ queryKey,
82
84
  }: {
83
85
  summary: Record<string, unknown>;
84
86
  hidePresents?: boolean;
87
+ /** Applies one summary column's filter to this listing, in place. */
88
+ onSelect: (setting: QuerySetting) => void;
89
+ /** The filter key the listing is showing right now. */
90
+ queryKey: string;
85
91
  }) => ReactNode;
86
92
  renderItem?: (props: ModelProps<any, any>) => ReactNode;
87
93
  renderTemplate?: (props: any) => ReactNode | null;
@@ -174,10 +180,14 @@ export default function ListContainer<
174
180
  const searchParams = st.use.searchParams({ agent: false });
175
181
  const filter = Array.isArray(searchParams.filter) ? searchParams.filter[0] : searchParams.filter;
176
182
  const initQuery = query ?? (filter ? queryMap?.[filter] : undefined);
183
+ const queryState = useQueryMaker({ slice, query: initQuery });
177
184
  useEffect(() => {
178
185
 
179
186
  const queryArgs = new Array(slice.argLength).fill(null) as unknown[];
180
- if (initQuery) [queryArgs[0], queryArgs[1]] = [initQuery.queryKey, initQuery.args ?? []];
187
+ if (initQuery) {
188
+ const { queryKey, args } = resolveQuerySetting(initQuery);
189
+ [queryArgs[0], queryArgs[1]] = [queryKey, args];
190
+ }
181
191
  void storeDo[namesOfSlice.initModel](...queryArgs, { sort, ...init });
182
192
  }, []);
183
193
 
@@ -251,19 +261,28 @@ export default function ListContainer<
251
261
 
252
262
  const modelLabel = dictLabel(l._, `${sliceName}.modelName`, refName);
253
263
  const RenderTitle = renderTitle ?? ((model: Full) => `${modelLabel} - ${model.id ? model.id : "New"}`);
254
- const ModelDashboard = (): ReactNode => {
255
- const Stat = renderDashboard;
256
264
 
257
- const summary = storeSel<Record<string, unknown> | undefined>(
258
- (state) => (state as { summary?: Record<string, unknown> }).summary,
265
+ const summary = storeSel<Record<string, unknown> | undefined>(
266
+ (state) => (state as { summary?: Record<string, unknown> }).summary,
267
+ );
268
+ const summaryLoading = storeSel<boolean>((state) => !!(state as { summaryLoading?: boolean }).summaryLoading);
269
+ const modelDashboard =
270
+ !renderDashboard || !summary ? null : summaryLoading ? (
271
+ <Loading.Skeleton className="mb-4" active />
272
+ ) : (
273
+ renderDashboard({
274
+ summary,
275
+ hidePresents: true,
276
+ onSelect: queryState.applySetting,
277
+ queryKey: queryState.setting.queryKey,
278
+ })
259
279
  );
260
- const summaryLoading = storeSel<boolean>((state) => !!(state as { summaryLoading?: boolean }).summaryLoading);
261
- if (!Stat || !summary) return null;
262
- return summaryLoading ? <Loading.Skeleton className="mb-4" active /> : <Stat summary={summary} hidePresents />;
263
- };
264
-
265
- const RenderQueryMaker =
266
- renderQueryMaker ?? (query ? undefined : () => <DataQueryMaker slice={slice} query={initQuery} />);
280
+
281
+ const queryMakerArgs = renderQueryMaker ? (
282
+ renderQueryMaker()
283
+ ) : query ? null : (
284
+ <QueryMakerArgs slice={slice} state={queryState} />
285
+ );
267
286
  const RenderInsight = (): ReactNode => (renderInsight ? renderInsight({ insight: modelInsight }) : null);
268
287
  const RenderTemplate = renderTemplate;
269
288
  const RenderTools = (): ReactNode => {
@@ -356,6 +375,14 @@ export default function ListContainer<
356
375
  </button>
357
376
  ))}
358
377
  </div>
378
+ {query ? null : (
379
+ <QueryMakerKey
380
+ className="w-44 min-w-0"
381
+ selectClassName={cn("min-h-0", controlClassName)}
382
+ slice={slice}
383
+ state={queryState}
384
+ />
385
+ )}
359
386
  <RenderSort />
360
387
  <Select<number>
361
388
  className="w-36 min-w-0"
@@ -389,8 +416,8 @@ export default function ListContainer<
389
416
  ) : null}
390
417
  </div>
391
418
  </div>
392
- {query ? null : <ModelDashboard />}
393
- {RenderQueryMaker ? <RenderQueryMaker /> : null}
419
+ {query ? null : modelDashboard}
420
+ {queryMakerArgs}
394
421
  <RenderInsight />
395
422
  {view === "card" ? (
396
423
  <DataCardList