@byline/admin 2.5.2 → 2.6.1

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 (260) hide show
  1. package/dist/fields/array/array-field.d.ts +14 -0
  2. package/dist/fields/array/array-field.js +177 -0
  3. package/dist/fields/array/array-field.module.js +11 -0
  4. package/dist/fields/array/array-field_module.css +32 -0
  5. package/dist/fields/blocks/blocks-field.d.ts +13 -0
  6. package/dist/fields/blocks/blocks-field.js +245 -0
  7. package/dist/fields/blocks/blocks-field.module.js +26 -0
  8. package/dist/fields/blocks/blocks-field_module.css +107 -0
  9. package/dist/fields/checkbox/checkbox-field.d.ts +16 -0
  10. package/dist/fields/checkbox/checkbox-field.js +28 -0
  11. package/dist/fields/checkbox/checkbox-field.module.js +6 -0
  12. package/dist/fields/checkbox/checkbox-field_module.css +4 -0
  13. package/dist/fields/column-formatter.d.ts +20 -0
  14. package/dist/fields/column-formatter.js +15 -0
  15. package/dist/fields/date-time-formatter.d.ts +16 -0
  16. package/dist/fields/date-time-formatter.js +8 -0
  17. package/dist/fields/datetime/datetime-field.d.ts +16 -0
  18. package/dist/fields/datetime/datetime-field.js +37 -0
  19. package/dist/fields/datetime/datetime-field.module.js +5 -0
  20. package/dist/fields/datetime/datetime-field_module.css +4 -0
  21. package/dist/fields/draggable-context-menu.d.ts +6 -0
  22. package/dist/fields/draggable-context-menu.js +85 -0
  23. package/dist/fields/draggable-context-menu.module.js +15 -0
  24. package/dist/fields/draggable-context-menu_module.css +91 -0
  25. package/dist/fields/field-helpers.d.ts +26 -0
  26. package/dist/fields/field-helpers.js +50 -0
  27. package/dist/fields/field-renderer.d.ts +37 -0
  28. package/dist/fields/field-renderer.js +206 -0
  29. package/dist/fields/field-renderer.module.js +8 -0
  30. package/dist/fields/field-renderer_module.css +11 -0
  31. package/dist/fields/field-services-context.d.ts +16 -0
  32. package/dist/fields/field-services-context.js +13 -0
  33. package/dist/fields/field-services-types.d.ts +63 -0
  34. package/dist/fields/field-services-types.js +1 -0
  35. package/dist/fields/file/file-field.d.ts +19 -0
  36. package/dist/fields/file/file-field.js +225 -0
  37. package/dist/fields/file/file-field.module.js +18 -0
  38. package/dist/fields/file/file-field_module.css +131 -0
  39. package/dist/fields/file/file-upload-field.d.ts +21 -0
  40. package/dist/fields/file/file-upload-field.js +130 -0
  41. package/dist/fields/file/file-upload-field.module.js +15 -0
  42. package/dist/fields/file/file-upload-field_module.css +74 -0
  43. package/dist/fields/group/group-field.d.ts +15 -0
  44. package/dist/fields/group/group-field.js +59 -0
  45. package/dist/fields/group/group-field.module.js +9 -0
  46. package/dist/fields/group/group-field_module.css +27 -0
  47. package/dist/fields/image/image-field.d.ts +19 -0
  48. package/dist/fields/image/image-field.js +241 -0
  49. package/dist/fields/image/image-field.module.js +22 -0
  50. package/dist/fields/image/image-field_module.css +121 -0
  51. package/dist/fields/image/image-upload-field.d.ts +21 -0
  52. package/dist/fields/image/image-upload-field.js +190 -0
  53. package/dist/fields/image/image-upload-field.module.js +19 -0
  54. package/dist/fields/image/image-upload-field_module.css +92 -0
  55. package/dist/fields/local-date-time.d.ts +27 -0
  56. package/dist/fields/local-date-time.js +49 -0
  57. package/dist/fields/locale-badge.d.ts +18 -0
  58. package/dist/fields/locale-badge.js +10 -0
  59. package/dist/fields/locale-badge.module.js +5 -0
  60. package/dist/fields/locale-badge_module.css +27 -0
  61. package/dist/fields/numerical/numerical-field.d.ts +18 -0
  62. package/dist/fields/numerical/numerical-field.js +74 -0
  63. package/dist/fields/relation/relation-display.d.ts +40 -0
  64. package/dist/fields/relation/relation-display.js +61 -0
  65. package/dist/fields/relation/relation-display.module.js +9 -0
  66. package/dist/fields/relation/relation-display_module.css +21 -0
  67. package/dist/fields/relation/relation-field.d.ts +18 -0
  68. package/dist/fields/relation/relation-field.js +138 -0
  69. package/dist/fields/relation/relation-field.module.js +13 -0
  70. package/dist/fields/relation/relation-field_module.css +62 -0
  71. package/dist/fields/relation/relation-picker.d.ts +59 -0
  72. package/dist/fields/relation/relation-picker.js +237 -0
  73. package/dist/fields/relation/relation-picker.module.js +26 -0
  74. package/dist/fields/relation/relation-picker_module.css +124 -0
  75. package/dist/fields/relation/relation-summary.d.ts +31 -0
  76. package/dist/fields/relation/relation-summary.js +50 -0
  77. package/dist/fields/relation/relation-summary.module.js +11 -0
  78. package/dist/fields/relation/relation-summary_module.css +37 -0
  79. package/dist/fields/select/select-field.d.ts +16 -0
  80. package/dist/fields/select/select-field.js +50 -0
  81. package/dist/fields/select/select-field.module.js +5 -0
  82. package/dist/fields/select/select-field_module.css +4 -0
  83. package/dist/fields/sortable-item.d.ts +15 -0
  84. package/dist/fields/sortable-item.js +81 -0
  85. package/dist/fields/sortable-item.module.js +22 -0
  86. package/dist/fields/sortable-item_module.css +124 -0
  87. package/dist/fields/text/text-field.d.ts +20 -0
  88. package/dist/fields/text/text-field.js +104 -0
  89. package/dist/fields/text/text-field.module.js +6 -0
  90. package/dist/fields/text/text-field_module.css +5 -0
  91. package/dist/fields/text-area/text-area-field.d.ts +20 -0
  92. package/dist/fields/text-area/text-area-field.js +105 -0
  93. package/dist/fields/text-area/text-area-field.module.js +6 -0
  94. package/dist/fields/text-area/text-area-field_module.css +5 -0
  95. package/dist/fields/use-field-change-handler.d.ts +23 -0
  96. package/dist/fields/use-field-change-handler.js +52 -0
  97. package/dist/forms/document-actions.d.ts +48 -0
  98. package/dist/forms/document-actions.js +475 -0
  99. package/dist/forms/document-actions.module.js +34 -0
  100. package/dist/forms/document-actions_module.css +118 -0
  101. package/dist/forms/form-context.d.ts +89 -0
  102. package/dist/forms/form-context.js +466 -0
  103. package/dist/forms/form-renderer.d.ts +98 -0
  104. package/dist/forms/form-renderer.js +597 -0
  105. package/dist/forms/form-renderer.module.js +46 -0
  106. package/dist/forms/form-renderer_module.css +245 -0
  107. package/dist/forms/navigation-guard.d.ts +54 -0
  108. package/dist/forms/navigation-guard.js +22 -0
  109. package/dist/forms/path-widget.d.ts +36 -0
  110. package/dist/forms/path-widget.js +116 -0
  111. package/dist/forms/path-widget.module.js +8 -0
  112. package/dist/forms/path-widget_module.css +29 -0
  113. package/dist/forms/upload-executor.d.ts +57 -0
  114. package/dist/forms/upload-executor.js +94 -0
  115. package/dist/lib/translate-validation-error.d.ts +36 -0
  116. package/dist/lib/translate-validation-error.js +11 -0
  117. package/dist/modules/admin-account/commands.d.ts +2 -1
  118. package/dist/modules/admin-account/commands.js +13 -2
  119. package/dist/modules/admin-account/components/change-password.js +45 -36
  120. package/dist/modules/admin-account/components/container.js +185 -134
  121. package/dist/modules/admin-account/components/preferences.d.ts +8 -0
  122. package/dist/modules/admin-account/components/preferences.js +152 -0
  123. package/dist/modules/admin-account/components/preferences.module.js +11 -0
  124. package/dist/modules/admin-account/components/preferences_module.css +41 -0
  125. package/dist/modules/admin-account/components/update.js +50 -31
  126. package/dist/modules/admin-account/index.d.ts +3 -3
  127. package/dist/modules/admin-account/index.js +2 -2
  128. package/dist/modules/admin-account/schemas.d.ts +4 -0
  129. package/dist/modules/admin-account/schemas.js +4 -1
  130. package/dist/modules/admin-account/service.d.ts +1 -0
  131. package/dist/modules/admin-account/service.js +8 -0
  132. package/dist/modules/admin-permissions/components/inspector.js +31 -41
  133. package/dist/modules/admin-roles/components/create.js +43 -26
  134. package/dist/modules/admin-roles/components/permissions.js +26 -35
  135. package/dist/modules/admin-roles/components/update.js +26 -16
  136. package/dist/modules/admin-users/components/create.js +60 -40
  137. package/dist/modules/admin-users/components/roles.js +9 -15
  138. package/dist/modules/admin-users/components/set-password.js +30 -31
  139. package/dist/modules/admin-users/components/update.js +58 -39
  140. package/dist/modules/admin-users/dto.js +1 -0
  141. package/dist/modules/admin-users/repository.d.ts +17 -0
  142. package/dist/modules/admin-users/schemas.d.ts +4 -0
  143. package/dist/modules/admin-users/schemas.js +6 -2
  144. package/dist/modules/auth/components/sign-in-form.js +10 -8
  145. package/dist/presentation/group.d.ts +27 -0
  146. package/dist/presentation/group.js +14 -0
  147. package/dist/presentation/group.module.js +6 -0
  148. package/dist/presentation/group_module.css +19 -0
  149. package/dist/presentation/row.d.ts +25 -0
  150. package/dist/presentation/row.js +8 -0
  151. package/dist/presentation/row.module.js +5 -0
  152. package/dist/presentation/row_module.css +18 -0
  153. package/dist/presentation/tabs.d.ts +25 -0
  154. package/dist/presentation/tabs.js +39 -0
  155. package/dist/presentation/tabs.module.js +10 -0
  156. package/dist/presentation/tabs_module.css +68 -0
  157. package/dist/react.d.ts +66 -0
  158. package/dist/react.js +36 -0
  159. package/dist/services/admin-services-types.d.ts +16 -0
  160. package/dist/widgets/diff-viewer/diff-modal.d.ts +22 -0
  161. package/dist/widgets/diff-viewer/diff-modal.js +149 -0
  162. package/dist/widgets/diff-viewer/diff-modal.module.js +14 -0
  163. package/dist/widgets/diff-viewer/diff-modal_module.css +56 -0
  164. package/dist/widgets/status-badge/status-badge.d.ts +25 -0
  165. package/dist/widgets/status-badge/status-badge.js +37 -0
  166. package/dist/widgets/status-badge/status-badge.module.js +7 -0
  167. package/dist/widgets/status-badge/status-badge_module.css +20 -0
  168. package/package.json +14 -4
  169. package/src/fields/array/array-field.module.css +48 -0
  170. package/src/fields/array/array-field.tsx +267 -0
  171. package/src/fields/blocks/blocks-field.module.css +148 -0
  172. package/src/fields/blocks/blocks-field.tsx +323 -0
  173. package/src/fields/checkbox/checkbox-field.module.css +4 -0
  174. package/src/fields/checkbox/checkbox-field.tsx +54 -0
  175. package/src/fields/column-formatter.tsx +31 -0
  176. package/src/fields/date-time-formatter.tsx +22 -0
  177. package/src/fields/datetime/datetime-field.module.css +13 -0
  178. package/src/fields/datetime/datetime-field.tsx +54 -0
  179. package/src/fields/draggable-context-menu.module.css +127 -0
  180. package/src/fields/draggable-context-menu.tsx +87 -0
  181. package/src/fields/field-helpers.ts +69 -0
  182. package/src/fields/field-renderer.module.css +22 -0
  183. package/src/fields/field-renderer.tsx +288 -0
  184. package/src/fields/field-services-context.tsx +35 -0
  185. package/src/fields/field-services-types.ts +68 -0
  186. package/src/fields/file/file-field.module.css +153 -0
  187. package/src/fields/file/file-field.tsx +286 -0
  188. package/src/fields/file/file-upload-field.module.css +101 -0
  189. package/src/fields/file/file-upload-field.tsx +187 -0
  190. package/src/fields/group/group-field.module.css +43 -0
  191. package/src/fields/group/group-field.tsx +84 -0
  192. package/src/fields/image/image-field.module.css +155 -0
  193. package/src/fields/image/image-field.tsx +306 -0
  194. package/src/fields/image/image-upload-field.module.css +123 -0
  195. package/src/fields/image/image-upload-field.tsx +276 -0
  196. package/src/fields/local-date-time.tsx +88 -0
  197. package/src/fields/locale-badge.module.css +37 -0
  198. package/src/fields/locale-badge.tsx +32 -0
  199. package/src/fields/numerical/numerical-field.tsx +114 -0
  200. package/src/fields/relation/relation-display.module.css +36 -0
  201. package/src/fields/relation/relation-display.tsx +138 -0
  202. package/src/fields/relation/relation-field.module.css +83 -0
  203. package/src/fields/relation/relation-field.tsx +211 -0
  204. package/src/fields/relation/relation-picker.module.css +168 -0
  205. package/src/fields/relation/relation-picker.tsx +343 -0
  206. package/src/fields/relation/relation-summary.module.css +55 -0
  207. package/src/fields/relation/relation-summary.tsx +123 -0
  208. package/src/fields/select/select-field.module.css +13 -0
  209. package/src/fields/select/select-field.tsx +61 -0
  210. package/src/fields/sortable-item.module.css +167 -0
  211. package/src/fields/sortable-item.tsx +106 -0
  212. package/src/fields/text/text-field.module.css +13 -0
  213. package/src/fields/text/text-field.tsx +146 -0
  214. package/src/fields/text-area/text-area-field.module.css +13 -0
  215. package/src/fields/text-area/text-area-field.tsx +147 -0
  216. package/src/fields/use-field-change-handler.ts +112 -0
  217. package/src/forms/document-actions.module.css +160 -0
  218. package/src/forms/document-actions.tsx +482 -0
  219. package/src/forms/form-context.tsx +704 -0
  220. package/src/forms/form-renderer.module.css +321 -0
  221. package/src/forms/form-renderer.tsx +891 -0
  222. package/src/forms/navigation-guard.tsx +98 -0
  223. package/src/forms/path-widget.module.css +41 -0
  224. package/src/forms/path-widget.test.tsx +217 -0
  225. package/src/forms/path-widget.tsx +183 -0
  226. package/src/forms/upload-executor.ts +192 -0
  227. package/src/lib/translate-validation-error.ts +56 -0
  228. package/src/modules/admin-account/commands.ts +13 -0
  229. package/src/modules/admin-account/components/change-password.tsx +46 -31
  230. package/src/modules/admin-account/components/container.tsx +83 -38
  231. package/src/modules/admin-account/components/preferences.module.css +60 -0
  232. package/src/modules/admin-account/components/preferences.tsx +203 -0
  233. package/src/modules/admin-account/components/update.tsx +53 -27
  234. package/src/modules/admin-account/index.ts +3 -0
  235. package/src/modules/admin-account/schemas.ts +13 -0
  236. package/src/modules/admin-account/service.ts +12 -0
  237. package/src/modules/admin-permissions/components/inspector.tsx +22 -14
  238. package/src/modules/admin-roles/components/create.tsx +51 -23
  239. package/src/modules/admin-roles/components/permissions.tsx +25 -21
  240. package/src/modules/admin-roles/components/update.tsx +37 -19
  241. package/src/modules/admin-users/components/create.tsx +63 -34
  242. package/src/modules/admin-users/components/roles.tsx +9 -8
  243. package/src/modules/admin-users/components/set-password.tsx +34 -28
  244. package/src/modules/admin-users/components/update.tsx +58 -36
  245. package/src/modules/admin-users/dto.ts +1 -0
  246. package/src/modules/admin-users/repository.ts +17 -0
  247. package/src/modules/admin-users/schemas.ts +12 -0
  248. package/src/modules/auth/components/sign-in-form.tsx +14 -8
  249. package/src/presentation/group.module.css +41 -0
  250. package/src/presentation/group.tsx +40 -0
  251. package/src/presentation/row.module.css +32 -0
  252. package/src/presentation/row.tsx +33 -0
  253. package/src/presentation/tabs.module.css +107 -0
  254. package/src/presentation/tabs.tsx +84 -0
  255. package/src/react.ts +84 -0
  256. package/src/services/admin-services-types.ts +18 -0
  257. package/src/widgets/diff-viewer/diff-modal.module.css +79 -0
  258. package/src/widgets/diff-viewer/diff-modal.tsx +186 -0
  259. package/src/widgets/status-badge/status-badge.module.css +31 -0
  260. package/src/widgets/status-badge/status-badge.tsx +71 -0
@@ -0,0 +1,343 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { useCallback, useEffect, useState } from 'react'
10
+
11
+ import type { CollectionAdminConfig, CollectionDefinition } from '@byline/core'
12
+ import { getCollectionAdminConfig } from '@byline/core'
13
+ import { useTranslation } from '@byline/i18n/react'
14
+ import { Button, LoaderRing, Modal, Search } from '@byline/ui/react'
15
+ import cx from 'classnames'
16
+
17
+ import { useBylineFieldServices } from '../field-services-context'
18
+ import {
19
+ PickerCell,
20
+ resolveFallbackDisplayField,
21
+ resolveRowLabel,
22
+ resolveSelectFields,
23
+ } from './relation-display'
24
+ import styles from './relation-picker.module.css'
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // RelationPicker — modal listing for selecting a target document
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /**
31
+ * Row rendering strategy, in priority order:
32
+ * 1. `CollectionAdminConfig.picker` — a ColumnDefinition[] from the target
33
+ * admin config. Each row renders the declared columns side-by-side,
34
+ * reusing any column formatters (thumbnail, date, etc).
35
+ * 2. Explicit `displayField` prop on this component (forwarded from
36
+ * `RelationField.displayField`).
37
+ * 3. `CollectionDefinition.useAsTitle` on the target.
38
+ * 4. First top-level `text` field on the target.
39
+ *
40
+ * Paths 2–4 render a single-line label (primary) + `path` (secondary).
41
+ */
42
+ interface RelationPickerProps {
43
+ /** The target collection path (e.g. `'media'`). */
44
+ targetCollectionPath: string
45
+ /** The target collection definition (used for labels + displayField fallback). */
46
+ targetDefinition?: CollectionDefinition | null
47
+ /** Explicit display field to render as row label. */
48
+ displayField?: string
49
+ /**
50
+ * Extra field names to load into each row's `record.fields` beyond the
51
+ * display columns. Not rendered — available to the `onSelect` consumer
52
+ * (e.g. the inline-image modal seeding alt-text from the picked media).
53
+ *
54
+ * Pass a stable (module-level) array — this feeds the fetch effect's
55
+ * dependency list, so a fresh array each render would refetch on every
56
+ * render.
57
+ */
58
+ extraSelectFields?: string[]
59
+ /** Modal open/close state. */
60
+ isOpen: boolean
61
+ /**
62
+ * Called with the picked selection when the user confirms.
63
+ *
64
+ * `record` is the raw document the picker row rendered — the caller can
65
+ * use it to show the selected value in its own tile without a refetch.
66
+ * The fields available on `record` are whatever `resolveSelectFields`
67
+ * asked the listing endpoint for (picker columns + `useAsTitle` +
68
+ * `displayField`), so any display surface downstream of the picker that
69
+ * also renders from those same columns will find the data it needs.
70
+ */
71
+ onSelect: (selection: {
72
+ targetDocumentId: string
73
+ targetCollectionId: string
74
+ record?: Record<string, any>
75
+ }) => void
76
+ /** Called when the user dismisses the modal. */
77
+ onDismiss: () => void
78
+ }
79
+
80
+ const PAGE_SIZE = 15
81
+
82
+ export const RelationPicker = ({
83
+ targetCollectionPath,
84
+ targetDefinition,
85
+ displayField,
86
+ extraSelectFields,
87
+ isOpen,
88
+ onSelect,
89
+ onDismiss,
90
+ }: RelationPickerProps) => {
91
+ const [query, setQuery] = useState<string>('')
92
+ const [page, setPage] = useState<number>(1)
93
+ const { t } = useTranslation('byline-admin')
94
+ const [selectedDocumentId, setSelectedDocumentId] = useState<string | null>(null)
95
+ const [loading, setLoading] = useState<boolean>(false)
96
+ const [error, setError] = useState<string | null>(null)
97
+ const [documents, setDocuments] = useState<any[]>([])
98
+ const [totalPages, setTotalPages] = useState<number>(1)
99
+ const [collectionId, setCollectionId] = useState<string | null>(null)
100
+
101
+ const { getCollectionDocuments } = useBylineFieldServices()
102
+
103
+ const targetAdminConfig: CollectionAdminConfig | null =
104
+ getCollectionAdminConfig(targetCollectionPath)
105
+ const pickerColumns = targetAdminConfig?.picker
106
+
107
+ // Reset local state each time the modal opens so prior queries don't leak.
108
+ useEffect(() => {
109
+ if (isOpen) {
110
+ setQuery('')
111
+ setPage(1)
112
+ setSelectedDocumentId(null)
113
+ setError(null)
114
+ }
115
+ }, [isOpen])
116
+
117
+ // Fetch whenever the modal is open and the query / page changes.
118
+ useEffect(() => {
119
+ if (!isOpen) return
120
+ let cancelled = false
121
+
122
+ const selectFields = resolveSelectFields(
123
+ targetDefinition,
124
+ displayField,
125
+ pickerColumns,
126
+ extraSelectFields
127
+ )
128
+
129
+ setLoading(true)
130
+ setError(null)
131
+ getCollectionDocuments({
132
+ collection: targetCollectionPath,
133
+ params: {
134
+ page,
135
+ page_size: PAGE_SIZE,
136
+ query: query.length > 0 ? query : undefined,
137
+ fields: selectFields,
138
+ },
139
+ })
140
+ .then((response: any) => {
141
+ if (cancelled) return
142
+ setDocuments(response.docs)
143
+ setTotalPages(response.meta.totalPages ?? 1)
144
+ setCollectionId(response.included.collection.id as string)
145
+ })
146
+ .catch((err: any) => {
147
+ if (cancelled) return
148
+ setError(err instanceof Error ? err.message : t('fields.relation.picker.loadFailed'))
149
+ })
150
+ .finally(() => {
151
+ if (!cancelled) setLoading(false)
152
+ })
153
+
154
+ return () => {
155
+ cancelled = true
156
+ }
157
+ }, [
158
+ isOpen,
159
+ targetCollectionPath,
160
+ query,
161
+ page,
162
+ displayField,
163
+ extraSelectFields,
164
+ targetDefinition,
165
+ pickerColumns,
166
+ getCollectionDocuments,
167
+ t,
168
+ ])
169
+
170
+ const resolvedDisplayField =
171
+ displayField ??
172
+ targetDefinition?.useAsTitle ??
173
+ resolveFallbackDisplayField(targetDefinition) ??
174
+ null
175
+
176
+ const handleSelect = useCallback(() => {
177
+ if (!selectedDocumentId || !collectionId) return
178
+ const record = documents.find((d) => d?.id === selectedDocumentId)
179
+ onSelect({
180
+ targetDocumentId: selectedDocumentId,
181
+ targetCollectionId: collectionId,
182
+ record,
183
+ })
184
+ }, [selectedDocumentId, collectionId, documents, onSelect])
185
+
186
+ const title = t('fields.relation.selectPickerTitle', {
187
+ label: targetDefinition?.labels.singular ?? targetCollectionPath,
188
+ })
189
+
190
+ return (
191
+ <Modal isOpen={isOpen} onDismiss={onDismiss}>
192
+ <Modal.Container style={{ maxWidth: '600px', width: '100%' }}>
193
+ <Modal.Header className={cx('byline-field-relation-picker-header', styles.header)}>
194
+ <h3 className={cx('byline-field-relation-picker-title', styles.title)}>{title}</h3>
195
+ </Modal.Header>
196
+ <Modal.Content>
197
+ <div className={cx('byline-field-relation-picker-body', styles.body)}>
198
+ <Search
199
+ onSearch={(q) => {
200
+ setPage(1)
201
+ setQuery(q ?? '')
202
+ }}
203
+ onClear={() => {
204
+ setPage(1)
205
+ setQuery('')
206
+ }}
207
+ inputSize="sm"
208
+ placeholder={t('fields.relation.picker.searchPlaceholder')}
209
+ />
210
+
211
+ <div className={cx('byline-field-relation-picker-list', styles.list)}>
212
+ {loading && documents.length === 0 && (
213
+ <div className={cx('byline-field-relation-picker-loading', styles.loading)}>
214
+ <LoaderRing size={24} color="#888888" />
215
+ </div>
216
+ )}
217
+ {!loading && error && (
218
+ <div className={cx('byline-field-relation-picker-error', styles.error)}>
219
+ {error}
220
+ </div>
221
+ )}
222
+ {!loading && !error && documents.length === 0 && (
223
+ <div className={cx('byline-field-relation-picker-empty', styles.empty)}>
224
+ {t('fields.relation.picker.empty')}
225
+ </div>
226
+ )}
227
+ {documents.length > 0 && (
228
+ <ul className={cx('byline-field-relation-picker-rows', styles.rows)}>
229
+ {documents.map((doc) => {
230
+ const id = doc.id as string
231
+ const selected = selectedDocumentId === id
232
+ return (
233
+ <li key={id}>
234
+ <button
235
+ type="button"
236
+ className={cx(
237
+ 'byline-field-relation-picker-row-button',
238
+ styles['row-button'],
239
+ selected && [
240
+ 'byline-field-relation-picker-row-selected',
241
+ styles['row-selected'],
242
+ ]
243
+ )}
244
+ onClick={() => setSelectedDocumentId(id)}
245
+ >
246
+ {pickerColumns && pickerColumns.length > 0 ? (
247
+ <div
248
+ className={cx(
249
+ 'byline-field-relation-picker-row-cells',
250
+ styles['row-cells']
251
+ )}
252
+ >
253
+ {pickerColumns.map((col) => (
254
+ <PickerCell key={String(col.fieldName)} column={col} record={doc} />
255
+ ))}
256
+ </div>
257
+ ) : (
258
+ <div
259
+ className={cx(
260
+ 'byline-field-relation-picker-row-stack',
261
+ styles['row-stack']
262
+ )}
263
+ >
264
+ <span
265
+ className={cx(
266
+ 'byline-field-relation-picker-row-label',
267
+ styles['row-label']
268
+ )}
269
+ >
270
+ {resolveRowLabel(doc, resolvedDisplayField) || id}
271
+ </span>
272
+ {typeof doc.path === 'string' && doc.path.length > 0 && (
273
+ <span
274
+ className={cx(
275
+ 'byline-field-relation-picker-row-path',
276
+ styles['row-path']
277
+ )}
278
+ >
279
+ {doc.path}
280
+ </span>
281
+ )}
282
+ </div>
283
+ )}
284
+ </button>
285
+ </li>
286
+ )
287
+ })}
288
+ </ul>
289
+ )}
290
+ </div>
291
+
292
+ {totalPages > 1 && (
293
+ <div className={cx('byline-field-relation-picker-pager', styles.pager)}>
294
+ <Button
295
+ size="xs"
296
+ variant="outlined"
297
+ intent="noeffect"
298
+ type="button"
299
+ disabled={page <= 1 || loading}
300
+ onClick={() => setPage((p) => Math.max(1, p - 1))}
301
+ >
302
+ {t('common.pager.previous')}
303
+ </Button>
304
+ <span>{t('common.pager.pageOf', { page, total: totalPages })}</span>
305
+ <Button
306
+ size="xs"
307
+ variant="outlined"
308
+ intent="noeffect"
309
+ type="button"
310
+ disabled={page >= totalPages || loading}
311
+ onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
312
+ >
313
+ {t('common.pager.next')}
314
+ </Button>
315
+ </div>
316
+ )}
317
+ </div>
318
+ </Modal.Content>
319
+ <Modal.Actions>
320
+ <Button
321
+ size="sm"
322
+ intent="noeffect"
323
+ type="button"
324
+ onClick={onDismiss}
325
+ className={cx('byline-field-relation-picker-action', styles.action)}
326
+ >
327
+ {t('common.actions.cancel')}
328
+ </Button>
329
+ <Button
330
+ size="sm"
331
+ className={cx('byline-field-relation-picker-action', styles.action)}
332
+ intent="primary"
333
+ type="button"
334
+ disabled={!selectedDocumentId}
335
+ onClick={handleSelect}
336
+ >
337
+ {t('common.actions.select')}
338
+ </Button>
339
+ </Modal.Actions>
340
+ </Modal.Container>
341
+ </Modal>
342
+ )
343
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * RelationSummary — selected-value tile for the relation field widget.
3
+ *
4
+ * Override handles:
5
+ * .byline-relation-summary — root container
6
+ * .byline-relation-summary-stack — vertical layout used for label + value
7
+ * .byline-relation-summary-row — horizontal layout used for picker columns
8
+ * .byline-relation-summary-kind — small "type" label above the value
9
+ * .byline-relation-summary-value — primary value text
10
+ * .byline-relation-summary-value-mono — monospace fallback (raw uuid / not-found)
11
+ * .byline-relation-summary-missing — added to the value when target is unresolved
12
+ */
13
+
14
+ .stack,
15
+ :global(.byline-relation-summary-stack) {
16
+ display: flex;
17
+ flex-direction: column;
18
+ gap: 0.125rem;
19
+ min-width: 0;
20
+ }
21
+
22
+ .row,
23
+ :global(.byline-relation-summary-row) {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 0.75rem;
27
+ min-width: 0;
28
+ }
29
+
30
+ .kind,
31
+ :global(.byline-relation-summary-kind) {
32
+ color: var(--gray-500);
33
+ }
34
+
35
+ .value,
36
+ :global(.byline-relation-summary-value) {
37
+ color: var(--gray-100);
38
+ overflow: hidden;
39
+ text-overflow: ellipsis;
40
+ white-space: nowrap;
41
+ }
42
+
43
+ .value-mono,
44
+ :global(.byline-relation-summary-value-mono) {
45
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
46
+ overflow: hidden;
47
+ text-overflow: ellipsis;
48
+ white-space: nowrap;
49
+ }
50
+
51
+ .missing,
52
+ :global(.byline-relation-summary-missing) {
53
+ font-size: var(--font-size-xs);
54
+ color: var(--red-400);
55
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import type { CollectionAdminConfig, CollectionDefinition } from '@byline/core'
10
+ import cx from 'classnames'
11
+
12
+ import { PickerCell, resolveFallbackDisplayField, resolveRowLabel } from './relation-display'
13
+ import styles from './relation-summary.module.css'
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // RelationSummary — selected-value tile for the relation field widget.
17
+ //
18
+ // Rendering priority (mirrors RelationPicker so the tile and picker rows
19
+ // look identical):
20
+ // 1. target `CollectionAdminConfig.picker` columns (full fidelity, with
21
+ // formatters — e.g. MediaThumbnail + title)
22
+ // 2. explicit `displayField` prop (from source schema's RelationField)
23
+ // 3. `CollectionDefinition.useAsTitle`
24
+ // 4. first declared text field on the target
25
+ // 5. target UUID (only when nothing else is available — "resolved but
26
+ // naked" or unpopulated)
27
+ //
28
+ // Value source priority:
29
+ // 1. `populated.document` — a `PopulatedRelationValue` attached by the
30
+ // server-side populate pass on first page load.
31
+ // 2. `cachedRecord` — the raw document the picker just handed us after
32
+ // a fresh pick (no server round trip needed).
33
+ // 3. neither — we have only the stored ref; fall through to UUID.
34
+ // ---------------------------------------------------------------------------
35
+
36
+ interface RelationSummaryProps {
37
+ targetDefinition: CollectionDefinition
38
+ targetAdminConfig: CollectionAdminConfig | null
39
+ displayField?: string
40
+ /** The raw relation value from the form. May be a plain ref or a populated envelope. */
41
+ value: {
42
+ targetDocumentId: string
43
+ targetCollectionId: string
44
+ _resolved?: boolean
45
+ _cycle?: boolean
46
+ document?: Record<string, any>
47
+ }
48
+ /**
49
+ * A document record cached client-side from a recent picker selection.
50
+ * Used when `value` is a plain ref (post-pick state) but we still want
51
+ * the tile to render real display data without a refetch. Caller is
52
+ * responsible for clearing/replacing this when the value's
53
+ * `targetDocumentId` changes.
54
+ */
55
+ cachedRecord?: Record<string, any> | null
56
+ }
57
+
58
+ export function RelationSummary({
59
+ targetDefinition,
60
+ targetAdminConfig,
61
+ displayField,
62
+ value,
63
+ cachedRecord,
64
+ }: RelationSummaryProps) {
65
+ const pickerColumns = targetAdminConfig?.picker
66
+
67
+ // Unresolved (deleted target).
68
+ if (value._resolved === false) {
69
+ return (
70
+ <div className={cx('byline-relation-summary-stack', styles.stack)}>
71
+ <span className={cx('byline-relation-summary-kind', styles.kind)}>
72
+ {targetDefinition.labels.singular}
73
+ </span>
74
+ <span
75
+ className={cx(
76
+ 'byline-relation-summary-value-mono byline-relation-summary-missing',
77
+ styles['value-mono'],
78
+ styles.missing
79
+ )}
80
+ >
81
+ (target not found) {value.targetDocumentId}
82
+ </span>
83
+ </div>
84
+ )
85
+ }
86
+
87
+ // Prefer the populated envelope's document; fall back to the cached
88
+ // picker record; finally fall back to rendering just the raw ref.
89
+ const record: Record<string, any> | null =
90
+ (value._resolved === true && !value._cycle && value.document) || cachedRecord || null
91
+
92
+ if (record && pickerColumns && pickerColumns.length > 0) {
93
+ return (
94
+ <div className={cx('byline-relation-summary-row', styles.row)}>
95
+ {pickerColumns.map((col) => (
96
+ <PickerCell key={String(col.fieldName)} column={col} record={record} />
97
+ ))}
98
+ </div>
99
+ )
100
+ }
101
+
102
+ const resolvedDisplayField =
103
+ displayField ??
104
+ targetDefinition.useAsTitle ??
105
+ resolveFallbackDisplayField(targetDefinition) ??
106
+ null
107
+ const label = record ? resolveRowLabel(record, resolvedDisplayField) : null
108
+
109
+ return (
110
+ <div className={cx('byline-relation-summary-stack', styles.stack)}>
111
+ <span className={cx('byline-relation-summary-kind', styles.kind)}>
112
+ {targetDefinition.labels.singular}
113
+ </span>
114
+ {label ? (
115
+ <span className={cx('byline-relation-summary-value', styles.value)}>{label}</span>
116
+ ) : (
117
+ <span className={cx('byline-relation-summary-value-mono', styles['value-mono'])}>
118
+ {value.targetDocumentId}
119
+ </span>
120
+ )}
121
+ </div>
122
+ )
123
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * SelectField — dropdown form widget.
3
+ *
4
+ * Override handles:
5
+ * .byline-field-select — the wrapper div
6
+ * .byline-field-select-dirty — added to the inner select when the
7
+ * field has unsaved local changes
8
+ */
9
+
10
+ .dirty,
11
+ :global(.byline-field-select-dirty) {
12
+ border-color: var(--blue-300);
13
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import type { SelectField as FieldType } from '@byline/core'
10
+ import { ErrorText, Label, Select } from '@byline/ui/react'
11
+ import cx from 'classnames'
12
+
13
+ import { useFieldError, useFieldValue, useIsDirty } from '../../forms/form-context'
14
+ import styles from './select-field.module.css'
15
+
16
+ export const SelectField = ({
17
+ field,
18
+ value,
19
+ defaultValue,
20
+ onChange,
21
+ id,
22
+ path,
23
+ }: {
24
+ field: FieldType
25
+ value?: string
26
+ defaultValue?: string
27
+ onChange?: (value: string) => void
28
+ id?: string
29
+ path?: string
30
+ }) => {
31
+ const fieldPath = path ?? field.name
32
+ const fieldError = useFieldError(fieldPath)
33
+ const isDirty = useIsDirty(fieldPath)
34
+ const fieldValue = useFieldValue<string | undefined>(fieldPath)
35
+ const incomingValue = value ?? fieldValue ?? defaultValue ?? ''
36
+ const htmlId = id ?? fieldPath
37
+
38
+ return (
39
+ <div className={`byline-field-select ${field.name}`}>
40
+ {field.label && (
41
+ <Label id={htmlId} htmlFor={htmlId} label={field.label} required={!field.optional} />
42
+ )}
43
+ <Select<string>
44
+ size="sm"
45
+ id={htmlId}
46
+ name={field.name}
47
+ placeholder="Select an option"
48
+ required={!field.optional}
49
+ value={incomingValue}
50
+ ariaLabel={field.label}
51
+ helpText={field.helpText}
52
+ items={field.options.map((opt) => ({ value: opt.value, label: opt.label }))}
53
+ onValueChange={(value) => {
54
+ if (value != null) onChange?.(value)
55
+ }}
56
+ className={cx(isDirty && ['byline-field-select-dirty', styles.dirty])}
57
+ />
58
+ {fieldError && <ErrorText id={`${field.name}-error`} text={fieldError} />}
59
+ </div>
60
+ )
61
+ }