@byline/admin 5.1.3 → 5.2.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.
- package/dist/modules/analytics/components/dashboard.d.ts +13 -3
- package/dist/modules/analytics/components/dashboard.js +86 -68
- package/dist/modules/analytics/components/dashboard.module.js +10 -1
- package/dist/modules/analytics/components/dashboard_module.css +57 -0
- package/dist/modules/analytics/components/ranked-list-modal.d.ts +32 -0
- package/dist/modules/analytics/components/ranked-list-modal.js +145 -0
- package/dist/modules/analytics/components/ranking.d.ts +45 -0
- package/dist/modules/analytics/components/ranking.js +71 -0
- package/dist/modules/analytics/components/ranking.test.node.d.ts +8 -0
- package/package.json +14 -14
- package/src/modules/analytics/components/dashboard.module.css +69 -0
- package/src/modules/analytics/components/dashboard.tsx +105 -83
- package/src/modules/analytics/components/ranked-list-modal.test.tsx +154 -0
- package/src/modules/analytics/components/ranked-list-modal.tsx +159 -0
- package/src/modules/analytics/components/ranking.test.node.ts +43 -0
- package/src/modules/analytics/components/ranking.tsx +122 -0
- package/dist/exports-parity.test.node.js +0 -37
- package/dist/fields/array/array-field.jsx +0 -209
- package/dist/fields/blocks/blocks-field.jsx +0 -248
- package/dist/fields/checkbox/checkbox-field.jsx +0 -24
- package/dist/fields/code/code-editor.jsx +0 -211
- package/dist/fields/code/code-field.jsx +0 -109
- package/dist/fields/column-formatter.jsx +0 -25
- package/dist/fields/date-time-formatter.jsx +0 -18
- package/dist/fields/datetime/datetime-field.jsx +0 -22
- package/dist/fields/draggable-context-menu.jsx +0 -50
- package/dist/fields/field-admin.test.node.js +0 -40
- package/dist/fields/field-renderer.jsx +0 -136
- package/dist/fields/field-services-context.jsx +0 -17
- package/dist/fields/file/file-field.jsx +0 -176
- package/dist/fields/file/file-upload-field.jsx +0 -89
- package/dist/fields/group/group-field.jsx +0 -46
- package/dist/fields/image/image-field.jsx +0 -183
- package/dist/fields/image/image-upload-field.jsx +0 -157
- package/dist/fields/local-date-time.jsx +0 -69
- package/dist/fields/locale-badge.jsx +0 -19
- package/dist/fields/numerical/numerical-field.jsx +0 -93
- package/dist/fields/relation/relation-column-formatter.jsx +0 -74
- package/dist/fields/relation/relation-display.jsx +0 -118
- package/dist/fields/relation/relation-field.jsx +0 -108
- package/dist/fields/relation/relation-many-field.jsx +0 -142
- package/dist/fields/relation/relation-picker.jsx +0 -240
- package/dist/fields/relation/relation-summary.jsx +0 -45
- package/dist/fields/select/select-field.jsx +0 -27
- package/dist/fields/sortable-item.jsx +0 -54
- package/dist/fields/text/text-field.jsx +0 -79
- package/dist/fields/text-area/text-area-field.jsx +0 -79
- package/dist/forms/available-locales-reconcile.test.node.js +0 -43
- package/dist/forms/available-locales-widget.jsx +0 -63
- package/dist/forms/document-actions.jsx +0 -486
- package/dist/forms/form-context.jsx +0 -598
- package/dist/forms/form-modals.jsx +0 -128
- package/dist/forms/form-renderer.jsx +0 -532
- package/dist/forms/form-status-display.jsx +0 -69
- package/dist/forms/navigation-guard.jsx +0 -68
- package/dist/forms/nested-path.test.node.js +0 -115
- package/dist/forms/path-widget.jsx +0 -125
- package/dist/forms/pending-uploads.test.node.js +0 -19
- package/dist/forms/repeating-items.test.node.js +0 -29
- package/dist/forms/scheduled-publication-control.jsx +0 -389
- package/dist/forms/scheduled-publication-state.test.node.js +0 -121
- package/dist/forms/scheduled-publication-time.test.node.js +0 -70
- package/dist/forms/status-transitions.test.node.js +0 -76
- package/dist/forms/tree-placement-widget.jsx +0 -160
- package/dist/forms/upload-executor.test.node.js +0 -368
- package/dist/forms/use-form-layout.test.node.js +0 -69
- package/dist/modules/admin-account/components/change-password.jsx +0 -169
- package/dist/modules/admin-account/components/container.jsx +0 -164
- package/dist/modules/admin-account/components/preferences.jsx +0 -148
- package/dist/modules/admin-account/components/theme-switch.jsx +0 -52
- package/dist/modules/admin-account/components/update.jsx +0 -186
- package/dist/modules/admin-permissions/components/inspector.jsx +0 -200
- package/dist/modules/admin-preferences/schemas.test.node.js +0 -48
- package/dist/modules/admin-roles/components/create.jsx +0 -155
- package/dist/modules/admin-roles/components/permissions.jsx +0 -239
- package/dist/modules/admin-roles/components/update.jsx +0 -148
- package/dist/modules/admin-users/components/create.jsx +0 -206
- package/dist/modules/admin-users/components/roles.jsx +0 -126
- package/dist/modules/admin-users/components/set-password.jsx +0 -133
- package/dist/modules/admin-users/components/update.jsx +0 -215
- package/dist/modules/analytics/components/dashboard.jsx +0 -208
- package/dist/modules/analytics/components/dashboard.test.node.js +0 -162
- package/dist/modules/analytics/components/timeseries.jsx +0 -193
- package/dist/modules/auth/components/sign-in-form.jsx +0 -89
- package/dist/modules/auth/safe-redirect.test.node.js +0 -40
- package/dist/modules/auth/sign-in-form-props.test.node.js +0 -7
- package/dist/presentation/group.jsx +0 -26
- package/dist/presentation/row.jsx +0 -23
- package/dist/presentation/tabs.jsx +0 -39
- package/dist/services/admin-services-context.jsx +0 -17
- package/dist/widgets/diff-viewer/diff-modal.jsx +0 -110
- package/dist/widgets/source-locale-badge/source-locale-badge.jsx +0 -28
- package/dist/widgets/status-badge/status-badge.jsx +0 -46
|
@@ -1,248 +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 { useEffect, useMemo, useState } from 'react';
|
|
9
|
-
import { getAdminConfig } from '@byline/core';
|
|
10
|
-
import { useTranslation } from '@byline/i18n/react';
|
|
11
|
-
import { Card, CloseIcon, DraggableSortable, IconButton, Modal, moveItem, PlusIcon, } from '@byline/ui/react';
|
|
12
|
-
import cx from 'clsx';
|
|
13
|
-
import { defaultScalarForField } from '../../fields/field-helpers';
|
|
14
|
-
import { GroupField } from '../../fields/group/group-field';
|
|
15
|
-
import { SortableItem } from '../../fields/sortable-item';
|
|
16
|
-
import { useFormContext } from '../../forms/form-context';
|
|
17
|
-
import { hasExistingIdTargets } from '../../forms/nested-path';
|
|
18
|
-
import { moveRepeatingItems, repeatingItemId, repeatingItemPath } from '../../forms/repeating-items';
|
|
19
|
-
import styles from './blocks-field.module.css';
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// BlocksField — renders `type: 'blocks'` fields. Children are heterogeneous
|
|
22
|
-
// group fields selected via a modal picker. Supports D&D.
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
export const BlocksField = ({ field, defaultValue, path, contentLocale, }) => {
|
|
25
|
-
const { appendPatch, getFieldValue, getFieldValues, removePendingUploadsUnder, setFieldStore } = useFormContext();
|
|
26
|
-
const { t } = useTranslation('byline-admin');
|
|
27
|
-
const [items, setItems] = useState([]);
|
|
28
|
-
const [showAddBlockModal, setShowAddBlockModal] = useState(false);
|
|
29
|
-
const [pendingInsertIndex, setPendingInsertIndex] = useState(null);
|
|
30
|
-
const availableBlocks = useMemo(() => field.blocks ?? [], [field.blocks]);
|
|
31
|
-
// Site-wide per-block admin config (`AdminConfig.blockAdmin`), resolved by
|
|
32
|
-
// blockType. Registry-based on purpose: blocks are cross-collection units,
|
|
33
|
-
// so their admin config applies wherever the block renders — no threading
|
|
34
|
-
// from the collection admin config is needed (mirrors how FieldRenderer
|
|
35
|
-
// resolves the global richText editor from admin config).
|
|
36
|
-
const blockAdminByType = useMemo(() => {
|
|
37
|
-
const map = new Map();
|
|
38
|
-
for (const entry of getAdminConfig().blockAdmin ?? []) {
|
|
39
|
-
map.set(entry.blockType, entry);
|
|
40
|
-
}
|
|
41
|
-
return map;
|
|
42
|
-
}, []);
|
|
43
|
-
const [selectedBlockName, setSelectedBlockName] = useState(() => availableBlocks[0]?.blockType ?? '');
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (selectedBlockName == null ||
|
|
46
|
-
!availableBlocks.some((b) => b.blockType === selectedBlockName)) {
|
|
47
|
-
setSelectedBlockName(availableBlocks[0]?.blockType ?? '');
|
|
48
|
-
}
|
|
49
|
-
}, [availableBlocks, selectedBlockName]);
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
// Prefer the live form-store value over `defaultValue` when seeding the
|
|
52
|
-
// rendered list. The store lives in FormProvider — above the tab layout —
|
|
53
|
-
// so it survives this field unmounting/remounting as the editor switches
|
|
54
|
-
// tabs; `defaultValue` is only the initial seed and is empty in create
|
|
55
|
-
// mode. Re-seeding from `defaultValue` on remount would drop blocks the
|
|
56
|
-
// editor had already added on an earlier visit to this tab.
|
|
57
|
-
const storeValue = getFieldValue(path);
|
|
58
|
-
const source = Array.isArray(storeValue) ? storeValue : defaultValue;
|
|
59
|
-
if (Array.isArray(source)) {
|
|
60
|
-
setItems(source.map((item) => ({
|
|
61
|
-
id: item && typeof item === 'object' && '_id' in item
|
|
62
|
-
? String(item._id)
|
|
63
|
-
: crypto.randomUUID(),
|
|
64
|
-
data: item,
|
|
65
|
-
})));
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
setItems([]);
|
|
69
|
-
}
|
|
70
|
-
}, [defaultValue, getFieldValue, path]);
|
|
71
|
-
const handleDragEnd = ({ moveFromIndex, moveToIndex, }) => {
|
|
72
|
-
const currentArray = (getFieldValue(path) ?? defaultValue);
|
|
73
|
-
if (!Array.isArray(currentArray))
|
|
74
|
-
return;
|
|
75
|
-
const move = moveRepeatingItems(currentArray, moveFromIndex, moveToIndex);
|
|
76
|
-
if (move == null)
|
|
77
|
-
return;
|
|
78
|
-
setItems((prev) => moveItem(prev, move.fromIndex, move.toIndex));
|
|
79
|
-
setFieldStore(path, move.items);
|
|
80
|
-
appendPatch({
|
|
81
|
-
kind: 'array.move',
|
|
82
|
-
path,
|
|
83
|
-
itemId: move.itemId,
|
|
84
|
-
toIndex: move.toIndex,
|
|
85
|
-
});
|
|
86
|
-
};
|
|
87
|
-
const handleAddItem = async (forcedVariantName, atIndex) => {
|
|
88
|
-
setShowAddBlockModal(false);
|
|
89
|
-
setPendingInsertIndex(null);
|
|
90
|
-
const variant = (forcedVariantName != null
|
|
91
|
-
? availableBlocks.find((v) => v.blockType === forcedVariantName)
|
|
92
|
-
: undefined) ?? availableBlocks[0];
|
|
93
|
-
if (!variant)
|
|
94
|
-
return;
|
|
95
|
-
const compositeFields = (variant.fields ?? []);
|
|
96
|
-
const newId = crypto.randomUUID();
|
|
97
|
-
const newItem = {
|
|
98
|
-
_id: newId,
|
|
99
|
-
_type: variant.blockType,
|
|
100
|
-
};
|
|
101
|
-
for (const f of compositeFields) {
|
|
102
|
-
newItem[f.name] = await defaultScalarForField(f, getFieldValues);
|
|
103
|
-
}
|
|
104
|
-
if (!hasExistingIdTargets(getFieldValues(), path))
|
|
105
|
-
return;
|
|
106
|
-
const currentArray = (getFieldValue(path) ?? defaultValue);
|
|
107
|
-
const insertAt = atIndex != null ? atIndex : currentArray ? currentArray.length : 0;
|
|
108
|
-
const newItemWrapper = { id: newId, data: newItem };
|
|
109
|
-
setItems((prev) => {
|
|
110
|
-
const next = [...prev];
|
|
111
|
-
next.splice(insertAt, 0, newItemWrapper);
|
|
112
|
-
return next;
|
|
113
|
-
});
|
|
114
|
-
appendPatch({
|
|
115
|
-
kind: 'array.insert',
|
|
116
|
-
path: path,
|
|
117
|
-
index: insertAt,
|
|
118
|
-
item: newItem,
|
|
119
|
-
});
|
|
120
|
-
const newArrayValue = currentArray ? [...currentArray] : [];
|
|
121
|
-
newArrayValue.splice(insertAt, 0, newItem);
|
|
122
|
-
setFieldStore(path, newArrayValue);
|
|
123
|
-
};
|
|
124
|
-
const handleRemoveItem = (index) => {
|
|
125
|
-
const currentArray = (getFieldValue(path) ?? defaultValue);
|
|
126
|
-
if (!Array.isArray(currentArray) || index < 0 || index >= currentArray.length)
|
|
127
|
-
return;
|
|
128
|
-
const item = currentArray[index];
|
|
129
|
-
const itemPath = repeatingItemPath(path, item, index);
|
|
130
|
-
const itemId = repeatingItemId(item) ?? String(index);
|
|
131
|
-
setItems((prev) => prev.filter((_, i) => i !== index));
|
|
132
|
-
removePendingUploadsUnder(itemPath);
|
|
133
|
-
appendPatch({
|
|
134
|
-
kind: 'array.remove',
|
|
135
|
-
path: path,
|
|
136
|
-
itemId,
|
|
137
|
-
});
|
|
138
|
-
const newArrayValue = [...currentArray];
|
|
139
|
-
newArrayValue.splice(index, 1);
|
|
140
|
-
setFieldStore(path, newArrayValue);
|
|
141
|
-
};
|
|
142
|
-
const handleInsertBelow = (index, forcedVariantName) => {
|
|
143
|
-
if (availableBlocks.length > 1 && forcedVariantName == null) {
|
|
144
|
-
setPendingInsertIndex(index + 1);
|
|
145
|
-
setShowAddBlockModal(true);
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
void handleAddItem(forcedVariantName, index + 1);
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
const renderItem = (itemWrapper, index) => {
|
|
152
|
-
const item = itemWrapper.data;
|
|
153
|
-
const arrayElementPath = repeatingItemPath(path, item, index);
|
|
154
|
-
if (!item || typeof item !== 'object' || typeof item._type !== 'string')
|
|
155
|
-
return null;
|
|
156
|
-
const subField = field.blocks?.find((b) => b.blockType === item._type);
|
|
157
|
-
if (subField == null)
|
|
158
|
-
return null;
|
|
159
|
-
// Extract field data (everything except _id and _type)
|
|
160
|
-
const { _id, _type, ...fieldData } = item;
|
|
161
|
-
const label = subField.label ?? _type;
|
|
162
|
-
// Render the block's children directly with arrayElementPath as the
|
|
163
|
-
// path (not basePath). FieldRenderer would append the group name
|
|
164
|
-
// (e.g. "richTextBlock") producing paths like
|
|
165
|
-
// "content[id=...].richTextBlock.constrainedWidth", but the flat block
|
|
166
|
-
// shape stores fields directly on the item so the correct path is
|
|
167
|
-
// "content[id=...].constrainedWidth".
|
|
168
|
-
const body = (<GroupField key={subField.blockType} field={{
|
|
169
|
-
type: 'group',
|
|
170
|
-
name: subField.blockType,
|
|
171
|
-
fields: subField.fields,
|
|
172
|
-
label: undefined,
|
|
173
|
-
}} defaultValue={fieldData} path={arrayElementPath}
|
|
174
|
-
// Arrays directly inside a block are fully sortable: each
|
|
175
|
-
// DraggableSortable is an independent DndContext and drag listeners
|
|
176
|
-
// are grip-scoped, so the inner array's drags can't leak into the
|
|
177
|
-
// block-level context (and vice versa).
|
|
178
|
-
disableSorting={false} contentLocale={contentLocale} fieldAdmin={blockAdminByType.get(item._type)?.fields}/>);
|
|
179
|
-
return (<SortableItem key={itemWrapper.id} id={itemWrapper.id} label={label ?? subField.blockType} onAddBelow={() => handleInsertBelow(index)} onRemove={() => handleRemoveItem(index)}>
|
|
180
|
-
{body}
|
|
181
|
-
</SortableItem>);
|
|
182
|
-
};
|
|
183
|
-
return (<div className={`byline-field-blocks ${field.name}`}>
|
|
184
|
-
{field.label && (<h3 className={cx('byline-field-blocks-title', styles.title)}>{field.label}</h3>)}
|
|
185
|
-
<DraggableSortable ids={items.map((i) => i.id)} onDragEnd={handleDragEnd} className={cx('byline-field-blocks-stack', styles.stack)}>
|
|
186
|
-
{items.map((item, index) => renderItem(item, index))}
|
|
187
|
-
<div className={cx('byline-field-blocks-add-row', styles['add-row'])}>
|
|
188
|
-
<IconButton onClick={() => {
|
|
189
|
-
setPendingInsertIndex(null);
|
|
190
|
-
setShowAddBlockModal(true);
|
|
191
|
-
}} disabled={!selectedBlockName} aria-label={t('fields.blocks.addBlockAriaLabel')} variant="outlined">
|
|
192
|
-
<PlusIcon />
|
|
193
|
-
</IconButton>
|
|
194
|
-
{/* Text-styled button so the label is clickable too. Removed from
|
|
195
|
-
the tab order (tabIndex -1) — the IconButton is the single
|
|
196
|
-
keyboard/screen-reader control for this action. */}
|
|
197
|
-
<button type="button" tabIndex={-1} disabled={!selectedBlockName} onClick={() => {
|
|
198
|
-
setPendingInsertIndex(null);
|
|
199
|
-
setShowAddBlockModal(true);
|
|
200
|
-
}} className={cx('byline-field-blocks-add-label', styles['add-label'])}>
|
|
201
|
-
{t('fields.blocks.addBlock')}
|
|
202
|
-
</button>
|
|
203
|
-
</div>
|
|
204
|
-
</DraggableSortable>
|
|
205
|
-
<Modal isOpen={showAddBlockModal} closeOnOverlayClick={true} onDismiss={() => {
|
|
206
|
-
setShowAddBlockModal(false);
|
|
207
|
-
setPendingInsertIndex(null);
|
|
208
|
-
}}>
|
|
209
|
-
<Modal.Container style={{ maxWidth: '700px' }}>
|
|
210
|
-
<Modal.Header className={cx('byline-field-blocks-modal-head', styles['modal-head'])}>
|
|
211
|
-
<h3 className={cx('byline-field-blocks-modal-title', styles['modal-title'])}>
|
|
212
|
-
{t('fields.blocks.modalTitle')}
|
|
213
|
-
</h3>
|
|
214
|
-
<IconButton arial-label={t('common.actions.close')} size="xs" onClick={() => {
|
|
215
|
-
setShowAddBlockModal(false);
|
|
216
|
-
setPendingInsertIndex(null);
|
|
217
|
-
}}>
|
|
218
|
-
<CloseIcon width="16px" height="16px" svgClassName="white-icon"/>
|
|
219
|
-
</IconButton>
|
|
220
|
-
</Modal.Header>
|
|
221
|
-
<Modal.Content className={cx('byline-field-blocks-card-cursor', styles['modal-content'])}>
|
|
222
|
-
<div className={cx('byline-field-blocks-grid', styles.grid)}>
|
|
223
|
-
{availableBlocks.map((b, index) => (<Card key={b.blockType} hover onClick={() => void handleAddItem(b.blockType, pendingInsertIndex ?? undefined)} className={cx('byline-field-blocks-card', styles.card)}>
|
|
224
|
-
<Card.Header>
|
|
225
|
-
<div className={cx('byline-field-blocks-card-head', styles['card-head'])}>
|
|
226
|
-
<Card.Title className={cx('byline-field-blocks-card-title', styles['card-title'])}>
|
|
227
|
-
{b.label ?? b.blockType}
|
|
228
|
-
</Card.Title>
|
|
229
|
-
<span className={cx('byline-field-blocks-card-index', styles['card-index'])}>
|
|
230
|
-
{index + 1}
|
|
231
|
-
</span>
|
|
232
|
-
</div>
|
|
233
|
-
<code className={cx('byline-field-blocks-card-code', styles['card-code'])}>
|
|
234
|
-
{b.blockType}
|
|
235
|
-
</code>
|
|
236
|
-
</Card.Header>
|
|
237
|
-
<Card.Content>
|
|
238
|
-
<p className={cx('byline-field-blocks-card-body', styles['card-body'])}>
|
|
239
|
-
{b.helpText ?? b.label ?? b.blockType}
|
|
240
|
-
</p>
|
|
241
|
-
</Card.Content>
|
|
242
|
-
</Card>))}
|
|
243
|
-
</div>
|
|
244
|
-
</Modal.Content>
|
|
245
|
-
</Modal.Container>
|
|
246
|
-
</Modal>
|
|
247
|
-
</div>);
|
|
248
|
-
};
|
|
@@ -1,24 +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 { Checkbox } from '@byline/ui/react';
|
|
9
|
-
import { useFieldError, useFieldValue } from '../../forms/form-context';
|
|
10
|
-
import styles from './checkbox-field.module.css';
|
|
11
|
-
export const CheckboxField = ({ field, value, defaultValue, onChange, id, path, }) => {
|
|
12
|
-
const fieldPath = path ?? field.name;
|
|
13
|
-
const fieldError = useFieldError(fieldPath);
|
|
14
|
-
const fieldValue = useFieldValue(fieldPath);
|
|
15
|
-
const checked = value ?? fieldValue ?? defaultValue ?? false;
|
|
16
|
-
return (<div className={`byline-field-checkbox ${field.name} ${styles['field-checkbox']}`}>
|
|
17
|
-
<Checkbox id={id ?? fieldPath} name={field.name} label={field.label} checked={checked} size="sm" helpText={field.helpText}
|
|
18
|
-
// TODO: Handle indeterminate state
|
|
19
|
-
onCheckedChange={(value) => {
|
|
20
|
-
const next = value === 'indeterminate' ? false : Boolean(value);
|
|
21
|
-
onChange?.(next);
|
|
22
|
-
}} error={fieldError != null} errorText={fieldError}/>
|
|
23
|
-
</div>);
|
|
24
|
-
};
|
|
@@ -1,211 +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 { useEffect, useRef } from 'react';
|
|
9
|
-
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
|
10
|
-
import { bracketMatching, HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
11
|
-
import { Compartment, EditorState } from '@codemirror/state';
|
|
12
|
-
import { EditorView, keymap, lineNumbers } from '@codemirror/view';
|
|
13
|
-
import { tags } from '@lezer/highlight';
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// CodeEditor — the CodeMirror 6 half of the `code` field widget.
|
|
16
|
-
//
|
|
17
|
-
// This module owns EVERY CodeMirror import and is loaded through
|
|
18
|
-
// `React.lazy` from `code-field.tsx`, so none of it lands in the main admin
|
|
19
|
-
// chunk — the consuming app's bundler splits it out and fetches it the first
|
|
20
|
-
// time a code field actually renders. Keep it dependency-tight: anything
|
|
21
|
-
// imported here ships in the lazy chunk.
|
|
22
|
-
//
|
|
23
|
-
// Theming: one theme + one highlight style, both defined entirely in terms
|
|
24
|
-
// of `--byline-code-*` CSS custom properties (declared with light/dark
|
|
25
|
-
// values in `code-field.module.css`). The admin theme contract is a
|
|
26
|
-
// `.dark`/`.light` class on <html>, so the editor follows theme flips live
|
|
27
|
-
// with zero JS involvement.
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
/**
|
|
30
|
-
* Per-language lazy loaders. Each grammar stays in its own chunk — adding a
|
|
31
|
-
* code field to a document only fetches the grammar it actually uses.
|
|
32
|
-
* Unknown languages resolve to `null` → plain text (no language extension).
|
|
33
|
-
*/
|
|
34
|
-
const LANGUAGE_LOADERS = {
|
|
35
|
-
javascript: () => import('@codemirror/lang-javascript').then((m) => m.javascript()),
|
|
36
|
-
jsx: () => import('@codemirror/lang-javascript').then((m) => m.javascript({ jsx: true })),
|
|
37
|
-
typescript: () => import('@codemirror/lang-javascript').then((m) => m.javascript({ typescript: true })),
|
|
38
|
-
tsx: () => import('@codemirror/lang-javascript').then((m) => m.javascript({ jsx: true, typescript: true })),
|
|
39
|
-
json: () => import('@codemirror/lang-json').then((m) => m.json()),
|
|
40
|
-
html: () => import('@codemirror/lang-html').then((m) => m.html()),
|
|
41
|
-
css: () => import('@codemirror/lang-css').then((m) => m.css()),
|
|
42
|
-
markdown: () => import('@codemirror/lang-markdown').then((m) => m.markdown()),
|
|
43
|
-
python: () => import('@codemirror/lang-python').then((m) => m.python()),
|
|
44
|
-
sql: () => import('@codemirror/lang-sql').then((m) => m.sql()),
|
|
45
|
-
yaml: () => import('@codemirror/lang-yaml').then((m) => m.yaml()),
|
|
46
|
-
};
|
|
47
|
-
/** Common aliases → canonical loader keys. */
|
|
48
|
-
const LANGUAGE_ALIASES = {
|
|
49
|
-
js: 'javascript',
|
|
50
|
-
ts: 'typescript',
|
|
51
|
-
md: 'markdown',
|
|
52
|
-
py: 'python',
|
|
53
|
-
yml: 'yaml',
|
|
54
|
-
};
|
|
55
|
-
const resolveLanguageLoader = (language) => {
|
|
56
|
-
if (!language)
|
|
57
|
-
return null;
|
|
58
|
-
const key = LANGUAGE_ALIASES[language] ?? language;
|
|
59
|
-
return LANGUAGE_LOADERS[key] ?? null;
|
|
60
|
-
};
|
|
61
|
-
const bylineCodeTheme = EditorView.theme({
|
|
62
|
-
'&': {
|
|
63
|
-
backgroundColor: 'var(--byline-code-bg)',
|
|
64
|
-
color: 'var(--byline-code-fg)',
|
|
65
|
-
fontSize: 'var(--byline-code-font-size, 13px)',
|
|
66
|
-
border: '1px solid var(--byline-code-border)',
|
|
67
|
-
borderRadius: 'var(--byline-code-radius, 4px)',
|
|
68
|
-
},
|
|
69
|
-
'&.cm-focused': {
|
|
70
|
-
outline: '2px solid var(--byline-code-focus-ring)',
|
|
71
|
-
outlineOffset: '-1px',
|
|
72
|
-
},
|
|
73
|
-
'.cm-content': {
|
|
74
|
-
fontFamily: 'var(--byline-code-font, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)',
|
|
75
|
-
caretColor: 'var(--byline-code-caret)',
|
|
76
|
-
minHeight: 'var(--byline-code-min-height, 6rem)',
|
|
77
|
-
},
|
|
78
|
-
'.cm-scroller': {
|
|
79
|
-
fontFamily: 'inherit',
|
|
80
|
-
maxHeight: 'var(--byline-code-max-height, 32rem)',
|
|
81
|
-
},
|
|
82
|
-
'.cm-gutters': {
|
|
83
|
-
backgroundColor: 'var(--byline-code-gutter-bg)',
|
|
84
|
-
color: 'var(--byline-code-gutter-fg)',
|
|
85
|
-
border: 'none',
|
|
86
|
-
borderRight: '1px solid var(--byline-code-border)',
|
|
87
|
-
},
|
|
88
|
-
'.cm-activeLine': { backgroundColor: 'var(--byline-code-active-line)' },
|
|
89
|
-
'.cm-activeLineGutter': { backgroundColor: 'var(--byline-code-active-line)' },
|
|
90
|
-
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': {
|
|
91
|
-
backgroundColor: 'var(--byline-code-selection) !important',
|
|
92
|
-
},
|
|
93
|
-
'.cm-cursor': { borderLeftColor: 'var(--byline-code-caret)' },
|
|
94
|
-
});
|
|
95
|
-
const bylineHighlightStyle = HighlightStyle.define([
|
|
96
|
-
{ tag: tags.keyword, color: 'var(--byline-code-keyword)' },
|
|
97
|
-
{ tag: [tags.string, tags.special(tags.string)], color: 'var(--byline-code-string)' },
|
|
98
|
-
{ tag: tags.comment, color: 'var(--byline-code-comment)', fontStyle: 'italic' },
|
|
99
|
-
{ tag: [tags.number, tags.bool, tags.null], color: 'var(--byline-code-number)' },
|
|
100
|
-
{
|
|
101
|
-
tag: [tags.function(tags.variableName), tags.function(tags.propertyName)],
|
|
102
|
-
color: 'var(--byline-code-function)',
|
|
103
|
-
},
|
|
104
|
-
{ tag: [tags.typeName, tags.className, tags.tagName], color: 'var(--byline-code-type)' },
|
|
105
|
-
{ tag: [tags.propertyName, tags.attributeName], color: 'var(--byline-code-property)' },
|
|
106
|
-
{ tag: [tags.operator, tags.definitionKeyword], color: 'var(--byline-code-operator)' },
|
|
107
|
-
]);
|
|
108
|
-
const CodeEditor = ({ id, value, language, readOnly, onChange, ariaInvalid, ariaDescribedBy, }) => {
|
|
109
|
-
const containerRef = useRef(null);
|
|
110
|
-
const viewRef = useRef(null);
|
|
111
|
-
// Keep the latest onChange without recreating the EditorView.
|
|
112
|
-
const onChangeRef = useRef(onChange);
|
|
113
|
-
onChangeRef.current = onChange;
|
|
114
|
-
const languageCompartment = useRef(new Compartment());
|
|
115
|
-
const readOnlyCompartment = useRef(new Compartment());
|
|
116
|
-
const ariaCompartment = useRef(new Compartment());
|
|
117
|
-
const initialValueRef = useRef(value);
|
|
118
|
-
// Create the view once per mount. The form store lives above the tab
|
|
119
|
-
// layout, so remounts (tab switches, conditional visibility) re-seed from
|
|
120
|
-
// the store-backed `value` — same contract the other widgets follow.
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
if (containerRef.current == null)
|
|
123
|
-
return;
|
|
124
|
-
const state = EditorState.create({
|
|
125
|
-
doc: initialValueRef.current,
|
|
126
|
-
extensions: [
|
|
127
|
-
lineNumbers(),
|
|
128
|
-
history(),
|
|
129
|
-
bracketMatching(),
|
|
130
|
-
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
|
|
131
|
-
EditorView.lineWrapping,
|
|
132
|
-
bylineCodeTheme,
|
|
133
|
-
syntaxHighlighting(bylineHighlightStyle),
|
|
134
|
-
languageCompartment.current.of([]),
|
|
135
|
-
readOnlyCompartment.current.of([]),
|
|
136
|
-
ariaCompartment.current.of([]),
|
|
137
|
-
EditorView.updateListener.of((update) => {
|
|
138
|
-
if (update.docChanged) {
|
|
139
|
-
onChangeRef.current?.(update.state.doc.toString());
|
|
140
|
-
}
|
|
141
|
-
}),
|
|
142
|
-
],
|
|
143
|
-
});
|
|
144
|
-
const view = new EditorView({ state, parent: containerRef.current });
|
|
145
|
-
viewRef.current = view;
|
|
146
|
-
return () => {
|
|
147
|
-
view.destroy();
|
|
148
|
-
viewRef.current = null;
|
|
149
|
-
};
|
|
150
|
-
}, []);
|
|
151
|
-
// External value sync — only while unfocused, so we never fight the
|
|
152
|
-
// editor's own keystrokes (our own onChange echoes back as an equal doc).
|
|
153
|
-
useEffect(() => {
|
|
154
|
-
const view = viewRef.current;
|
|
155
|
-
if (view == null || view.hasFocus)
|
|
156
|
-
return;
|
|
157
|
-
const current = view.state.doc.toString();
|
|
158
|
-
if (current !== value) {
|
|
159
|
-
view.dispatch({ changes: { from: 0, to: current.length, insert: value } });
|
|
160
|
-
}
|
|
161
|
-
}, [value]);
|
|
162
|
-
// Language switching through a compartment: each grammar loads lazily the
|
|
163
|
-
// first time it's requested and reconfigures in place (no view recreation).
|
|
164
|
-
useEffect(() => {
|
|
165
|
-
let cancelled = false;
|
|
166
|
-
const view = viewRef.current;
|
|
167
|
-
if (view == null)
|
|
168
|
-
return;
|
|
169
|
-
const loader = resolveLanguageLoader(language);
|
|
170
|
-
if (loader == null) {
|
|
171
|
-
view.dispatch({ effects: languageCompartment.current.reconfigure([]) });
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
loader()
|
|
175
|
-
.then((extension) => {
|
|
176
|
-
if (!cancelled && viewRef.current != null) {
|
|
177
|
-
viewRef.current.dispatch({
|
|
178
|
-
effects: languageCompartment.current.reconfigure(extension),
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
})
|
|
182
|
-
.catch(() => {
|
|
183
|
-
// Grammar chunk failed to load — degrade to plain text.
|
|
184
|
-
});
|
|
185
|
-
return () => {
|
|
186
|
-
cancelled = true;
|
|
187
|
-
};
|
|
188
|
-
}, [language]);
|
|
189
|
-
useEffect(() => {
|
|
190
|
-
viewRef.current?.dispatch({
|
|
191
|
-
effects: readOnlyCompartment.current.reconfigure([
|
|
192
|
-
EditorState.readOnly.of(readOnly === true),
|
|
193
|
-
EditorView.editable.of(readOnly !== true),
|
|
194
|
-
]),
|
|
195
|
-
});
|
|
196
|
-
}, [readOnly]);
|
|
197
|
-
// ARIA lives on CodeMirror's own contenteditable (which already carries
|
|
198
|
-
// role="textbox"), not on the wrapper div.
|
|
199
|
-
useEffect(() => {
|
|
200
|
-
const attributes = {};
|
|
201
|
-
if (ariaInvalid)
|
|
202
|
-
attributes['aria-invalid'] = 'true';
|
|
203
|
-
if (ariaDescribedBy)
|
|
204
|
-
attributes['aria-describedby'] = ariaDescribedBy;
|
|
205
|
-
viewRef.current?.dispatch({
|
|
206
|
-
effects: ariaCompartment.current.reconfigure(EditorView.contentAttributes.of(attributes)),
|
|
207
|
-
});
|
|
208
|
-
}, [ariaInvalid, ariaDescribedBy]);
|
|
209
|
-
return <div ref={containerRef} id={id} className="byline-code-editor"/>;
|
|
210
|
-
};
|
|
211
|
-
export default CodeEditor;
|
|
@@ -1,109 +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 React, { Suspense, useCallback } from 'react';
|
|
9
|
-
import { ErrorText, HelpText, Label } from '@byline/ui/react';
|
|
10
|
-
import cx from 'clsx';
|
|
11
|
-
import { useFieldError, useFieldValue } from '../../forms/form-context';
|
|
12
|
-
import { LocaleBadge } from '../locale-badge';
|
|
13
|
-
import styles from './code-field.module.css';
|
|
14
|
-
// The CodeMirror half of the widget. `React.lazy` is the bundle-splitting
|
|
15
|
-
// boundary: `code-editor.tsx` owns every CodeMirror import, so the editor
|
|
16
|
-
// (and its per-language grammars) stays out of the main admin chunk and is
|
|
17
|
-
// fetched the first time a code field actually renders.
|
|
18
|
-
const CodeEditor = React.lazy(() => import('./code-editor'));
|
|
19
|
-
/**
|
|
20
|
-
* Resolve the form-store path of a sibling field (same group/block/array
|
|
21
|
-
* item scope). `content[id=x].code` + `language` → `content[id=x].language`;
|
|
22
|
-
* a top-level `code` + `language` → `language`.
|
|
23
|
-
*/
|
|
24
|
-
const siblingFieldPath = (fieldPath, siblingName) => {
|
|
25
|
-
const lastDot = fieldPath.lastIndexOf('.');
|
|
26
|
-
return lastDot === -1 ? siblingName : `${fieldPath.slice(0, lastDot + 1)}${siblingName}`;
|
|
27
|
-
};
|
|
28
|
-
export const CodeField = ({ field, value, defaultValue, onChange, id, path, locale, components, }) => {
|
|
29
|
-
const fieldPath = path ?? field.name;
|
|
30
|
-
const fieldError = useFieldError(fieldPath);
|
|
31
|
-
const fieldValue = useFieldValue(fieldPath);
|
|
32
|
-
const incomingValue = value ?? fieldValue ?? defaultValue ?? '';
|
|
33
|
-
const htmlId = id ?? fieldPath;
|
|
34
|
-
// Effective highlight language: a sibling `languageField` selection (e.g.
|
|
35
|
-
// a `select` next to the code field) wins over the schema's static
|
|
36
|
-
// `language` hint. The hook subscribes to the sibling path, so switching
|
|
37
|
-
// the select re-highlights live. When no `languageField` is declared we
|
|
38
|
-
// subscribe to our own path — a stable no-op (hooks must run
|
|
39
|
-
// unconditionally).
|
|
40
|
-
const siblingPath = field.languageField
|
|
41
|
-
? siblingFieldPath(fieldPath, field.languageField)
|
|
42
|
-
: fieldPath;
|
|
43
|
-
const siblingLanguage = useFieldValue(siblingPath);
|
|
44
|
-
const effectiveLanguage = (field.languageField ? siblingLanguage : undefined) || field.language;
|
|
45
|
-
const handleChange = useCallback((value) => {
|
|
46
|
-
if (onChange) {
|
|
47
|
-
onChange(value);
|
|
48
|
-
}
|
|
49
|
-
}, [onChange]);
|
|
50
|
-
// Custom component slots (from admin config)
|
|
51
|
-
const slots = components;
|
|
52
|
-
const CustomLabel = slots?.Label;
|
|
53
|
-
const CustomHelpText = slots?.HelpText;
|
|
54
|
-
const CustomField = slots?.Field;
|
|
55
|
-
const BeforeField = slots?.beforeField;
|
|
56
|
-
const AfterField = slots?.afterField;
|
|
57
|
-
// Shared props available to every slot component
|
|
58
|
-
const slotBaseProps = {
|
|
59
|
-
field: field,
|
|
60
|
-
path: fieldPath,
|
|
61
|
-
value: incomingValue,
|
|
62
|
-
error: fieldError,
|
|
63
|
-
id: htmlId,
|
|
64
|
-
};
|
|
65
|
-
const showBadge = !!locale && !!field.label;
|
|
66
|
-
const hasCustomLabel = !!CustomLabel;
|
|
67
|
-
const labelRowClass = cx('byline-field-code-label-row', styles['label-row']);
|
|
68
|
-
// ── Label rendering ──────────────────────────────────────────
|
|
69
|
-
const renderLabel = () => {
|
|
70
|
-
if (hasCustomLabel) {
|
|
71
|
-
return (<div className={labelRowClass}>
|
|
72
|
-
<CustomLabel {...slotBaseProps} label={field.label} required={!field.optional}/>
|
|
73
|
-
{showBadge && <LocaleBadge locale={locale}/>}
|
|
74
|
-
</div>);
|
|
75
|
-
}
|
|
76
|
-
if (field.label) {
|
|
77
|
-
return (<div className={labelRowClass}>
|
|
78
|
-
<Label id={`${htmlId}-label`} htmlFor={htmlId} label={field.label} required={!field.optional}/>
|
|
79
|
-
{showBadge && <LocaleBadge locale={locale}/>}
|
|
80
|
-
</div>);
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
};
|
|
84
|
-
// ── Field input rendering ────────────────────────────────────
|
|
85
|
-
const renderInput = () => {
|
|
86
|
-
if (CustomField) {
|
|
87
|
-
return (<CustomField {...slotBaseProps} onChange={handleChange} defaultValue={defaultValue} placeholder={field.placeholder}/>);
|
|
88
|
-
}
|
|
89
|
-
return (<Suspense fallback={
|
|
90
|
-
// Read-only monospace textarea: keeps the layout stable and the
|
|
91
|
-
// content visible while the editor chunk loads.
|
|
92
|
-
<textarea className={cx('byline-field-code-loading', styles.loading)} readOnly value={incomingValue} rows={6} aria-label={field.label}/>}>
|
|
93
|
-
<CodeEditor id={htmlId} value={incomingValue} language={effectiveLanguage} onChange={handleChange} readOnly={field.readOnly === true} ariaInvalid={fieldError != null} ariaDescribedBy={fieldError != null
|
|
94
|
-
? `error-for-${htmlId}`
|
|
95
|
-
: field.helpText
|
|
96
|
-
? `help-for-${htmlId}`
|
|
97
|
-
: undefined}/>
|
|
98
|
-
</Suspense>);
|
|
99
|
-
};
|
|
100
|
-
return (<div className={`byline-field-code ${field.name}`}>
|
|
101
|
-
{renderLabel()}
|
|
102
|
-
{BeforeField && <BeforeField {...slotBaseProps}/>}
|
|
103
|
-
{renderInput()}
|
|
104
|
-
{AfterField && <AfterField {...slotBaseProps}/>}
|
|
105
|
-
{fieldError != null && <ErrorText id={`error-for-${htmlId}`} text={fieldError}/>}
|
|
106
|
-
{CustomHelpText ? (<CustomHelpText {...slotBaseProps} helpText={field.helpText}/>) : (fieldError == null &&
|
|
107
|
-
field.helpText && <HelpText id={`help-for-${htmlId}`} text={field.helpText}/>)}
|
|
108
|
-
</div>);
|
|
109
|
-
};
|
|
@@ -1,25 +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
|
-
/**
|
|
9
|
-
* Type guard: returns true when the formatter is a `{ component }` wrapper
|
|
10
|
-
* rather than a plain function.
|
|
11
|
-
*/
|
|
12
|
-
export function isComponentFormatter(fmt) {
|
|
13
|
-
return typeof fmt === 'object' && fmt !== null && 'component' in fmt;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Render a cell value through its column formatter (if any).
|
|
17
|
-
* Handles both plain-function and `{ component }` formatters.
|
|
18
|
-
*/
|
|
19
|
-
export function renderFormatted(value, document, formatter) {
|
|
20
|
-
if (isComponentFormatter(formatter)) {
|
|
21
|
-
const Component = formatter.component;
|
|
22
|
-
return <Component value={value} record={document}/>;
|
|
23
|
-
}
|
|
24
|
-
return formatter(value, document);
|
|
25
|
-
}
|
|
@@ -1,18 +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 { LocalDateTime } from './local-date-time';
|
|
9
|
-
/**
|
|
10
|
-
* SSR-safe date-time column formatter for list views.
|
|
11
|
-
*
|
|
12
|
-
* Uses the `LocalDateTime` component which renders a placeholder on the server
|
|
13
|
-
* and formats to the user's locale/timezone after hydration, avoiding
|
|
14
|
-
* server/client mismatches.
|
|
15
|
-
*/
|
|
16
|
-
export function DateTimeFormatter({ value }) {
|
|
17
|
-
return <LocalDateTime value={value}/>;
|
|
18
|
-
}
|
|
@@ -1,22 +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 { DatePicker, ErrorText } from '@byline/ui/react';
|
|
9
|
-
import cx from 'clsx';
|
|
10
|
-
import { useFieldError, useFieldValue, useIsDirty } from '../../forms/form-context';
|
|
11
|
-
import styles from './datetime-field.module.css';
|
|
12
|
-
export const DateTimeField = ({ field, value, defaultValue, onChange, id, path, }) => {
|
|
13
|
-
const fieldPath = path ?? field.name;
|
|
14
|
-
const fieldError = useFieldError(fieldPath);
|
|
15
|
-
const isDirty = useIsDirty(fieldPath);
|
|
16
|
-
const fieldValue = useFieldValue(fieldPath);
|
|
17
|
-
const incomingValue = value ?? fieldValue ?? defaultValue ?? null;
|
|
18
|
-
return (<div className={`byline-field-datetime ${field.name}`}>
|
|
19
|
-
<DatePicker id={id ?? fieldPath} name={field.name} label={field.label} required={!field.optional} initialValue={incomingValue} mode={field.mode || 'datetime'} yearsInFuture={field.yearsInFuture || 1} yearsInPast={field.yearsInPast || 10} onDateChange={(date) => onChange?.(date)} className={cx(isDirty && ['byline-field-datetime-dirty', styles.dirty])}/>
|
|
20
|
-
{fieldError && <ErrorText id={`${field.name}-error`} text={fieldError}/>}
|
|
21
|
-
</div>);
|
|
22
|
-
};
|