@byline/admin 6.0.1 → 6.0.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 (92) hide show
  1. package/dist/modules/admin-preferences/index.d.ts +7 -3
  2. package/dist/modules/admin-preferences/index.js +1 -1
  3. package/dist/modules/admin-preferences/schemas.d.ts +19 -16
  4. package/dist/modules/admin-preferences/schemas.js +46 -2
  5. package/dist/modules/analytics/components/timeseries.d.ts +28 -0
  6. package/dist/modules/analytics/components/timeseries.js +107 -35
  7. package/dist/modules/analytics/components/timeseries.module.js +15 -0
  8. package/dist/modules/analytics/components/timeseries_module.css +110 -3
  9. package/package.json +7 -7
  10. package/src/modules/admin-preferences/index.ts +7 -2
  11. package/src/modules/admin-preferences/schemas.test.node.ts +74 -0
  12. package/src/modules/admin-preferences/schemas.ts +85 -8
  13. package/src/modules/analytics/components/dashboard.test.node.ts +51 -0
  14. package/src/modules/analytics/components/timeseries.module.css +176 -3
  15. package/src/modules/analytics/components/timeseries.tsx +153 -40
  16. package/dist/exports-parity.test.node.js +0 -37
  17. package/dist/fields/array/array-field.jsx +0 -209
  18. package/dist/fields/blocks/blocks-field.jsx +0 -248
  19. package/dist/fields/checkbox/checkbox-field.jsx +0 -24
  20. package/dist/fields/code/code-editor.jsx +0 -211
  21. package/dist/fields/code/code-field.jsx +0 -109
  22. package/dist/fields/column-formatter.jsx +0 -25
  23. package/dist/fields/date-time-formatter.jsx +0 -18
  24. package/dist/fields/datetime/datetime-field.jsx +0 -22
  25. package/dist/fields/draggable-context-menu.jsx +0 -50
  26. package/dist/fields/field-admin.test.node.js +0 -40
  27. package/dist/fields/field-renderer.jsx +0 -136
  28. package/dist/fields/field-services-context.jsx +0 -17
  29. package/dist/fields/file/file-field.jsx +0 -176
  30. package/dist/fields/file/file-upload-field.jsx +0 -89
  31. package/dist/fields/group/group-field.jsx +0 -46
  32. package/dist/fields/image/image-field.jsx +0 -183
  33. package/dist/fields/image/image-upload-field.jsx +0 -157
  34. package/dist/fields/local-date-time.jsx +0 -69
  35. package/dist/fields/locale-badge.jsx +0 -19
  36. package/dist/fields/numerical/numerical-field.jsx +0 -93
  37. package/dist/fields/relation/relation-column-formatter.jsx +0 -74
  38. package/dist/fields/relation/relation-display.jsx +0 -118
  39. package/dist/fields/relation/relation-field.jsx +0 -108
  40. package/dist/fields/relation/relation-many-field.jsx +0 -142
  41. package/dist/fields/relation/relation-picker.jsx +0 -240
  42. package/dist/fields/relation/relation-summary.jsx +0 -45
  43. package/dist/fields/select/select-field.jsx +0 -27
  44. package/dist/fields/sortable-item.jsx +0 -54
  45. package/dist/fields/text/text-field.jsx +0 -79
  46. package/dist/fields/text-area/text-area-field.jsx +0 -79
  47. package/dist/forms/available-locales-reconcile.test.node.js +0 -43
  48. package/dist/forms/available-locales-widget.jsx +0 -63
  49. package/dist/forms/document-actions.jsx +0 -486
  50. package/dist/forms/form-context.jsx +0 -598
  51. package/dist/forms/form-modals.jsx +0 -128
  52. package/dist/forms/form-renderer.jsx +0 -532
  53. package/dist/forms/form-status-display.jsx +0 -69
  54. package/dist/forms/navigation-guard.jsx +0 -68
  55. package/dist/forms/nested-path.test.node.js +0 -115
  56. package/dist/forms/path-widget.jsx +0 -125
  57. package/dist/forms/pending-uploads.test.node.js +0 -19
  58. package/dist/forms/repeating-items.test.node.js +0 -29
  59. package/dist/forms/scheduled-publication-control.jsx +0 -389
  60. package/dist/forms/scheduled-publication-state.test.node.js +0 -121
  61. package/dist/forms/scheduled-publication-time.test.node.js +0 -70
  62. package/dist/forms/status-transitions.test.node.js +0 -76
  63. package/dist/forms/tree-placement-widget.jsx +0 -160
  64. package/dist/forms/upload-executor.test.node.js +0 -368
  65. package/dist/forms/use-form-layout.test.node.js +0 -69
  66. package/dist/modules/admin-account/components/change-password.jsx +0 -169
  67. package/dist/modules/admin-account/components/container.jsx +0 -164
  68. package/dist/modules/admin-account/components/preferences.jsx +0 -148
  69. package/dist/modules/admin-account/components/theme-switch.jsx +0 -52
  70. package/dist/modules/admin-account/components/update.jsx +0 -186
  71. package/dist/modules/admin-permissions/components/inspector.jsx +0 -200
  72. package/dist/modules/admin-preferences/schemas.test.node.js +0 -48
  73. package/dist/modules/admin-roles/components/create.jsx +0 -155
  74. package/dist/modules/admin-roles/components/permissions.jsx +0 -239
  75. package/dist/modules/admin-roles/components/update.jsx +0 -148
  76. package/dist/modules/admin-users/components/create.jsx +0 -206
  77. package/dist/modules/admin-users/components/roles.jsx +0 -126
  78. package/dist/modules/admin-users/components/set-password.jsx +0 -133
  79. package/dist/modules/admin-users/components/update.jsx +0 -215
  80. package/dist/modules/analytics/components/dashboard.jsx +0 -208
  81. package/dist/modules/analytics/components/dashboard.test.node.js +0 -162
  82. package/dist/modules/analytics/components/timeseries.jsx +0 -193
  83. package/dist/modules/auth/components/sign-in-form.jsx +0 -89
  84. package/dist/modules/auth/safe-redirect.test.node.js +0 -40
  85. package/dist/modules/auth/sign-in-form-props.test.node.js +0 -7
  86. package/dist/presentation/group.jsx +0 -26
  87. package/dist/presentation/row.jsx +0 -23
  88. package/dist/presentation/tabs.jsx +0 -39
  89. package/dist/services/admin-services-context.jsx +0 -17
  90. package/dist/widgets/diff-viewer/diff-modal.jsx +0 -110
  91. package/dist/widgets/source-locale-badge/source-locale-badge.jsx +0 -28
  92. package/dist/widgets/status-badge/status-badge.jsx +0 -46
@@ -1,118 +0,0 @@
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
- import cx from 'clsx';
9
- import styles from './relation-display.module.css';
10
- // ---------------------------------------------------------------------------
11
- // Shared render helpers used by both the relation picker modal rows and
12
- // the relation-summary tile on the edit form. Kept in lock-step so a row
13
- // rendered in the picker looks identical to the selected tile.
14
- // ---------------------------------------------------------------------------
15
- /**
16
- * Render a single row cell from a `ColumnDefinition`, reading the value
17
- * from the document's `fields` (with a fallback to top-level metadata like
18
- * `status`, `updated_at`, `path`). Honours both formatter shapes — plain
19
- * function → its return value, `{ component }` → the component is rendered.
20
- */
21
- export function PickerCell({ column, record, }) {
22
- const name = String(column.fieldName);
23
- const value = record?.fields?.[name] ?? record?.[name];
24
- let content;
25
- if (column.formatter) {
26
- if (typeof column.formatter === 'function') {
27
- content = column.formatter(value, record);
28
- }
29
- else {
30
- const Comp = column.formatter.component;
31
- content = <Comp value={value} record={record}/>;
32
- }
33
- }
34
- else if (value == null) {
35
- content = null;
36
- }
37
- else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
38
- content = String(value);
39
- }
40
- else {
41
- content = null;
42
- }
43
- return (<div className={cx('byline-relation-cell', styles.cell, column.align === 'center' && ['byline-relation-cell-center', styles['cell-center']], column.align === 'right' && ['byline-relation-cell-right', styles['cell-right']], column.className)}>
44
- {content}
45
- </div>);
46
- }
47
- /** First top-level `text` field name on a collection, or null. */
48
- export function resolveFallbackDisplayField(def) {
49
- if (!def)
50
- return null;
51
- const textField = def.fields.find((f) => f.type === 'text');
52
- return textField?.name ?? null;
53
- }
54
- /** Resolve the row's primary label text from the document. */
55
- export function resolveRowLabel(doc, displayField) {
56
- if (!doc)
57
- return null;
58
- if (displayField) {
59
- const v = doc.fields?.[displayField];
60
- if (typeof v === 'string' && v.length > 0)
61
- return v;
62
- }
63
- if (typeof doc.fields?.title === 'string' && doc.fields.title.length > 0) {
64
- return doc.fields.title;
65
- }
66
- if (typeof doc.path === 'string' && doc.path.length > 0)
67
- return doc.path;
68
- return null;
69
- }
70
- /**
71
- * Build the `fields` projection for the picker listing. Unions:
72
- * - caller-supplied `displayField`
73
- * - target schema's `useAsTitle` (always included, even when not visible)
74
- * - every `fieldName` declared in the admin config's `picker` columns
75
- * - `title` (metadata fallback for rows with no explicit picker columns)
76
- *
77
- * Note that the document `path` is top-level metadata on every list response
78
- * — it is always returned by `getCollectionDocuments` regardless of the
79
- * `fields` projection. Callers that need `{ title, path }` for downstream
80
- * normalisation (e.g. the rich-text link plugin) can rely on:
81
- * - `record.fields[def.useAsTitle]` for the title
82
- * - `record.path` for the path
83
- *
84
- * Returns `undefined` when no target definition is available, leaving the
85
- * listing endpoint to decide its own default projection.
86
- */
87
- export function resolveSelectFields(def, displayField, pickerColumns, extraFields) {
88
- if (!def)
89
- return undefined;
90
- const out = new Set();
91
- if (displayField)
92
- out.add(displayField);
93
- if (def.useAsTitle)
94
- out.add(def.useAsTitle);
95
- const fallback = resolveFallbackDisplayField(def);
96
- if (fallback)
97
- out.add(fallback);
98
- if (pickerColumns) {
99
- for (const col of pickerColumns) {
100
- const name = String(col.fieldName);
101
- if (def.fields.some((f) => f.name === name))
102
- out.add(name);
103
- }
104
- }
105
- // Extra non-display fields a consumer needs on `record.fields` (e.g. the
106
- // inline-image modal seeding alt-text). Loaded but not rendered as columns.
107
- if (extraFields) {
108
- for (const name of extraFields) {
109
- if (def.fields.some((f) => f.name === name))
110
- out.add(name);
111
- }
112
- }
113
- if (def.fields.some((f) => f.name === 'title'))
114
- out.add('title');
115
- if (out.size === 0)
116
- return undefined;
117
- return Array.from(out);
118
- }
@@ -1,108 +0,0 @@
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
- import { useState } from 'react';
9
- import { getCollectionAdminConfig, getCollectionDefinition, isSingleton } from '@byline/core';
10
- import { useTranslation } from '@byline/i18n/react';
11
- import { Button, CloseIcon, EditIcon, ErrorText, HelpText, IconButton, Label, } from '@byline/ui/react';
12
- import cx from 'clsx';
13
- import { useFieldError, useFieldValue } from '../../forms/form-context';
14
- import styles from './relation-field.module.css';
15
- import { RelationPicker } from './relation-picker';
16
- import { RelationSummary } from './relation-summary';
17
- export const RelationField = ({ field, value, defaultValue, onChange, id, path, }) => {
18
- const fieldPath = path ?? field.name;
19
- const htmlId = id ?? fieldPath;
20
- const fieldError = useFieldError(fieldPath);
21
- const fieldValue = useFieldValue(fieldPath);
22
- const incomingValue = fieldValue !== undefined
23
- ? (fieldValue ?? null)
24
- : (value ??
25
- defaultValue ??
26
- null);
27
- // Resolve the target collection definition + admin config. The admin
28
- // config drives the picker-column rendering inside RelationSummary so
29
- // the selected tile matches the picker row exactly. Missing target →
30
- // render an inline error and disable the picker.
31
- const targetDefinition = getCollectionDefinition(field.targetCollection);
32
- const targetDef = targetDefinition != null && !isSingleton(targetDefinition) ? targetDefinition : null;
33
- const targetAdminConfig = getCollectionAdminConfig(field.targetCollection);
34
- const { t } = useTranslation('byline-admin');
35
- const [pickerOpen, setPickerOpen] = useState(false);
36
- // Cached target document from the most recent picker selection. Lets the
37
- // tile render real display data (name, thumbnail) immediately after a
38
- // pick without a round trip. Cleared via the `targetDocumentId`
39
- // comparison in the render path.
40
- const [pickedRecord, setPickedRecord] = useState(null);
41
- const handleSelect = (selection) => {
42
- setPickerOpen(false);
43
- if (selection.record) {
44
- setPickedRecord({ id: selection.targetDocumentId, record: selection.record });
45
- }
46
- else {
47
- setPickedRecord(null);
48
- }
49
- onChange?.({
50
- targetDocumentId: selection.targetDocumentId,
51
- targetCollectionId: selection.targetCollectionId,
52
- });
53
- };
54
- const handleRemove = () => {
55
- setPickedRecord(null);
56
- onChange?.(null);
57
- };
58
- // Only carry the cached picker record through to the summary when it
59
- // still matches the current value — guards against a stale cache after
60
- // an external value change (e.g. patch rollback).
61
- const cachedRecord = pickedRecord && incomingValue && pickedRecord.id === incomingValue.targetDocumentId
62
- ? pickedRecord.record
63
- : null;
64
- const isUnknown = targetDef == null;
65
- return (<div className={`byline-field-relation ${field.name}`}>
66
- <div className={cx('byline-field-relation-header', styles.header)}>
67
- <Label id={`${htmlId}-label`} htmlFor={htmlId} label={field.label ?? field.name} required={!field.optional}/>
68
- </div>
69
- {isUnknown ? (<div className={cx('byline-field-relation-error-tile', styles['error-tile'])}>
70
- <span>
71
- {t('fields.relation.unknownError', {
72
- name: field.name,
73
- target: field.targetCollection,
74
- })}
75
- </span>
76
- <span className={cx('byline-field-relation-error-text', styles['error-text'])}>
77
- {t('fields.relation.unknownHint')}
78
- </span>
79
- </div>) : (
80
- // Rounded frame below the label wrapping the interactive body
81
- // (selected tile or select button) — see `.frame` in the CSS module.
82
- <div className={cx('byline-field-relation-frame', styles.frame)}>
83
- {incomingValue ? (<div className={cx('byline-field-relation-tile', styles.tile)}>
84
- <RelationSummary targetDefinition={targetDef} targetAdminConfig={targetAdminConfig} displayField={field.displayField} value={incomingValue} cachedRecord={cachedRecord}/>
85
- <div className={cx('byline-field-relation-actions', styles.actions)}>
86
- <IconButton id={htmlId} type="button" intent="noeffect" size="xs" aria-label={t('fields.relation.changeAriaLabel', {
87
- label: targetDef.labels.singular,
88
- })} onClick={() => setPickerOpen(true)}>
89
- <EditIcon width="15px" height="15px"/>
90
- </IconButton>
91
- <IconButton type="button" intent="noeffect" size="xs" aria-label={t('fields.relation.removeAriaLabel', {
92
- label: targetDef.labels.singular,
93
- })} onClick={handleRemove}>
94
- <CloseIcon width="15px" height="15px"/>
95
- </IconButton>
96
- </div>
97
- </div>) : (<Button id={htmlId} size="xs" variant="outlined" intent="noeffect" type="button" onClick={() => setPickerOpen(true)}>
98
- {t('fields.relation.selectButton', { label: targetDef.labels.singular })}
99
- </Button>)}
100
- </div>)}
101
-
102
- {field.helpText != null && <HelpText text={field.helpText}/>}
103
-
104
- {fieldError && <ErrorText id={`${field.name}-error`} text={fieldError}/>}
105
-
106
- {!isUnknown && (<RelationPicker targetCollectionPath={field.targetCollection} targetDefinition={targetDef} displayField={field.displayField} isOpen={pickerOpen} onSelect={handleSelect} onDismiss={() => setPickerOpen(false)}/>)}
107
- </div>);
108
- };
@@ -1,142 +0,0 @@
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
- import { useState } from 'react';
9
- import { getCollectionAdminConfig, getCollectionDefinition, isSingleton } from '@byline/core';
10
- import { useTranslation } from '@byline/i18n/react';
11
- import { Button, CloseIcon, DraggableSortable, ErrorText, GripperVerticalIcon, HelpText, IconButton, Label, moveItem, PlusIcon, useSortable, } from '@byline/ui/react';
12
- import cx from 'clsx';
13
- import { useFieldError, useFieldValue, useFormContext } from '../../forms/form-context';
14
- import styles from './relation-field.module.css';
15
- import { RelationPicker } from './relation-picker';
16
- import { RelationSummary } from './relation-summary';
17
- const RelationManyTile = ({ id, children, onRemove, removeAriaLabel }) => {
18
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
19
- id,
20
- transition: { duration: 250, easing: 'cubic-bezier(0, 0.2, 0.2, 1)' },
21
- });
22
- const style = {
23
- transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined,
24
- transition,
25
- zIndex: isDragging ? 10 : 'auto',
26
- };
27
- return (<div ref={setNodeRef} style={style} className={cx('byline-field-relation-many-tile', styles['many-tile'], isDragging && ['byline-field-relation-many-tile-dragging', styles['many-tile-dragging']])}>
28
- <button type="button" className={cx('byline-field-relation-many-grip', styles['many-grip'])} {...attributes} {...listeners}>
29
- <GripperVerticalIcon />
30
- </button>
31
- <div className={cx('byline-field-relation-many-body', styles['many-body'])}>{children}</div>
32
- <IconButton type="button" size="xs" intent="noeffect" aria-label={removeAriaLabel} className={cx('byline-field-relation-many-remove', styles['many-remove'])} onClick={onRemove}>
33
- <CloseIcon width="14px" height="14px"/>
34
- </IconButton>
35
- </div>);
36
- };
37
- export const RelationManyField = ({ field, defaultValue, id, path }) => {
38
- const fieldPath = path ?? field.name;
39
- const htmlId = id ?? fieldPath;
40
- const { t } = useTranslation('byline-admin');
41
- const fieldError = useFieldError(fieldPath);
42
- const { getFieldValue, setFieldValue } = useFormContext();
43
- const targetDefinition = getCollectionDefinition(field.targetCollection);
44
- const targetDef = targetDefinition != null && !isSingleton(targetDefinition) ? targetDefinition : null;
45
- const targetAdminConfig = getCollectionAdminConfig(field.targetCollection);
46
- const isUnknown = targetDef == null;
47
- const targetLabel = targetDef?.labels.singular ?? field.targetCollection;
48
- const [pickerOpen, setPickerOpen] = useState(false);
49
- // Records handed back by the picker, keyed by target id — lets a freshly
50
- // added tile render real display data (title / thumbnail) without a refetch.
51
- const [pickedRecords, setPickedRecords] = useState({});
52
- // Subscribe to the live array so reorders / adds / removes re-render.
53
- const liveValue = useFieldValue(fieldPath);
54
- const items = Array.isArray(liveValue)
55
- ? liveValue
56
- : Array.isArray(defaultValue)
57
- ? defaultValue
58
- : [];
59
- const currentArray = () => {
60
- const v = getFieldValue(fieldPath);
61
- if (Array.isArray(v))
62
- return v;
63
- return Array.isArray(defaultValue) ? defaultValue : [];
64
- };
65
- const handleAddMany = (selections) => {
66
- setPickerOpen(false);
67
- const current = currentArray();
68
- // Dedup the batch against the current array — a target may appear at most
69
- // once. The picker already disables already-added rows, so this is a
70
- // belt-and-braces guard for stale state (e.g. a concurrent remove).
71
- const existing = new Set(current.map((v) => v.targetDocumentId));
72
- const additions = selections.filter((s) => !existing.has(s.targetDocumentId));
73
- if (additions.length === 0)
74
- return;
75
- const records = additions.filter((s) => s.record != null);
76
- if (records.length > 0) {
77
- setPickedRecords((prev) => {
78
- const next = { ...prev };
79
- for (const s of records)
80
- next[s.targetDocumentId] = s.record;
81
- return next;
82
- });
83
- }
84
- setFieldValue(fieldPath, [
85
- ...current,
86
- ...additions.map((s) => ({
87
- targetDocumentId: s.targetDocumentId,
88
- targetCollectionId: s.targetCollectionId,
89
- })),
90
- ]);
91
- };
92
- const handleRemove = (targetDocumentId) => {
93
- setFieldValue(fieldPath, currentArray().filter((v) => v.targetDocumentId !== targetDocumentId));
94
- };
95
- const handleDragEnd = ({ moveFromIndex, moveToIndex, }) => {
96
- if (moveFromIndex === moveToIndex)
97
- return;
98
- setFieldValue(fieldPath, moveItem(currentArray(), moveFromIndex, moveToIndex));
99
- };
100
- return (<div className={`byline-field-relation ${field.name}`}>
101
- <div className={cx('byline-field-relation-header', styles.header)}>
102
- <Label id={`${htmlId}-label`} htmlFor={htmlId} label={field.label ?? field.name} required={!field.optional}/>
103
- </div>
104
- {isUnknown ? (<div className={cx('byline-field-relation-error-tile', styles['error-tile'])}>
105
- <span>
106
- {t('fields.relation.unknownError', {
107
- name: field.name,
108
- target: field.targetCollection,
109
- })}
110
- </span>
111
- <span className={cx('byline-field-relation-error-text', styles['error-text'])}>
112
- {t('fields.relation.unknownHint')}
113
- </span>
114
- </div>) : (<>
115
- {/* Rounded frame below the label wrapping the interactive
116
- body (tile list and add button) — see `.frame` in the CSS
117
- module. */}
118
- <div className={cx('byline-field-relation-frame', styles.frame)}>
119
- {items.length === 0 ? (<p className={cx('byline-field-relation-many-empty', styles['many-empty'])}>
120
- {t('fields.relation.manyEmpty', { label: targetDef.labels.plural })}
121
- </p>) : (<DraggableSortable ids={items.map((v) => v.targetDocumentId)} onDragEnd={handleDragEnd} className={cx('byline-field-relation-many', styles.many)}>
122
- {items.map((value) => (<RelationManyTile key={value.targetDocumentId} id={value.targetDocumentId} onRemove={() => handleRemove(value.targetDocumentId)} removeAriaLabel={t('fields.relation.removeAriaLabel', { label: targetLabel })}>
123
- <RelationSummary targetDefinition={targetDef} targetAdminConfig={targetAdminConfig} displayField={field.displayField} value={value} cachedRecord={pickedRecords[value.targetDocumentId] ?? null}/>
124
- </RelationManyTile>))}
125
- </DraggableSortable>)}
126
-
127
- <div className={cx('byline-field-relation-many-add', styles['many-add'])}>
128
- <Button id={htmlId} size="xs" variant="outlined" intent="noeffect" type="button" onClick={() => setPickerOpen(true)}>
129
- <PlusIcon width="14px" height="14px"/>
130
- {t('fields.relation.addButton', { label: targetLabel })}
131
- </Button>
132
- </div>
133
- </div>
134
-
135
- {field.helpText != null && <HelpText text={field.helpText}/>}
136
-
137
- {fieldError && <ErrorText id={`${field.name}-error`} text={fieldError}/>}
138
-
139
- <RelationPicker multiple targetCollectionPath={field.targetCollection} targetDefinition={targetDef} displayField={field.displayField} isOpen={pickerOpen} excludeIds={items.map((v) => v.targetDocumentId)} onSelectMany={handleAddMany} onDismiss={() => setPickerOpen(false)}/>
140
- </>)}
141
- </div>);
142
- };
@@ -1,240 +0,0 @@
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
- import { useCallback, useEffect, useState } from 'react';
9
- import { getCollectionAdminConfig } from '@byline/core';
10
- import { useTranslation } from '@byline/i18n/react';
11
- import { Button, CheckIcon, LoaderRing, Modal, Search } from '@byline/ui/react';
12
- import cx from 'clsx';
13
- import { useBylineFieldServices } from '../field-services-context';
14
- import { PickerCell, resolveFallbackDisplayField, resolveRowLabel, resolveSelectFields, } from './relation-display';
15
- import styles from './relation-picker.module.css';
16
- const PAGE_SIZE = 15;
17
- export const RelationPicker = ({ targetCollectionPath, targetDefinition, displayField, extraSelectFields, isOpen, multiple = false, excludeIds, onSelect, onSelectMany, onDismiss, }) => {
18
- const [query, setQuery] = useState('');
19
- const [page, setPage] = useState(1);
20
- const { t } = useTranslation('byline-admin');
21
- const [selectedDocumentId, setSelectedDocumentId] = useState(null);
22
- // Multi-select state. A Map keyed by target id preserves pick order (the
23
- // confirmed batch appends in that order) and carries each row's record so
24
- // picks survive page/search changes that swap out `documents`.
25
- const [selectedMap, setSelectedMap] = useState(() => new Map());
26
- const [loading, setLoading] = useState(false);
27
- const [error, setError] = useState(null);
28
- const [documents, setDocuments] = useState([]);
29
- const [totalPages, setTotalPages] = useState(1);
30
- const [collectionId, setCollectionId] = useState(null);
31
- const { getCollectionDocuments } = useBylineFieldServices();
32
- const targetAdminConfig = getCollectionAdminConfig(targetCollectionPath);
33
- const pickerColumns = targetAdminConfig?.itemView;
34
- // Reset local state each time the modal opens so prior queries don't leak.
35
- useEffect(() => {
36
- if (isOpen) {
37
- setQuery('');
38
- setPage(1);
39
- setSelectedDocumentId(null);
40
- setSelectedMap(new Map());
41
- setError(null);
42
- }
43
- }, [isOpen]);
44
- // Fetch whenever the modal is open and the query / page changes.
45
- useEffect(() => {
46
- if (!isOpen)
47
- return;
48
- let cancelled = false;
49
- const selectFields = resolveSelectFields(targetDefinition, displayField, pickerColumns, extraSelectFields);
50
- setLoading(true);
51
- setError(null);
52
- // Item-view sort: the target collection's `itemViewSort` (boot-validated)
53
- // orders the picker independently of its list view's `defaultSort`.
54
- // Passed as explicit params because the list server fn gives an explicit
55
- // `order` top precedence; when absent the server falls back through
56
- // `defaultSort` → `created_at desc` (or `order_key asc` for orderable
57
- // collections) exactly as before.
58
- const itemViewSort = targetAdminConfig?.itemViewSort;
59
- getCollectionDocuments({
60
- collection: targetCollectionPath,
61
- params: {
62
- page,
63
- page_size: PAGE_SIZE,
64
- query: query.length > 0 ? query : undefined,
65
- fields: selectFields,
66
- ...(itemViewSort != null
67
- ? { order: String(itemViewSort.field), desc: itemViewSort.direction === 'desc' }
68
- : {}),
69
- },
70
- })
71
- .then((response) => {
72
- if (cancelled)
73
- return;
74
- setDocuments(response.docs);
75
- setTotalPages(response.meta.totalPages ?? 1);
76
- setCollectionId(response.included.collection.id);
77
- })
78
- .catch((err) => {
79
- if (cancelled)
80
- return;
81
- setError(err instanceof Error ? err.message : t('fields.relation.picker.loadFailed'));
82
- })
83
- .finally(() => {
84
- if (!cancelled)
85
- setLoading(false);
86
- });
87
- return () => {
88
- cancelled = true;
89
- };
90
- }, [
91
- isOpen,
92
- targetCollectionPath,
93
- query,
94
- page,
95
- displayField,
96
- extraSelectFields,
97
- targetDefinition,
98
- pickerColumns,
99
- getCollectionDocuments,
100
- t,
101
- targetAdminConfig?.itemViewSort,
102
- ]);
103
- const resolvedDisplayField = displayField ??
104
- targetDefinition?.useAsTitle ??
105
- resolveFallbackDisplayField(targetDefinition) ??
106
- null;
107
- const handleSelect = useCallback(() => {
108
- if (!selectedDocumentId || !collectionId || !onSelect)
109
- return;
110
- const record = documents.find((d) => d?.id === selectedDocumentId);
111
- onSelect({
112
- targetDocumentId: selectedDocumentId,
113
- targetCollectionId: collectionId,
114
- record,
115
- });
116
- }, [selectedDocumentId, collectionId, documents, onSelect]);
117
- const handleSelectMany = useCallback(() => {
118
- if (selectedMap.size === 0 || !collectionId || !onSelectMany)
119
- return;
120
- onSelectMany(Array.from(selectedMap, ([targetDocumentId, record]) => ({
121
- targetDocumentId,
122
- targetCollectionId: collectionId,
123
- record,
124
- })));
125
- }, [selectedMap, collectionId, onSelectMany]);
126
- const toggleSelected = useCallback((doc) => {
127
- const id = doc.id;
128
- setSelectedMap((prev) => {
129
- const next = new Map(prev);
130
- if (next.has(id)) {
131
- next.delete(id);
132
- }
133
- else {
134
- next.set(id, doc);
135
- }
136
- return next;
137
- });
138
- }, []);
139
- const title = t('fields.relation.selectPickerTitle', {
140
- label: targetDefinition?.labels.singular ?? targetCollectionPath,
141
- });
142
- return (<Modal isOpen={isOpen} onDismiss={onDismiss}>
143
- <Modal.Container style={{ maxWidth: '600px', width: '100%' }}>
144
- <Modal.Header className={cx('byline-field-relation-picker-header', styles.header)}>
145
- <h3 className={cx('byline-field-relation-picker-title', styles.title)}>{title}</h3>
146
- </Modal.Header>
147
- <Modal.Content>
148
- <div className={cx('byline-field-relation-picker-body', styles.body)}>
149
- <Search onSearch={(q) => {
150
- setPage(1);
151
- setQuery(q ?? '');
152
- }} onClear={() => {
153
- setPage(1);
154
- setQuery('');
155
- }} inputSize="sm" placeholder={t('fields.relation.picker.searchPlaceholder')}/>
156
-
157
- <div className={cx('byline-field-relation-picker-list', styles.list)}>
158
- {loading && documents.length === 0 && (<div className={cx('byline-field-relation-picker-loading', styles.loading)}>
159
- <LoaderRing size={24} color="#888888"/>
160
- </div>)}
161
- {!loading && error && (<div className={cx('byline-field-relation-picker-error', styles.error)}>
162
- {error}
163
- </div>)}
164
- {!loading && !error && documents.length === 0 && (<div className={cx('byline-field-relation-picker-empty', styles.empty)}>
165
- {t('fields.relation.picker.empty')}
166
- </div>)}
167
- {documents.length > 0 && (<ul className={cx('byline-field-relation-picker-rows', styles.rows)}>
168
- {documents.map((doc) => {
169
- const id = doc.id;
170
- const selected = multiple ? selectedMap.has(id) : selectedDocumentId === id;
171
- const excluded = multiple && (excludeIds?.includes(id) ?? false);
172
- return (<li key={id}>
173
- <button type="button" disabled={excluded} aria-pressed={multiple ? selected : undefined} title={excluded ? t('fields.relation.picker.alreadyAdded') : undefined} className={cx('byline-field-relation-picker-row-button', styles['row-button'], multiple && [
174
- 'byline-field-relation-picker-row-multi',
175
- styles['row-multi'],
176
- ], selected && [
177
- 'byline-field-relation-picker-row-selected',
178
- styles['row-selected'],
179
- ], excluded && [
180
- 'byline-field-relation-picker-row-added',
181
- styles['row-added'],
182
- ])} onClick={() => {
183
- if (excluded)
184
- return;
185
- if (multiple) {
186
- toggleSelected(doc);
187
- }
188
- else {
189
- setSelectedDocumentId(id);
190
- }
191
- }}>
192
- {multiple && (<span aria-hidden="true" className={cx('byline-field-relation-picker-check', styles.check, (selected || excluded) && [
193
- 'byline-field-relation-picker-check-checked',
194
- styles['check-checked'],
195
- ])}>
196
- {(selected || excluded) && <CheckIcon width="12px" height="12px"/>}
197
- </span>)}
198
- {pickerColumns && pickerColumns.length > 0 ? (<div className={cx('byline-field-relation-picker-row-cells', styles['row-cells'])}>
199
- {pickerColumns.map((col) => (<PickerCell key={String(col.fieldName)} column={col} record={doc}/>))}
200
- </div>) : (<div className={cx('byline-field-relation-picker-row-stack', styles['row-stack'])}>
201
- <span className={cx('byline-field-relation-picker-row-label', styles['row-label'])}>
202
- {resolveRowLabel(doc, resolvedDisplayField) || id}
203
- </span>
204
- {typeof doc.path === 'string' && doc.path.length > 0 && (<span className={cx('byline-field-relation-picker-row-path', styles['row-path'])}>
205
- {doc.path}
206
- </span>)}
207
- </div>)}
208
- {excluded && (<span className={cx('byline-field-relation-picker-added-hint', styles['added-hint'])}>
209
- {t('fields.relation.picker.alreadyAdded')}
210
- </span>)}
211
- </button>
212
- </li>);
213
- })}
214
- </ul>)}
215
- </div>
216
-
217
- {totalPages > 1 && (<div className={cx('byline-field-relation-picker-pager', styles.pager)}>
218
- <Button size="xs" variant="outlined" intent="noeffect" type="button" disabled={page <= 1 || loading} onClick={() => setPage((p) => Math.max(1, p - 1))}>
219
- {t('common.pager.previous')}
220
- </Button>
221
- <span>{t('common.pager.pageOf', { page, total: totalPages })}</span>
222
- <Button size="xs" variant="outlined" intent="noeffect" type="button" disabled={page >= totalPages || loading} onClick={() => setPage((p) => Math.min(totalPages, p + 1))}>
223
- {t('common.pager.next')}
224
- </Button>
225
- </div>)}
226
- </div>
227
- </Modal.Content>
228
- <Modal.Actions>
229
- <Button size="sm" intent="noeffect" type="button" onClick={onDismiss} className={cx('byline-field-relation-picker-action', styles.action)}>
230
- {t('common.actions.cancel')}
231
- </Button>
232
- <Button size="sm" className={cx('byline-field-relation-picker-action', styles.action)} intent="primary" type="button" disabled={multiple ? selectedMap.size === 0 : !selectedDocumentId} onClick={multiple ? handleSelectMany : handleSelect}>
233
- {multiple
234
- ? t('fields.relation.picker.addSelected', { count: selectedMap.size })
235
- : t('common.actions.select')}
236
- </Button>
237
- </Modal.Actions>
238
- </Modal.Container>
239
- </Modal>);
240
- };
@@ -1,45 +0,0 @@
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
- import cx from 'clsx';
9
- import { PickerCell, resolveFallbackDisplayField, resolveRowLabel } from './relation-display';
10
- import styles from './relation-summary.module.css';
11
- export function RelationSummary({ targetDefinition, targetAdminConfig, displayField, value, cachedRecord, }) {
12
- const pickerColumns = targetAdminConfig?.itemView;
13
- // Unresolved (deleted target).
14
- if (value._resolved === false) {
15
- return (<div className={cx('byline-relation-summary-stack', styles.stack)}>
16
- <span className={cx('byline-relation-summary-kind', styles.kind)}>
17
- {targetDefinition.labels.singular}
18
- </span>
19
- <span className={cx('byline-relation-summary-value-mono byline-relation-summary-missing', styles['value-mono'], styles.missing)}>
20
- (target not found) {value.targetDocumentId}
21
- </span>
22
- </div>);
23
- }
24
- // Prefer the populated envelope's document; fall back to the cached
25
- // picker record; finally fall back to rendering just the raw ref.
26
- const record = (value._resolved === true && !value._cycle && value.document) || cachedRecord || null;
27
- if (record && pickerColumns && pickerColumns.length > 0) {
28
- return (<div className={cx('byline-relation-summary-row', styles.row)}>
29
- {pickerColumns.map((col) => (<PickerCell key={String(col.fieldName)} column={col} record={record}/>))}
30
- </div>);
31
- }
32
- const resolvedDisplayField = displayField ??
33
- targetDefinition.useAsTitle ??
34
- resolveFallbackDisplayField(targetDefinition) ??
35
- null;
36
- const label = record ? resolveRowLabel(record, resolvedDisplayField) : null;
37
- return (<div className={cx('byline-relation-summary-stack', styles.stack)}>
38
- <span className={cx('byline-relation-summary-kind', styles.kind)}>
39
- {targetDefinition.labels.singular}
40
- </span>
41
- {label ? (<span className={cx('byline-relation-summary-value', styles.value)}>{label}</span>) : (<span className={cx('byline-relation-summary-value-mono', styles['value-mono'])}>
42
- {value.targetDocumentId}
43
- </span>)}
44
- </div>);
45
- }