@qoretechnologies/reqraft 0.10.46-pr.95.ge595a13 → 0.10.46
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 +14 -0
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +199 -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 +65 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +276 -108
- 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 +55 -13
- 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.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +15 -3
- 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 +24 -0
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +84 -6
- 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/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 +81 -27
- 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/expressions.d.ts.map +1 -1
- package/dist/helpers/expressions.js +15 -2
- package/dist/helpers/expressions.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 +156 -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 +279 -33
- package/src/components/form/engine/CompactToolbar.tsx +105 -98
- package/src/components/form/engine/FormEngine.stories.tsx +995 -12
- package/src/components/form/engine/FormEngine.tsx +377 -94
- package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +104 -0
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +158 -101
- package/src/components/form/engine/_structuredData/structuredData.ts +20 -0
- package/src/components/form/engine/compactRowStyles.ts +55 -1
- package/src/components/form/engine/compactToolbarContext.ts +8 -0
- package/src/components/form/engine/readFirst.ts +125 -35
- 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/ArrayAutoField.stories.tsx +81 -0
- 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 +389 -1
- package/src/components/form/fields/template/TemplateField.tsx +100 -35
- 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/expressions.ts +16 -2
- package/src/helpers/optionUiTypes.ts +161 -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
|
@@ -55,6 +55,78 @@ const methodValue = [
|
|
|
55
55
|
{ type: 'hash', value: { name: 'onConnect', desc: 'connection lifecycle hook' } },
|
|
56
56
|
];
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* A schema whose hash values hold hash values of their own — three levels, not
|
|
60
|
+
* one.
|
|
61
|
+
*
|
|
62
|
+
* Every other fixture here is a flat record, and a flat record cannot show what
|
|
63
|
+
* this view does with depth: whether a nested level is legible, whether the
|
|
64
|
+
* rules stack sensibly, whether a heading two levels down still reads as a
|
|
65
|
+
* heading. Judging the treatment on a single `key: value` pair is what the
|
|
66
|
+
* review said was impossible, and it was right.
|
|
67
|
+
*/
|
|
68
|
+
const NestedSchema = {
|
|
69
|
+
name: { type: 'string', display_name: 'Endpoint' },
|
|
70
|
+
auth: {
|
|
71
|
+
type: 'hash',
|
|
72
|
+
display_name: 'Authentication',
|
|
73
|
+
arg_schema: {
|
|
74
|
+
scheme: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
display_name: 'Scheme',
|
|
77
|
+
allowed_values: [
|
|
78
|
+
{ value: 'oauth2', display_name: 'OAuth2' },
|
|
79
|
+
{ value: 'basic', display_name: 'Basic' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
token: {
|
|
83
|
+
type: 'hash',
|
|
84
|
+
display_name: 'Token',
|
|
85
|
+
arg_schema: {
|
|
86
|
+
url: { type: 'string', display_name: 'Token URL' },
|
|
87
|
+
ttl: { type: 'int', display_name: 'Lifetime (s)' },
|
|
88
|
+
rotate: { type: 'bool', display_name: 'Rotate Automatically' },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
retry: {
|
|
94
|
+
type: 'hash',
|
|
95
|
+
display_name: 'Retry Policy',
|
|
96
|
+
arg_schema: {
|
|
97
|
+
attempts: { type: 'int', display_name: 'Attempts' },
|
|
98
|
+
backoff: { type: 'string', display_name: 'Backoff' },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
} as never;
|
|
102
|
+
|
|
103
|
+
const nestedValue = [
|
|
104
|
+
{
|
|
105
|
+
type: 'hash',
|
|
106
|
+
value: {
|
|
107
|
+
name: 'POST /orders',
|
|
108
|
+
auth: {
|
|
109
|
+
scheme: 'oauth2',
|
|
110
|
+
token: {
|
|
111
|
+
url: 'https://auth.example.com/oauth2/token',
|
|
112
|
+
ttl: 3600,
|
|
113
|
+
rotate: true,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
retry: { attempts: 3, backoff: 'exponential' },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
// A second record that fills only part of the tree: the nested level is
|
|
121
|
+
// present but shallow, which is where "no data under the label" shows up.
|
|
122
|
+
type: 'hash',
|
|
123
|
+
value: {
|
|
124
|
+
name: 'GET /orders/{id}',
|
|
125
|
+
auth: { scheme: 'basic' },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
|
|
58
130
|
const colors = { key: '#f1f0ee', muted: '#a0a0a0', border: '#a0a0a066', accent: '#3b8eea' };
|
|
59
131
|
|
|
60
132
|
export const Schemes: Story = {
|
|
@@ -127,3 +199,35 @@ export const Undescribed: Story = {
|
|
|
127
199
|
await expect(label!.offsetHeight).toBeLessThan(lineHeight * 1.6);
|
|
128
200
|
},
|
|
129
201
|
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The deeper case: hash values holding hash values.
|
|
205
|
+
*
|
|
206
|
+
* Asked for in review because the flat fixtures could not answer whether the
|
|
207
|
+
* treatment survives nesting — three levels here (endpoint → authentication →
|
|
208
|
+
* token), plus a second record that stops after one, so a nested level with
|
|
209
|
+
* nothing under it renders beside one that is full.
|
|
210
|
+
*/
|
|
211
|
+
export const NestedSchemas: Story = {
|
|
212
|
+
parameters: {
|
|
213
|
+
docs: {
|
|
214
|
+
description: {
|
|
215
|
+
story:
|
|
216
|
+
'A described list whose records nest two levels deeper: an endpoint carrying an Authentication hash, which itself carries a Token hash of URL, lifetime and a bool. The second record fills only the first nested level, so a shallow branch sits next to a full one.',
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
args: { value: nestedValue, schema: NestedSchema, colors },
|
|
221
|
+
play: async ({ canvasElement }) => {
|
|
222
|
+
const titles = [...canvasElement.querySelectorAll('.schema-view-item-title')].map((element) =>
|
|
223
|
+
(element.textContent ?? '').trim()
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
await expect(titles).toEqual(['POST /orders', 'GET /orders/{id}']);
|
|
227
|
+
// The deepest level actually rendered: a label from the third tier, and the
|
|
228
|
+
// bool beneath it resolved to a word rather than printed as `true`.
|
|
229
|
+
await expect(canvasElement.textContent).toContain('Token URL');
|
|
230
|
+
await expect(canvasElement.textContent).toContain('Lifetime (s)');
|
|
231
|
+
await expect(canvasElement.textContent).not.toContain('[object Object]');
|
|
232
|
+
},
|
|
233
|
+
};
|
|
@@ -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,7 +14,12 @@ 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 {
|
|
17
|
+
import {
|
|
18
|
+
MONO_FONT_STACK,
|
|
19
|
+
StyledCodePreview,
|
|
20
|
+
StyledRowLabel,
|
|
21
|
+
StyledRowValue,
|
|
22
|
+
} from '../compactRowStyles';
|
|
12
23
|
import {
|
|
13
24
|
fieldLabel,
|
|
14
25
|
findAllowedValueOption,
|
|
@@ -19,7 +30,7 @@ import {
|
|
|
19
30
|
recordIdentity,
|
|
20
31
|
titleKeyFor,
|
|
21
32
|
} from '../readFirst';
|
|
22
|
-
import { isUiEncodedValue } from './structuredData';
|
|
33
|
+
import { isEmptyUiEnvelope, isUiEncodedValue } from './structuredData';
|
|
23
34
|
|
|
24
35
|
/**
|
|
25
36
|
* A read-only rendering of a value THROUGH the schema that describes it.
|
|
@@ -139,10 +150,38 @@ const StyledItemTitle = styled.div<{ $color: string; $mono: boolean; $accent: st
|
|
|
139
150
|
${({ $mono }) => ($mono ? `font-family: ${MONO_FONT_STACK}; font-size: 15px;` : '')}
|
|
140
151
|
`;
|
|
141
152
|
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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;
|
|
146
185
|
min-width: 0;
|
|
147
186
|
`;
|
|
148
187
|
|
|
@@ -349,24 +388,22 @@ const FieldValue = ({
|
|
|
349
388
|
style={{ flexShrink: 0, marginRight: 6, verticalAlign: 'middle' }}
|
|
350
389
|
/>
|
|
351
390
|
: null}
|
|
352
|
-
{formatted}
|
|
353
391
|
{isNavigableUrl(field.value) ?
|
|
354
|
-
// The
|
|
355
|
-
//
|
|
356
|
-
// is
|
|
357
|
-
//
|
|
358
|
-
|
|
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
|
|
359
398
|
className='schema-view-open-url'
|
|
360
399
|
href={field.value}
|
|
361
400
|
target='_blank'
|
|
362
401
|
rel='noreferrer noopener'
|
|
363
|
-
|
|
364
|
-
style={{ marginLeft: 4, verticalAlign: 'middle' }}
|
|
365
|
-
onClick={(event) => event.stopPropagation()}
|
|
402
|
+
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
|
366
403
|
>
|
|
367
|
-
|
|
368
|
-
</
|
|
369
|
-
:
|
|
404
|
+
{formatted}
|
|
405
|
+
</ReqoreLink>
|
|
406
|
+
: formatted}
|
|
370
407
|
</span>
|
|
371
408
|
}
|
|
372
409
|
</StyledRowValue>
|
|
@@ -391,92 +428,109 @@ const SchemaRecord = ({
|
|
|
391
428
|
// The SAME host renderer the row above uses for a `code-editor` field. Read
|
|
392
429
|
// from context rather than threaded through props: a nested level is arbitrarily
|
|
393
430
|
// deep, and the renderer is a property of the form, not of any one level.
|
|
394
|
-
const codePreviewRenderer = useContextSelector(
|
|
395
|
-
CompactRowContext,
|
|
396
|
-
(v) => v.codePreviewRenderer
|
|
397
|
-
);
|
|
431
|
+
const codePreviewRenderer = useContextSelector(CompactRowContext, (v) => v.codePreviewRenderer);
|
|
398
432
|
const cHover = useContextSelector(CompactRowContext, (v) => v.cHover);
|
|
399
433
|
|
|
400
434
|
return (
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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' }}
|
|
456
496
|
>
|
|
457
|
-
{
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
+
}
|
|
465
519
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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>
|
|
480
534
|
);
|
|
481
535
|
};
|
|
482
536
|
|
|
@@ -502,7 +556,10 @@ const SchemaLevel = ({
|
|
|
502
556
|
return (
|
|
503
557
|
<StyledItem key={index} className='schema-view-item'>
|
|
504
558
|
<StyledMarker $color={colors.muted}>{index + 1}.</StyledMarker>
|
|
505
|
-
<StyledItemFields
|
|
559
|
+
<StyledItemFields
|
|
560
|
+
$border={colors.border}
|
|
561
|
+
$rule={hasContentUnderHeading(record, schema)}
|
|
562
|
+
>
|
|
506
563
|
{isRecord(record) ?
|
|
507
564
|
<>
|
|
508
565
|
{(() => {
|
|
@@ -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
|
|
@@ -437,6 +450,10 @@ export const StyledGroupBody = styled.div<{
|
|
|
437
450
|
$success: string;
|
|
438
451
|
$rowBg: string;
|
|
439
452
|
$lineColor: string;
|
|
453
|
+
/** Edge colour for the "N more fields" row. Its own value because the row
|
|
454
|
+
HAS to be seen — the divider tint that separates two fields is ~8% of the
|
|
455
|
+
text colour, which disappears entirely as a 1px dashed border. */
|
|
456
|
+
$moreBorder: string;
|
|
440
457
|
}>`
|
|
441
458
|
display: flex;
|
|
442
459
|
flex-flow: column;
|
|
@@ -516,6 +533,43 @@ export const StyledGroupBody = styled.div<{
|
|
|
516
533
|
.readfirst-row-hidden {
|
|
517
534
|
opacity: 0.65;
|
|
518
535
|
}
|
|
536
|
+
/* The "N more fields" control IS a field row — same grid, same padding, same
|
|
537
|
+
hover — so it lines up with the rows above instead of floating as a scrap
|
|
538
|
+
of text in the box's corner. A dashed edge and a lower-contrast label are
|
|
539
|
+
what say it is not itself a field: nothing here can be set.
|
|
540
|
+
|
|
541
|
+
It spans both content columns because it has no label/value split, and it
|
|
542
|
+
drops the inter-row hairline (the ::after rule) since it always sits last. */
|
|
543
|
+
.readfirst-more-row {
|
|
544
|
+
grid-template-columns: 1fr auto;
|
|
545
|
+
border: 1px dashed ${({ $moreBorder }) => $moreBorder};
|
|
546
|
+
background: transparent;
|
|
547
|
+
width: 100%;
|
|
548
|
+
text-align: left;
|
|
549
|
+
font: inherit;
|
|
550
|
+
color: inherit;
|
|
551
|
+
/* The dashed edge replaces the hairline; two lines would read as a divider
|
|
552
|
+
AND a border stacked. */
|
|
553
|
+
margin-top: 2px;
|
|
554
|
+
}
|
|
555
|
+
.readfirst-more-row::after {
|
|
556
|
+
display: none;
|
|
557
|
+
}
|
|
558
|
+
.readfirst-more-row:hover {
|
|
559
|
+
background: ${({ $hover }) => $hover};
|
|
560
|
+
border-style: solid;
|
|
561
|
+
}
|
|
562
|
+
.readfirst-more-row:focus-visible {
|
|
563
|
+
outline: 2px solid ${({ $focus }) => $focus};
|
|
564
|
+
outline-offset: -2px;
|
|
565
|
+
}
|
|
566
|
+
/* The chevron points the way the click goes: down to reveal, up to fold. */
|
|
567
|
+
.readfirst-more-row .readfirst-more-chevron {
|
|
568
|
+
transition: transform 0.15s ease;
|
|
569
|
+
}
|
|
570
|
+
.readfirst-more-row[aria-expanded='true'] .readfirst-more-chevron {
|
|
571
|
+
transform: rotate(180deg);
|
|
572
|
+
}
|
|
519
573
|
/* A disabled field (schema disabled flag or unmet dependencies) is not a
|
|
520
574
|
click target — no hover invite, not-allowed cursor; a lock replaces the
|
|
521
575
|
pencil. */
|
|
@@ -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;
|