@qoretechnologies/reqraft 0.10.43-pr.95.gf7f712f → 0.10.43
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/composer/Composer.d.ts.map +1 -1
- package/dist/components/composer/Composer.js +5 -1
- package/dist/components/composer/Composer.js.map +1 -1
- 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 +279 -41
- 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 +117 -88
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/_structuredData/structuredData.d.ts +15 -0
- package/dist/components/form/engine/_structuredData/structuredData.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/structuredData.js +22 -1
- package/dist/components/form/engine/_structuredData/structuredData.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 +21 -4
- 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 +141 -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 +64 -14
- 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/components/searchFilterBar/SearchFilterBar.d.ts +12 -1
- package/dist/components/searchFilterBar/SearchFilterBar.d.ts.map +1 -1
- package/dist/components/searchFilterBar/SearchFilterBar.js +2 -2
- package/dist/components/searchFilterBar/SearchFilterBar.js.map +1 -1
- package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
- package/dist/components/ticketThread/TicketThread.js +1 -1
- package/dist/components/ticketThread/TicketThread.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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/composer/Composer.tsx +7 -3
- package/src/components/form/engine/CompactRow.tsx +391 -29
- 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 +123 -0
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +232 -159
- package/src/components/form/engine/_structuredData/structuredData.ts +20 -0
- package/src/components/form/engine/compactRowStyles.ts +38 -1
- package/src/components/form/engine/compactToolbarContext.ts +8 -0
- package/src/components/form/engine/readFirst.ts +166 -5
- 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.stories.tsx +47 -1
- package/src/components/form/fields/auto/AutoFormField.tsx +73 -11
- 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/components/searchFilterBar/SearchFilterBar.stories.tsx +62 -0
- package/src/components/searchFilterBar/SearchFilterBar.tsx +13 -0
- package/src/components/ticketThread/TicketThread.tsx +7 -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 +1 -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,
|
|
@@ -8,9 +14,23 @@ import React from 'react';
|
|
|
8
14
|
import styled from 'styled-components';
|
|
9
15
|
import { useContextSelector } from 'use-context-selector';
|
|
10
16
|
import { CompactRowContext } from '../compactRowContext';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
import {
|
|
18
|
+
MONO_FONT_STACK,
|
|
19
|
+
StyledCodePreview,
|
|
20
|
+
StyledRowLabel,
|
|
21
|
+
StyledRowValue,
|
|
22
|
+
} from '../compactRowStyles';
|
|
23
|
+
import {
|
|
24
|
+
fieldLabel,
|
|
25
|
+
findAllowedValueOption,
|
|
26
|
+
formatOptionValue,
|
|
27
|
+
getAllowedValueImage,
|
|
28
|
+
isCodeField,
|
|
29
|
+
orderedKeys,
|
|
30
|
+
recordIdentity,
|
|
31
|
+
titleKeyFor,
|
|
32
|
+
} from '../readFirst';
|
|
33
|
+
import { isEmptyUiEnvelope, isUiEncodedValue } from './structuredData';
|
|
14
34
|
|
|
15
35
|
/**
|
|
16
36
|
* A read-only rendering of a value THROUGH the schema that describes it.
|
|
@@ -26,10 +46,11 @@ import { isUiEncodedValue } from './structuredData';
|
|
|
26
46
|
* So a described value is rendered the way the form asked for it, in the form's
|
|
27
47
|
* own clothes:
|
|
28
48
|
*
|
|
29
|
-
* - Field names use `
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
49
|
+
* - Field names use `StyledFieldLabel` (a `StyledRowLabel` in the read-only
|
|
50
|
+
* eyebrow treatment) and values use `StyledRowValue` — the very components the
|
|
51
|
+
* option rows above are built from. Not a copy of their CSS: a copy is a
|
|
52
|
+
* second definition of "what a form label looks like", and the two drift the
|
|
53
|
+
* first time either is touched.
|
|
33
54
|
* - Values are formatted by the same `formatOptionValue` the rows use, so an
|
|
34
55
|
* `allowed_values` entry resolves to its display name, a bool to Yes/No, a
|
|
35
56
|
* date to the app's format. The same words appear in the summary, the preview
|
|
@@ -129,10 +150,38 @@ const StyledItemTitle = styled.div<{ $color: string; $mono: boolean; $accent: st
|
|
|
129
150
|
${({ $mono }) => ($mono ? `font-family: ${MONO_FONT_STACK}; font-size: 15px;` : '')}
|
|
130
151
|
`;
|
|
131
152
|
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Does this record have anything to show BELOW its heading?
|
|
155
|
+
*
|
|
156
|
+
* The heading consumes the identifying field, so a record holding only that has
|
|
157
|
+
* an empty body — and a rule drawn down an empty body is a line to nowhere.
|
|
158
|
+
*/
|
|
159
|
+
const hasContentUnderHeading = (record: unknown, schema?: IQorusFormSchema): boolean => {
|
|
160
|
+
if (!isRecord(record)) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
const titleKey = titleKeyFor(record, schema);
|
|
164
|
+
|
|
165
|
+
return Object.entries(record).some(
|
|
166
|
+
([key, value]) => key !== titleKey && value !== undefined && value !== null && value !== ''
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** The rule tying an item's fields to its number.
|
|
171
|
+
*
|
|
172
|
+
* Same 3px as the heading's accent bar directly above it: at 1px the two read as
|
|
173
|
+
* two different devices stacked on each other rather than one rule running down
|
|
174
|
+
* the item, and the padding drops to 8px so the text stays on the same column.
|
|
175
|
+
*
|
|
176
|
+
* The rule is drawn only when something is actually under the heading. The
|
|
177
|
+
* heading is pulled left of the rule by its own negative margin, so an item
|
|
178
|
+
* whose record holds nothing but its identity showed a 1-2px sliver of line and
|
|
179
|
+
* nothing else — which reads as a rendering artefact, because that is what it
|
|
180
|
+
* was. It keeps a transparent border in that case so the column does not shift.
|
|
181
|
+
*/
|
|
182
|
+
const StyledItemFields = styled.div<{ $border: string; $rule: boolean }>`
|
|
183
|
+
border-left: 3px solid ${({ $border, $rule }) => ($rule ? $border : 'transparent')};
|
|
184
|
+
padding-left: 8px;
|
|
136
185
|
min-width: 0;
|
|
137
186
|
`;
|
|
138
187
|
|
|
@@ -144,6 +193,30 @@ const StyledItemFields = styled.div<{ $border: string }>`
|
|
|
144
193
|
* and leave the items ragged against each other, which reads as a rendering
|
|
145
194
|
* fault rather than as a list.
|
|
146
195
|
*/
|
|
196
|
+
/**
|
|
197
|
+
* A record field's name, in the detail-page vocabulary.
|
|
198
|
+
*
|
|
199
|
+
* Uppercase, tracked and dimmed, so a field name reads as furniture and the
|
|
200
|
+
* VALUE beside it is what the eye lands on. Same treatment the read-only
|
|
201
|
+
* interface panels use, which is the point: an in-editor list of records and a
|
|
202
|
+
* detail-page panel showing the same shape should not look like two components.
|
|
203
|
+
*
|
|
204
|
+
* A variant rather than a change to `StyledRowLabel`: that one is the FORM's own
|
|
205
|
+
* option-row label, and the form's fields are not read-only furniture — they are
|
|
206
|
+
* the things you are about to edit. Composing it keeps the colour, weight base
|
|
207
|
+
* and wrapping behaviour in one place instead of copying them.
|
|
208
|
+
*
|
|
209
|
+
* Applies to the label only. A VALUE is never uppercased: values are frequently
|
|
210
|
+
* case-sensitive — a service method is `onOrderStatus`, not `ONORDERSTATUS` —
|
|
211
|
+
* and the item heading is a value.
|
|
212
|
+
*/
|
|
213
|
+
const StyledFieldLabel = styled(StyledRowLabel)`
|
|
214
|
+
font-size: 12px;
|
|
215
|
+
text-transform: uppercase;
|
|
216
|
+
letter-spacing: 0.06em;
|
|
217
|
+
opacity: 0.55;
|
|
218
|
+
`;
|
|
219
|
+
|
|
147
220
|
const StyledFields = styled.div`
|
|
148
221
|
/* The supporting fields step in under the heading, so the heading keeps the
|
|
149
222
|
left edge to itself. This is the half that answers "the name is written in
|
|
@@ -224,59 +297,6 @@ const unwrap = (item: unknown): unknown => (isUiEncodedValue(item) ? item.value
|
|
|
224
297
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
225
298
|
!!value && typeof value === 'object' && !Array.isArray(value);
|
|
226
299
|
|
|
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
300
|
/**
|
|
281
301
|
* The width of the shared label column, measured from the longest name that will
|
|
282
302
|
* actually be rendered.
|
|
@@ -286,6 +306,12 @@ const fieldLabel = (key: string, fieldSchema: TQorusFormFieldSchema | undefined)
|
|
|
286
306
|
* the `%` ceiling stops one long name from taking the whole row when the inset
|
|
287
307
|
* is narrow — a phone, or a deeply nested level.
|
|
288
308
|
*/
|
|
309
|
+
/** Approximate rendered width of one label character, in px. See `labelColumn`. */
|
|
310
|
+
const LABEL_CHAR_PX = 9.5;
|
|
311
|
+
|
|
312
|
+
/** Gap to the value, so the longest label is not flush against it. */
|
|
313
|
+
const LABEL_SLACK_PX = 14;
|
|
314
|
+
|
|
289
315
|
const labelColumn = (value: unknown, schema: IQorusFormSchema): string => {
|
|
290
316
|
const inner = unwrap(value);
|
|
291
317
|
const records = (Array.isArray(inner) ? inner.map(unwrap) : [inner]).filter(isRecord);
|
|
@@ -297,9 +323,26 @@ const labelColumn = (value: unknown, schema: IQorusFormSchema): string => {
|
|
|
297
323
|
);
|
|
298
324
|
return Math.max(widest, own);
|
|
299
325
|
}, 0);
|
|
300
|
-
|
|
326
|
+
// px, not `ch`. `ch` is the base font's `0` advance and knows nothing about
|
|
327
|
+
// the label's own treatment — uppercase, semibold, `letter-spacing: 0.06em`,
|
|
328
|
+
// at 12px — so it under-measures by about a tenth. With `overflow-wrap:
|
|
329
|
+
// anywhere` on the label, an under-measured column does not ellipsise, it
|
|
330
|
+
// BREAKS THE NAME IN HALF: `LEGACY_FLAG` came out over two lines the moment
|
|
331
|
+
// the labels became uppercase. Erring high costs a few px of gap; erring low
|
|
332
|
+
// costs a broken word.
|
|
333
|
+
return longest ? `min(${Math.ceil(longest * LABEL_CHAR_PX + LABEL_SLACK_PX)}px, 45%)` : '140px';
|
|
301
334
|
};
|
|
302
335
|
|
|
336
|
+
/**
|
|
337
|
+
* `true` when a value is an address a browser can open.
|
|
338
|
+
*
|
|
339
|
+
* Deliberately per VALUE, not per field: a field named `url` frequently holds
|
|
340
|
+
* something that is not one — a bare path, a regex, a template — and offering
|
|
341
|
+
* those a link promises a destination there is not.
|
|
342
|
+
*/
|
|
343
|
+
const isNavigableUrl = (value: unknown): value is string =>
|
|
344
|
+
typeof value === 'string' && /^(https?|wss?|ftps?):\/\//i.test(value);
|
|
345
|
+
|
|
303
346
|
/** One field's value, rendered the way its row above renders it. */
|
|
304
347
|
const FieldValue = ({
|
|
305
348
|
field,
|
|
@@ -345,7 +388,22 @@ const FieldValue = ({
|
|
|
345
388
|
style={{ flexShrink: 0, marginRight: 6, verticalAlign: 'middle' }}
|
|
346
389
|
/>
|
|
347
390
|
: null}
|
|
348
|
-
{
|
|
391
|
+
{isNavigableUrl(field.value) ?
|
|
392
|
+
// The address IS the link — it used to render as inert text with a
|
|
393
|
+
// separate icon button beside it to open it, which is two things
|
|
394
|
+
// where there is one. ReqoreLink renders a real anchor, so
|
|
395
|
+
// middle-click and "copy link address" still work;
|
|
396
|
+
// `stopPropagation` because the row around it toggles.
|
|
397
|
+
<ReqoreLink
|
|
398
|
+
className='schema-view-open-url'
|
|
399
|
+
href={field.value}
|
|
400
|
+
target='_blank'
|
|
401
|
+
rel='noreferrer noopener'
|
|
402
|
+
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
|
403
|
+
>
|
|
404
|
+
{formatted}
|
|
405
|
+
</ReqoreLink>
|
|
406
|
+
: formatted}
|
|
349
407
|
</span>
|
|
350
408
|
}
|
|
351
409
|
</StyledRowValue>
|
|
@@ -370,92 +428,109 @@ const SchemaRecord = ({
|
|
|
370
428
|
// The SAME host renderer the row above uses for a `code-editor` field. Read
|
|
371
429
|
// from context rather than threaded through props: a nested level is arbitrarily
|
|
372
430
|
// deep, and the renderer is a property of the form, not of any one level.
|
|
373
|
-
const codePreviewRenderer = useContextSelector(
|
|
374
|
-
CompactRowContext,
|
|
375
|
-
(v) => v.codePreviewRenderer
|
|
376
|
-
);
|
|
431
|
+
const codePreviewRenderer = useContextSelector(CompactRowContext, (v) => v.codePreviewRenderer);
|
|
377
432
|
const cHover = useContextSelector(CompactRowContext, (v) => v.cHover);
|
|
378
433
|
|
|
379
434
|
return (
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
+
<StyledFields className='schema-view-fields'>
|
|
436
|
+
{orderedKeys(record, schema)
|
|
437
|
+
.filter((key) => key !== skipKey)
|
|
438
|
+
.map((key) => {
|
|
439
|
+
const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
|
|
440
|
+
const nested = (fieldSchema as { arg_schema?: IQorusFormSchema } | undefined)?.arg_schema;
|
|
441
|
+
// A schema-declared field the form materialised but never filled in
|
|
442
|
+
// arrives as `{type: 'int'}` with no `value` key. `unwrap` cannot see
|
|
443
|
+
// that — `isUiEncodedValue` requires a `value` — so it fell through as
|
|
444
|
+
// CONTENT and was wrapped as one below, and `formatOptionValue` counted
|
|
445
|
+
// the envelope's own `type` key: an untouched Runtime Defaults read
|
|
446
|
+
// "Timeout (Seconds) 1 field" three times over while the sub-form it
|
|
447
|
+
// previews correctly said 0/3 set. `fieldSchema` is what licenses the
|
|
448
|
+
// reading: the schema declares this field, so a `{type}`-only object
|
|
449
|
+
// under it is an empty envelope, never somebody's data.
|
|
450
|
+
const raw = record[key];
|
|
451
|
+
// `!nested` is the discriminator, and it is not a detail: a field that
|
|
452
|
+
// declares its OWN level describes what is inside it, so `{type: 'default'}`
|
|
453
|
+
// there is content — the scheme's Scheme Type, reading "Default RBAC" —
|
|
454
|
+
// not an envelope. Only a leaf can be empty in this sense.
|
|
455
|
+
const value =
|
|
456
|
+
fieldSchema && !nested && isEmptyUiEnvelope(raw) ? undefined : unwrap(raw);
|
|
457
|
+
const label = fieldLabel(key, fieldSchema);
|
|
458
|
+
const labelWithType =
|
|
459
|
+
showTypes && fieldSchema?.type ? `${label} · ${fieldSchema.type}` : label;
|
|
460
|
+
|
|
461
|
+
// A field that describes its own level renders AS that level, indented
|
|
462
|
+
// under its name — the same decision, one step down.
|
|
463
|
+
if (nested && (isRecord(value) || Array.isArray(value))) {
|
|
464
|
+
return (
|
|
465
|
+
<React.Fragment key={key}>
|
|
466
|
+
<StyledNestedLabel>
|
|
467
|
+
<StyledFieldLabel $color={colors.key}>{labelWithType}</StyledFieldLabel>
|
|
468
|
+
</StyledNestedLabel>
|
|
469
|
+
<StyledNested $border={colors.border}>
|
|
470
|
+
<SchemaLevel
|
|
471
|
+
value={value}
|
|
472
|
+
schema={nested}
|
|
473
|
+
showTypes={showTypes}
|
|
474
|
+
colors={colors}
|
|
475
|
+
/>
|
|
476
|
+
</StyledNested>
|
|
477
|
+
</React.Fragment>
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// A code value is source, and source is read as source: the host's renderer
|
|
482
|
+
// gives it the syntax highlighting (and read-only LSP hover) it has in the
|
|
483
|
+
// Source Code field, and "Show more" keeps a long body from taking over the
|
|
484
|
+
// preview. Flattening it to one mono line — which is what a plain value cell
|
|
485
|
+
// does — throws all of that away for a field whose whole content is code.
|
|
486
|
+
if (isCodeField(fieldSchema) && typeof value === 'string') {
|
|
487
|
+
return (
|
|
488
|
+
<React.Fragment key={key}>
|
|
489
|
+
<StyledNestedLabel>
|
|
490
|
+
<StyledFieldLabel $color={colors.key}>{labelWithType}</StyledFieldLabel>
|
|
491
|
+
</StyledNestedLabel>
|
|
492
|
+
<StyledCodeCell onClick={(event) => event.stopPropagation()}>
|
|
493
|
+
<ReqoreCollapsibleContent
|
|
494
|
+
maxCollapsedHeight={96}
|
|
495
|
+
buttonProps={{ className: 'options-readfirst-viewmore' }}
|
|
435
496
|
>
|
|
436
|
-
{
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
497
|
+
{codePreviewRenderer ?
|
|
498
|
+
codePreviewRenderer({
|
|
499
|
+
value,
|
|
500
|
+
name: key,
|
|
501
|
+
schema: fieldSchema,
|
|
502
|
+
options: schema,
|
|
503
|
+
values: record as never,
|
|
504
|
+
})
|
|
505
|
+
: <StyledCodePreview
|
|
506
|
+
className='options-readfirst-code'
|
|
507
|
+
$bg={cHover}
|
|
508
|
+
$border={`${colors.border}88`}
|
|
509
|
+
$fg={colors.key}
|
|
510
|
+
>
|
|
511
|
+
{value}
|
|
512
|
+
</StyledCodePreview>
|
|
513
|
+
}
|
|
514
|
+
</ReqoreCollapsibleContent>
|
|
515
|
+
</StyledCodeCell>
|
|
516
|
+
</React.Fragment>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
444
519
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
520
|
+
return (
|
|
521
|
+
<React.Fragment key={key}>
|
|
522
|
+
<StyledFieldLabel $color={colors.key} title={label}>
|
|
523
|
+
{labelWithType}
|
|
524
|
+
</StyledFieldLabel>
|
|
525
|
+
<FieldValue
|
|
526
|
+
field={{ type: fieldSchema?.type, value } as IQorusFormField}
|
|
527
|
+
fieldSchema={fieldSchema}
|
|
528
|
+
color={colors.key}
|
|
529
|
+
/>
|
|
530
|
+
</React.Fragment>
|
|
531
|
+
);
|
|
532
|
+
})}
|
|
533
|
+
</StyledFields>
|
|
459
534
|
);
|
|
460
535
|
};
|
|
461
536
|
|
|
@@ -481,16 +556,17 @@ const SchemaLevel = ({
|
|
|
481
556
|
return (
|
|
482
557
|
<StyledItem key={index} className='schema-view-item'>
|
|
483
558
|
<StyledMarker $color={colors.muted}>{index + 1}.</StyledMarker>
|
|
484
|
-
<StyledItemFields
|
|
559
|
+
<StyledItemFields
|
|
560
|
+
$border={colors.border}
|
|
561
|
+
$rule={hasContentUnderHeading(record, schema)}
|
|
562
|
+
>
|
|
485
563
|
{isRecord(record) ?
|
|
486
564
|
<>
|
|
487
565
|
{(() => {
|
|
488
|
-
const
|
|
489
|
-
if (!
|
|
566
|
+
const identity = recordIdentity(record, schema);
|
|
567
|
+
if (!identity) {
|
|
490
568
|
return null;
|
|
491
569
|
}
|
|
492
|
-
const fieldSchema = schema[titleKey] as TQorusFormFieldSchema | undefined;
|
|
493
|
-
const raw = unwrap(record[titleKey]);
|
|
494
570
|
return (
|
|
495
571
|
<StyledItemTitle
|
|
496
572
|
className='schema-view-item-title'
|
|
@@ -498,14 +574,11 @@ const SchemaLevel = ({
|
|
|
498
574
|
// A literal keeps the mono treatment it has as a value —
|
|
499
575
|
// promoting it must not change what it IS. A chosen
|
|
500
576
|
// label ("Default RBAC") is prose and stays prose.
|
|
501
|
-
$mono={
|
|
577
|
+
$mono={identity.mono}
|
|
502
578
|
$accent={colors.accent}
|
|
503
|
-
title={
|
|
579
|
+
title={identity.label}
|
|
504
580
|
>
|
|
505
|
-
{
|
|
506
|
-
{ type: fieldSchema?.type, value: raw } as IQorusFormField,
|
|
507
|
-
fieldSchema
|
|
508
|
-
)}
|
|
581
|
+
{identity.text}
|
|
509
582
|
</StyledItemTitle>
|
|
510
583
|
);
|
|
511
584
|
})()}
|
|
@@ -52,6 +52,26 @@ export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
52
52
|
export const isStructuredContainerValue = (value: unknown): boolean =>
|
|
53
53
|
Array.isArray(value) || isRecord(value);
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* A field the form materialised but left UNSET: `{type: 'int'}`, no `value` key.
|
|
57
|
+
*
|
|
58
|
+
* `fixOptions` omits the key entirely when the value is undefined, so this is
|
|
59
|
+
* what an untouched sub-field of a schema-declared hash looks like on the wire.
|
|
60
|
+
* It is an EMPTY ENVELOPE, never content — and every renderer has to agree on
|
|
61
|
+
* that, because the ones that do not count the envelope's own `type` key and
|
|
62
|
+
* report an unset field as "1 field" while the sub-form correctly says 0 set.
|
|
63
|
+
*
|
|
64
|
+
* Deliberately separate from {@link isUiEncodedValue}, which requires a `value`
|
|
65
|
+
* and therefore cannot see this shape. Callers gate on knowing the field is
|
|
66
|
+
* schema-declared: without a schema saying so, a `{type: …}` object could
|
|
67
|
+
* legitimately be somebody's data.
|
|
68
|
+
*/
|
|
69
|
+
export const isEmptyUiEnvelope = (value: unknown): boolean =>
|
|
70
|
+
isRecord(value) &&
|
|
71
|
+
typeof value.type === 'string' &&
|
|
72
|
+
!('value' in value) &&
|
|
73
|
+
Object.keys(value).every((key) => UI_ENVELOPE_KEYS.has(key));
|
|
74
|
+
|
|
55
75
|
export const isUiEncodedValue = (value: unknown): value is IUiEncodedValue => {
|
|
56
76
|
if (!isRecord(value) || typeof value.type !== 'string' || !('value' in value)) {
|
|
57
77
|
return false;
|
|
@@ -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
|
|
@@ -690,3 +703,27 @@ export const StyledGroupBody = styled.div<{
|
|
|
690
703
|
padding-bottom: 0;
|
|
691
704
|
}
|
|
692
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;
|