@qoretechnologies/reqraft 0.10.36 → 0.10.38-pr.95.gf1f6403
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/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +121 -2
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +20 -0
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +91 -22
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts +27 -0
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -0
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +259 -0
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -0
- package/dist/components/form/engine/compactRowStyles.d.ts +11 -0
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +14 -3
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +16 -0
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +72 -5
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.js +13 -1
- package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts +4 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +8 -1
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +112 -11
- package/src/components/form/engine/FormEngine.stories.tsx +313 -13
- package/src/components/form/engine/FormEngine.tsx +98 -7
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +462 -0
- package/src/components/form/engine/compactRowStyles.ts +14 -2
- package/src/components/form/engine/readFirst.ts +81 -4
- package/src/components/form/fields/array/ArrayAuto.tsx +12 -0
- package/src/components/form/fields/auto/AutoFormField.tsx +11 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { ReqoreCollapsibleContent, ReqoreIcon, ReqoreP, ReqoreTag } from '@qoretechnologies/reqore';
|
|
2
|
+
import {
|
|
3
|
+
IQorusFormField,
|
|
4
|
+
IQorusFormSchema,
|
|
5
|
+
TQorusFormFieldSchema,
|
|
6
|
+
} from '@qoretechnologies/ts-toolkit';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import styled from 'styled-components';
|
|
9
|
+
import { useContextSelector } from 'use-context-selector';
|
|
10
|
+
import { CompactRowContext } from '../compactRowContext';
|
|
11
|
+
import { MONO_FONT_STACK, StyledCodePreview, StyledRowLabel, StyledRowValue } from '../compactRowStyles';
|
|
12
|
+
import { findAllowedValueOption, formatOptionValue, getAllowedValueImage } from '../readFirst';
|
|
13
|
+
import { isUiEncodedValue } from './structuredData';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A read-only rendering of a value THROUGH the schema that describes it.
|
|
17
|
+
*
|
|
18
|
+
* `StructuredDataView` renders data nobody has described: it has to announce
|
|
19
|
+
* what it found ("Object · 2 fields"), print raw keys, and colour values by
|
|
20
|
+
* inferred type, because inference is all it has. That is the right rendering
|
|
21
|
+
* for untyped data and the wrong one for ours — a form field with an
|
|
22
|
+
* `arg_schema` is a value whose shape, names and choices are all known in
|
|
23
|
+
* advance, and showing it as a data tree asks the reader to decode a structure
|
|
24
|
+
* the form already knows how to say out loud.
|
|
25
|
+
*
|
|
26
|
+
* So a described value is rendered the way the form asked for it, in the form's
|
|
27
|
+
* own clothes:
|
|
28
|
+
*
|
|
29
|
+
* - Field names use `StyledRowLabel` and values use `StyledRowValue` — the very
|
|
30
|
+
* components the option rows above are built from. Not a copy of their CSS: a
|
|
31
|
+
* copy is a second definition of "what a form label looks like", and the two
|
|
32
|
+
* drift the first time either is touched.
|
|
33
|
+
* - Values are formatted by the same `formatOptionValue` the rows use, so an
|
|
34
|
+
* `allowed_values` entry resolves to its display name, a bool to Yes/No, a
|
|
35
|
+
* date to the app's format. The same words appear in the summary, the preview
|
|
36
|
+
* and the editor.
|
|
37
|
+
* - Fields appear in the schema's order — the order the form puts them in.
|
|
38
|
+
*
|
|
39
|
+
* A list renders as numbered items: the number sits in a left margin with a rule
|
|
40
|
+
* running down beside the fields belonging to it, and the item's first field
|
|
41
|
+
* starts on the number's own line. The rule is what makes a list of multi-field
|
|
42
|
+
* items readable — without it, "which item does this field belong to?" is
|
|
43
|
+
* answered only by counting.
|
|
44
|
+
*
|
|
45
|
+
* Two deliberate properties:
|
|
46
|
+
*
|
|
47
|
+
* - **Nothing is hidden.** A key the schema does not describe is still shown,
|
|
48
|
+
* under its raw name, after the described ones. A preview that quietly dropped
|
|
49
|
+
* data would be worse than a raw tree, not better.
|
|
50
|
+
* - **It degrades, it does not fail.** With no schema for a level, the caller
|
|
51
|
+
* keeps using `StructuredDataView` — undescribed data still deserves the
|
|
52
|
+
* renderer built for undescribed data.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/** Width of the shared label column, set once on the root (see `labelColumn`). */
|
|
56
|
+
const LABEL_COL_VAR = '--schema-view-label-col';
|
|
57
|
+
|
|
58
|
+
const StyledSchemaView = styled.div`
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
gap: 6px;
|
|
62
|
+
min-width: 0;
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* One list item: its number in the left margin, its fields beside it.
|
|
67
|
+
*
|
|
68
|
+
* `align-items: start` over a single grid row is what puts the first field on
|
|
69
|
+
* the number's line — the fields are one block starting at the top, not a grid
|
|
70
|
+
* row each, so nothing can push them down past their own marker.
|
|
71
|
+
*/
|
|
72
|
+
const StyledItem = styled.div`
|
|
73
|
+
display: grid;
|
|
74
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
75
|
+
column-gap: 8px;
|
|
76
|
+
align-items: start;
|
|
77
|
+
min-width: 0;
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
/** The item number. Furniture, so it reads quieter than the data it indexes;
|
|
81
|
+
* `tabular-nums` keeps "9." and "10." the same width. */
|
|
82
|
+
const StyledMarker = styled.div<{ $color: string }>`
|
|
83
|
+
color: ${({ $color }) => $color};
|
|
84
|
+
font-size: 12px;
|
|
85
|
+
font-variant-numeric: tabular-nums;
|
|
86
|
+
line-height: 20px;
|
|
87
|
+
white-space: nowrap;
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
/** The rule tying an item's fields to its number. */
|
|
91
|
+
const StyledItemFields = styled.div<{ $border: string }>`
|
|
92
|
+
border-left: 1px solid ${({ $border }) => $border};
|
|
93
|
+
padding-left: 10px;
|
|
94
|
+
min-width: 0;
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A record's fields: one shared label column, values in the next.
|
|
99
|
+
*
|
|
100
|
+
* The column width is a variable set once on the root, so every field of every
|
|
101
|
+
* item lines up on one edge. Sizing per record would align an item's own fields
|
|
102
|
+
* and leave the items ragged against each other, which reads as a rendering
|
|
103
|
+
* fault rather than as a list.
|
|
104
|
+
*/
|
|
105
|
+
const StyledFields = styled.div`
|
|
106
|
+
display: grid;
|
|
107
|
+
grid-template-columns: var(${LABEL_COL_VAR}, 140px) minmax(0, 1fr);
|
|
108
|
+
column-gap: 10px;
|
|
109
|
+
row-gap: 2px;
|
|
110
|
+
align-items: baseline;
|
|
111
|
+
min-width: 0;
|
|
112
|
+
|
|
113
|
+
/* A row's value truncates because a row is one line high. The preview is the
|
|
114
|
+
place that shows what the row could not, so a long value wraps here instead
|
|
115
|
+
of ellipsising — otherwise opening the preview would reveal exactly as
|
|
116
|
+
little as the line above it. */
|
|
117
|
+
.options-readfirst-valuetext {
|
|
118
|
+
white-space: normal;
|
|
119
|
+
overflow: visible;
|
|
120
|
+
text-overflow: clip;
|
|
121
|
+
overflow-wrap: anywhere;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Mono faces run visually larger than the UI face at the same pixel size, so
|
|
125
|
+
a literal set at the row's 13px looks a step bolder than the label naming
|
|
126
|
+
it. A hair smaller puts the two back on the same footing. */
|
|
127
|
+
.schema-view-data {
|
|
128
|
+
font-size: 12.5px;
|
|
129
|
+
}
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
/** A nested level spans both columns and indents under its own name. */
|
|
133
|
+
const StyledNested = styled.div<{ $border: string }>`
|
|
134
|
+
grid-column: 1 / -1;
|
|
135
|
+
margin-left: 2px;
|
|
136
|
+
padding-left: 10px;
|
|
137
|
+
border-left: 1px solid ${({ $border }) => $border};
|
|
138
|
+
min-width: 0;
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
const StyledNestedLabel = styled.div`
|
|
142
|
+
grid-column: 1 / -1;
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
/** A code value is a block, not a cell: it spans the grid under its own name. */
|
|
146
|
+
const StyledCodeCell = styled.div`
|
|
147
|
+
grid-column: 1 / -1;
|
|
148
|
+
min-width: 0;
|
|
149
|
+
margin: 2px 0 4px;
|
|
150
|
+
`;
|
|
151
|
+
|
|
152
|
+
export interface ISchemaDataViewProps {
|
|
153
|
+
/** The stored value, with or without `{type, value}` envelopes. */
|
|
154
|
+
value: unknown;
|
|
155
|
+
/** The schema describing this level — a field's `arg_schema`. */
|
|
156
|
+
schema: IQorusFormSchema;
|
|
157
|
+
/** Show each field's declared type beside its name. */
|
|
158
|
+
showTypes?: boolean;
|
|
159
|
+
/** Colours, taken from the row so the preview matches its surroundings. */
|
|
160
|
+
colors: { key: string; muted: string; border: string };
|
|
161
|
+
onItemClick?: () => void;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Look inside a `{type, value}` envelope, once. `isUiEncodedValue` is the
|
|
165
|
+
* engine's single authority on what an envelope is — a looser test would unwrap
|
|
166
|
+
* a real user hash that happens to carry a `value` key and drop its siblings. */
|
|
167
|
+
const unwrap = (item: unknown): unknown => (isUiEncodedValue(item) ? item.value : item);
|
|
168
|
+
|
|
169
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
170
|
+
!!value && typeof value === 'object' && !Array.isArray(value);
|
|
171
|
+
|
|
172
|
+
/** Whether a value is worth a row of its own. `false` and `0` are values. */
|
|
173
|
+
const isSet = (value: unknown): boolean =>
|
|
174
|
+
value !== undefined && value !== null && value !== '' && !(Array.isArray(value) && !value.length);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The keys to render, in the order to render them: the schema's own order first
|
|
178
|
+
* (that is the order the form puts the fields in), then anything stored that the
|
|
179
|
+
* schema does not mention, so undescribed data is shown rather than dropped.
|
|
180
|
+
*/
|
|
181
|
+
const orderedKeys = (record: Record<string, unknown>, schema: IQorusFormSchema): string[] => {
|
|
182
|
+
const described = Object.keys(schema).filter((key) => isSet(unwrap(record[key])));
|
|
183
|
+
const extra = Object.keys(record).filter((key) => !(key in schema) && isSet(unwrap(record[key])));
|
|
184
|
+
return [...described, ...extra];
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** A field whose content is source code. `ui_type` is what the form renders by,
|
|
188
|
+
* so it is what the preview reads by too — the storage type is just `string`. */
|
|
189
|
+
const isCodeField = (fieldSchema: TQorusFormFieldSchema | undefined): boolean => {
|
|
190
|
+
const uiType = (fieldSchema as { ui_type?: string } | undefined)?.ui_type;
|
|
191
|
+
return uiType === 'code-editor';
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const fieldLabel = (key: string, fieldSchema: TQorusFormFieldSchema | undefined): string =>
|
|
195
|
+
(fieldSchema as { display_name?: string } | undefined)?.display_name || key;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The width of the shared label column, measured from the longest name that will
|
|
199
|
+
* actually be rendered.
|
|
200
|
+
*
|
|
201
|
+
* A fixed width is a guess in both directions: too wide and a name sits far from
|
|
202
|
+
* its value, too narrow and every name ellipsises. Measuring fits the content;
|
|
203
|
+
* the `%` ceiling stops one long name from taking the whole row when the inset
|
|
204
|
+
* is narrow — a phone, or a deeply nested level.
|
|
205
|
+
*/
|
|
206
|
+
const labelColumn = (value: unknown, schema: IQorusFormSchema): string => {
|
|
207
|
+
const inner = unwrap(value);
|
|
208
|
+
const records = (Array.isArray(inner) ? inner.map(unwrap) : [inner]).filter(isRecord);
|
|
209
|
+
const longest = records.reduce((widest, record) => {
|
|
210
|
+
const own = orderedKeys(record, schema).reduce(
|
|
211
|
+
(max, key) =>
|
|
212
|
+
Math.max(max, fieldLabel(key, schema[key] as TQorusFormFieldSchema | undefined).length),
|
|
213
|
+
0
|
|
214
|
+
);
|
|
215
|
+
return Math.max(widest, own);
|
|
216
|
+
}, 0);
|
|
217
|
+
return longest ? `min(${longest + 1}ch, 45%)` : '140px';
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/** One field's value, rendered the way its row above renders it. */
|
|
221
|
+
const FieldValue = ({
|
|
222
|
+
field,
|
|
223
|
+
fieldSchema,
|
|
224
|
+
color,
|
|
225
|
+
}: {
|
|
226
|
+
field: IQorusFormField;
|
|
227
|
+
fieldSchema: TQorusFormFieldSchema | undefined;
|
|
228
|
+
color: string;
|
|
229
|
+
}) => {
|
|
230
|
+
const formatted = formatOptionValue(field, fieldSchema);
|
|
231
|
+
// An allowed value that carries a logo shows it here too, exactly as the row
|
|
232
|
+
// above does — the preview is the same value, so it gets the same treatment.
|
|
233
|
+
const image = getAllowedValueImage(field.value, fieldSchema);
|
|
234
|
+
|
|
235
|
+
// Data reads in mono; a chosen label does not.
|
|
236
|
+
//
|
|
237
|
+
// "qorus-session" is a literal the author typed — an identifier where the
|
|
238
|
+
// characters themselves matter, and where a proportional font makes `l`/`1`
|
|
239
|
+
// and `O`/`0` a guess. "Default RBAC" is not data at all: it is the name of a
|
|
240
|
+
// choice, written by whoever wrote the schema, and setting it in mono would
|
|
241
|
+
// dress up a label as a value. Resolving through `allowed_values` is what
|
|
242
|
+
// separates the two, and it is the same lookup that produced the text.
|
|
243
|
+
const isChosenLabel = !!findAllowedValueOption(field.value, fieldSchema);
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<StyledRowValue $color={color}>
|
|
247
|
+
{field.value === true || field.value === false ?
|
|
248
|
+
<ReqoreTag
|
|
249
|
+
size='small'
|
|
250
|
+
minimal
|
|
251
|
+
intent={field.value === true ? 'success' : 'danger'}
|
|
252
|
+
label={formatted}
|
|
253
|
+
/>
|
|
254
|
+
: <span
|
|
255
|
+
className={`options-readfirst-valuetext${isChosenLabel ? '' : ' schema-view-data'}`}
|
|
256
|
+
style={isChosenLabel ? undefined : { fontFamily: MONO_FONT_STACK }}
|
|
257
|
+
>
|
|
258
|
+
{image ?
|
|
259
|
+
<ReqoreIcon
|
|
260
|
+
image={image}
|
|
261
|
+
size='14px'
|
|
262
|
+
style={{ flexShrink: 0, marginRight: 6, verticalAlign: 'middle' }}
|
|
263
|
+
/>
|
|
264
|
+
: null}
|
|
265
|
+
{formatted}
|
|
266
|
+
</span>
|
|
267
|
+
}
|
|
268
|
+
</StyledRowValue>
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/** One described hash: its fields as label/value pairs in the shared columns. */
|
|
273
|
+
const SchemaRecord = ({
|
|
274
|
+
record,
|
|
275
|
+
schema,
|
|
276
|
+
showTypes,
|
|
277
|
+
colors,
|
|
278
|
+
}: {
|
|
279
|
+
record: Record<string, unknown>;
|
|
280
|
+
schema: IQorusFormSchema;
|
|
281
|
+
showTypes?: boolean;
|
|
282
|
+
colors: ISchemaDataViewProps['colors'];
|
|
283
|
+
}) => {
|
|
284
|
+
// The SAME host renderer the row above uses for a `code-editor` field. Read
|
|
285
|
+
// from context rather than threaded through props: a nested level is arbitrarily
|
|
286
|
+
// deep, and the renderer is a property of the form, not of any one level.
|
|
287
|
+
const codePreviewRenderer = useContextSelector(
|
|
288
|
+
CompactRowContext,
|
|
289
|
+
(v) => v.codePreviewRenderer
|
|
290
|
+
);
|
|
291
|
+
const cHover = useContextSelector(CompactRowContext, (v) => v.cHover);
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<StyledFields className='schema-view-fields'>
|
|
295
|
+
{orderedKeys(record, schema).map((key) => {
|
|
296
|
+
const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
|
|
297
|
+
const nested = (fieldSchema as { arg_schema?: IQorusFormSchema } | undefined)?.arg_schema;
|
|
298
|
+
const value = unwrap(record[key]);
|
|
299
|
+
const label = fieldLabel(key, fieldSchema);
|
|
300
|
+
const labelWithType =
|
|
301
|
+
showTypes && fieldSchema?.type ? `${label} · ${fieldSchema.type}` : label;
|
|
302
|
+
|
|
303
|
+
// A field that describes its own level renders AS that level, indented
|
|
304
|
+
// under its name — the same decision, one step down.
|
|
305
|
+
if (nested && (isRecord(value) || Array.isArray(value))) {
|
|
306
|
+
return (
|
|
307
|
+
<React.Fragment key={key}>
|
|
308
|
+
<StyledNestedLabel>
|
|
309
|
+
<StyledRowLabel $color={colors.key}>{labelWithType}</StyledRowLabel>
|
|
310
|
+
</StyledNestedLabel>
|
|
311
|
+
<StyledNested $border={colors.border}>
|
|
312
|
+
<SchemaLevel value={value} schema={nested} showTypes={showTypes} colors={colors} />
|
|
313
|
+
</StyledNested>
|
|
314
|
+
</React.Fragment>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// A code value is source, and source is read as source: the host's renderer
|
|
319
|
+
// gives it the syntax highlighting (and read-only LSP hover) it has in the
|
|
320
|
+
// Source Code field, and "Show more" keeps a long body from taking over the
|
|
321
|
+
// preview. Flattening it to one mono line — which is what a plain value cell
|
|
322
|
+
// does — throws all of that away for a field whose whole content is code.
|
|
323
|
+
if (isCodeField(fieldSchema) && typeof value === 'string') {
|
|
324
|
+
return (
|
|
325
|
+
<React.Fragment key={key}>
|
|
326
|
+
<StyledNestedLabel>
|
|
327
|
+
<StyledRowLabel $color={colors.key}>{labelWithType}</StyledRowLabel>
|
|
328
|
+
</StyledNestedLabel>
|
|
329
|
+
<StyledCodeCell onClick={(event) => event.stopPropagation()}>
|
|
330
|
+
<ReqoreCollapsibleContent
|
|
331
|
+
maxCollapsedHeight={96}
|
|
332
|
+
buttonProps={{ className: 'options-readfirst-viewmore' }}
|
|
333
|
+
>
|
|
334
|
+
{codePreviewRenderer ?
|
|
335
|
+
codePreviewRenderer({
|
|
336
|
+
value,
|
|
337
|
+
name: key,
|
|
338
|
+
schema: fieldSchema,
|
|
339
|
+
options: schema,
|
|
340
|
+
values: record as never,
|
|
341
|
+
})
|
|
342
|
+
: <StyledCodePreview
|
|
343
|
+
className='options-readfirst-code'
|
|
344
|
+
$bg={cHover}
|
|
345
|
+
$border={`${colors.border}88`}
|
|
346
|
+
$fg={colors.key}
|
|
347
|
+
>
|
|
348
|
+
{value}
|
|
349
|
+
</StyledCodePreview>
|
|
350
|
+
}
|
|
351
|
+
</ReqoreCollapsibleContent>
|
|
352
|
+
</StyledCodeCell>
|
|
353
|
+
</React.Fragment>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<React.Fragment key={key}>
|
|
359
|
+
<StyledRowLabel $color={colors.key} title={label}>
|
|
360
|
+
{labelWithType}
|
|
361
|
+
</StyledRowLabel>
|
|
362
|
+
<FieldValue
|
|
363
|
+
field={{ type: fieldSchema?.type, value } as IQorusFormField}
|
|
364
|
+
fieldSchema={fieldSchema}
|
|
365
|
+
color={colors.key}
|
|
366
|
+
/>
|
|
367
|
+
</React.Fragment>
|
|
368
|
+
);
|
|
369
|
+
})}
|
|
370
|
+
</StyledFields>
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/** A level: a list of numbered items, or a single described hash. */
|
|
375
|
+
const SchemaLevel = ({
|
|
376
|
+
value,
|
|
377
|
+
schema,
|
|
378
|
+
showTypes,
|
|
379
|
+
colors,
|
|
380
|
+
}: {
|
|
381
|
+
value: unknown;
|
|
382
|
+
schema: IQorusFormSchema;
|
|
383
|
+
showTypes?: boolean;
|
|
384
|
+
colors: ISchemaDataViewProps['colors'];
|
|
385
|
+
}) => {
|
|
386
|
+
const inner = unwrap(value);
|
|
387
|
+
|
|
388
|
+
if (Array.isArray(inner)) {
|
|
389
|
+
return (
|
|
390
|
+
<>
|
|
391
|
+
{inner.map((item, index) => {
|
|
392
|
+
const record = unwrap(item);
|
|
393
|
+
return (
|
|
394
|
+
<StyledItem key={index} className='schema-view-item'>
|
|
395
|
+
<StyledMarker $color={colors.muted}>{index + 1}.</StyledMarker>
|
|
396
|
+
<StyledItemFields $border={colors.border}>
|
|
397
|
+
{isRecord(record) ?
|
|
398
|
+
<SchemaRecord
|
|
399
|
+
record={record}
|
|
400
|
+
schema={schema}
|
|
401
|
+
showTypes={showTypes}
|
|
402
|
+
colors={colors}
|
|
403
|
+
/>
|
|
404
|
+
: <StyledRowValue $color={colors.key}>
|
|
405
|
+
<span
|
|
406
|
+
className='options-readfirst-valuetext schema-view-data'
|
|
407
|
+
style={{ fontFamily: MONO_FONT_STACK }}
|
|
408
|
+
>
|
|
409
|
+
{formatOptionValue({ value: record } as IQorusFormField)}
|
|
410
|
+
</span>
|
|
411
|
+
</StyledRowValue>
|
|
412
|
+
}
|
|
413
|
+
</StyledItemFields>
|
|
414
|
+
</StyledItem>
|
|
415
|
+
);
|
|
416
|
+
})}
|
|
417
|
+
</>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (isRecord(inner)) {
|
|
422
|
+
return <SchemaRecord record={inner} schema={schema} showTypes={showTypes} colors={colors} />;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<ReqoreP size='small' style={{ color: colors.key }}>
|
|
427
|
+
{formatOptionValue({ value: inner } as IQorusFormField)}
|
|
428
|
+
</ReqoreP>
|
|
429
|
+
);
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
export const SchemaDataView = React.memo(
|
|
433
|
+
({ value, schema, showTypes, colors, onItemClick }: ISchemaDataViewProps) => (
|
|
434
|
+
<StyledSchemaView
|
|
435
|
+
className='schema-data-view'
|
|
436
|
+
onClick={onItemClick}
|
|
437
|
+
style={{ [LABEL_COL_VAR]: labelColumn(value, schema) } as React.CSSProperties}
|
|
438
|
+
>
|
|
439
|
+
<SchemaLevel value={value} schema={schema} showTypes={showTypes} colors={colors} />
|
|
440
|
+
</StyledSchemaView>
|
|
441
|
+
)
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
SchemaDataView.displayName = 'SchemaDataView';
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Whether a value can be rendered through a schema at all.
|
|
448
|
+
*
|
|
449
|
+
* A schema describes fields, so there has to be a described level to show: a
|
|
450
|
+
* hash, or a list of them. A list of scalars has no field names to use and falls
|
|
451
|
+
* back to the untyped renderer, which is the right tool for it.
|
|
452
|
+
*/
|
|
453
|
+
export const canRenderWithSchema = (value: unknown, schema: unknown): boolean => {
|
|
454
|
+
if (!schema || typeof schema !== 'object' || !Object.keys(schema).length) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
const inner = unwrap(value);
|
|
458
|
+
if (Array.isArray(inner)) {
|
|
459
|
+
return inner.some((item) => isRecord(unwrap(item)));
|
|
460
|
+
}
|
|
461
|
+
return isRecord(inner);
|
|
462
|
+
};
|
|
@@ -381,6 +381,19 @@ export const StyledRowInset = styled.div`
|
|
|
381
381
|
margin-top: 4px;
|
|
382
382
|
`;
|
|
383
383
|
|
|
384
|
+
/**
|
|
385
|
+
* The stack literal text renders in: source, ids, tokens, data strings.
|
|
386
|
+
*
|
|
387
|
+
* Defined HERE rather than taken from reqore's `ReqoreFonts.mono`, which is the
|
|
388
|
+
* same stack. Reqore is a peer dependency at `>=0.71.16` and that export arrived
|
|
389
|
+
* later, so reading it would crash every consumer inside the declared range —
|
|
390
|
+
* the whole form preview lost over a font choice. One local constant costs
|
|
391
|
+
* nothing and keeps the code preview and the schema view's data values
|
|
392
|
+
* identical, which is the only reason to share it in the first place.
|
|
393
|
+
*/
|
|
394
|
+
export const MONO_FONT_STACK =
|
|
395
|
+
"ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace";
|
|
396
|
+
|
|
384
397
|
// A collapsed code-block preview shown under the value summary for a
|
|
385
398
|
// `code-editor` field. Multi-line, monospace, subtle background — matches
|
|
386
399
|
// the aesthetic of the classic code-view surface but small enough to sit
|
|
@@ -393,8 +406,7 @@ export const StyledCodePreview = styled.pre<{ $bg: string; $border: string; $fg:
|
|
|
393
406
|
background: ${({ $bg }) => $bg};
|
|
394
407
|
border: 1px solid ${({ $border }) => $border};
|
|
395
408
|
color: ${({ $fg }) => $fg};
|
|
396
|
-
font-family:
|
|
397
|
-
ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
409
|
+
font-family: ${MONO_FONT_STACK};
|
|
398
410
|
font-size: 11.5px;
|
|
399
411
|
line-height: 1.5;
|
|
400
412
|
white-space: pre;
|
|
@@ -40,6 +40,57 @@ const summarizeListItem = (item: unknown): string | number | undefined => {
|
|
|
40
40
|
return undefined;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Label ONE list item from the sub-schema that describes it.
|
|
45
|
+
*
|
|
46
|
+
* `summarizeListItem` can only read an item that names ITSELF — one carrying a
|
|
47
|
+
* `name` or `display_name` key. A hash whose fields are all domain fields names
|
|
48
|
+
* itself nowhere: an auth profile's scheme has `type`, `cookie_name`,
|
|
49
|
+
* `redirect_url`, and the row fell back to "2 items" — true, and silent about
|
|
50
|
+
* which two.
|
|
51
|
+
*
|
|
52
|
+
* The `arg_schema` is the same description the sub-form was built from, so the
|
|
53
|
+
* row can read an item the way the form asked for it: the FIRST declared field
|
|
54
|
+
* the item actually has, resolved through that field's allowed values. The
|
|
55
|
+
* scheme list then reads "Default RBAC, Cookie" — the words the author picked —
|
|
56
|
+
* instead of a count.
|
|
57
|
+
*
|
|
58
|
+
* Schema order, not value order: the first declared field is the one the form
|
|
59
|
+
* puts at the top of the item, which is the one that identifies it to its author.
|
|
60
|
+
* Value order is insertion order and would vary between two equal items.
|
|
61
|
+
*/
|
|
62
|
+
const summarizeHashItemFromSchema = (
|
|
63
|
+
item: unknown,
|
|
64
|
+
argSchema: Record<string, TQorusFormFieldSchema> | undefined
|
|
65
|
+
): string | number | undefined => {
|
|
66
|
+
if (!argSchema) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
const record = isTypedEnvelope(item) ? item.value : item;
|
|
70
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const fields = record as Record<string, unknown>;
|
|
74
|
+
const key = Object.keys(argSchema).find((name) => {
|
|
75
|
+
const raw = fields[name];
|
|
76
|
+
const value = isTypedEnvelope(raw) ? raw.value : raw;
|
|
77
|
+
return value !== undefined && value !== null && value !== '';
|
|
78
|
+
});
|
|
79
|
+
if (!key) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const raw = fields[key];
|
|
83
|
+
const value = isTypedEnvelope(raw) ? raw.value : raw;
|
|
84
|
+
const label = getAllowedValueLabel(value, argSchema[key]);
|
|
85
|
+
if (label) {
|
|
86
|
+
return label;
|
|
87
|
+
}
|
|
88
|
+
// No allowed values (a free-text field): the stored value IS what was typed,
|
|
89
|
+
// so it reads correctly as-is. An object at this position has no short form,
|
|
90
|
+
// and a count beats printing its shape.
|
|
91
|
+
return typeof value === 'string' || typeof value === 'number' ? value : undefined;
|
|
92
|
+
};
|
|
93
|
+
|
|
43
94
|
/** Summarise a list value: join item labels, or fall back to an "N items" count
|
|
44
95
|
* (e.g. a list of anonymous hashes). Never prints raw objects. */
|
|
45
96
|
/**
|
|
@@ -52,8 +103,15 @@ const summarizeListItem = (item: unknown): string | number | undefined => {
|
|
|
52
103
|
* does, so it can read the same way instead of printing the wire form back.
|
|
53
104
|
*/
|
|
54
105
|
const formatList = (items: unknown[], schema?: TQorusFormFieldSchema): string => {
|
|
106
|
+
const argSchema = (schema as { arg_schema?: Record<string, TQorusFormFieldSchema> } | undefined)
|
|
107
|
+
?.arg_schema;
|
|
55
108
|
const parts = items
|
|
56
|
-
.map(
|
|
109
|
+
.map(
|
|
110
|
+
(item) =>
|
|
111
|
+
getAllowedValueLabel(item, schema) ??
|
|
112
|
+
summarizeListItem(item) ??
|
|
113
|
+
summarizeHashItemFromSchema(item, argSchema)
|
|
114
|
+
)
|
|
57
115
|
.filter((part) => part !== '' && part !== undefined && part !== null);
|
|
58
116
|
|
|
59
117
|
return parts.length ? parts.join(', ') : pluralize(items.length, 'item');
|
|
@@ -133,9 +191,25 @@ export const shouldAutoCollapseCompactOption = (
|
|
|
133
191
|
!isOptionValueEmpty(value) &&
|
|
134
192
|
(isCompactBooleanOption(schema) || isFixedCompactAllowedValueOption(schema));
|
|
135
193
|
|
|
136
|
-
/**
|
|
137
|
-
*
|
|
138
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Prefer the matching allowed_values entry's display_name (fallback `name`)
|
|
196
|
+
* over the raw stored value.
|
|
197
|
+
*
|
|
198
|
+
* Exported (as `findAllowedValueOption`) because the form engine's value
|
|
199
|
+
* validation has to answer the SAME question — "is this stored value one of the
|
|
200
|
+
* declared choices?" — and answering it differently loses data. An
|
|
201
|
+
* `allowed_values` entry is written three ways: an envelope (`{value: {type,
|
|
202
|
+
* value}}`), a bare value (`{value: 'default'}`) or a named entry
|
|
203
|
+
* (`{name: 'default'}`). This has always accepted all three; the engine's
|
|
204
|
+
* clearing guard accepted only the first and the third, so a schema using the
|
|
205
|
+
* bare form had its value ERASED on load while this function went on rendering
|
|
206
|
+
* the display name for it — the row read "Default RBAC" collapsed and "—" when
|
|
207
|
+
* opened, and the value was gone from the submitted data.
|
|
208
|
+
*/
|
|
209
|
+
export const findAllowedValueOption = (
|
|
210
|
+
value: unknown,
|
|
211
|
+
schema?: TQorusFormFieldSchema
|
|
212
|
+
): any | undefined => {
|
|
139
213
|
const s = schema as { allowed_values?: any[]; items?: any[] } | undefined;
|
|
140
214
|
const options = s?.allowed_values?.length ? s.allowed_values : s?.items;
|
|
141
215
|
if (!options?.length) {
|
|
@@ -154,6 +228,9 @@ const getAllowedValueLabel = (
|
|
|
154
228
|
return match ? match.display_name || match.title || match.name || undefined : undefined;
|
|
155
229
|
};
|
|
156
230
|
|
|
231
|
+
/** Local alias for the module's own callers. */
|
|
232
|
+
const findAllowedOption = findAllowedValueOption;
|
|
233
|
+
|
|
157
234
|
/** True when the field's selectable options carry images (logos). Such a choice
|
|
158
235
|
* renders too tall/rich for an inline row, so compact opens it in the card. */
|
|
159
236
|
export const optionHasImages = (schema?: TQorusFormFieldSchema): boolean => {
|
|
@@ -64,6 +64,12 @@ export const ArrayAuto = ({
|
|
|
64
64
|
}: IArrayAutoProps): any => {
|
|
65
65
|
const confirmAction = useReqoreProperty('confirmAction');
|
|
66
66
|
const [localValue, setLocalValue] = useState(value);
|
|
67
|
+
// The row this component just created, so its sub-form can open the field it
|
|
68
|
+
// cannot be saved without. Only a row added HERE gets it: the rows already in
|
|
69
|
+
// the value were added by an earlier session (or arrived from the server), and
|
|
70
|
+
// opening one of those on mount would be this component reopening a decision
|
|
71
|
+
// the author already made.
|
|
72
|
+
const [justAddedIndex, setJustAddedIndex] = useState<number | undefined>(undefined);
|
|
67
73
|
|
|
68
74
|
useWhyDidYouUpdate(`Array Auto ${name}`, {
|
|
69
75
|
name,
|
|
@@ -87,6 +93,7 @@ export const ArrayAuto = ({
|
|
|
87
93
|
);
|
|
88
94
|
|
|
89
95
|
const addValue: () => void = () => {
|
|
96
|
+
setJustAddedIndex(localValue.length);
|
|
90
97
|
setLocalValue([...localValue, defaultValueByType[rest.type]]);
|
|
91
98
|
};
|
|
92
99
|
|
|
@@ -171,6 +178,11 @@ export const ArrayAuto = ({
|
|
|
171
178
|
allowTemplates
|
|
172
179
|
defaultType={rest.type}
|
|
173
180
|
name={`${name}-${idx}`}
|
|
181
|
+
// AFTER `{...rest}` on purpose: this is the one prop that differs
|
|
182
|
+
// per row, and spreading rest over it would give every row the same
|
|
183
|
+
// answer. Rides TemplateField's `rest` into the row's AutoFormField,
|
|
184
|
+
// which hands it to the row's nested FormEngine (case 'hash').
|
|
185
|
+
expandFirstRequired={Number(idx) === justAddedIndex}
|
|
174
186
|
value={val}
|
|
175
187
|
onChange={(_name, value) => handleChange(idx, value)}
|
|
176
188
|
/>
|
|
@@ -75,6 +75,10 @@ export interface IAutoFieldProps
|
|
|
75
75
|
/** Render the arg_schema sub-form in compact (read-first) mode, matching the
|
|
76
76
|
* parent engine. */
|
|
77
77
|
compact?: boolean;
|
|
78
|
+
/** Open the arg_schema sub-form's first attention row on mount, without taking
|
|
79
|
+
* focus — see FormEngine's `expandFirstRequired`. Set by ArrayAuto on a
|
|
80
|
+
* just-added row so its required choice is visible without a second click. */
|
|
81
|
+
expandFirstRequired?: boolean;
|
|
78
82
|
path?: string;
|
|
79
83
|
column?: boolean;
|
|
80
84
|
level?: number;
|
|
@@ -161,6 +165,10 @@ function AutoField<T = any>({
|
|
|
161
165
|
// Only the nested arg_schema mount sites re-forward this into their
|
|
162
166
|
// sub-forms explicitly.
|
|
163
167
|
inheritedFromParent,
|
|
168
|
+
// Destructured for the same reason as `inheritedFromParent`: only the nested
|
|
169
|
+
// sub-form mount site consumes it, and leaving it in `...rest` would spread an
|
|
170
|
+
// unknown attribute onto the primitive field renderers' DOM nodes.
|
|
171
|
+
expandFirstRequired,
|
|
164
172
|
...rest
|
|
165
173
|
}: IAutoFieldProps & T) {
|
|
166
174
|
const [currentType, setType] = useState<IQorusType>(defaultInternalType || null);
|
|
@@ -559,6 +567,9 @@ function AutoField<T = any>({
|
|
|
559
567
|
wrapperPadding='top'
|
|
560
568
|
flat
|
|
561
569
|
compact={compact}
|
|
570
|
+
// A row added just now: open the field it cannot be saved
|
|
571
|
+
// without, instead of making the author find it and click it.
|
|
572
|
+
expandFirstRequired={expandFirstRequired}
|
|
562
573
|
// Embedded sub-form: no scroll context of its own, so its toolbar
|
|
563
574
|
// isn't sticky and its header stays transparent (no dark backdrop).
|
|
564
575
|
compactNested
|