@riducms/admin 0.1.0

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 (226) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/THIRD_PARTY_NOTICES.md +217 -0
  4. package/package.json +62 -0
  5. package/src/app/admin-core-view-route.svelte +89 -0
  6. package/src/app/admin-layout.svelte +457 -0
  7. package/src/app/admin-navigation-progress.svelte +204 -0
  8. package/src/app/admin-page-title.svelte +94 -0
  9. package/src/app/admin-provider-layer.svelte +26 -0
  10. package/src/app/admin-router-root.svelte +9 -0
  11. package/src/app/admin-routes.svelte +116 -0
  12. package/src/app/deferred-redirect.svelte +23 -0
  13. package/src/app/loading-boundary.svelte +70 -0
  14. package/src/app/not-found-route.svelte +29 -0
  15. package/src/app-meta.ts +2 -0
  16. package/src/app.css +467 -0
  17. package/src/app.svelte +88 -0
  18. package/src/assets/ridu-logo-arch.svg +3 -0
  19. package/src/assets/ridu-word-logo.svg +7 -0
  20. package/src/components/brand/ridu-logo.svelte +24 -0
  21. package/src/components/code-block/code-block.svelte +107 -0
  22. package/src/components/json-tree/json-tree-node.svelte +77 -0
  23. package/src/components/json-tree/json-viewer.svelte +113 -0
  24. package/src/components/ui/banner/banner.svelte +41 -0
  25. package/src/components/ui/banner/index.ts +5 -0
  26. package/src/components/ui/bulk-action-bar/bulk-action-bar.svelte +50 -0
  27. package/src/components/ui/bulk-action-bar/index.ts +1 -0
  28. package/src/components/ui/button/index.ts +8 -0
  29. package/src/components/ui/calendar/calendar-caption.svelte +72 -0
  30. package/src/components/ui/calendar/calendar-cell.svelte +20 -0
  31. package/src/components/ui/calendar/calendar-day.svelte +27 -0
  32. package/src/components/ui/calendar/calendar-grid-row.svelte +13 -0
  33. package/src/components/ui/calendar/calendar-grid.svelte +17 -0
  34. package/src/components/ui/calendar/calendar-head-cell.svelte +20 -0
  35. package/src/components/ui/calendar/calendar-header.svelte +20 -0
  36. package/src/components/ui/calendar/calendar-heading.svelte +17 -0
  37. package/src/components/ui/calendar/calendar-month-select.svelte +45 -0
  38. package/src/components/ui/calendar/calendar-month.svelte +16 -0
  39. package/src/components/ui/calendar/calendar-months.svelte +20 -0
  40. package/src/components/ui/calendar/calendar-nav.svelte +20 -0
  41. package/src/components/ui/calendar/calendar-next-button.svelte +30 -0
  42. package/src/components/ui/calendar/calendar-prev-button.svelte +30 -0
  43. package/src/components/ui/calendar/calendar-year-select.svelte +44 -0
  44. package/src/components/ui/calendar/calendar.svelte +118 -0
  45. package/src/components/ui/calendar/index.ts +1 -0
  46. package/src/components/ui/checkbox/checkbox.svelte +35 -0
  47. package/src/components/ui/checkbox/index.ts +1 -0
  48. package/src/components/ui/confirmation-dialog/confirmation-dialog.svelte +37 -0
  49. package/src/components/ui/confirmation-dialog/index.ts +1 -0
  50. package/src/components/ui/date-value-control/date-value-calendar.svelte +37 -0
  51. package/src/components/ui/date-value-control/date-value-control-loader.svelte +36 -0
  52. package/src/components/ui/date-value-control/date-value-control.svelte +177 -0
  53. package/src/components/ui/date-value-control/index.ts +3 -0
  54. package/src/components/ui/dialog/dialog-close.svelte +11 -0
  55. package/src/components/ui/dialog/dialog-content.svelte +52 -0
  56. package/src/components/ui/dialog/dialog-description.svelte +20 -0
  57. package/src/components/ui/dialog/dialog-footer.svelte +20 -0
  58. package/src/components/ui/dialog/dialog-header.svelte +20 -0
  59. package/src/components/ui/dialog/dialog-overlay.svelte +20 -0
  60. package/src/components/ui/dialog/dialog-title.svelte +17 -0
  61. package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
  62. package/src/components/ui/dialog/dialog.svelte +7 -0
  63. package/src/components/ui/dialog/index.ts +8 -0
  64. package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +27 -0
  65. package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +20 -0
  66. package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
  67. package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +16 -0
  68. package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
  69. package/src/components/ui/dropdown-menu/index.ts +5 -0
  70. package/src/components/ui/empty-state/empty-state.svelte +34 -0
  71. package/src/components/ui/empty-state/index.ts +1 -0
  72. package/src/components/ui/input/index.ts +1 -0
  73. package/src/components/ui/kbd/index.ts +1 -0
  74. package/src/components/ui/kbd/kbd.svelte +24 -0
  75. package/src/components/ui/pagination/index.ts +1 -0
  76. package/src/components/ui/pagination/pagination.svelte +88 -0
  77. package/src/components/ui/popover/index.ts +3 -0
  78. package/src/components/ui/popover/popover-content.svelte +29 -0
  79. package/src/components/ui/popover/popover-trigger.svelte +11 -0
  80. package/src/components/ui/popover/popover.svelte +7 -0
  81. package/src/components/ui/radio-group/index.ts +3 -0
  82. package/src/components/ui/radio-group/radio-card-item.svelte +38 -0
  83. package/src/components/ui/radio-group/radio-group-item.svelte +17 -0
  84. package/src/components/ui/radio-group/radio-group.svelte +19 -0
  85. package/src/components/ui/select/index.ts +4 -0
  86. package/src/components/ui/select/select-content.svelte +38 -0
  87. package/src/components/ui/select/select-item.svelte +40 -0
  88. package/src/components/ui/select/select-scroll-down-button.svelte +23 -0
  89. package/src/components/ui/select/select-scroll-up-button.svelte +23 -0
  90. package/src/components/ui/select/select-trigger.svelte +29 -0
  91. package/src/components/ui/select/select.svelte +11 -0
  92. package/src/components/ui/sheet/index.ts +6 -0
  93. package/src/components/ui/sheet/sheet-close.svelte +7 -0
  94. package/src/components/ui/sheet/sheet-content.svelte +77 -0
  95. package/src/components/ui/sheet/sheet-description.svelte +17 -0
  96. package/src/components/ui/sheet/sheet-overlay.svelte +36 -0
  97. package/src/components/ui/sheet/sheet-title.svelte +17 -0
  98. package/src/components/ui/sheet/sheet-trigger.svelte +7 -0
  99. package/src/components/ui/sheet/sheet.svelte +7 -0
  100. package/src/components/ui/skeleton/index.ts +1 -0
  101. package/src/components/ui/skeleton/skeleton.svelte +31 -0
  102. package/src/components/ui/slider/index.ts +3 -0
  103. package/src/components/ui/slider/slider-content.svelte +59 -0
  104. package/src/components/ui/slider/slider.svelte +35 -0
  105. package/src/components/ui/sonner/index.ts +1 -0
  106. package/src/components/ui/sonner/sonner.svelte +55 -0
  107. package/src/components/ui/status-indicator/index.ts +5 -0
  108. package/src/components/ui/status-indicator/status-indicator.svelte +54 -0
  109. package/src/components/ui/table/index.ts +6 -0
  110. package/src/components/ui/table/table-body.svelte +20 -0
  111. package/src/components/ui/table/table-cell.svelte +23 -0
  112. package/src/components/ui/table/table-head.svelte +23 -0
  113. package/src/components/ui/table/table-header.svelte +20 -0
  114. package/src/components/ui/table/table-row.svelte +23 -0
  115. package/src/components/ui/table/table.svelte +22 -0
  116. package/src/components/ui/tabs/index.ts +8 -0
  117. package/src/components/ui/tabs/tabs-content.svelte +17 -0
  118. package/src/components/ui/tabs/tabs-list.svelte +40 -0
  119. package/src/components/ui/tabs/tabs-trigger.svelte +23 -0
  120. package/src/components/ui/tabs/tabs.svelte +19 -0
  121. package/src/components/ui/textarea/index.ts +1 -0
  122. package/src/components/ui/tooltip/index.ts +4 -0
  123. package/src/components/ui/tooltip/tooltip-content.svelte +50 -0
  124. package/src/components/ui/tooltip/tooltip-provider.svelte +7 -0
  125. package/src/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  126. package/src/components/ui/tooltip/tooltip.svelte +7 -0
  127. package/src/core/api/admin-client.ts +45 -0
  128. package/src/core/forms/field-condition.ts +63 -0
  129. package/src/core/forms/form-controller.svelte.ts +369 -0
  130. package/src/core/forms/form-draft-recovery.ts +80 -0
  131. package/src/core/forms/form-schema.ts +609 -0
  132. package/src/core/forms/form-validation.ts +750 -0
  133. package/src/core/i18n/admin-i18n.svelte.ts +364 -0
  134. package/src/core/i18n/localized-schema.ts +111 -0
  135. package/src/core/notifications/notification-center.svelte.ts +69 -0
  136. package/src/core/notifications/validation-toast.svelte +18 -0
  137. package/src/core/plugins/field-registry.ts +60 -0
  138. package/src/core/plugins/row-label-registry.ts +62 -0
  139. package/src/core/preferences/preference-write-queue.ts +107 -0
  140. package/src/core/routing/admin-paths.ts +137 -0
  141. package/src/core/runtime/admin-runtime.svelte.ts +625 -0
  142. package/src/features/auth/account-menu.svelte +159 -0
  143. package/src/features/auth/account-navigation.svelte +44 -0
  144. package/src/features/auth/account-route.svelte +395 -0
  145. package/src/features/auth/account-security-controller.svelte.ts +119 -0
  146. package/src/features/auth/account-security-route.svelte +460 -0
  147. package/src/features/auth/auth-brand.svelte +17 -0
  148. package/src/features/auth/auth-frame.svelte +18 -0
  149. package/src/features/auth/create-first-user-controller.svelte.ts +220 -0
  150. package/src/features/auth/create-first-user-route.svelte +174 -0
  151. package/src/features/auth/forgot-password-route.svelte +78 -0
  152. package/src/features/auth/login-route.svelte +221 -0
  153. package/src/features/auth/request-verification-route.svelte +73 -0
  154. package/src/features/auth/reset-password-route.svelte +107 -0
  155. package/src/features/auth/verify-email-route.svelte +60 -0
  156. package/src/features/collections/collection-list-bulk-actions.svelte +143 -0
  157. package/src/features/collections/collection-list-bulk-editor.svelte +157 -0
  158. package/src/features/collections/collection-list-controller.svelte.ts +717 -0
  159. package/src/features/collections/collection-list-filter-popover.svelte +238 -0
  160. package/src/features/collections/collection-list-preferences-controller.svelte.ts +393 -0
  161. package/src/features/collections/collection-list-results.svelte +410 -0
  162. package/src/features/collections/collection-list-route.svelte +951 -0
  163. package/src/features/collections/collection-list-workspace-toolbar.svelte +409 -0
  164. package/src/features/collections/collection-table-skeleton.svelte +44 -0
  165. package/src/features/collections/list-workspace.ts +265 -0
  166. package/src/features/dashboard/dashboard-route.svelte +141 -0
  167. package/src/features/development/development-tools.svelte +217 -0
  168. package/src/features/documents/document-api-view-loader.svelte +31 -0
  169. package/src/features/documents/document-api-view.svelte +547 -0
  170. package/src/features/documents/document-controller.svelte.ts +1252 -0
  171. package/src/features/documents/document-field-partition.ts +18 -0
  172. package/src/features/documents/document-field-sections.svelte +46 -0
  173. package/src/features/documents/document-heading.ts +20 -0
  174. package/src/features/documents/document-route-dirty.ts +43 -0
  175. package/src/features/documents/document-route-loader.svelte +22 -0
  176. package/src/features/documents/document-route.svelte +1124 -0
  177. package/src/features/documents/live-preview-panel.svelte +390 -0
  178. package/src/features/documents/live-preview.ts +61 -0
  179. package/src/features/documents/upload-document-preview-loader.svelte +40 -0
  180. package/src/features/documents/upload-document-preview.svelte +525 -0
  181. package/src/features/localization/content-locale-switcher.svelte +93 -0
  182. package/src/features/navigation/admin-breadcrumbs.svelte +124 -0
  183. package/src/features/navigation/admin-command-menu.svelte +245 -0
  184. package/src/features/navigation/admin-resource-groups.ts +72 -0
  185. package/src/features/reference-browser/reference-browser-loader.svelte +15 -0
  186. package/src/features/reference-browser/reference-browser-workflow.svelte.ts +691 -0
  187. package/src/features/reference-browser/reference-browser.svelte +844 -0
  188. package/src/features/uploads/bulk-upload-route.svelte +624 -0
  189. package/src/features/uploads/bulk-upload.ts +39 -0
  190. package/src/features/uploads/upload-metadata-fields.svelte +112 -0
  191. package/src/features/versions/version-diff.ts +72 -0
  192. package/src/features/versions/version-history-route.svelte +581 -0
  193. package/src/fields/field-clipboard.ts +132 -0
  194. package/src/fields/field-layout.svelte +60 -0
  195. package/src/fields/field-layout.ts +57 -0
  196. package/src/fields/field-messages.svelte +29 -0
  197. package/src/fields/field-renderer.svelte +152 -0
  198. package/src/fields/field-shell.svelte +30 -0
  199. package/src/fields/field-tab-layout.svelte +99 -0
  200. package/src/fields/join/join-field.svelte +358 -0
  201. package/src/fields/localized-field-presentation.ts +17 -0
  202. package/src/fields/nested/nested-field.svelte +602 -0
  203. package/src/fields/nested/scoped-field.ts +78 -0
  204. package/src/fields/nested/sortable-row.svelte +32 -0
  205. package/src/fields/point/point-field.svelte +52 -0
  206. package/src/fields/radio/radio-field.svelte +34 -0
  207. package/src/fields/relationship/relationship-field-controller.svelte.ts +245 -0
  208. package/src/fields/relationship/relationship-field.svelte +258 -0
  209. package/src/fields/relationship/relationship-lookup-controller.svelte.ts +129 -0
  210. package/src/fields/relationship/relationship-query.ts +88 -0
  211. package/src/fields/relationship/relationship-quick-picker-controller.svelte.ts +198 -0
  212. package/src/fields/relationship/relationship-quick-picker.svelte +327 -0
  213. package/src/fields/relationship/relationship-value.ts +47 -0
  214. package/src/fields/scalar/date-control-value.ts +60 -0
  215. package/src/fields/scalar/date-value.ts +26 -0
  216. package/src/fields/scalar/scalar-field.svelte +137 -0
  217. package/src/fields/select/select-field.svelte +132 -0
  218. package/src/fields/select/select-value.ts +14 -0
  219. package/src/fields/text/slug-field.svelte +87 -0
  220. package/src/fields/text/slug.ts +38 -0
  221. package/src/fields/text/text-field.svelte +41 -0
  222. package/src/fields/ui/ui-field.svelte +12 -0
  223. package/src/fields/virtual/virtual-field.svelte +28 -0
  224. package/src/index.ts +49 -0
  225. package/src/main.ts +12 -0
  226. package/src/vite-env.d.ts +9 -0
@@ -0,0 +1,129 @@
1
+ import type { Pagination } from "@riducms/protocol";
2
+ import type { AdminI18n } from "@riducms/plugin";
3
+ import { createAdminI18n } from "@riducms/translations";
4
+
5
+ import type { AdminClient, AdminDocument } from "@admin/core/api/admin-client";
6
+
7
+ import {
8
+ buildRelationshipWhere,
9
+ type RelationshipFilter,
10
+ } from "@admin/fields/relationship/relationship-query";
11
+
12
+ export interface RelationshipLookupRequest {
13
+ slug: string;
14
+ page: number;
15
+ limit: number;
16
+ search: string;
17
+ searchField: string | undefined;
18
+ filter: RelationshipFilter;
19
+ locale?: string;
20
+ }
21
+
22
+ type RelationshipLookupStatus = "initial" | "loading" | "ready" | "failed";
23
+
24
+ const emptyPagination: Pagination = {
25
+ page: 1,
26
+ limit: 10,
27
+ totalDocs: 0,
28
+ totalPages: 0,
29
+ hasNextPage: false,
30
+ hasPrevPage: false,
31
+ };
32
+
33
+ export class RelationshipLookupController {
34
+ #status = $state<RelationshipLookupStatus>("initial");
35
+ #docs = $state.raw<AdminDocument[]>([]);
36
+ #known = $state.raw<Record<string, AdminDocument>>({});
37
+ #pagination = $state.raw<Pagination>(emptyPagination);
38
+ #error = $state<string>();
39
+ #request?: AbortController;
40
+ #requestGeneration = 0;
41
+
42
+ constructor(
43
+ readonly client: AdminClient,
44
+ readonly i18n: AdminI18n = createAdminI18n()
45
+ ) {}
46
+
47
+ get status() {
48
+ return this.#status;
49
+ }
50
+
51
+ get docs() {
52
+ return this.#docs;
53
+ }
54
+
55
+ get pagination() {
56
+ return this.#pagination;
57
+ }
58
+
59
+ get error() {
60
+ return this.#error;
61
+ }
62
+
63
+ document(id: string) {
64
+ return this.#known[id];
65
+ }
66
+
67
+ remember(document: AdminDocument) {
68
+ this.#known = { ...this.#known, [document.id]: document };
69
+ this.#docs = this.#docs.map((candidate) =>
70
+ candidate.id === document.id ? document : candidate
71
+ );
72
+ }
73
+
74
+ search = async (request: RelationshipLookupRequest) => {
75
+ this.#request?.abort();
76
+ const activeRequest = new AbortController();
77
+ const generation = ++this.#requestGeneration;
78
+ this.#request = activeRequest;
79
+ this.#status = "loading";
80
+ this.#error = undefined;
81
+
82
+ try {
83
+ const page = await this.client.list(request.slug, {
84
+ page: request.page,
85
+ limit: request.limit,
86
+ where: buildRelationshipWhere(request.searchField, request.search, request.filter),
87
+ signal: activeRequest.signal,
88
+ locale: request.locale,
89
+ });
90
+ if (activeRequest.signal.aborted || generation !== this.#requestGeneration) return;
91
+ this.#docs = page.docs;
92
+ this.#known = {
93
+ ...this.#known,
94
+ ...Object.fromEntries(page.docs.map((document) => [document.id, document])),
95
+ };
96
+ this.#pagination = page.pagination;
97
+ this.#status = "ready";
98
+ } catch (cause) {
99
+ if (activeRequest.signal.aborted || generation !== this.#requestGeneration) return;
100
+ this.#error =
101
+ cause instanceof Error ? cause.message : this.i18n.t("errors:relationshipDocumentsLoad");
102
+ this.#status = "failed";
103
+ } finally {
104
+ if (this.#request === activeRequest) this.#request = undefined;
105
+ }
106
+ };
107
+
108
+ loadDocument = async (slug: string, id: string, locale?: string, signal?: AbortSignal) => {
109
+ const known = this.#known[id];
110
+ if (known !== undefined) return known;
111
+ try {
112
+ const document = await this.client.find(slug, id, { signal, locale });
113
+ if (signal?.aborted) return undefined;
114
+ this.remember(document);
115
+ return document;
116
+ } catch (cause) {
117
+ if (signal?.aborted) return undefined;
118
+ this.#error =
119
+ cause instanceof Error ? cause.message : this.i18n.t("errors:relatedDocumentLoad");
120
+ return undefined;
121
+ }
122
+ };
123
+
124
+ dispose() {
125
+ this.#requestGeneration += 1;
126
+ this.#request?.abort();
127
+ this.#request = undefined;
128
+ }
129
+ }
@@ -0,0 +1,88 @@
1
+ import type { FieldReferenceFilter } from "@riducms/plugin";
2
+ import type { SchemaField } from "@riducms/protocol";
3
+
4
+ export type RelationshipFilter = FieldReferenceFilter | readonly FieldReferenceFilter[] | undefined;
5
+
6
+ export function buildRelationshipWhere(
7
+ searchField: string | undefined,
8
+ search: string,
9
+ filter: RelationshipFilter
10
+ ): Record<string, unknown> | undefined {
11
+ const predicates: Record<string, unknown>[] = [];
12
+ const query = search.trim();
13
+ if (searchField !== undefined && query !== "") {
14
+ predicates.push({ [searchField]: { like: query } });
15
+ }
16
+ const filters = filter === undefined ? [] : Array.isArray(filter) ? filter : [filter];
17
+ for (const candidate of filters) {
18
+ predicates.push({ [candidate.field]: { [candidate.operator]: candidate.value } });
19
+ }
20
+ if (predicates.length === 0) return undefined;
21
+ if (predicates.length === 1) return predicates[0];
22
+ return { and: predicates };
23
+ }
24
+
25
+ export function combineRelationshipFilters(
26
+ ...filters: readonly RelationshipFilter[]
27
+ ): RelationshipFilter {
28
+ const combined = filters.flatMap((filter) =>
29
+ filter === undefined ? [] : Array.isArray(filter) ? filter : [filter]
30
+ );
31
+ return combined.length === 0 ? undefined : combined;
32
+ }
33
+
34
+ export function relationshipOptionFilters(
35
+ field: SchemaField,
36
+ values: Record<string, unknown>,
37
+ targetCollection: string | undefined
38
+ ): readonly FieldReferenceFilter[] | undefined {
39
+ const reference = field.relationship ?? field.upload;
40
+ const configured = reference?.optionFilters ?? [];
41
+ const filters = configured.flatMap((candidate) => {
42
+ if (candidate.collectionSlug !== undefined && candidate.collectionSlug !== targetCollection) {
43
+ return [];
44
+ }
45
+ const value =
46
+ candidate.value === undefined
47
+ ? candidate.sourcePath === undefined
48
+ ? undefined
49
+ : readPath(values, candidate.sourcePath)
50
+ : relationshipFilterLiteral(candidate.value);
51
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
52
+ return [];
53
+ }
54
+ return [
55
+ {
56
+ field: candidate.targetPath,
57
+ operator: candidate.operator ?? "equals",
58
+ value,
59
+ } satisfies FieldReferenceFilter,
60
+ ];
61
+ });
62
+ return filters.length === 0 ? undefined : filters;
63
+ }
64
+
65
+ function relationshipFilterLiteral(value: {
66
+ type: "string" | "number" | "boolean";
67
+ value: string;
68
+ }) {
69
+ switch (value.type) {
70
+ case "string":
71
+ return value.value;
72
+ case "number": {
73
+ const parsed = Number(value.value);
74
+ return Number.isFinite(parsed) ? parsed : undefined;
75
+ }
76
+ case "boolean":
77
+ return value.value === "true" ? true : value.value === "false" ? false : undefined;
78
+ }
79
+ }
80
+
81
+ function readPath(values: Record<string, unknown>, path: string) {
82
+ let current: unknown = values;
83
+ for (const segment of path.split(".")) {
84
+ if (typeof current !== "object" || current === null || Array.isArray(current)) return undefined;
85
+ current = (current as Record<string, unknown>)[segment];
86
+ }
87
+ return current;
88
+ }
@@ -0,0 +1,198 @@
1
+ import type { SchemaCollection, SchemaField } from "@riducms/protocol";
2
+ import { SvelteMap } from "svelte/reactivity";
3
+
4
+ import type { AdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
5
+
6
+ import { RelationshipLookupController } from "@admin/fields/relationship/relationship-lookup-controller.svelte";
7
+ import type { RelationshipFilter } from "@admin/fields/relationship/relationship-query";
8
+
9
+ export interface RelationshipQuickPickerTarget {
10
+ collection: SchemaCollection;
11
+ filter: RelationshipFilter;
12
+ }
13
+
14
+ interface RelationshipQuickPickerControllerOptions {
15
+ runtime: AdminRuntime;
16
+ get targets(): readonly RelationshipQuickPickerTarget[];
17
+ get selectedTarget(): string | undefined;
18
+ get selectedID(): string | undefined;
19
+ get locale(): string | undefined;
20
+ onPick(target: string, id: string): void;
21
+ onRemove(): void;
22
+ onBrowse(target: string): void;
23
+ }
24
+
25
+ export class RelationshipQuickPickerController {
26
+ open = $state(false);
27
+ query = $state("");
28
+ readonly #lookups = new SvelteMap<string, RelationshipLookupController>();
29
+
30
+ constructor(readonly options: RelationshipQuickPickerControllerOptions) {
31
+ $effect(() => {
32
+ const targets = this.options.targets;
33
+ const activeTargets = new Set(targets.map((target) => target.collection.slug));
34
+ for (const [slug, lookup] of this.#lookups) {
35
+ if (activeTargets.has(slug)) continue;
36
+ lookup.dispose();
37
+ this.#lookups.delete(slug);
38
+ }
39
+ for (const target of targets) {
40
+ if (this.#lookups.has(target.collection.slug)) continue;
41
+ this.#lookups.set(
42
+ target.collection.slug,
43
+ new RelationshipLookupController(this.options.runtime.client, this.options.runtime.i18n)
44
+ );
45
+ }
46
+ });
47
+
48
+ $effect(() => {
49
+ const targets = this.options.targets;
50
+ if (!this.open) return;
51
+ const search = this.query;
52
+ const locale = this.options.locale;
53
+ this.options.runtime.documentRevision;
54
+ const timer = window.setTimeout(
55
+ () => {
56
+ for (const target of targets) {
57
+ this.#lookups.get(target.collection.slug)?.search({
58
+ slug: target.collection.slug,
59
+ page: 1,
60
+ limit: 5,
61
+ search,
62
+ searchField: findDisplayField(target.collection.fields)?.name,
63
+ filter: target.filter,
64
+ locale,
65
+ });
66
+ }
67
+ },
68
+ search.trim() === "" ? 0 : 180
69
+ );
70
+ return () => window.clearTimeout(timer);
71
+ });
72
+
73
+ $effect(() => () => this.#disposeLookups());
74
+ }
75
+
76
+ get groups() {
77
+ return this.options.targets.flatMap((target) => {
78
+ const lookup = this.#lookups.get(target.collection.slug);
79
+ return lookup === undefined
80
+ ? []
81
+ : [
82
+ {
83
+ collection: target.collection,
84
+ docs: lookup.docs,
85
+ error: lookup.error,
86
+ status: lookup.status,
87
+ },
88
+ ];
89
+ });
90
+ }
91
+
92
+ get comboboxItems() {
93
+ return this.groups.flatMap((group) =>
94
+ group.docs.map((document) => ({
95
+ value: encodeSelection(group.collection.slug, document.id),
96
+ label: this.documentLabel(group.collection, document),
97
+ }))
98
+ );
99
+ }
100
+
101
+ get selectedValue() {
102
+ const target = this.options.selectedTarget;
103
+ const id = this.options.selectedID;
104
+ return target === undefined || id === undefined ? "" : encodeSelection(target, id);
105
+ }
106
+
107
+ documentLabel = (collection: SchemaCollection, document: Record<string, unknown>) => {
108
+ const searchField = findDisplayField(collection.fields);
109
+ return String(
110
+ (searchField === undefined ? undefined : document[searchField.name]) ?? document.id
111
+ );
112
+ };
113
+
114
+ initials = (value: string) =>
115
+ value
116
+ .trim()
117
+ .split(/\s+/)
118
+ .filter(Boolean)
119
+ .slice(0, 2)
120
+ .map((part) => part[0]?.toLocaleUpperCase(this.options.runtime.i18n.language) ?? "")
121
+ .join("");
122
+
123
+ changeOpen = (open: boolean) => {
124
+ this.open = open;
125
+ if (open) return;
126
+ this.query = "";
127
+ this.#disposeLookups();
128
+ };
129
+
130
+ toggleOpen = () => {
131
+ const open = !this.open;
132
+ this.changeOpen(open);
133
+ return open;
134
+ };
135
+
136
+ setQuery = (query: string) => {
137
+ this.query = query;
138
+ };
139
+
140
+ pick = (value: string) => {
141
+ const selection = decodeSelection(value);
142
+ if (
143
+ selection === undefined ||
144
+ (selection.target === this.options.selectedTarget && selection.id === this.options.selectedID)
145
+ ) {
146
+ this.changeOpen(false);
147
+ return;
148
+ }
149
+ this.options.onPick(selection.target, selection.id);
150
+ this.changeOpen(false);
151
+ };
152
+
153
+ browseAll = (target?: string) => {
154
+ const targetSlug =
155
+ target ?? this.options.selectedTarget ?? this.options.targets[0]?.collection.slug;
156
+ if (targetSlug === undefined) return;
157
+ this.changeOpen(false);
158
+ this.options.onBrowse(targetSlug);
159
+ };
160
+
161
+ remove = () => {
162
+ this.changeOpen(false);
163
+ this.options.onRemove();
164
+ };
165
+
166
+ #disposeLookups() {
167
+ for (const lookup of this.#lookups.values()) lookup.dispose();
168
+ }
169
+ }
170
+
171
+ function encodeSelection(target: string, id: string) {
172
+ return JSON.stringify([target, id]);
173
+ }
174
+
175
+ function decodeSelection(value: string): { target: string; id: string } | undefined {
176
+ try {
177
+ const selection: unknown = JSON.parse(value);
178
+ if (
179
+ !Array.isArray(selection) ||
180
+ selection.length !== 2 ||
181
+ typeof selection[0] !== "string" ||
182
+ typeof selection[1] !== "string"
183
+ ) {
184
+ return undefined;
185
+ }
186
+ return { target: selection[0], id: selection[1] };
187
+ } catch {
188
+ return undefined;
189
+ }
190
+ }
191
+
192
+ function findDisplayField(fields: readonly SchemaField[]) {
193
+ return (
194
+ fields.find((field) => field.name === "title" || field.name === "name") ??
195
+ fields.find((field) => field.type === "text" && field.name !== "email") ??
196
+ fields.find((field) => field.type === "text" || field.type === "email")
197
+ );
198
+ }