@qoretechnologies/reqraft 0.10.41-pr.95.g294e5aa → 0.10.41
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/.claude/CLAUDE.md +12 -0
- package/.tasks/INDEX.md +1 -0
- package/.tasks/TEMPLATE_TOKEN_GRAMMAR.md +72 -0
- package/dist/components/codeSize/index.d.ts +37 -0
- package/dist/components/codeSize/index.d.ts.map +1 -0
- package/dist/components/codeSize/index.js +87 -0
- package/dist/components/codeSize/index.js.map +1 -0
- package/dist/components/form/engine/CompactRow.d.ts +20 -1
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +284 -42
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +72 -72
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +48 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +163 -75
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +102 -87
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts +2 -0
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +22 -5
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +8 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +57 -1
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +143 -4
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +1 -1
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +12 -2
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +16 -6
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.js +44 -26
- package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.js +42 -25
- package/dist/components/form/fields/array/ArrayAutoField.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +32 -4
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
- package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
- package/dist/components/form/fields/long-string/LongString.js +25 -4
- package/dist/components/form/fields/long-string/LongString.js.map +1 -1
- package/dist/components/form/fields/template/ExampleValueModal.d.ts +29 -0
- package/dist/components/form/fields/template/ExampleValueModal.d.ts.map +1 -0
- package/dist/components/form/fields/template/ExampleValueModal.js +122 -0
- package/dist/components/form/fields/template/ExampleValueModal.js.map +1 -0
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -3
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +35 -6
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +2 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +1 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +9 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +1 -0
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/helpers/common.d.ts +8 -1
- package/dist/helpers/common.d.ts.map +1 -1
- package/dist/helpers/common.js +9 -1
- package/dist/helpers/common.js.map +1 -1
- package/dist/helpers/optionUiTypes.d.ts +30 -0
- package/dist/helpers/optionUiTypes.d.ts.map +1 -0
- package/dist/helpers/optionUiTypes.js +148 -0
- package/dist/helpers/optionUiTypes.js.map +1 -0
- package/dist/helpers/options.d.ts +19 -0
- package/dist/helpers/options.d.ts.map +1 -1
- package/dist/helpers/options.js +25 -1
- package/dist/helpers/options.js.map +1 -1
- package/dist/helpers/providerValue.d.ts +68 -0
- package/dist/helpers/providerValue.d.ts.map +1 -0
- package/dist/helpers/providerValue.js +50 -0
- package/dist/helpers/providerValue.js.map +1 -0
- package/dist/helpers/singleLineString.d.ts +37 -0
- package/dist/helpers/singleLineString.d.ts.map +1 -0
- package/dist/helpers/singleLineString.js +66 -0
- package/dist/helpers/singleLineString.js.map +1 -0
- package/dist/helpers/templates.d.ts +100 -0
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +237 -4
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts +103 -2
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +858 -91
- package/dist/helpers/validations.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/codeSize/index.tsx +79 -0
- package/src/components/form/engine/CompactRow.tsx +407 -34
- package/src/components/form/engine/CompactToolbar.tsx +105 -98
- package/src/components/form/engine/FormEngine.stories.tsx +863 -12
- package/src/components/form/engine/FormEngine.tsx +183 -33
- package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +110 -103
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +132 -80
- package/src/components/form/engine/compactRowStyles.ts +44 -1
- package/src/components/form/engine/compactToolbarContext.ts +8 -0
- package/src/components/form/engine/readFirst.ts +163 -4
- package/src/components/form/fields/Field.tsx +3 -1
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
- package/src/components/form/fields/array/ArrayAuto.tsx +23 -4
- package/src/components/form/fields/array/ArrayAutoField.stories.tsx +134 -0
- package/src/components/form/fields/array/ArrayAutoField.tsx +22 -4
- package/src/components/form/fields/auto/AutoFormField.tsx +31 -1
- package/src/components/form/fields/long-string/LongString.tsx +46 -5
- package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +5 -0
- package/src/components/form/fields/template/ExampleValueModal.stories.tsx +114 -0
- package/src/components/form/fields/template/ExampleValueModal.tsx +99 -0
- package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
- package/src/components/form/fields/template/TemplateField.stories.tsx +314 -1
- package/src/components/form/fields/template/TemplateField.tsx +47 -8
- package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +7 -0
- package/src/components/form/index.tsx +1 -0
- package/src/helpers/common.ts +11 -1
- package/src/helpers/optionUiTypes.ts +153 -0
- package/src/helpers/options.ts +23 -0
- package/src/helpers/providerValue.ts +125 -0
- package/src/helpers/singleLineString.ts +61 -0
- package/src/helpers/templates.ts +272 -3
- package/src/helpers/validations.ts +1177 -109
- package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
- package/src/index.tsx +2 -0
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ReqoreCollapsibleContent,
|
|
3
|
+
ReqoreIcon,
|
|
4
|
+
ReqoreLink,
|
|
5
|
+
ReqoreP,
|
|
6
|
+
ReqoreTag,
|
|
7
|
+
} from '@qoretechnologies/reqore';
|
|
2
8
|
import {
|
|
3
9
|
IQorusFormField,
|
|
4
10
|
IQorusFormSchema,
|
|
@@ -9,7 +15,16 @@ import styled from 'styled-components';
|
|
|
9
15
|
import { useContextSelector } from 'use-context-selector';
|
|
10
16
|
import { CompactRowContext } from '../compactRowContext';
|
|
11
17
|
import { MONO_FONT_STACK, StyledCodePreview, StyledRowLabel, StyledRowValue } from '../compactRowStyles';
|
|
12
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
fieldLabel,
|
|
20
|
+
findAllowedValueOption,
|
|
21
|
+
formatOptionValue,
|
|
22
|
+
getAllowedValueImage,
|
|
23
|
+
isCodeField,
|
|
24
|
+
orderedKeys,
|
|
25
|
+
recordIdentity,
|
|
26
|
+
titleKeyFor,
|
|
27
|
+
} from '../readFirst';
|
|
13
28
|
import { isUiEncodedValue } from './structuredData';
|
|
14
29
|
|
|
15
30
|
/**
|
|
@@ -26,10 +41,11 @@ import { isUiEncodedValue } from './structuredData';
|
|
|
26
41
|
* So a described value is rendered the way the form asked for it, in the form's
|
|
27
42
|
* own clothes:
|
|
28
43
|
*
|
|
29
|
-
* - Field names use `
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
44
|
+
* - Field names use `StyledFieldLabel` (a `StyledRowLabel` in the read-only
|
|
45
|
+
* eyebrow treatment) and values use `StyledRowValue` — the very components the
|
|
46
|
+
* option rows above are built from. Not a copy of their CSS: a copy is a
|
|
47
|
+
* second definition of "what a form label looks like", and the two drift the
|
|
48
|
+
* first time either is touched.
|
|
33
49
|
* - Values are formatted by the same `formatOptionValue` the rows use, so an
|
|
34
50
|
* `allowed_values` entry resolves to its display name, a bool to Yes/No, a
|
|
35
51
|
* date to the app's format. The same words appear in the summary, the preview
|
|
@@ -129,10 +145,39 @@ const StyledItemTitle = styled.div<{ $color: string; $mono: boolean; $accent: st
|
|
|
129
145
|
${({ $mono }) => ($mono ? `font-family: ${MONO_FONT_STACK}; font-size: 15px;` : '')}
|
|
130
146
|
`;
|
|
131
147
|
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Does this record have anything to show BELOW its heading?
|
|
150
|
+
*
|
|
151
|
+
* The heading consumes the identifying field, so a record holding only that has
|
|
152
|
+
* an empty body — and a rule drawn down an empty body is a line to nowhere.
|
|
153
|
+
*/
|
|
154
|
+
const hasContentUnderHeading = (record: unknown, schema?: IQorusFormSchema): boolean => {
|
|
155
|
+
if (!isRecord(record)) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
const titleKey = titleKeyFor(record, schema);
|
|
159
|
+
|
|
160
|
+
return Object.entries(record).some(
|
|
161
|
+
([key, value]) =>
|
|
162
|
+
key !== titleKey && value !== undefined && value !== null && value !== ''
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** The rule tying an item's fields to its number.
|
|
167
|
+
*
|
|
168
|
+
* Same 3px as the heading's accent bar directly above it: at 1px the two read as
|
|
169
|
+
* two different devices stacked on each other rather than one rule running down
|
|
170
|
+
* the item, and the padding drops to 8px so the text stays on the same column.
|
|
171
|
+
*
|
|
172
|
+
* The rule is drawn only when something is actually under the heading. The
|
|
173
|
+
* heading is pulled left of the rule by its own negative margin, so an item
|
|
174
|
+
* whose record holds nothing but its identity showed a 1-2px sliver of line and
|
|
175
|
+
* nothing else — which reads as a rendering artefact, because that is what it
|
|
176
|
+
* was. It keeps a transparent border in that case so the column does not shift.
|
|
177
|
+
*/
|
|
178
|
+
const StyledItemFields = styled.div<{ $border: string; $rule: boolean }>`
|
|
179
|
+
border-left: 3px solid ${({ $border, $rule }) => ($rule ? $border : 'transparent')};
|
|
180
|
+
padding-left: 8px;
|
|
136
181
|
min-width: 0;
|
|
137
182
|
`;
|
|
138
183
|
|
|
@@ -144,6 +189,30 @@ const StyledItemFields = styled.div<{ $border: string }>`
|
|
|
144
189
|
* and leave the items ragged against each other, which reads as a rendering
|
|
145
190
|
* fault rather than as a list.
|
|
146
191
|
*/
|
|
192
|
+
/**
|
|
193
|
+
* A record field's name, in the detail-page vocabulary.
|
|
194
|
+
*
|
|
195
|
+
* Uppercase, tracked and dimmed, so a field name reads as furniture and the
|
|
196
|
+
* VALUE beside it is what the eye lands on. Same treatment the read-only
|
|
197
|
+
* interface panels use, which is the point: an in-editor list of records and a
|
|
198
|
+
* detail-page panel showing the same shape should not look like two components.
|
|
199
|
+
*
|
|
200
|
+
* A variant rather than a change to `StyledRowLabel`: that one is the FORM's own
|
|
201
|
+
* option-row label, and the form's fields are not read-only furniture — they are
|
|
202
|
+
* the things you are about to edit. Composing it keeps the colour, weight base
|
|
203
|
+
* and wrapping behaviour in one place instead of copying them.
|
|
204
|
+
*
|
|
205
|
+
* Applies to the label only. A VALUE is never uppercased: values are frequently
|
|
206
|
+
* case-sensitive — a service method is `onOrderStatus`, not `ONORDERSTATUS` —
|
|
207
|
+
* and the item heading is a value.
|
|
208
|
+
*/
|
|
209
|
+
const StyledFieldLabel = styled(StyledRowLabel)`
|
|
210
|
+
font-size: 12px;
|
|
211
|
+
text-transform: uppercase;
|
|
212
|
+
letter-spacing: 0.06em;
|
|
213
|
+
opacity: 0.55;
|
|
214
|
+
`;
|
|
215
|
+
|
|
147
216
|
const StyledFields = styled.div`
|
|
148
217
|
/* The supporting fields step in under the heading, so the heading keeps the
|
|
149
218
|
left edge to itself. This is the half that answers "the name is written in
|
|
@@ -224,59 +293,6 @@ const unwrap = (item: unknown): unknown => (isUiEncodedValue(item) ? item.value
|
|
|
224
293
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
225
294
|
!!value && typeof value === 'object' && !Array.isArray(value);
|
|
226
295
|
|
|
227
|
-
/** Whether a value is worth a row of its own. `false` and `0` are values. */
|
|
228
|
-
const isSet = (value: unknown): boolean =>
|
|
229
|
-
value !== undefined && value !== null && value !== '' && !(Array.isArray(value) && !value.length);
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* The keys to render, in the order to render them: the schema's own order first
|
|
233
|
-
* (that is the order the form puts the fields in), then anything stored that the
|
|
234
|
-
* schema does not mention, so undescribed data is shown rather than dropped.
|
|
235
|
-
*/
|
|
236
|
-
const orderedKeys = (record: Record<string, unknown>, schema: IQorusFormSchema): string[] => {
|
|
237
|
-
const described = Object.keys(schema).filter((key) => isSet(unwrap(record[key])));
|
|
238
|
-
const extra = Object.keys(record).filter((key) => !(key in schema) && isSet(unwrap(record[key])));
|
|
239
|
-
return [...described, ...extra];
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
/** A field whose content is source code. `ui_type` is what the form renders by,
|
|
243
|
-
* so it is what the preview reads by too — the storage type is just `string`. */
|
|
244
|
-
const isCodeField = (fieldSchema: TQorusFormFieldSchema | undefined): boolean => {
|
|
245
|
-
const uiType = (fieldSchema as { ui_type?: string } | undefined)?.ui_type;
|
|
246
|
-
return uiType === 'code-editor';
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* The field whose value heads the item — the first DECLARED one holding a plain
|
|
251
|
-
* scalar.
|
|
252
|
-
*
|
|
253
|
-
* Schema order, not value order: the first declared field is the one the form
|
|
254
|
-
* puts at the top of an item, which is the one that says which item it is. A
|
|
255
|
-
* code body or a nested level is skipped, not because it is unimportant but
|
|
256
|
-
* because it has no one-line form — it belongs in the rows below where it can
|
|
257
|
-
* actually be read.
|
|
258
|
-
*
|
|
259
|
-
* One definition, used both to RENDER the heading and to omit that field from
|
|
260
|
-
* the rows. Computing it twice is how the heading and the rows start disagreeing
|
|
261
|
-
* about which field was promoted, and the item shows its name twice or not at
|
|
262
|
-
* all.
|
|
263
|
-
*/
|
|
264
|
-
const titleKeyFor = (
|
|
265
|
-
record: Record<string, unknown>,
|
|
266
|
-
schema: IQorusFormSchema
|
|
267
|
-
): string | undefined =>
|
|
268
|
-
orderedKeys(record, schema).find((key) => {
|
|
269
|
-
const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
|
|
270
|
-
if (isCodeField(fieldSchema)) {
|
|
271
|
-
return false;
|
|
272
|
-
}
|
|
273
|
-
const raw = unwrap(record[key]);
|
|
274
|
-
return typeof raw === 'string' || typeof raw === 'number';
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
const fieldLabel = (key: string, fieldSchema: TQorusFormFieldSchema | undefined): string =>
|
|
278
|
-
(fieldSchema as { display_name?: string } | undefined)?.display_name || key;
|
|
279
|
-
|
|
280
296
|
/**
|
|
281
297
|
* The width of the shared label column, measured from the longest name that will
|
|
282
298
|
* actually be rendered.
|
|
@@ -286,6 +302,12 @@ const fieldLabel = (key: string, fieldSchema: TQorusFormFieldSchema | undefined)
|
|
|
286
302
|
* the `%` ceiling stops one long name from taking the whole row when the inset
|
|
287
303
|
* is narrow — a phone, or a deeply nested level.
|
|
288
304
|
*/
|
|
305
|
+
/** Approximate rendered width of one label character, in px. See `labelColumn`. */
|
|
306
|
+
const LABEL_CHAR_PX = 9.5;
|
|
307
|
+
|
|
308
|
+
/** Gap to the value, so the longest label is not flush against it. */
|
|
309
|
+
const LABEL_SLACK_PX = 14;
|
|
310
|
+
|
|
289
311
|
const labelColumn = (value: unknown, schema: IQorusFormSchema): string => {
|
|
290
312
|
const inner = unwrap(value);
|
|
291
313
|
const records = (Array.isArray(inner) ? inner.map(unwrap) : [inner]).filter(isRecord);
|
|
@@ -297,9 +319,26 @@ const labelColumn = (value: unknown, schema: IQorusFormSchema): string => {
|
|
|
297
319
|
);
|
|
298
320
|
return Math.max(widest, own);
|
|
299
321
|
}, 0);
|
|
300
|
-
|
|
322
|
+
// px, not `ch`. `ch` is the base font's `0` advance and knows nothing about
|
|
323
|
+
// the label's own treatment — uppercase, semibold, `letter-spacing: 0.06em`,
|
|
324
|
+
// at 12px — so it under-measures by about a tenth. With `overflow-wrap:
|
|
325
|
+
// anywhere` on the label, an under-measured column does not ellipsise, it
|
|
326
|
+
// BREAKS THE NAME IN HALF: `LEGACY_FLAG` came out over two lines the moment
|
|
327
|
+
// the labels became uppercase. Erring high costs a few px of gap; erring low
|
|
328
|
+
// costs a broken word.
|
|
329
|
+
return longest ? `min(${Math.ceil(longest * LABEL_CHAR_PX + LABEL_SLACK_PX)}px, 45%)` : '140px';
|
|
301
330
|
};
|
|
302
331
|
|
|
332
|
+
/**
|
|
333
|
+
* `true` when a value is an address a browser can open.
|
|
334
|
+
*
|
|
335
|
+
* Deliberately per VALUE, not per field: a field named `url` frequently holds
|
|
336
|
+
* something that is not one — a bare path, a regex, a template — and offering
|
|
337
|
+
* those a link promises a destination there is not.
|
|
338
|
+
*/
|
|
339
|
+
const isNavigableUrl = (value: unknown): value is string =>
|
|
340
|
+
typeof value === 'string' && /^(https?|wss?|ftps?):\/\//i.test(value);
|
|
341
|
+
|
|
303
342
|
/** One field's value, rendered the way its row above renders it. */
|
|
304
343
|
const FieldValue = ({
|
|
305
344
|
field,
|
|
@@ -345,7 +384,22 @@ const FieldValue = ({
|
|
|
345
384
|
style={{ flexShrink: 0, marginRight: 6, verticalAlign: 'middle' }}
|
|
346
385
|
/>
|
|
347
386
|
: null}
|
|
348
|
-
{
|
|
387
|
+
{isNavigableUrl(field.value) ?
|
|
388
|
+
// The address IS the link — it used to render as inert text with a
|
|
389
|
+
// separate icon button beside it to open it, which is two things
|
|
390
|
+
// where there is one. ReqoreLink renders a real anchor, so
|
|
391
|
+
// middle-click and "copy link address" still work;
|
|
392
|
+
// `stopPropagation` because the row around it toggles.
|
|
393
|
+
<ReqoreLink
|
|
394
|
+
className='schema-view-open-url'
|
|
395
|
+
href={field.value}
|
|
396
|
+
target='_blank'
|
|
397
|
+
rel='noreferrer noopener'
|
|
398
|
+
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
|
399
|
+
>
|
|
400
|
+
{formatted}
|
|
401
|
+
</ReqoreLink>
|
|
402
|
+
: formatted}
|
|
349
403
|
</span>
|
|
350
404
|
}
|
|
351
405
|
</StyledRowValue>
|
|
@@ -394,7 +448,7 @@ const SchemaRecord = ({
|
|
|
394
448
|
return (
|
|
395
449
|
<React.Fragment key={key}>
|
|
396
450
|
<StyledNestedLabel>
|
|
397
|
-
<
|
|
451
|
+
<StyledFieldLabel $color={colors.key}>{labelWithType}</StyledFieldLabel>
|
|
398
452
|
</StyledNestedLabel>
|
|
399
453
|
<StyledNested $border={colors.border}>
|
|
400
454
|
<SchemaLevel value={value} schema={nested} showTypes={showTypes} colors={colors} />
|
|
@@ -412,7 +466,7 @@ const SchemaRecord = ({
|
|
|
412
466
|
return (
|
|
413
467
|
<React.Fragment key={key}>
|
|
414
468
|
<StyledNestedLabel>
|
|
415
|
-
<
|
|
469
|
+
<StyledFieldLabel $color={colors.key}>{labelWithType}</StyledFieldLabel>
|
|
416
470
|
</StyledNestedLabel>
|
|
417
471
|
<StyledCodeCell onClick={(event) => event.stopPropagation()}>
|
|
418
472
|
<ReqoreCollapsibleContent
|
|
@@ -444,9 +498,9 @@ const SchemaRecord = ({
|
|
|
444
498
|
|
|
445
499
|
return (
|
|
446
500
|
<React.Fragment key={key}>
|
|
447
|
-
<
|
|
501
|
+
<StyledFieldLabel $color={colors.key} title={label}>
|
|
448
502
|
{labelWithType}
|
|
449
|
-
</
|
|
503
|
+
</StyledFieldLabel>
|
|
450
504
|
<FieldValue
|
|
451
505
|
field={{ type: fieldSchema?.type, value } as IQorusFormField}
|
|
452
506
|
fieldSchema={fieldSchema}
|
|
@@ -481,16 +535,17 @@ const SchemaLevel = ({
|
|
|
481
535
|
return (
|
|
482
536
|
<StyledItem key={index} className='schema-view-item'>
|
|
483
537
|
<StyledMarker $color={colors.muted}>{index + 1}.</StyledMarker>
|
|
484
|
-
<StyledItemFields
|
|
538
|
+
<StyledItemFields
|
|
539
|
+
$border={colors.border}
|
|
540
|
+
$rule={hasContentUnderHeading(record, schema)}
|
|
541
|
+
>
|
|
485
542
|
{isRecord(record) ?
|
|
486
543
|
<>
|
|
487
544
|
{(() => {
|
|
488
|
-
const
|
|
489
|
-
if (!
|
|
545
|
+
const identity = recordIdentity(record, schema);
|
|
546
|
+
if (!identity) {
|
|
490
547
|
return null;
|
|
491
548
|
}
|
|
492
|
-
const fieldSchema = schema[titleKey] as TQorusFormFieldSchema | undefined;
|
|
493
|
-
const raw = unwrap(record[titleKey]);
|
|
494
549
|
return (
|
|
495
550
|
<StyledItemTitle
|
|
496
551
|
className='schema-view-item-title'
|
|
@@ -498,14 +553,11 @@ const SchemaLevel = ({
|
|
|
498
553
|
// A literal keeps the mono treatment it has as a value —
|
|
499
554
|
// promoting it must not change what it IS. A chosen
|
|
500
555
|
// label ("Default RBAC") is prose and stays prose.
|
|
501
|
-
$mono={
|
|
556
|
+
$mono={identity.mono}
|
|
502
557
|
$accent={colors.accent}
|
|
503
|
-
title={
|
|
558
|
+
title={identity.label}
|
|
504
559
|
>
|
|
505
|
-
{
|
|
506
|
-
{ type: fieldSchema?.type, value: raw } as IQorusFormField,
|
|
507
|
-
fieldSchema
|
|
508
|
-
)}
|
|
560
|
+
{identity.text}
|
|
509
561
|
</StyledItemTitle>
|
|
510
562
|
);
|
|
511
563
|
})()}
|
|
@@ -61,6 +61,9 @@ export const PANEL_LEFT_CSS = `calc(${LABEL_COL} + ${COMPACT_ROW_PAD_X + COMPACT
|
|
|
61
61
|
export const StyledCompactPanel = styled(ReqorePanel)<{
|
|
62
62
|
$headerBg: string;
|
|
63
63
|
$nested?: boolean;
|
|
64
|
+
/** True when the toolbar renders no search row (hidden via `compactToolbar`,
|
|
65
|
+
* self-hidden on a single-option form, or no toolbar at all). */
|
|
66
|
+
$tightHeader?: boolean;
|
|
64
67
|
}>`
|
|
65
68
|
> .reqore-panel-title {
|
|
66
69
|
/* The blur + translateZ exist only to make the STICKY top-level toolbar ghost
|
|
@@ -71,7 +74,17 @@ export const StyledCompactPanel = styled(ReqorePanel)<{
|
|
|
71
74
|
'backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); transform: translateZ(0);'
|
|
72
75
|
)}
|
|
73
76
|
padding-top: ${GAP_FROM_SIZE[HEADER_GAP]}px;
|
|
74
|
-
|
|
77
|
+
/* A header with a SEARCH ROW keeps the \`big\` gap below it — a tall
|
|
78
|
+
interactive control earns separation from the content it filters. A
|
|
79
|
+
header without one (search hidden via \`compactToolbar\`, self-hidden on
|
|
80
|
+
a single-option form, or no toolbar at all) is a thin strip, and at
|
|
81
|
+
\`big\` its padding stacked onto the panel content's own 8px into a 26px
|
|
82
|
+
dead band between the completion meter and the first status box —
|
|
83
|
+
rejected on build #140 as "unnecessarily big". \`normal\` lands it at
|
|
84
|
+
the boxes' own 10px rhythm, and scoping the change here keeps every
|
|
85
|
+
search-bearing form pixel-identical. */
|
|
86
|
+
padding-bottom: ${({ $tightHeader }) =>
|
|
87
|
+
$tightHeader ? GAP_FROM_SIZE.normal : GAP_FROM_SIZE[HEADER_GAP]}px;
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
/* Group framing. The HORIZONTAL rule is a real element in the header
|
|
@@ -320,6 +333,12 @@ export const StyledRowValue = styled.div<{ $color: string; $empty?: boolean }>`
|
|
|
320
333
|
text-overflow: ellipsis;
|
|
321
334
|
white-space: nowrap;
|
|
322
335
|
}
|
|
336
|
+
/* The code size chip kept above an OPEN editor. The read row puts the same chip
|
|
337
|
+
at the top of the value cell with a 2px row-gap under it; matching that here is
|
|
338
|
+
what stops the editor moving as the field opens. */
|
|
339
|
+
.options-readfirst-editing-summary {
|
|
340
|
+
margin-bottom: 2px;
|
|
341
|
+
}
|
|
323
342
|
.options-readfirst-reason {
|
|
324
343
|
font-style: italic;
|
|
325
344
|
font-size: 12px;
|
|
@@ -684,3 +703,27 @@ export const StyledGroupBody = styled.div<{
|
|
|
684
703
|
padding-bottom: 0;
|
|
685
704
|
}
|
|
686
705
|
`;
|
|
706
|
+
|
|
707
|
+
// One absorbed sibling, rendered inside its host row's container rather than
|
|
708
|
+
// on a row of its own (see `absorb_fields`). The pair this exists for is a code
|
|
709
|
+
// editor and its language: reading them as two unrelated rows one above the
|
|
710
|
+
// other said they were separate decisions, and they are not.
|
|
711
|
+
//
|
|
712
|
+
// Laid out as label-then-control on one line so it reads as a property of the
|
|
713
|
+
// element it now sits in, and separated from the editor below it by a hairline
|
|
714
|
+
// rather than a gap — the two share one container, so the seam has to look like
|
|
715
|
+
// a division inside it and not like two stacked things.
|
|
716
|
+
export const StyledAbsorbedField = styled.div`
|
|
717
|
+
display: flex;
|
|
718
|
+
align-items: center;
|
|
719
|
+
gap: 10px;
|
|
720
|
+
min-width: 0;
|
|
721
|
+
padding-bottom: 8px;
|
|
722
|
+
margin-bottom: 8px;
|
|
723
|
+
border-bottom: 1px solid ${({ theme }) => `${theme.main}22`};
|
|
724
|
+
`;
|
|
725
|
+
|
|
726
|
+
export const StyledAbsorbedLabel = styled(ReqoreP)`
|
|
727
|
+
flex: 0 0 auto;
|
|
728
|
+
margin: 0;
|
|
729
|
+
`;
|
|
@@ -24,6 +24,14 @@ export interface ICompactToolbarOptionalField {
|
|
|
24
24
|
*/
|
|
25
25
|
export interface ICompactToolbarContext {
|
|
26
26
|
readOnly?: boolean;
|
|
27
|
+
/** Which parts to render, already resolved from the engine's `compactToolbar`
|
|
28
|
+
* prop — every key present, no undefined to re-default here. */
|
|
29
|
+
parts: {
|
|
30
|
+
completion: boolean;
|
|
31
|
+
search: boolean;
|
|
32
|
+
fields: boolean;
|
|
33
|
+
help: boolean;
|
|
34
|
+
};
|
|
27
35
|
// Completion meter
|
|
28
36
|
invalidCount: number;
|
|
29
37
|
attentionCount: number;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
IQorusFormField,
|
|
3
|
+
IQorusFormSchema,
|
|
4
|
+
TQorusFormFieldSchema,
|
|
5
|
+
TQorusType,
|
|
6
|
+
} from '@qoretechnologies/ts-toolkit';
|
|
2
7
|
import { isRendererOnlyUiType } from './rendererTypes';
|
|
3
8
|
import { isUiEncodedValue } from './_structuredData/structuredData';
|
|
4
9
|
import { renderExpressionToText } from '../expressions/renderExpressionToText';
|
|
@@ -195,6 +200,13 @@ export const shouldAutoCollapseCompactOption = (
|
|
|
195
200
|
* Prefer the matching allowed_values entry's display_name (fallback `name`)
|
|
196
201
|
* over the raw stored value.
|
|
197
202
|
*
|
|
203
|
+
* A LIST carries its options under `element_allowed_values` — they constrain
|
|
204
|
+
* each element, not the list itself — so a multi-select that was missing here
|
|
205
|
+
* printed what it stores rather than what you picked: `orders, batch` for
|
|
206
|
+
* fields whose picker reads "Orders, Batch". The gap shows worst exactly where
|
|
207
|
+
* allowed values earn their keep, since the stored form is often not readable
|
|
208
|
+
* at all (a permission code, `PO_REQUIRE_TYPES`, an app-specific id).
|
|
209
|
+
*
|
|
198
210
|
* Exported (as `findAllowedValueOption`) because the form engine's value
|
|
199
211
|
* validation has to answer the SAME question — "is this stored value one of the
|
|
200
212
|
* declared choices?" — and answering it differently loses data. An
|
|
@@ -210,13 +222,31 @@ export const findAllowedValueOption = (
|
|
|
210
222
|
value: unknown,
|
|
211
223
|
schema?: TQorusFormFieldSchema
|
|
212
224
|
): any | undefined => {
|
|
213
|
-
const s = schema as
|
|
214
|
-
|
|
225
|
+
const s = schema as
|
|
226
|
+
| { allowed_values?: any[]; element_allowed_values?: any[]; items?: any[] }
|
|
227
|
+
| undefined;
|
|
228
|
+
const options =
|
|
229
|
+
(s?.allowed_values?.length && s.allowed_values) ||
|
|
230
|
+
(s?.element_allowed_values?.length && s.element_allowed_values) ||
|
|
231
|
+
s?.items;
|
|
215
232
|
if (!options?.length) {
|
|
216
233
|
return undefined;
|
|
217
234
|
}
|
|
235
|
+
// A stored element can be the bare value or the typed `{type, value}`
|
|
236
|
+
// envelope the form engine round-trips; match either against either.
|
|
237
|
+
const stored =
|
|
238
|
+
value && typeof value === 'object' && 'value' in (value as Record<string, unknown>) ?
|
|
239
|
+
(value as Record<string, unknown>).value
|
|
240
|
+
: value;
|
|
241
|
+
|
|
218
242
|
return options.find(
|
|
219
|
-
(option) =>
|
|
243
|
+
(option) =>
|
|
244
|
+
option?.value?.value === value ||
|
|
245
|
+
option?.value === value ||
|
|
246
|
+
option?.name === value ||
|
|
247
|
+
option?.value?.value === stored ||
|
|
248
|
+
option?.value === stored ||
|
|
249
|
+
option?.name === stored
|
|
220
250
|
);
|
|
221
251
|
};
|
|
222
252
|
|
|
@@ -646,12 +676,27 @@ export const getHashEntries = (
|
|
|
646
676
|
// whose entries are `{ type: 'string', value: 'x' }`) — recognise those by
|
|
647
677
|
// their strict shape (a string `type`, a `value`, and no foreign keys) so
|
|
648
678
|
// the sub-row shows 'x' rather than counting the envelope's own keys.
|
|
679
|
+
// `'value' in raw` is FALSE for a materialised-but-unset field: `fixOptions`
|
|
680
|
+
// omits the key entirely when the value is undefined, leaving `{type: 'x'}`.
|
|
681
|
+
// Requiring `value` therefore classed that envelope as a raw hash VALUE and
|
|
682
|
+
// counted its own `type` key, so an untouched Runtime Defaults collapsed to
|
|
683
|
+
// "Timeout (Seconds) 1 field" three times over while the sub-form correctly
|
|
684
|
+
// said 0/3 set. With a sub-schema present, a `type`-only object is an empty
|
|
685
|
+
// envelope, never content.
|
|
686
|
+
const isEmptyEnvelope =
|
|
687
|
+
!!subSchema &&
|
|
688
|
+
!!raw &&
|
|
689
|
+
typeof raw === 'object' &&
|
|
690
|
+
!Array.isArray(raw) &&
|
|
691
|
+
!('value' in (raw as object)) &&
|
|
692
|
+
Object.keys(raw as object).every((k) => k === 'type' || k === 'is_expression');
|
|
649
693
|
const isFieldShape =
|
|
650
694
|
(!!subSchema &&
|
|
651
695
|
!!raw &&
|
|
652
696
|
typeof raw === 'object' &&
|
|
653
697
|
!Array.isArray(raw) &&
|
|
654
698
|
'value' in (raw as object)) ||
|
|
699
|
+
isEmptyEnvelope ||
|
|
655
700
|
isTypedEnvelope(raw);
|
|
656
701
|
const subOption: IQorusFormField =
|
|
657
702
|
isFieldShape ?
|
|
@@ -793,3 +838,117 @@ export const getFirstAttentionOptionName = (
|
|
|
793
838
|
|
|
794
839
|
return undefined;
|
|
795
840
|
};
|
|
841
|
+
|
|
842
|
+
/* ------------------------------------------------------------------------- */
|
|
843
|
+
/* Record identity — which field says WHICH item this is */
|
|
844
|
+
/* */
|
|
845
|
+
/* Lives here, beside the formatters it uses, rather than in the view that */
|
|
846
|
+
/* first needed it: it is data logic, not rendering, and BOTH list renderers */
|
|
847
|
+
/* plus their tests import it. Keeping it in `SchemaDataView` dragged React, */
|
|
848
|
+
/* styled-components and every Reqore component it renders into any module */
|
|
849
|
+
/* that only wanted to know an item's name — which broke `arrayAutoField`'s */
|
|
850
|
+
/* mocked-Reqore test the moment the editable list started using it. */
|
|
851
|
+
/* ------------------------------------------------------------------------- */
|
|
852
|
+
|
|
853
|
+
/** A UI-encoded value carries its type alongside it; the record's own value is inside. */
|
|
854
|
+
const unwrap = (value: unknown): unknown =>
|
|
855
|
+
isUiEncodedValue(value) ? (value as { value: unknown }).value : value;
|
|
856
|
+
|
|
857
|
+
/** Whether a value is worth a row of its own. `false` and `0` are values. */
|
|
858
|
+
export const isSet = (value: unknown): boolean =>
|
|
859
|
+
value !== undefined && value !== null && value !== '' && !(Array.isArray(value) && !value.length);
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* The keys to render, in the order to render them: the schema's own order first
|
|
863
|
+
* (that is the order the form puts the fields in), then anything stored that the
|
|
864
|
+
* schema does not mention, so undescribed data is shown rather than dropped.
|
|
865
|
+
*/
|
|
866
|
+
export const orderedKeys = (record: Record<string, unknown>, schema: IQorusFormSchema): string[] => {
|
|
867
|
+
const described = Object.keys(schema).filter((key) => isSet(unwrap(record[key])));
|
|
868
|
+
const extra = Object.keys(record).filter((key) => !(key in schema) && isSet(unwrap(record[key])));
|
|
869
|
+
return [...described, ...extra];
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
/** A field whose content is source code. `ui_type` is what the form renders by,
|
|
873
|
+
* so it is what the preview reads by too — the storage type is just `string`. */
|
|
874
|
+
export const isCodeField = (fieldSchema: TQorusFormFieldSchema | undefined): boolean => {
|
|
875
|
+
const uiType = (fieldSchema as { ui_type?: string } | undefined)?.ui_type;
|
|
876
|
+
return uiType === 'code-editor';
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* The field whose value heads the item — the first DECLARED one holding a plain
|
|
881
|
+
* scalar.
|
|
882
|
+
*
|
|
883
|
+
* Schema order, not value order: the first declared field is the one the form
|
|
884
|
+
* puts at the top of an item, which is the one that says which item it is. A
|
|
885
|
+
* code body or a nested level is skipped, not because it is unimportant but
|
|
886
|
+
* because it has no one-line form — it belongs in the rows below where it can
|
|
887
|
+
* actually be read.
|
|
888
|
+
*
|
|
889
|
+
* One definition, used both to RENDER the heading and to omit that field from
|
|
890
|
+
* the rows. Computing it twice is how the heading and the rows start disagreeing
|
|
891
|
+
* about which field was promoted, and the item shows its name twice or not at
|
|
892
|
+
* all.
|
|
893
|
+
*/
|
|
894
|
+
export const titleKeyFor = (
|
|
895
|
+
record: Record<string, unknown>,
|
|
896
|
+
schema: IQorusFormSchema
|
|
897
|
+
): string | undefined =>
|
|
898
|
+
orderedKeys(record, schema).find((key) => {
|
|
899
|
+
const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
|
|
900
|
+
if (isCodeField(fieldSchema)) {
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
const raw = unwrap(record[key]);
|
|
904
|
+
return typeof raw === 'string' || typeof raw === 'number';
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
/** What identifies one record: which field was promoted, and how to draw it. */
|
|
908
|
+
export interface IRecordIdentity {
|
|
909
|
+
/** The promoted field's key, so a caller can omit it from the rows below. */
|
|
910
|
+
key: string;
|
|
911
|
+
/** The value, formatted the way its own row would format it. */
|
|
912
|
+
text: string;
|
|
913
|
+
/** The promoted field's label — a caption for the heading, not a second line. */
|
|
914
|
+
label: string;
|
|
915
|
+
/** A literal keeps its mono face; a chosen label is prose. */
|
|
916
|
+
mono: boolean;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* The identity of one record, for any surface that heads a list item with it.
|
|
921
|
+
*
|
|
922
|
+
* Exported because the preview is not the only place a list of records is shown:
|
|
923
|
+
* the editable list (`ArrayAuto`) heads the same records, and it must promote the
|
|
924
|
+
* same field and format it the same way. Two implementations would drift the
|
|
925
|
+
* moment one of them learned about a new field type, and the reader would meet
|
|
926
|
+
* an item called `init` in the preview and `#1` in the editor.
|
|
927
|
+
*/
|
|
928
|
+
export const recordIdentity = (
|
|
929
|
+
record: Record<string, unknown>,
|
|
930
|
+
schema: IQorusFormSchema | undefined
|
|
931
|
+
): IRecordIdentity | undefined => {
|
|
932
|
+
if (!schema) {
|
|
933
|
+
return undefined;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const key = titleKeyFor(record, schema);
|
|
937
|
+
|
|
938
|
+
if (!key) {
|
|
939
|
+
return undefined;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
|
|
943
|
+
const raw = unwrap(record[key]);
|
|
944
|
+
|
|
945
|
+
return {
|
|
946
|
+
key,
|
|
947
|
+
text: formatOptionValue({ type: fieldSchema?.type, value: raw } as IQorusFormField, fieldSchema),
|
|
948
|
+
label: fieldLabel(key, fieldSchema),
|
|
949
|
+
mono: !findAllowedValueOption(raw, fieldSchema),
|
|
950
|
+
};
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
export const fieldLabel = (key: string, fieldSchema: TQorusFormFieldSchema | undefined): string =>
|
|
954
|
+
(fieldSchema as { display_name?: string } | undefined)?.display_name || key;
|
|
@@ -569,7 +569,9 @@ export const FormField = <T extends TFormFieldType>({
|
|
|
569
569
|
return (
|
|
570
570
|
<ReqoreControlGroup vertical size='tiny'>
|
|
571
571
|
<ReqoreSpan effect={{ opacity: 0.6, uppercase: true, weight: 'bold' }} size='tiny'>
|
|
572
|
-
Select one:
|
|
572
|
+
{/* Headed by the field's own name; "Select one:" only when it has
|
|
573
|
+
none. Same rule as FieldAllowedValuesCheckGroup. */}
|
|
574
|
+
{display_name ? `${display_name}:` : 'Select one:'}
|
|
573
575
|
</ReqoreSpan>
|
|
574
576
|
{allowed_values.map((item, index) => {
|
|
575
577
|
const itemValue = item.value?.value;
|