@questpie/admin 3.5.0 → 3.5.2

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.
Files changed (78) hide show
  1. package/dist/client/blocks/block-renderer.d.mts +2 -2
  2. package/dist/client/components/blocks/block-canvas.mjs +1 -1
  3. package/dist/client/components/blocks/block-editor-layout.mjs +2 -2
  4. package/dist/client/components/blocks/block-insert-button.mjs +3 -3
  5. package/dist/client/components/blocks/block-item-menu.mjs +9 -9
  6. package/dist/client/components/blocks/block-item.mjs +5 -5
  7. package/dist/client/components/blocks/block-library-sidebar.mjs +3 -3
  8. package/dist/client/components/fields/array-field.mjs +2 -2
  9. package/dist/client/components/fields/date-field.mjs +6 -5
  10. package/dist/client/components/fields/datetime-field.mjs +6 -1
  11. package/dist/client/components/fields/object-array-field.mjs +2 -2
  12. package/dist/client/components/fields/relation/displays/cards-display.mjs +1 -1
  13. package/dist/client/components/fields/relation/displays/grid-display.mjs +1 -1
  14. package/dist/client/components/fields/relation/displays/list-display.mjs +3 -3
  15. package/dist/client/components/fields/relation/displays/table-display.mjs +1 -1
  16. package/dist/client/components/fields/relation-picker.mjs +3 -3
  17. package/dist/client/components/fields/relation-select.mjs +2 -2
  18. package/dist/client/components/filter-builder/filter-builder-sheet.mjs +16 -16
  19. package/dist/client/components/history-sidebar.mjs +12 -4
  20. package/dist/client/components/layout/field-layout-renderer.mjs +8 -3
  21. package/dist/client/components/media/media-grid.mjs +2 -2
  22. package/dist/client/components/preview/live-preview-mode.mjs +4 -4
  23. package/dist/client/components/preview/preview-pane.mjs +4 -4
  24. package/dist/client/components/primitives/asset-preview.mjs +5 -5
  25. package/dist/client/components/primitives/dropzone.mjs +1 -1
  26. package/dist/client/components/ui/kbd.mjs +1 -1
  27. package/dist/client/components/ui/scroll-fade.mjs +4 -4
  28. package/dist/client/components/ui/sidebar.mjs +1 -1
  29. package/dist/client/components/ui/skeleton.mjs +1 -1
  30. package/dist/client/components/ui/table.mjs +1 -1
  31. package/dist/client/components/widgets/quick-actions-widget.mjs +6 -6
  32. package/dist/client/components/widgets/timeline-widget.mjs +3 -3
  33. package/dist/client/components/widgets/value-widget.mjs +1 -1
  34. package/dist/client/components/widgets/widget-skeletons.mjs +2 -2
  35. package/dist/client/hooks/typed-hooks.mjs +66 -21
  36. package/dist/client/hooks/use-collection.mjs +48 -7
  37. package/dist/client/i18n/date-locale.mjs +0 -14
  38. package/dist/client/preview/block-scope-context.d.mts +2 -2
  39. package/dist/client/preview/diff.mjs +4 -1
  40. package/dist/client/preview/patch.mjs +1 -1
  41. package/dist/client/preview/paths.mjs +85 -0
  42. package/dist/client/preview/preview-banner.d.mts +2 -2
  43. package/dist/client/preview/preview-field.d.mts +4 -4
  44. package/dist/client/runtime/translations-provider.mjs +1 -1
  45. package/dist/client/styles/base.css +51 -1
  46. package/dist/client/views/auth/accept-invite-form.d.mts +2 -2
  47. package/dist/client/views/auth/auth-layout.d.mts +3 -3
  48. package/dist/client/views/auth/login-form.d.mts +2 -2
  49. package/dist/client/views/auth/reset-password-form.d.mts +2 -2
  50. package/dist/client/views/collection/auto-form-fields.mjs +1 -1
  51. package/dist/client/views/collection/cells/primitive-cells.mjs +2 -2
  52. package/dist/client/views/collection/cells/upload-cells.mjs +2 -2
  53. package/dist/client/views/collection/form-view.mjs +45 -26
  54. package/dist/client/views/collection/table-view.mjs +33 -20
  55. package/dist/client/views/collection/view-skeletons.mjs +37 -38
  56. package/dist/client/views/common/global-search.mjs +3 -3
  57. package/dist/client/views/dashboard/widget-card.mjs +7 -7
  58. package/dist/client/views/layout/admin-router.mjs +84 -37
  59. package/dist/client/views/layout/admin-sidebar.mjs +22 -21
  60. package/dist/components/rich-text/rich-text-renderer.d.mts +2 -2
  61. package/dist/server/codegen/admin-client-template.mjs +48 -32
  62. package/dist/server/modules/admin/collections/account.d.mts +50 -50
  63. package/dist/server/modules/admin/collections/admin-locks.d.mts +54 -54
  64. package/dist/server/modules/admin/collections/admin-preferences.d.mts +35 -35
  65. package/dist/server/modules/admin/collections/admin-saved-views.d.mts +47 -47
  66. package/dist/server/modules/admin/collections/apikey.d.mts +64 -64
  67. package/dist/server/modules/admin/collections/assets.d.mts +34 -34
  68. package/dist/server/modules/admin/collections/session.d.mts +38 -38
  69. package/dist/server/modules/admin/collections/user.d.mts +53 -53
  70. package/dist/server/modules/admin/routes/execute-action.d.mts +9 -9
  71. package/dist/server/modules/admin/routes/locales.d.mts +2 -2
  72. package/dist/server/modules/admin/routes/preview.d.mts +11 -11
  73. package/dist/server/modules/admin/routes/reactive.d.mts +9 -9
  74. package/dist/server/modules/admin/routes/setup.d.mts +7 -7
  75. package/dist/server/modules/admin/routes/translations.d.mts +4 -4
  76. package/dist/server/modules/admin/routes/widget-data.d.mts +5 -5
  77. package/dist/server/modules/admin-preferences/collections/saved-views.d.mts +23 -23
  78. package/package.json +3 -3
@@ -16,7 +16,7 @@ import { DashboardGrid } from "../dashboard/dashboard-grid.mjs";
16
16
  import { Icon } from "@iconify/react";
17
17
  import * as React from "react";
18
18
  import { useQueryClient } from "@tanstack/react-query";
19
- import { jsx, jsxs } from "react/jsx-runtime";
19
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
20
20
 
21
21
  //#region src/client/views/layout/admin-router.tsx
22
22
  /**
@@ -201,16 +201,30 @@ function UnknownViewState({ viewKind, viewId }) {
201
201
  */
202
202
  function RouterSkeleton() {
203
203
  return /* @__PURE__ */ jsxs("div", {
204
- className: "qa-router-skeleton container space-y-4 py-6",
204
+ className: "qa-router-skeleton min-w-0 space-y-4",
205
+ "aria-busy": "true",
205
206
  children: [
206
- /* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-48" }),
207
- /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }),
207
+ /* @__PURE__ */ jsx("span", {
208
+ className: "sr-only",
209
+ children: "Loading admin view"
210
+ }),
211
+ /* @__PURE__ */ jsx(AdminViewHeader, {
212
+ title: /* @__PURE__ */ jsx(Skeleton, {
213
+ variant: "text",
214
+ className: "h-7 w-44 max-w-full"
215
+ }),
216
+ description: /* @__PURE__ */ jsx(Skeleton, {
217
+ variant: "text",
218
+ className: "h-4 w-64 max-w-full"
219
+ }),
220
+ actions: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Skeleton, { className: "size-8" }), /* @__PURE__ */ jsx(Skeleton, { className: "size-8" })] })
221
+ }),
208
222
  /* @__PURE__ */ jsxs("div", {
209
- className: "space-y-2",
223
+ className: "grid gap-3",
210
224
  children: [
211
- /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-full" }),
212
- /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-full" }),
213
- /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-full" })
225
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" }),
226
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" }),
227
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" })
214
228
  ]
215
229
  })
216
230
  ]
@@ -363,7 +377,7 @@ function DefaultDashboard() {
363
377
  children: /* @__PURE__ */ jsxs("div", { children: [
364
378
  /* @__PURE__ */ jsxs("div", {
365
379
  className: "mb-4 flex items-center gap-3",
366
- children: [/* @__PURE__ */ jsx("div", { className: "bg-primary h-2 w-2 rounded-full" }), /* @__PURE__ */ jsx("h3", {
380
+ children: [/* @__PURE__ */ jsx("div", { className: "bg-primary size-2 rounded-full" }), /* @__PURE__ */ jsx("h3", {
367
381
  className: "text-muted-foreground font-chrome chrome-meta text-xs font-medium",
368
382
  children: t("dashboard.systemStatus")
369
383
  })]
@@ -386,7 +400,7 @@ function DefaultNotFound() {
386
400
  return /* @__PURE__ */ jsxs("div", {
387
401
  className: "qa-not-found container",
388
402
  children: [/* @__PURE__ */ jsx("h1", {
389
- className: "mb-4 text-2xl font-bold",
403
+ className: "mb-4 text-2xl font-semibold",
390
404
  children: t("error.pageNotFound")
391
405
  }), /* @__PURE__ */ jsx("p", {
392
406
  className: "text-muted-foreground",
@@ -402,10 +416,10 @@ function RestrictedAccess({ type, name, navigate, basePath }) {
402
416
  className: "mx-auto max-w-lg p-8 text-center",
403
417
  children: /* @__PURE__ */ jsxs("div", { children: [
404
418
  /* @__PURE__ */ jsx("div", {
405
- className: "bg-muted mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full",
419
+ className: "bg-muted mx-auto mb-6 flex size-16 items-center justify-center rounded-full",
406
420
  children: /* @__PURE__ */ jsx(Icon, {
407
421
  icon: "ph:lock-simple",
408
- className: "text-muted-foreground h-8 w-8"
422
+ className: "text-muted-foreground size-8"
409
423
  })
410
424
  }),
411
425
  /* @__PURE__ */ jsx("h1", {
@@ -424,7 +438,7 @@ function RestrictedAccess({ type, name, navigate, basePath }) {
424
438
  onClick: () => navigate(basePath),
425
439
  children: [/* @__PURE__ */ jsx(Icon, {
426
440
  icon: "ph:arrow-left",
427
- className: "h-4 w-4"
441
+ className: "size-4"
428
442
  }), t("error.backToDashboard")]
429
443
  })
430
444
  ] })
@@ -434,26 +448,35 @@ function RestrictedAccess({ type, name, navigate, basePath }) {
434
448
  function LazyPageRenderer({ config }) {
435
449
  const { t } = useTranslation();
436
450
  const component = config.component;
437
- const [Component, setComponent] = React.useState(() => getCachedComponent(component) ?? null);
438
- const [loading, setLoading] = React.useState(() => Component == null);
439
- const [error, setError] = React.useState(null);
451
+ const [pageState, setPageState] = React.useState(() => {
452
+ const Component = getCachedComponent(component) ?? null;
453
+ return {
454
+ source: component,
455
+ Component,
456
+ loading: Component == null,
457
+ error: null
458
+ };
459
+ });
440
460
  React.useEffect(() => {
441
461
  let mounted = true;
442
462
  async function load() {
443
463
  try {
444
464
  const cachedComponent = getCachedComponent(component);
445
465
  if (cachedComponent) {
446
- if (mounted) {
447
- setComponent(() => cachedComponent);
448
- setLoading(false);
449
- setError(null);
450
- }
466
+ if (mounted) setPageState({
467
+ source: component,
468
+ Component: cachedComponent,
469
+ loading: false,
470
+ error: null
471
+ });
451
472
  return;
452
473
  }
453
- if (mounted) {
454
- setLoading(true);
455
- setError(null);
456
- }
474
+ if (mounted) setPageState({
475
+ source: component,
476
+ Component: null,
477
+ loading: true,
478
+ error: null
479
+ });
457
480
  if (typeof component === "function") {
458
481
  const result = component();
459
482
  let isThenable = false;
@@ -467,21 +490,44 @@ function LazyPageRenderer({ config }) {
467
490
  if (mod.default) resolved = mod.default;
468
491
  else resolved = mod;
469
492
  cacheComponent(component, resolved);
470
- setComponent(() => resolved);
493
+ setPageState({
494
+ source: component,
495
+ Component: resolved,
496
+ loading: false,
497
+ error: null
498
+ });
471
499
  }
472
- } else if (mounted) setComponent(() => component);
500
+ } else if (mounted) setPageState({
501
+ source: component,
502
+ Component: component,
503
+ loading: false,
504
+ error: null
505
+ });
473
506
  } else if (component) {
474
- if (mounted) setComponent(() => component);
475
- }
476
- if (mounted) setLoading(false);
507
+ if (mounted) setPageState({
508
+ source: component,
509
+ Component: component,
510
+ loading: false,
511
+ error: null
512
+ });
513
+ } else if (mounted) setPageState({
514
+ source: component,
515
+ Component: null,
516
+ loading: false,
517
+ error: null
518
+ });
477
519
  } catch (err) {
478
520
  if (mounted) {
479
521
  let resolvedError;
480
522
  if (err instanceof Error) resolvedError = err;
481
523
  else resolvedError = new Error(t("error.failedToLoad"));
482
- setError(resolvedError);
524
+ setPageState({
525
+ source: component,
526
+ Component: null,
527
+ loading: false,
528
+ error: resolvedError
529
+ });
483
530
  }
484
- if (mounted) setLoading(false);
485
531
  }
486
532
  }
487
533
  load();
@@ -489,21 +535,22 @@ function LazyPageRenderer({ config }) {
489
535
  mounted = false;
490
536
  };
491
537
  }, [component, t]);
492
- if (loading) {
538
+ if (!(pageState.source === component) || pageState.loading) {
493
539
  const path = config.path?.replace(/^\//, "") ?? "";
494
540
  return AUTH_ROUTE_SEGMENTS.has(path) ? /* @__PURE__ */ jsx(AuthPageSkeleton, {}) : /* @__PURE__ */ jsx(RouterSkeleton, {});
495
541
  }
496
- if (error) return /* @__PURE__ */ jsxs("div", {
542
+ if (pageState.error) return /* @__PURE__ */ jsxs("div", {
497
543
  className: "container",
498
544
  children: [/* @__PURE__ */ jsx("h1", {
499
- className: "text-destructive mb-4 text-2xl font-bold",
545
+ className: "text-destructive mb-4 text-2xl font-semibold",
500
546
  children: t("error.unexpectedError")
501
547
  }), /* @__PURE__ */ jsx("p", {
502
548
  className: "text-muted-foreground",
503
- children: error.message
549
+ children: pageState.error.message
504
550
  })]
505
551
  });
506
- return Component ? /* @__PURE__ */ jsx(Component, {}) : /* @__PURE__ */ jsx(DefaultNotFound, {});
552
+ const PageComponent = pageState.Component;
553
+ return PageComponent ? /* @__PURE__ */ jsx(PageComponent, {}) : /* @__PURE__ */ jsx(DefaultNotFound, {});
507
554
  }
508
555
  /**
509
556
  * AdminRouter Component
@@ -5,7 +5,7 @@ import { useSafeContentLocales } from "../../runtime/content-locales-provider.mj
5
5
  import { ComponentRenderer } from "../../components/component-renderer.mjs";
6
6
  import { Button } from "../../components/ui/button.mjs";
7
7
  import { getFlagUrl } from "../../utils/locale-to-flag.mjs";
8
- import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "../../components/ui/dropdown-menu.mjs";
8
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "../../components/ui/dropdown-menu.mjs";
9
9
  import { Tooltip, TooltipContent, TooltipTrigger } from "../../components/ui/tooltip.mjs";
10
10
  import { useAdminConfig } from "../../hooks/use-admin-config.mjs";
11
11
  import { useLazyComponent } from "../../utils/use-lazy-component.mjs";
@@ -222,7 +222,7 @@ function isRouteActive(activeRoute, itemHref, basePath, exact = false) {
222
222
  /**
223
223
  * Menu button styles - QUESTPIE design: clean, technical look
224
224
  */
225
- const menuButtonStyles = cn("item-surface font-chrome flex w-full items-center gap-2 px-2 py-2 text-[13px] font-medium transition-[background-color,color,border-color,transform] duration-[var(--motion-duration-base)] ease-[var(--motion-ease-standard)] active:scale-[0.96] motion-reduce:transition-none motion-reduce:active:scale-100", "text-sidebar-foreground/75 hover:bg-sidebar-accent hover:text-sidebar-foreground", "focus-visible:ring-sidebar-ring focus-visible:ring-1 focus-visible:outline-none", "group-data-[collapsible=icon]:size-8 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-2");
225
+ const menuButtonStyles = cn("item-surface font-chrome flex w-full items-center gap-2 p-2 text-[13px] font-medium transition-[background-color,color,border-color,transform] duration-[var(--motion-duration-base)] ease-[var(--motion-ease-standard)] active:scale-[0.96] motion-reduce:transition-none motion-reduce:active:scale-100", "text-sidebar-foreground/75 hover:bg-sidebar-accent hover:text-sidebar-foreground", "focus-visible:ring-sidebar-ring focus-visible:ring-1 focus-visible:outline-none", "group-data-[collapsible=icon]:size-8 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-2");
226
226
  const menuButtonActiveStyles = cn("border-transparent bg-[var(--sidebar-active-background)] text-[var(--sidebar-active-foreground)]");
227
227
  function NavItem({ item, isActive, LinkComponent, renderNavItem, useActiveProps, className, depth = 0 }) {
228
228
  const { state, isMobile, setOpenMobile } = useSidebar();
@@ -423,7 +423,7 @@ function UserFooter({ theme = "system", setTheme, showThemeToggle }) {
423
423
  const setContentLocale = useAdminStore(selectSetContentLocale);
424
424
  const hasMultipleContentLocales = (contentLocales?.locales?.length ?? 0) > 1;
425
425
  const shouldShowThemeToggle = !!setTheme && showThemeToggle !== false;
426
- const themeOptions = [
426
+ const themeOptions = React.useMemo(() => [
427
427
  {
428
428
  value: "light",
429
429
  label: t("ui.themeLight"),
@@ -439,7 +439,10 @@ function UserFooter({ theme = "system", setTheme, showThemeToggle }) {
439
439
  label: t("ui.themeSystem"),
440
440
  icon: "ph:monitor"
441
441
  }
442
- ];
442
+ ], [t]);
443
+ const handleThemeChange = React.useCallback((value) => {
444
+ setTheme?.(value);
445
+ }, [setTheme]);
443
446
  const closeSidebarOnMobile = React.useCallback(() => {
444
447
  if (isMobile) setOpenMobile(false);
445
448
  }, [isMobile, setOpenMobile]);
@@ -527,23 +530,21 @@ function UserFooter({ theme = "system", setTheme, showThemeToggle }) {
527
530
  className: "size-4"
528
531
  }), t("auth.myAccount")]
529
532
  }),
530
- shouldShowThemeToggle && /* @__PURE__ */ jsxs(DropdownMenuSub, { children: [/* @__PURE__ */ jsxs(DropdownMenuSubTrigger, { children: [/* @__PURE__ */ jsx(Icon, { icon: "ph:circle-half" }), t("ui.toggleTheme")] }), /* @__PURE__ */ jsx(DropdownMenuSubContent, { children: themeOptions.map((option) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
531
- onClick: () => setTheme(option.value),
532
- children: [
533
- /* @__PURE__ */ jsx(Icon, {
534
- icon: option.icon,
535
- className: "size-4"
536
- }),
537
- /* @__PURE__ */ jsx("span", {
538
- className: "flex-1",
539
- children: option.label
540
- }),
541
- theme === option.value && /* @__PURE__ */ jsx(Icon, {
542
- icon: "ph:check",
543
- className: "text-foreground size-4"
544
- })
545
- ]
546
- }, option.value)) })] }),
533
+ shouldShowThemeToggle && /* @__PURE__ */ jsxs(Fragment, { children: [
534
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
535
+ /* @__PURE__ */ jsx(DropdownMenuLabel, { children: t("ui.toggleTheme") }),
536
+ /* @__PURE__ */ jsx(DropdownMenuRadioGroup, {
537
+ value: theme,
538
+ onValueChange: handleThemeChange,
539
+ children: themeOptions.map((option) => /* @__PURE__ */ jsxs(DropdownMenuRadioItem, {
540
+ value: option.value,
541
+ children: [/* @__PURE__ */ jsx(Icon, {
542
+ icon: option.icon,
543
+ className: "size-4"
544
+ }), /* @__PURE__ */ jsx("span", { children: option.label })]
545
+ }, option.value))
546
+ })
547
+ ] }),
547
548
  hasMultipleUiLocales && /* @__PURE__ */ jsxs(DropdownMenuSub, { children: [/* @__PURE__ */ jsxs(DropdownMenuSubTrigger, { children: [/* @__PURE__ */ jsx(Icon, { icon: "ph:globe" }), t("locale.uiLanguage")] }), /* @__PURE__ */ jsx(DropdownMenuSubContent, { children: uiLocaleOptions.map((locale) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
548
549
  onClick: () => setUiLocale(locale.code),
549
550
  children: [
@@ -1,4 +1,4 @@
1
- import * as react_jsx_runtime20 from "react/jsx-runtime";
1
+ import * as react_jsx_runtime19 from "react/jsx-runtime";
2
2
 
3
3
  //#region src/components/rich-text/rich-text-renderer.d.ts
4
4
  /**
@@ -98,6 +98,6 @@ declare function RichTextRenderer({
98
98
  content,
99
99
  styles: customStyles,
100
100
  className
101
- }: RichTextRendererProps): react_jsx_runtime20.JSX.Element | null;
101
+ }: RichTextRendererProps): react_jsx_runtime19.JSX.Element | null;
102
102
  //#endregion
103
103
  export { RichTextRenderer, RichTextStyles, TipTapDoc, TipTapNode };
@@ -1,5 +1,47 @@
1
+ import { categoryRecordEntry, importStatement, sortedValues } from "questpie/codegen";
2
+
1
3
  //#region src/server/codegen/admin-client-template.ts
2
4
  /**
5
+ * Admin Client Template Generator
6
+ *
7
+ * Custom generator for the `admin-client` codegen target.
8
+ * Produces `questpie/admin/.generated/client.ts` — a plain admin config
9
+ * object that merges module defaults with user-discovered files.
10
+ *
11
+ * ## Architecture
12
+ *
13
+ * The admin-client target works identically to the server target:
14
+ *
15
+ * 1. **Package mode** (`generatePackageModules`): discovers files from
16
+ * `modules/admin/client/` → generates `modules/admin/client/.generated/module.ts`
17
+ * using the standard `generateModuleTemplate()` primitive.
18
+ *
19
+ * 2. **Root app mode** (`runAllTargets`): user has `admin/modules.ts` that imports
20
+ * module packages → this template imports `_mod` and merges generically with
21
+ * user-discovered files from `admin/{blocks,views,fields,...}/`.
22
+ *
23
+ * ## How to create an admin-client module (for third-party packages)
24
+ *
25
+ * 1. Create wrapper files in `modules/<name>/client/{fields,views,...}/`
26
+ * 2. Run `questpie generate` — generates `client/.generated/module.ts`
27
+ * 3. Create `client/index.ts` that re-exports the generated module
28
+ * 4. Add `"./client/module"` to `package.json` exports
29
+ *
30
+ * ## How users extend admin
31
+ *
32
+ * Add files in `questpie/admin/{views,fields,pages,widgets,blocks,components}/`.
33
+ * They are discovered automatically and merged over module defaults.
34
+ *
35
+ * ## Category key strategies
36
+ *
37
+ * - Default: use file key — `"bookingCta": _block_bookingCta`
38
+ * - `keyFromProperty: "name"`: use runtime key — `[_view_kanban.name]: _view_kanban`
39
+ *
40
+ * The strategy is declared per-category in `CategoryDeclaration.keyFromProperty`.
41
+ *
42
+ * @see PLAN-PLUGIN-CONSISTENCY.md §6.2 (admin-client target)
43
+ */
44
+ /**
3
45
  * Generate the admin client config file.
4
46
  *
5
47
  * Called by `runAllTargets()` for the `admin-client` target.
@@ -18,7 +60,7 @@ function generateAdminClientTemplate(ctx) {
18
60
  lines.push(" */");
19
61
  lines.push("");
20
62
  if (modulesFile) {
21
- lines.push(generateImportLine(modulesFile));
63
+ lines.push(importStatement(modulesFile));
22
64
  lines.push(`const _mergedModules = Array.isArray(${modulesFile.varName})`);
23
65
  lines.push(`\t? ${modulesFile.varName}.reduce((acc: any, m: any) => {`);
24
66
  lines.push(" for (const [k, v] of Object.entries(m)) acc[k] = typeof v === \"object\" && v !== null && !Array.isArray(v) ? { ...acc[k], ...v } : v;");
@@ -27,15 +69,15 @@ function generateAdminClientTemplate(ctx) {
27
69
  }
28
70
  const categoryFiles = /* @__PURE__ */ new Map();
29
71
  for (const [cat, fileMap] of ctx.discovered.categories) if (fileMap.size > 0) {
30
- const sorted = [...fileMap.values()].sort((a, b) => a.key.localeCompare(b.key));
72
+ const sorted = sortedValues(fileMap);
31
73
  categoryFiles.set(cat, sorted);
32
- for (const file of sorted) lines.push(generateImportLine(file));
74
+ for (const file of sorted) lines.push(importStatement(file));
33
75
  }
34
76
  const singleFiles = /* @__PURE__ */ new Map();
35
77
  for (const [key, file] of ctx.discovered.singles) {
36
78
  if (key === "modules") continue;
37
79
  singleFiles.set(key, file);
38
- lines.push(generateImportLine(file));
80
+ lines.push(importStatement(file));
39
81
  }
40
82
  for (const imp of ctx.extraImports) lines.push(`import ${imp.name} from "${imp.path}";`);
41
83
  lines.push("");
@@ -52,11 +94,11 @@ function generateAdminClientTemplate(ctx) {
52
94
  const files = categoryFiles.get(cat);
53
95
  const catDecl = ctx.target.categories?.[cat];
54
96
  if (modulesFile && files && files.length > 0) {
55
- const entries = generateCategoryEntries(files, catDecl);
97
+ const entries = files.map((f) => categoryRecordEntry(f, catDecl)).join(", ");
56
98
  lines.push(`\t${cat}: { ..._mergedModules.${cat}, ${entries} },`);
57
99
  } else if (modulesFile && (!files || files.length === 0)) lines.push(`\t${cat}: { ..._mergedModules.${cat} },`);
58
100
  else if (files && files.length > 0) {
59
- const entries = generateCategoryEntries(files, catDecl);
101
+ const entries = files.map((f) => categoryRecordEntry(f, catDecl)).join(", ");
60
102
  lines.push(`\t${cat}: { ${entries} },`);
61
103
  }
62
104
  }
@@ -73,32 +115,6 @@ function generateAdminClientTemplate(ctx) {
73
115
  lines.push("");
74
116
  return { code: lines.join("\n") };
75
117
  }
76
- /**
77
- * Generate category entries string based on the category's key strategy.
78
- *
79
- * When `keyFromProperty` is set (e.g., views with `keyFromProperty: "name"`),
80
- * uses runtime property as key: `[_view_kanban.name]: _view_kanban`.
81
- *
82
- * Otherwise uses the file-derived key: `"bookingCta": _block_bookingCta`.
83
- */
84
- function generateCategoryEntries(files, catDecl) {
85
- const keyProp = catDecl?.keyFromProperty;
86
- return files.map((f) => {
87
- if (keyProp) return `[${f.varName}.${keyProp}]: ${f.varName}`;
88
- if (catDecl?.keyFromSource === "basename") return `${JSON.stringify(getSourceBasename(f))}: ${f.varName}`;
89
- return `${JSON.stringify(f.key)}: ${f.varName}`;
90
- }).join(", ");
91
- }
92
- function getSourceBasename(file) {
93
- return (file.source.replaceAll("\\", "/").split("/").pop() ?? file.key).replace(/\.[^.]+$/, "");
94
- }
95
- /**
96
- * Generate an import line for a discovered file.
97
- */
98
- function generateImportLine(file) {
99
- if (file.exportType === "named" && file.namedExportName) return `import { ${file.namedExportName} as ${file.varName} } from "${file.importPath}";`;
100
- return `import ${file.varName} from "${file.importPath}";`;
101
- }
102
118
 
103
119
  //#endregion
104
120
  export { generateAdminClientTemplate };
@@ -1,71 +1,71 @@
1
- import * as questpie_shared23 from "questpie/shared";
2
- import * as questpie152 from "questpie";
3
- import * as questpie_src_server_modules_core_fields_email_js3 from "questpie/src/server/modules/core/fields/email.js";
4
- import * as questpie_src_server_modules_core_fields_json_js3 from "questpie/src/server/modules/core/fields/json.js";
5
- import * as drizzle_orm_pg_core32 from "drizzle-orm/pg-core";
6
- import * as drizzle_orm15 from "drizzle-orm";
1
+ import * as questpie_shared40 from "questpie/shared";
2
+ import * as questpie242 from "questpie";
3
+ import * as questpie_src_server_modules_core_fields_email_js6 from "questpie/src/server/modules/core/fields/email.js";
4
+ import * as questpie_src_server_modules_core_fields_json_js6 from "questpie/src/server/modules/core/fields/json.js";
5
+ import * as drizzle_orm_pg_core61 from "drizzle-orm/pg-core";
6
+ import * as drizzle_orm43 from "drizzle-orm";
7
7
 
8
8
  //#region src/server/modules/admin/collections/account.d.ts
9
- declare const _default: questpie152.CollectionBuilder<questpie_shared23.Override<questpie152.EmptyCollectionState<"account", undefined, {
10
- readonly text: typeof questpie152.text;
11
- readonly textarea: typeof questpie152.textarea;
12
- readonly email: typeof questpie_src_server_modules_core_fields_email_js3.email;
13
- readonly url: typeof questpie152.url;
14
- readonly number: typeof questpie152.number;
15
- readonly boolean: typeof questpie152.boolean;
16
- readonly date: typeof questpie152.date;
17
- readonly datetime: typeof questpie152.datetime;
18
- readonly time: typeof questpie152.time;
19
- readonly select: typeof questpie152.select;
20
- readonly upload: typeof questpie152.upload;
21
- readonly relation: typeof questpie152.relation;
22
- readonly object: typeof questpie152.object;
23
- readonly json: typeof questpie_src_server_modules_core_fields_json_js3.json;
24
- readonly from: typeof questpie152.from;
9
+ declare const _default: questpie242.CollectionBuilder<questpie_shared40.Override<questpie242.EmptyCollectionState<"account", undefined, {
10
+ readonly text: typeof questpie242.text;
11
+ readonly textarea: typeof questpie242.textarea;
12
+ readonly email: typeof questpie_src_server_modules_core_fields_email_js6.email;
13
+ readonly url: typeof questpie242.url;
14
+ readonly number: typeof questpie242.number;
15
+ readonly boolean: typeof questpie242.boolean;
16
+ readonly date: typeof questpie242.date;
17
+ readonly datetime: typeof questpie242.datetime;
18
+ readonly time: typeof questpie242.time;
19
+ readonly select: typeof questpie242.select;
20
+ readonly upload: typeof questpie242.upload;
21
+ readonly relation: typeof questpie242.relation;
22
+ readonly object: typeof questpie242.object;
23
+ readonly json: typeof questpie_src_server_modules_core_fields_json_js6.json;
24
+ readonly from: typeof questpie242.from;
25
25
  }>, {
26
26
  name: "account";
27
27
  fields: Record<string, any> & {
28
- readonly userId: drizzle_orm15.NotNull<drizzle_orm_pg_core32.PgVarcharBuilder<[string, ...string[]]>>;
29
- readonly accountId: drizzle_orm15.NotNull<drizzle_orm_pg_core32.PgVarcharBuilder<[string, ...string[]]>>;
30
- readonly providerId: drizzle_orm15.NotNull<drizzle_orm_pg_core32.PgVarcharBuilder<[string, ...string[]]>>;
31
- readonly accessToken: drizzle_orm_pg_core32.PgVarcharBuilder<[string, ...string[]]>;
32
- readonly refreshToken: drizzle_orm_pg_core32.PgVarcharBuilder<[string, ...string[]]>;
33
- readonly accessTokenExpiresAt: drizzle_orm_pg_core32.PgTimestampBuilder;
34
- readonly refreshTokenExpiresAt: drizzle_orm_pg_core32.PgTimestampBuilder;
35
- readonly scope: drizzle_orm_pg_core32.PgVarcharBuilder<[string, ...string[]]>;
36
- readonly idToken: drizzle_orm_pg_core32.PgVarcharBuilder<[string, ...string[]]>;
37
- readonly password: drizzle_orm_pg_core32.PgVarcharBuilder<[string, ...string[]]>;
28
+ readonly userId: drizzle_orm43.NotNull<drizzle_orm_pg_core61.PgVarcharBuilder<[string, ...string[]]>>;
29
+ readonly accountId: drizzle_orm43.NotNull<drizzle_orm_pg_core61.PgVarcharBuilder<[string, ...string[]]>>;
30
+ readonly providerId: drizzle_orm43.NotNull<drizzle_orm_pg_core61.PgVarcharBuilder<[string, ...string[]]>>;
31
+ readonly accessToken: drizzle_orm_pg_core61.PgVarcharBuilder<[string, ...string[]]>;
32
+ readonly refreshToken: drizzle_orm_pg_core61.PgVarcharBuilder<[string, ...string[]]>;
33
+ readonly accessTokenExpiresAt: drizzle_orm_pg_core61.PgTimestampBuilder;
34
+ readonly refreshTokenExpiresAt: drizzle_orm_pg_core61.PgTimestampBuilder;
35
+ readonly scope: drizzle_orm_pg_core61.PgVarcharBuilder<[string, ...string[]]>;
36
+ readonly idToken: drizzle_orm_pg_core61.PgVarcharBuilder<[string, ...string[]]>;
37
+ readonly password: drizzle_orm_pg_core61.PgVarcharBuilder<[string, ...string[]]>;
38
38
  };
39
39
  virtuals: undefined;
40
- relations: Record<string, questpie152.RelationConfig>;
40
+ relations: Record<string, questpie242.RelationConfig>;
41
41
  indexes: Record<string, any>;
42
42
  title: "providerId";
43
- options: questpie152.CollectionOptions & {
43
+ options: questpie242.CollectionOptions & {
44
44
  timestamps: true;
45
45
  };
46
46
  hooks: Record<string, any>;
47
47
  access: Record<string, any>;
48
48
  searchable: undefined;
49
49
  fieldDefinitions: {
50
- readonly userId: questpie152.FieldWithMethods<Omit<questpie152.TextFieldState, "notNull" | "column"> & {
50
+ readonly userId: questpie242.FieldWithMethods<Omit<questpie242.TextFieldState, "notNull" | "column"> & {
51
51
  notNull: true;
52
- column: drizzle_orm15.NotNull<drizzle_orm_pg_core32.PgVarcharBuilder<[string, ...string[]]>>;
53
- }, questpie152.TextFieldMethods>;
54
- readonly accountId: questpie152.FieldWithMethods<Omit<questpie152.TextFieldState, "notNull" | "column"> & {
52
+ column: drizzle_orm43.NotNull<drizzle_orm_pg_core61.PgVarcharBuilder<[string, ...string[]]>>;
53
+ }, questpie242.TextFieldMethods>;
54
+ readonly accountId: questpie242.FieldWithMethods<Omit<questpie242.TextFieldState, "notNull" | "column"> & {
55
55
  notNull: true;
56
- column: drizzle_orm15.NotNull<drizzle_orm_pg_core32.PgVarcharBuilder<[string, ...string[]]>>;
57
- }, questpie152.TextFieldMethods>;
58
- readonly providerId: questpie152.FieldWithMethods<Omit<questpie152.TextFieldState, "notNull" | "column"> & {
56
+ column: drizzle_orm43.NotNull<drizzle_orm_pg_core61.PgVarcharBuilder<[string, ...string[]]>>;
57
+ }, questpie242.TextFieldMethods>;
58
+ readonly providerId: questpie242.FieldWithMethods<Omit<questpie242.TextFieldState, "notNull" | "column"> & {
59
59
  notNull: true;
60
- column: drizzle_orm15.NotNull<drizzle_orm_pg_core32.PgVarcharBuilder<[string, ...string[]]>>;
61
- }, questpie152.TextFieldMethods>;
62
- readonly accessToken: questpie152.FieldWithMethods<questpie152.TextFieldState, questpie152.TextFieldMethods>;
63
- readonly refreshToken: questpie152.FieldWithMethods<questpie152.TextFieldState, questpie152.TextFieldMethods>;
64
- readonly accessTokenExpiresAt: questpie152.FieldWithMethods<questpie152.DatetimeFieldState, questpie152.DatetimeFieldMethods>;
65
- readonly refreshTokenExpiresAt: questpie152.FieldWithMethods<questpie152.DatetimeFieldState, questpie152.DatetimeFieldMethods>;
66
- readonly scope: questpie152.FieldWithMethods<questpie152.TextFieldState, questpie152.TextFieldMethods>;
67
- readonly idToken: questpie152.FieldWithMethods<questpie152.TextFieldState, questpie152.TextFieldMethods>;
68
- readonly password: questpie152.FieldWithMethods<questpie152.TextFieldState, questpie152.TextFieldMethods>;
60
+ column: drizzle_orm43.NotNull<drizzle_orm_pg_core61.PgVarcharBuilder<[string, ...string[]]>>;
61
+ }, questpie242.TextFieldMethods>;
62
+ readonly accessToken: questpie242.FieldWithMethods<questpie242.TextFieldState, questpie242.TextFieldMethods>;
63
+ readonly refreshToken: questpie242.FieldWithMethods<questpie242.TextFieldState, questpie242.TextFieldMethods>;
64
+ readonly accessTokenExpiresAt: questpie242.FieldWithMethods<questpie242.DatetimeFieldState, questpie242.DatetimeFieldMethods>;
65
+ readonly refreshTokenExpiresAt: questpie242.FieldWithMethods<questpie242.DatetimeFieldState, questpie242.DatetimeFieldMethods>;
66
+ readonly scope: questpie242.FieldWithMethods<questpie242.TextFieldState, questpie242.TextFieldMethods>;
67
+ readonly idToken: questpie242.FieldWithMethods<questpie242.TextFieldState, questpie242.TextFieldMethods>;
68
+ readonly password: questpie242.FieldWithMethods<questpie242.TextFieldState, questpie242.TextFieldMethods>;
69
69
  };
70
70
  upload: undefined;
71
71
  output: {};