@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
|
@@ -2,7 +2,7 @@ import { ReqoreInput } from '@qoretechnologies/reqore';
|
|
|
2
2
|
import { TSizes } from '@qoretechnologies/reqore/dist/constants/sizes';
|
|
3
3
|
import { IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
|
|
4
4
|
import { Meta, StoryObj } from '@storybook/react-vite';
|
|
5
|
-
import { ChangeEvent, useState } from 'react';
|
|
5
|
+
import { ChangeEvent, useEffect, useState } from 'react';
|
|
6
6
|
import { expect, fireEvent, fn, userEvent, waitFor, within } from 'storybook/test';
|
|
7
7
|
import { validateField } from '../../../helpers/validations';
|
|
8
8
|
import { defaultQorusTypes } from '../../../hooks/useQorusTypes';
|
|
@@ -786,6 +786,27 @@ const CodeEditorStandin = ({
|
|
|
786
786
|
// (rendered by a host-injected code-editor) inherits `language` from
|
|
787
787
|
// a sibling `lang` picker, so flipping the picker live-changes the
|
|
788
788
|
// editor's syntax highlighting with no refetch.
|
|
789
|
+
/**
|
|
790
|
+
* The same stand-in without the "syntax: <language>" tag.
|
|
791
|
+
*
|
|
792
|
+
* That tag exists to prove an inherited `language` prop reaches the renderer,
|
|
793
|
+
* and the stories that assert it keep it. Where the row already shows a Language
|
|
794
|
+
* control directly above the editor, it is the same fact stated twice — which is
|
|
795
|
+
* what the review flagged on `CompactRowAbsorbsLanguage`.
|
|
796
|
+
*/
|
|
797
|
+
const CodeEditorStandinNoSyntax = (props: Parameters<typeof CodeEditorStandin>[0]) => (
|
|
798
|
+
<div data-testid='code-editor-mock'>
|
|
799
|
+
<ReqoreInput
|
|
800
|
+
fluid
|
|
801
|
+
size={props.size}
|
|
802
|
+
icon='CodeLine'
|
|
803
|
+
placeholder='Source code (stand-in code-editor)'
|
|
804
|
+
value={typeof props.value === 'string' ? props.value : ''}
|
|
805
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) => props.onChange?.(event.target.value)}
|
|
806
|
+
/>
|
|
807
|
+
</div>
|
|
808
|
+
);
|
|
809
|
+
|
|
789
810
|
export const OptionInheritsRenderPropFromSibling: Story = {
|
|
790
811
|
parameters: {
|
|
791
812
|
docs: {
|
|
@@ -994,7 +1015,7 @@ export const CompactRowAbsorbsLanguage: Story = {
|
|
|
994
1015
|
args: {
|
|
995
1016
|
compact: true,
|
|
996
1017
|
minColumnWidth: '360px',
|
|
997
|
-
componentOverrides: { 'code-editor':
|
|
1018
|
+
componentOverrides: { 'code-editor': CodeEditorStandinNoSyntax },
|
|
998
1019
|
value: {
|
|
999
1020
|
language: { type: 'string', value: 'qore' },
|
|
1000
1021
|
source: { type: 'string', value: '%new-style\nclass Example {\n}\n' },
|
|
@@ -1080,14 +1101,14 @@ export const CompactRowCodeEditorPreview: Story = {
|
|
|
1080
1101
|
docs: {
|
|
1081
1102
|
description: {
|
|
1082
1103
|
story:
|
|
1083
|
-
'Renders a compact-mode code-editor row over a multi-line Qore source value — the value cell replaces the truncated string with a
|
|
1104
|
+
'Renders a compact-mode code-editor row over a multi-line Qore source value — the value cell replaces the truncated string with a monospaced size note under the field name and a collapsible monospace preview mounts under the row. The stand-in prints no "syntax:" line: this row carries a Language control directly above the editor, so it would be the same fact stated twice.',
|
|
1084
1105
|
},
|
|
1085
1106
|
},
|
|
1086
1107
|
},
|
|
1087
1108
|
args: {
|
|
1088
1109
|
compact: true,
|
|
1089
1110
|
minColumnWidth: '360px',
|
|
1090
|
-
componentOverrides: { 'code-editor':
|
|
1111
|
+
componentOverrides: { 'code-editor': CodeEditorStandinNoSyntax },
|
|
1091
1112
|
value: {
|
|
1092
1113
|
language: { type: 'string', value: 'qore' },
|
|
1093
1114
|
source: {
|
|
@@ -1143,6 +1164,132 @@ export const CompactRowCodeEditorPreview: Story = {
|
|
|
1143
1164
|
},
|
|
1144
1165
|
};
|
|
1145
1166
|
|
|
1167
|
+
export const CompactRowCodeEditorMessage: Story = {
|
|
1168
|
+
parameters: {
|
|
1169
|
+
docs: {
|
|
1170
|
+
description: {
|
|
1171
|
+
story:
|
|
1172
|
+
"A schema `messages` entry on a code-editor field. Every other field surfaces its messages on the collapsed row; a code field draws a preview in the same cell, and the message has to survive beside it — a diagnostic the reader only finds by opening the field is not reported. Qorus puts source-validation results here, so an invalid service is called out on the row rather than at save time.",
|
|
1173
|
+
},
|
|
1174
|
+
},
|
|
1175
|
+
},
|
|
1176
|
+
args: {
|
|
1177
|
+
...CompactRowCodeEditorPreview.args,
|
|
1178
|
+
options: {
|
|
1179
|
+
...(CompactRowCodeEditorPreview.args as any).options,
|
|
1180
|
+
source: {
|
|
1181
|
+
...(CompactRowCodeEditorPreview.args as any).options.source,
|
|
1182
|
+
// The field ABSORBS its language sibling, which is how Qorus ships it:
|
|
1183
|
+
// the language control renders inside the Source Code container. That
|
|
1184
|
+
// is the shape the message has to survive.
|
|
1185
|
+
absorb_fields: ['language'],
|
|
1186
|
+
// Qorus groups its fields (INFO / SCALING), so the row renders inside a
|
|
1187
|
+
// group rather than in a flat list.
|
|
1188
|
+
group: 'info',
|
|
1189
|
+
messages: [
|
|
1190
|
+
{
|
|
1191
|
+
intent: 'danger',
|
|
1192
|
+
title: 'Line 4',
|
|
1193
|
+
content: "syntax error, unexpected '}'",
|
|
1194
|
+
},
|
|
1195
|
+
],
|
|
1196
|
+
},
|
|
1197
|
+
},
|
|
1198
|
+
},
|
|
1199
|
+
play: async ({ canvasElement }) => {
|
|
1200
|
+
// The message renders on the collapsed row, beside the code preview rather
|
|
1201
|
+
// than instead of it.
|
|
1202
|
+
await waitFor(
|
|
1203
|
+
() => {
|
|
1204
|
+
const row = canvasElement.querySelector('[data-field="source"]');
|
|
1205
|
+
expect(row?.textContent ?? '').toContain('Line 4');
|
|
1206
|
+
expect(row?.textContent ?? '').toContain("syntax error, unexpected '}'");
|
|
1207
|
+
},
|
|
1208
|
+
{ timeout: 5000 }
|
|
1209
|
+
);
|
|
1210
|
+
|
|
1211
|
+
const row = canvasElement.querySelector('[data-field="source"]');
|
|
1212
|
+
// ... and the preview it sits beside is still there.
|
|
1213
|
+
expect(row?.textContent ?? '').toContain('class ExampleJob inherits QorusJob');
|
|
1214
|
+
},
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Adds a schema message to a field a moment AFTER the form has mounted, the way
|
|
1219
|
+
* a host that validates asynchronously does.
|
|
1220
|
+
*/
|
|
1221
|
+
const LateMessageHarness = (args: any) => {
|
|
1222
|
+
const [options, setOptions] = useState(args.options);
|
|
1223
|
+
|
|
1224
|
+
useEffect(() => {
|
|
1225
|
+
const timer = setTimeout(
|
|
1226
|
+
() =>
|
|
1227
|
+
setOptions((current: any) => ({
|
|
1228
|
+
...current,
|
|
1229
|
+
source: {
|
|
1230
|
+
...current.source,
|
|
1231
|
+
messages: [
|
|
1232
|
+
{ intent: 'danger', title: 'Line 4', content: "syntax error, unexpected '}'" },
|
|
1233
|
+
],
|
|
1234
|
+
},
|
|
1235
|
+
})),
|
|
1236
|
+
100
|
|
1237
|
+
);
|
|
1238
|
+
return () => clearTimeout(timer);
|
|
1239
|
+
}, []);
|
|
1240
|
+
|
|
1241
|
+
return <FormEngine {...args} options={options} />;
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
export const CompactRowCodeEditorLateMessage: Story = {
|
|
1245
|
+
parameters: {
|
|
1246
|
+
docs: {
|
|
1247
|
+
description: {
|
|
1248
|
+
story:
|
|
1249
|
+
'The same message, but added to the schema after the form has mounted — which is how a host that validates asynchronously delivers one. A message that only renders when it was present at mount is no use to a validator: the answer always arrives later than the field did.',
|
|
1250
|
+
},
|
|
1251
|
+
},
|
|
1252
|
+
},
|
|
1253
|
+
args: { ...CompactRowCodeEditorPreview.args },
|
|
1254
|
+
render: (args) => <LateMessageHarness {...args} />,
|
|
1255
|
+
play: async ({ canvasElement }) => {
|
|
1256
|
+
await waitFor(
|
|
1257
|
+
() => {
|
|
1258
|
+
const row = canvasElement.querySelector('[data-field="source"]');
|
|
1259
|
+
expect(row?.textContent ?? '').toContain('Line 4');
|
|
1260
|
+
},
|
|
1261
|
+
{ timeout: 10000 }
|
|
1262
|
+
);
|
|
1263
|
+
},
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
export const CompactRowMessageWhileEditing: Story = {
|
|
1267
|
+
parameters: {
|
|
1268
|
+
docs: {
|
|
1269
|
+
description: {
|
|
1270
|
+
story:
|
|
1271
|
+
"The same message, with the row OPEN. A schema message is guidance about the value, so the moment it matters most is while the value is being edited — and a validation diagnostic is useless anywhere else: it names a line the author can only fix with the editor in front of them. Qorus opens the Source Code row on arrival, so a message that only survives on the collapsed row was never seen at all.",
|
|
1272
|
+
},
|
|
1273
|
+
},
|
|
1274
|
+
},
|
|
1275
|
+
args: { ...CompactRowCodeEditorMessage.args },
|
|
1276
|
+
play: async ({ canvasElement }) => {
|
|
1277
|
+
// Open the row, the way an author does before fixing what the message says.
|
|
1278
|
+
await waitFor(() => {
|
|
1279
|
+
expect(canvasElement.querySelector('[data-field="source"]')).toBeTruthy();
|
|
1280
|
+
});
|
|
1281
|
+
await fireEvent.click(canvasElement.querySelector('[data-field="source"]') as HTMLElement);
|
|
1282
|
+
|
|
1283
|
+
await waitFor(() => {
|
|
1284
|
+
expect(canvasElement.querySelector('.readfirst-row-editing')).toBeTruthy();
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
const editingRow = canvasElement.querySelector('.readfirst-row-editing') as HTMLElement;
|
|
1288
|
+
expect(editingRow?.textContent ?? '').toContain('Line 4');
|
|
1289
|
+
expect(editingRow?.textContent ?? '').toContain("syntax error, unexpected '}'");
|
|
1290
|
+
},
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1146
1293
|
export const CompactRowMarkdownPreview: Story = {
|
|
1147
1294
|
parameters: {
|
|
1148
1295
|
docs: {
|
|
@@ -3031,9 +3178,10 @@ export const CompactMultiSelectEditing: Story = {
|
|
|
3031
3178
|
},
|
|
3032
3179
|
play: async () => {
|
|
3033
3180
|
await _testsWaitForText('Audiences');
|
|
3034
|
-
// Read row joins the selected items
|
|
3035
|
-
|
|
3036
|
-
await
|
|
3181
|
+
// Read row joins the selected items, labelled the way the picker labels
|
|
3182
|
+
// them (`Orders`) rather than the way they are stored (`orders`).
|
|
3183
|
+
await _testsWaitForText('Orders');
|
|
3184
|
+
await _testsClickText('Orders');
|
|
3037
3185
|
await waitFor(() => expect(document.querySelector('.options-readfirst-card')).toBeTruthy(), {
|
|
3038
3186
|
timeout: 10000,
|
|
3039
3187
|
});
|
|
@@ -3817,8 +3965,16 @@ export const CompactExpressions: Story = {
|
|
|
3817
3965
|
play: async () => {
|
|
3818
3966
|
const stop = startDpqlMockLsp();
|
|
3819
3967
|
try {
|
|
3820
|
-
// Read-first: the offline DPQL summary of the AST
|
|
3821
|
-
|
|
3968
|
+
// Read-first: the offline DPQL summary of the AST — the literals read as
|
|
3969
|
+
// the text they are, and the template reference reads as a chip rather
|
|
3970
|
+
// than as its raw `$local:name` token inside the string.
|
|
3971
|
+
await waitFor(() => {
|
|
3972
|
+
const chip = Array.from(document.querySelectorAll('.reqore-tag')).find((tag) =>
|
|
3973
|
+
tag.textContent?.includes('$local:name')
|
|
3974
|
+
);
|
|
3975
|
+
expect(chip, 'the reference in the summary renders as a chip').toBeTruthy();
|
|
3976
|
+
});
|
|
3977
|
+
await _testsWaitForText('" == "John"');
|
|
3822
3978
|
|
|
3823
3979
|
// Drill in → the card hosts the ExpressionField (Visual builder).
|
|
3824
3980
|
await fireEvent.click(
|
|
@@ -4614,7 +4770,8 @@ export const CompactFieldTypes: Story = {
|
|
|
4614
4770
|
await _testsWaitForText('0 0 * * *'); // cron
|
|
4615
4771
|
await _testsWaitForText('Python'); // select → allowed_value display label
|
|
4616
4772
|
await _testsWaitForText('Medium'); // enum → allowed_value display label
|
|
4617
|
-
|
|
4773
|
+
// multi-select joined, labelled the way the picker labels them
|
|
4774
|
+
await _testsWaitForText('Orders, Batch');
|
|
4618
4775
|
await _testsWaitForText('a, b'); // list joined
|
|
4619
4776
|
await _testsWaitForText('x, y, z'); // YAML-serialized list summarised
|
|
4620
4777
|
await _testsWaitForText('#0000FF'); // colour → uppercase hex
|
|
@@ -4644,8 +4801,15 @@ export const CompactFieldTypes: Story = {
|
|
|
4644
4801
|
// than a raw hash key-count.
|
|
4645
4802
|
await _testsWaitForText('512MiB');
|
|
4646
4803
|
await _testsWaitForText(/example_customer_addresses/);
|
|
4647
|
-
// Expression field → read-first shows the offline DPQL summary of the AST
|
|
4648
|
-
|
|
4804
|
+
// Expression field → read-first shows the offline DPQL summary of the AST,
|
|
4805
|
+
// with the template reference inside it chipped rather than printed raw.
|
|
4806
|
+
await waitFor(() => {
|
|
4807
|
+
const chip = Array.from(document.querySelectorAll('.reqore-tag')).find((tag) =>
|
|
4808
|
+
tag.textContent?.includes('$local:name')
|
|
4809
|
+
);
|
|
4810
|
+
expect(chip, 'the reference in the summary renders as a chip').toBeTruthy();
|
|
4811
|
+
});
|
|
4812
|
+
await _testsWaitForText('" == "John"');
|
|
4649
4813
|
await expect(document.querySelectorAll('.options-readfirst-card')).toHaveLength(0);
|
|
4650
4814
|
|
|
4651
4815
|
// A hash renders its read-first preview (the structured tree by default)
|
|
@@ -6107,6 +6271,173 @@ export const CompactReadFirstRichtextTemplateBaseline: Story = {
|
|
|
6107
6271
|
},
|
|
6108
6272
|
};
|
|
6109
6273
|
|
|
6274
|
+
/**
|
|
6275
|
+
* The same summary, built from the value the server actually sends.
|
|
6276
|
+
*
|
|
6277
|
+
* `CompactReadFirstRichtextTemplateBaseline` above separates its chips with
|
|
6278
|
+
* SPACES, and that is why it kept passing while the row stayed broken: an alert
|
|
6279
|
+
* rule's Gmail message body is five `\n`-separated lines, and the newline is the
|
|
6280
|
+
* whole difference. The richtext branch is the only read-first value that opts
|
|
6281
|
+
* into `white-space: pre` — it has to, or the space between a word and the chip
|
|
6282
|
+
* beside it is dropped — and `pre` honours the newlines too. Every prose segment
|
|
6283
|
+
* after the first then rendered a blank first line, measured 30px against the
|
|
6284
|
+
* chips' 14px, and the wrapper's `align-items: center` put each word 12px below
|
|
6285
|
+
* the chip next to it. The row read as a staircase (supah, 2026-08-28).
|
|
6286
|
+
*
|
|
6287
|
+
* The row is a single line by construction; every other value type reaches that
|
|
6288
|
+
* through `nowrap`, which eats newlines for free. So the fix is to collapse them
|
|
6289
|
+
* here as well, and this story holds the two things that prove it: the prose
|
|
6290
|
+
* boxes are one line tall, not two, and they still share a centre with the chips.
|
|
6291
|
+
*/
|
|
6292
|
+
const RichtextMultilineTemplateValue: IOptions = {
|
|
6293
|
+
body: {
|
|
6294
|
+
type: 'richtext',
|
|
6295
|
+
value: [
|
|
6296
|
+
{
|
|
6297
|
+
type: 'paragraph',
|
|
6298
|
+
children: [
|
|
6299
|
+
{ text: 'Alert: ' },
|
|
6300
|
+
{ type: 'tag', value: '$fsminput:alert_code', label: 'alert_code', children: [{ text: '' }] },
|
|
6301
|
+
{ text: '\nSeverity: ' },
|
|
6302
|
+
{ type: 'tag', value: '$fsminput:severity', label: 'severity', children: [{ text: '' }] },
|
|
6303
|
+
{ text: '\nRule: ' },
|
|
6304
|
+
{ type: 'tag', value: '$fsminput:rule_name', label: 'rule_name', children: [{ text: '' }] },
|
|
6305
|
+
],
|
|
6306
|
+
},
|
|
6307
|
+
],
|
|
6308
|
+
},
|
|
6309
|
+
} as unknown as IOptions;
|
|
6310
|
+
|
|
6311
|
+
export const CompactReadFirstRichtextMultilineTemplate: Story = {
|
|
6312
|
+
parameters: {
|
|
6313
|
+
docs: {
|
|
6314
|
+
description: {
|
|
6315
|
+
story:
|
|
6316
|
+
"A multi-line richtext value in the read-first row. The row is one line, so the value's newlines must collapse rather than break — and the row must not also hover the raw template source, which is what the chips are there to replace.",
|
|
6317
|
+
},
|
|
6318
|
+
},
|
|
6319
|
+
},
|
|
6320
|
+
args: {
|
|
6321
|
+
compact: true,
|
|
6322
|
+
minColumnWidth: '300px',
|
|
6323
|
+
options: RichtextTemplateSchema,
|
|
6324
|
+
value: RichtextMultilineTemplateValue,
|
|
6325
|
+
},
|
|
6326
|
+
play: async ({ canvasElement }) => {
|
|
6327
|
+
await _testsWaitForText('alert_code');
|
|
6328
|
+
|
|
6329
|
+
const proseSpans = Array.from(canvasElement.querySelectorAll('span')).filter(
|
|
6330
|
+
// the collapsed newline leaves the segment with a leading space
|
|
6331
|
+
(element) =>
|
|
6332
|
+
element.children.length === 0 && /^\s*(Alert|Severity|Rule):/.test(element.textContent || '')
|
|
6333
|
+
) as HTMLElement[];
|
|
6334
|
+
await expect(proseSpans.length).toBe(3);
|
|
6335
|
+
|
|
6336
|
+
// No hard break survives into the summary: a segment that still carried its
|
|
6337
|
+
// newline would render a blank first line and measure two lines tall.
|
|
6338
|
+
for (const span of proseSpans) {
|
|
6339
|
+
await expect(span.textContent).not.toContain('\n');
|
|
6340
|
+
}
|
|
6341
|
+
|
|
6342
|
+
const chipEl = Array.from(canvasElement.querySelectorAll('span')).find(
|
|
6343
|
+
(element) => element.children.length === 0 && element.textContent?.trim() === 'alert_code'
|
|
6344
|
+
) as HTMLElement;
|
|
6345
|
+
const chipHeight = chipEl.getBoundingClientRect().height;
|
|
6346
|
+
|
|
6347
|
+
// One line tall, like the chip beside it -- this is the assertion that fails
|
|
6348
|
+
// on the unfixed code, where the blank line doubled it.
|
|
6349
|
+
for (const span of proseSpans) {
|
|
6350
|
+
await expect(span.getBoundingClientRect().height).toBeLessThan(chipHeight * 1.6);
|
|
6351
|
+
}
|
|
6352
|
+
|
|
6353
|
+
// ...and they still sit on the chips' centre line, which is what the earlier
|
|
6354
|
+
// baseline fix bought and this must not give back.
|
|
6355
|
+
const chipRect = chipEl.getBoundingClientRect();
|
|
6356
|
+
for (const span of proseSpans) {
|
|
6357
|
+
const rect = span.getBoundingClientRect();
|
|
6358
|
+
const offset = Math.abs(rect.top + rect.height / 2 - (chipRect.top + chipRect.height / 2));
|
|
6359
|
+
await expect(offset).toBeLessThanOrEqual(1.5);
|
|
6360
|
+
}
|
|
6361
|
+
|
|
6362
|
+
// The row draws the value in full as chips, so it must not ALSO carry a
|
|
6363
|
+
// native tooltip of the raw source: hovering a chip fired that and the chip's
|
|
6364
|
+
// own popover together, and only the popover is visible in a screenshot.
|
|
6365
|
+
const valueCell = chipEl.closest('[title]');
|
|
6366
|
+
await expect(valueCell).toBeNull();
|
|
6367
|
+
},
|
|
6368
|
+
};
|
|
6369
|
+
|
|
6370
|
+
/**
|
|
6371
|
+
* A list of template-capable STRINGS, which is what an email `To` field is.
|
|
6372
|
+
*
|
|
6373
|
+
* Gmail's `to` is `type: "list"`, `element_type: "string"`,
|
|
6374
|
+
* `supports_templates: true`, so each element is edited with a rich-text editor
|
|
6375
|
+
* and its value arrives wrapped: `{type: "richtext", value: [{type:
|
|
6376
|
+
* "paragraph", …}]}`. The hash-list test unwrapped `item.value`, found the Slate
|
|
6377
|
+
* document — an object — and classed a list of addresses as a list of hashes, so
|
|
6378
|
+
* the row drew an expandable `type / children / text` tree instead of the
|
|
6379
|
+
* address (supah, 2026-08-29).
|
|
6380
|
+
*
|
|
6381
|
+
* A rich-text envelope is a string in a coat. The row reads it as one.
|
|
6382
|
+
*/
|
|
6383
|
+
const RichtextListSchema: IOptionsSchema = {
|
|
6384
|
+
to: {
|
|
6385
|
+
type: 'list',
|
|
6386
|
+
element_type: 'string',
|
|
6387
|
+
supports_templates: true,
|
|
6388
|
+
display_name: 'To',
|
|
6389
|
+
required: true,
|
|
6390
|
+
} as never,
|
|
6391
|
+
};
|
|
6392
|
+
|
|
6393
|
+
const RichtextListValue: IOptions = {
|
|
6394
|
+
to: {
|
|
6395
|
+
type: 'list',
|
|
6396
|
+
value: [
|
|
6397
|
+
{ type: 'richtext', value: [{ type: 'paragraph', children: [{ text: 'ops@example.com' }] }] },
|
|
6398
|
+
{ type: 'richtext', value: [{ type: 'paragraph', children: [{ text: 'sre@example.com' }] }] },
|
|
6399
|
+
],
|
|
6400
|
+
},
|
|
6401
|
+
} as unknown as IOptions;
|
|
6402
|
+
|
|
6403
|
+
export const CompactReadFirstRichtextStringList: Story = {
|
|
6404
|
+
parameters: {
|
|
6405
|
+
docs: {
|
|
6406
|
+
description: {
|
|
6407
|
+
story:
|
|
6408
|
+
'A list of template-capable string elements reads as its strings, not as a structured-data tree: the rich-text envelope each element arrives in is a string in a coat.',
|
|
6409
|
+
},
|
|
6410
|
+
},
|
|
6411
|
+
},
|
|
6412
|
+
args: {
|
|
6413
|
+
compact: true,
|
|
6414
|
+
minColumnWidth: '300px',
|
|
6415
|
+
options: RichtextListSchema,
|
|
6416
|
+
value: RichtextListValue,
|
|
6417
|
+
},
|
|
6418
|
+
play: async ({ canvasElement }) => {
|
|
6419
|
+
// The row itself, not a global text search: the addresses have to be IN the
|
|
6420
|
+
// `to` row, which is the whole point.
|
|
6421
|
+
await waitFor(
|
|
6422
|
+
() => {
|
|
6423
|
+
const row = canvasElement.querySelector('[data-field="to"]');
|
|
6424
|
+
expect(row).toBeTruthy();
|
|
6425
|
+
// The addresses themselves, not a bare "2 items" count.
|
|
6426
|
+
expect(row?.textContent ?? '').toContain('ops@example.com');
|
|
6427
|
+
},
|
|
6428
|
+
{ timeout: 10000 }
|
|
6429
|
+
);
|
|
6430
|
+
|
|
6431
|
+
const text = canvasElement.querySelector('[data-field="to"]')?.textContent ?? '';
|
|
6432
|
+
await expect(text).toContain('sre@example.com');
|
|
6433
|
+
|
|
6434
|
+
// ...and none of the document's internals leak into the row. These are the
|
|
6435
|
+
// labels the structured-data tree prints, and they are what the operator saw.
|
|
6436
|
+
await expect(text).not.toContain('paragraph');
|
|
6437
|
+
await expect(text).not.toContain('children');
|
|
6438
|
+
},
|
|
6439
|
+
};
|
|
6440
|
+
|
|
6110
6441
|
/**
|
|
6111
6442
|
* The other half of the markdown contract: with NO host renderer there is no
|
|
6112
6443
|
* inset at all. The row is not left empty though — its one line still carries
|
|
@@ -6388,3 +6719,655 @@ export const CompactListOfHashReadsInSchemaWordsMobile: Story = {
|
|
|
6388
6719
|
}
|
|
6389
6720
|
},
|
|
6390
6721
|
};
|
|
6722
|
+
|
|
6723
|
+
/**
|
|
6724
|
+
* The read-first summary of an EXPRESSION: it renders to one line of text, and
|
|
6725
|
+
* a template reference inside it used to print as its own raw token — the same
|
|
6726
|
+
* reference the editor shows as a named chip. This is the Save Reply row of the
|
|
6727
|
+
* Discord assistant template, whose value is
|
|
6728
|
+
* `trim($data:{dc_ai_reply.choices[0].message.content})`.
|
|
6729
|
+
*/
|
|
6730
|
+
export const CompactExpressionTemplateChips: Story = {
|
|
6731
|
+
parameters: {
|
|
6732
|
+
docs: {
|
|
6733
|
+
description: {
|
|
6734
|
+
story:
|
|
6735
|
+
'Renders a compact read-first row whose value is an expression wrapping a template reference — the reference renders as a named chip inside the summary text (trim("Choices[0].message.content")) rather than as its raw $data token.',
|
|
6736
|
+
},
|
|
6737
|
+
},
|
|
6738
|
+
chromatic: { disable: true },
|
|
6739
|
+
},
|
|
6740
|
+
render: () => (
|
|
6741
|
+
<FormEngine
|
|
6742
|
+
compact
|
|
6743
|
+
name='exprChips'
|
|
6744
|
+
stringTemplates={
|
|
6745
|
+
{
|
|
6746
|
+
items: [
|
|
6747
|
+
{
|
|
6748
|
+
label: 'Generate AI Reply',
|
|
6749
|
+
items: [
|
|
6750
|
+
{
|
|
6751
|
+
label: 'Choices',
|
|
6752
|
+
value: '$data:{3.choices}',
|
|
6753
|
+
metadata: { aliasValues: ['$data:{dc_ai_reply.choices}'] },
|
|
6754
|
+
},
|
|
6755
|
+
],
|
|
6756
|
+
},
|
|
6757
|
+
],
|
|
6758
|
+
} as any
|
|
6759
|
+
}
|
|
6760
|
+
options={
|
|
6761
|
+
{
|
|
6762
|
+
reply: {
|
|
6763
|
+
type: 'string',
|
|
6764
|
+
display_name: 'Value',
|
|
6765
|
+
desc: 'The new value for the variable',
|
|
6766
|
+
supports_expressions: true,
|
|
6767
|
+
required: true,
|
|
6768
|
+
},
|
|
6769
|
+
} as unknown as IQorusFormSchema
|
|
6770
|
+
}
|
|
6771
|
+
value={
|
|
6772
|
+
{
|
|
6773
|
+
reply: {
|
|
6774
|
+
type: 'string',
|
|
6775
|
+
is_expression: true,
|
|
6776
|
+
value: {
|
|
6777
|
+
exp: 'trim',
|
|
6778
|
+
args: [{ type: 'string', value: '$data:{dc_ai_reply.choices[0].message.content}' }],
|
|
6779
|
+
},
|
|
6780
|
+
},
|
|
6781
|
+
} as any
|
|
6782
|
+
}
|
|
6783
|
+
onChange={fn()}
|
|
6784
|
+
/>
|
|
6785
|
+
),
|
|
6786
|
+
play: async () => {
|
|
6787
|
+
// The summary keeps its literal text…
|
|
6788
|
+
await _testsWaitForText('trim("', undefined, 1);
|
|
6789
|
+
// …and the reference inside it is a named chip, not the raw token.
|
|
6790
|
+
await waitFor(() => {
|
|
6791
|
+
const chip = Array.from(document.querySelectorAll('.reqore-tag')).find((tag) =>
|
|
6792
|
+
tag.textContent?.includes('Choices[0].message.content')
|
|
6793
|
+
);
|
|
6794
|
+
expect(chip, 'the reference renders as a named chip in the summary').toBeTruthy();
|
|
6795
|
+
});
|
|
6796
|
+
const raw = Array.from(document.querySelectorAll('.readfirst-row')).filter((row) =>
|
|
6797
|
+
row.textContent?.includes('$data:{')
|
|
6798
|
+
);
|
|
6799
|
+
expect(raw, 'the row prints no raw token').toHaveLength(0);
|
|
6800
|
+
},
|
|
6801
|
+
};
|
|
6802
|
+
|
|
6803
|
+
/**
|
|
6804
|
+
* The same row on a surface with NO template catalogue — the Automation Hub
|
|
6805
|
+
* template preview, which never fetches one. There is no name to resolve the
|
|
6806
|
+
* reference to, so the chip carries the reference's own path: the wrapper still
|
|
6807
|
+
* reads as `trim("…")` text, and the reference stops reading as code.
|
|
6808
|
+
*/
|
|
6809
|
+
export const CompactExpressionWithoutCatalogue: Story = {
|
|
6810
|
+
parameters: {
|
|
6811
|
+
docs: {
|
|
6812
|
+
description: {
|
|
6813
|
+
story:
|
|
6814
|
+
'Renders a compact read-first row whose value is an expression wrapping a template reference, with no templates supplied — the reference still renders as a chip carrying its own path (dc_ai_reply.choices[0].message.content) rather than the raw $data token.',
|
|
6815
|
+
},
|
|
6816
|
+
},
|
|
6817
|
+
chromatic: { disable: true },
|
|
6818
|
+
},
|
|
6819
|
+
render: () => (
|
|
6820
|
+
<FormEngine
|
|
6821
|
+
compact
|
|
6822
|
+
name='exprNoCatalogue'
|
|
6823
|
+
options={
|
|
6824
|
+
{
|
|
6825
|
+
reply: {
|
|
6826
|
+
type: 'string',
|
|
6827
|
+
display_name: 'Value',
|
|
6828
|
+
desc: 'The new value for the variable',
|
|
6829
|
+
supports_expressions: true,
|
|
6830
|
+
required: true,
|
|
6831
|
+
},
|
|
6832
|
+
} as unknown as IQorusFormSchema
|
|
6833
|
+
}
|
|
6834
|
+
value={
|
|
6835
|
+
{
|
|
6836
|
+
reply: {
|
|
6837
|
+
type: 'string',
|
|
6838
|
+
is_expression: true,
|
|
6839
|
+
value: {
|
|
6840
|
+
exp: 'trim',
|
|
6841
|
+
args: [{ type: 'string', value: '$data:{dc_ai_reply.choices[0].message.content}' }],
|
|
6842
|
+
},
|
|
6843
|
+
},
|
|
6844
|
+
} as any
|
|
6845
|
+
}
|
|
6846
|
+
onChange={fn()}
|
|
6847
|
+
/>
|
|
6848
|
+
),
|
|
6849
|
+
play: async () => {
|
|
6850
|
+
await _testsWaitForText('trim("', undefined, 1);
|
|
6851
|
+
await waitFor(() => {
|
|
6852
|
+
const chip = Array.from(document.querySelectorAll('.reqore-tag')).find((tag) =>
|
|
6853
|
+
tag.textContent?.includes('dc_ai_reply.choices[0].message.content')
|
|
6854
|
+
);
|
|
6855
|
+
expect(chip, 'the reference chips even with no catalogue').toBeTruthy();
|
|
6856
|
+
});
|
|
6857
|
+
const raw = Array.from(document.querySelectorAll('.readfirst-row')).filter((row) =>
|
|
6858
|
+
row.textContent?.includes('$data:{')
|
|
6859
|
+
);
|
|
6860
|
+
expect(raw, 'the row prints no raw token').toHaveLength(0);
|
|
6861
|
+
},
|
|
6862
|
+
};
|
|
6863
|
+
|
|
6864
|
+
// --- PR #99: compactCollapsedGroups + compactToolbar --------------------------
|
|
6865
|
+
|
|
6866
|
+
/** Find a status box (`Needs attention` / `Set` / `Optional`) by its title.
|
|
6867
|
+
* A collapsed ReqorePanel unmounts its content, so "has no
|
|
6868
|
+
* `.reqore-panel-content`" IS the collapsed assertion. */
|
|
6869
|
+
const findStatusBox = (title: string) =>
|
|
6870
|
+
Array.from(document.querySelectorAll('.options-readfirst-group')).find((panel) =>
|
|
6871
|
+
panel.querySelector('.reqore-panel-title')?.textContent?.includes(title)
|
|
6872
|
+
);
|
|
6873
|
+
|
|
6874
|
+
const expectBoxCollapsed = async (title: string) => {
|
|
6875
|
+
await waitFor(
|
|
6876
|
+
() => {
|
|
6877
|
+
const box = findStatusBox(title);
|
|
6878
|
+
expect(box, `the "${title}" box renders`).toBeTruthy();
|
|
6879
|
+
expect(
|
|
6880
|
+
box!.querySelector('.reqore-panel-content'),
|
|
6881
|
+
`the "${title}" box is collapsed (content unmounted)`
|
|
6882
|
+
).toBeFalsy();
|
|
6883
|
+
},
|
|
6884
|
+
{ timeout: 10000 }
|
|
6885
|
+
);
|
|
6886
|
+
};
|
|
6887
|
+
|
|
6888
|
+
const expectBoxOpen = async (title: string) => {
|
|
6889
|
+
await waitFor(
|
|
6890
|
+
() => {
|
|
6891
|
+
const box = findStatusBox(title);
|
|
6892
|
+
expect(box, `the "${title}" box renders`).toBeTruthy();
|
|
6893
|
+
expect(
|
|
6894
|
+
box!.querySelector('.reqore-panel-content'),
|
|
6895
|
+
`the "${title}" box is open (content mounted)`
|
|
6896
|
+
).toBeTruthy();
|
|
6897
|
+
},
|
|
6898
|
+
{ timeout: 10000 }
|
|
6899
|
+
);
|
|
6900
|
+
};
|
|
6901
|
+
|
|
6902
|
+
/**
|
|
6903
|
+
* The template drawer's combination: a form that is one item in a longer list,
|
|
6904
|
+
* where the reader chooses WHICH form to open rather than reading this one.
|
|
6905
|
+
*/
|
|
6906
|
+
export const CompactChosenCollapsedBoxes: Story = {
|
|
6907
|
+
parameters: {
|
|
6908
|
+
docs: {
|
|
6909
|
+
description: {
|
|
6910
|
+
story:
|
|
6911
|
+
'Renders a compact form with `compactCollapsedGroups={[\'set\', \'optional\']}` — the Set and Optional boxes start collapsed while Needs attention stays open, and typing a search query forces every box open (a collapsed panel unmounts its content, so a match inside one would be unreachable); clearing the query collapses them again.',
|
|
6912
|
+
},
|
|
6913
|
+
},
|
|
6914
|
+
},
|
|
6915
|
+
args: {
|
|
6916
|
+
compact: true,
|
|
6917
|
+
minColumnWidth: '300px',
|
|
6918
|
+
options: CompactFieldsMenuSchema,
|
|
6919
|
+
value: CompactValue,
|
|
6920
|
+
groups: CompactGroups,
|
|
6921
|
+
compactCollapsedGroups: ['set', 'optional'],
|
|
6922
|
+
},
|
|
6923
|
+
play: async () => {
|
|
6924
|
+
// Needs attention is not in the collapse list — its row is on screen.
|
|
6925
|
+
await expectBoxOpen('Needs attention');
|
|
6926
|
+
await _testsWaitForText('Description');
|
|
6927
|
+
|
|
6928
|
+
// Set and Optional start collapsed: headers present, rows unmounted.
|
|
6929
|
+
await expectBoxCollapsed('Set');
|
|
6930
|
+
await expectBoxCollapsed('Optional');
|
|
6931
|
+
await _testsWaitForTextToNotExist('order-fulfilment');
|
|
6932
|
+
|
|
6933
|
+
// A search overrides the collapse list — the matching Set row surfaces…
|
|
6934
|
+
await _testsChangeStringField({
|
|
6935
|
+
selector: 'input[placeholder="Filter fields..."]',
|
|
6936
|
+
value: 'name',
|
|
6937
|
+
});
|
|
6938
|
+
await expectBoxOpen('Set');
|
|
6939
|
+
await _testsWaitForText('order-fulfilment');
|
|
6940
|
+
|
|
6941
|
+
// …and clearing the query hands control back to the collapse list.
|
|
6942
|
+
await _testsChangeStringField({
|
|
6943
|
+
selector: 'input[placeholder="Filter fields..."]',
|
|
6944
|
+
value: '',
|
|
6945
|
+
});
|
|
6946
|
+
await expectBoxCollapsed('Set');
|
|
6947
|
+
await _testsWaitForTextToNotExist('order-fulfilment');
|
|
6948
|
+
},
|
|
6949
|
+
};
|
|
6950
|
+
|
|
6951
|
+
export const CompactToolbarHidden: Story = {
|
|
6952
|
+
parameters: {
|
|
6953
|
+
docs: {
|
|
6954
|
+
description: {
|
|
6955
|
+
story:
|
|
6956
|
+
"Renders a compact form with `compactToolbar={false}` — the completion meter, search, field picker and help button are all gone, and the header carries only the consumer's own `compactPanelProps.actions`; the rows themselves are untouched.",
|
|
6957
|
+
},
|
|
6958
|
+
},
|
|
6959
|
+
},
|
|
6960
|
+
args: {
|
|
6961
|
+
...Compact.args,
|
|
6962
|
+
compactToolbar: false,
|
|
6963
|
+
compactPanelProps: {
|
|
6964
|
+
label: 'Save Reply',
|
|
6965
|
+
actions: [{ label: 'Apply', icon: 'CheckLine', responsive: false }],
|
|
6966
|
+
},
|
|
6967
|
+
},
|
|
6968
|
+
play: async () => {
|
|
6969
|
+
// The rows and the consumer's own header action render…
|
|
6970
|
+
await _testsWaitForText('order-fulfilment');
|
|
6971
|
+
await _testsWaitForText('Apply');
|
|
6972
|
+
|
|
6973
|
+
// …and none of the engine's toolbar does.
|
|
6974
|
+
await waitFor(() => {
|
|
6975
|
+
expect(document.querySelector('.options-readfirst-completion')).toBeFalsy();
|
|
6976
|
+
expect(document.querySelector('input[placeholder="Filter fields..."]')).toBeFalsy();
|
|
6977
|
+
expect(document.querySelector('.options-readfirst-fields')).toBeFalsy();
|
|
6978
|
+
expect(document.querySelector('.options-readfirst-descriptions')).toBeFalsy();
|
|
6979
|
+
});
|
|
6980
|
+
},
|
|
6981
|
+
};
|
|
6982
|
+
|
|
6983
|
+
export const CompactToolbarTrimmed: Story = {
|
|
6984
|
+
parameters: {
|
|
6985
|
+
docs: {
|
|
6986
|
+
description: {
|
|
6987
|
+
story:
|
|
6988
|
+
'Renders a compact form with `compactToolbar={{ completion: false, help: false }}` — the keys listed turn their parts off (no meter, no help button) while the unlisted ones keep their defaults (search and the field picker still render), proving the object form merges over all-on rather than replacing it.',
|
|
6989
|
+
},
|
|
6990
|
+
},
|
|
6991
|
+
},
|
|
6992
|
+
args: {
|
|
6993
|
+
...Compact.args,
|
|
6994
|
+
options: CompactFieldsMenuSchema,
|
|
6995
|
+
compactToolbar: { completion: false, help: false },
|
|
6996
|
+
},
|
|
6997
|
+
play: async () => {
|
|
6998
|
+
await _testsWaitForText('order-fulfilment');
|
|
6999
|
+
|
|
7000
|
+
// Listed parts are off…
|
|
7001
|
+
await waitFor(() => {
|
|
7002
|
+
expect(document.querySelector('.options-readfirst-completion')).toBeFalsy();
|
|
7003
|
+
expect(document.querySelector('.options-readfirst-descriptions')).toBeFalsy();
|
|
7004
|
+
});
|
|
7005
|
+
|
|
7006
|
+
// …unlisted parts keep rendering.
|
|
7007
|
+
await waitFor(() => {
|
|
7008
|
+
expect(document.querySelector('input[placeholder="Filter fields..."]')).toBeTruthy();
|
|
7009
|
+
expect(document.querySelector('.options-readfirst-fields')).toBeTruthy();
|
|
7010
|
+
});
|
|
7011
|
+
},
|
|
7012
|
+
};
|
|
7013
|
+
|
|
7014
|
+
export const CompactCollapseListLeavesLoneOptionalOpen: Story = {
|
|
7015
|
+
parameters: {
|
|
7016
|
+
docs: {
|
|
7017
|
+
description: {
|
|
7018
|
+
story:
|
|
7019
|
+
"Renders an all-optional compact form with an explicit `compactCollapsedGroups={['optional']}` — the Optional box stays open anyway, because it is the whole form and collapsing it would render a titled card containing one collapsed strip and nothing else. The invariant outranks the consumer's collapse list.",
|
|
7020
|
+
},
|
|
7021
|
+
},
|
|
7022
|
+
},
|
|
7023
|
+
args: {
|
|
7024
|
+
compact: true,
|
|
7025
|
+
minColumnWidth: '300px',
|
|
7026
|
+
options: {
|
|
7027
|
+
alpha: { type: 'string', ui_type: 'string', display_name: 'Alpha' },
|
|
7028
|
+
beta: { type: 'string', ui_type: 'string', display_name: 'Beta' },
|
|
7029
|
+
} as unknown as IOptionsSchema,
|
|
7030
|
+
value: {} as IOptions,
|
|
7031
|
+
compactCollapsedGroups: ['optional'],
|
|
7032
|
+
},
|
|
7033
|
+
play: async () => {
|
|
7034
|
+
// The lone Optional box is open — its rows are on screen with no click.
|
|
7035
|
+
await expectBoxOpen('Optional');
|
|
7036
|
+
await _testsWaitForText('Alpha');
|
|
7037
|
+
await _testsWaitForText('Beta');
|
|
7038
|
+
},
|
|
7039
|
+
};
|
|
7040
|
+
|
|
7041
|
+
export const CompactNoSearchCollapseIsFinal: Story = {
|
|
7042
|
+
parameters: {
|
|
7043
|
+
docs: {
|
|
7044
|
+
description: {
|
|
7045
|
+
story:
|
|
7046
|
+
"Renders a compact form with `compactToolbar={{ search: false }}` and `compactCollapsedGroups={['set']}` — with the search gone there is no query to force a box open, so the collapse list fully determines what is open: Set stays collapsed, and Optional (not listed — the list replaces the default rather than adding to it) renders open.",
|
|
7047
|
+
},
|
|
7048
|
+
},
|
|
7049
|
+
},
|
|
7050
|
+
args: {
|
|
7051
|
+
compact: true,
|
|
7052
|
+
minColumnWidth: '300px',
|
|
7053
|
+
options: CompactFieldsMenuSchema,
|
|
7054
|
+
value: CompactValue,
|
|
7055
|
+
groups: CompactGroups,
|
|
7056
|
+
compactCollapsedGroups: ['set'],
|
|
7057
|
+
compactToolbar: { search: false },
|
|
7058
|
+
},
|
|
7059
|
+
play: async () => {
|
|
7060
|
+
await _testsWaitForText('Description');
|
|
7061
|
+
|
|
7062
|
+
// No search input — nothing can override the collapse list.
|
|
7063
|
+
await waitFor(() =>
|
|
7064
|
+
expect(document.querySelector('input[placeholder="Filter fields..."]')).toBeFalsy()
|
|
7065
|
+
);
|
|
7066
|
+
|
|
7067
|
+
// Set is collapsed for good; Optional is NOT in the list, so it is open —
|
|
7068
|
+
// the list replaces the `['optional']` default rather than extending it.
|
|
7069
|
+
await expectBoxCollapsed('Set');
|
|
7070
|
+
await expectBoxOpen('Optional');
|
|
7071
|
+
await _testsWaitForTextToNotExist('order-fulfilment');
|
|
7072
|
+
},
|
|
7073
|
+
};
|
|
7074
|
+
|
|
7075
|
+
/**
|
|
7076
|
+
* A field that opens as a CARD can be cancelled.
|
|
7077
|
+
*
|
|
7078
|
+
* Which shape an open field takes is decided by its type: `COMPACT_COMPLEX_TYPES`
|
|
7079
|
+
* includes `any`, so an `any`-typed option — a test assertion's Expected Value, a
|
|
7080
|
+
* Qog action state option — never edits inline and always opens the card below.
|
|
7081
|
+
*
|
|
7082
|
+
* The card's action strip had no Cancel and no Revert, and the Escape handler
|
|
7083
|
+
* lives on the inline row's editor, so a card ignored it. Every way out of a
|
|
7084
|
+
* card therefore COMMITTED: Done, clicking away, and Escape. There was no way to
|
|
7085
|
+
* abandon an edit and get the original value back.
|
|
7086
|
+
*/
|
|
7087
|
+
export const CompactCardCancelDiscardsTheEdit: Story = {
|
|
7088
|
+
parameters: {
|
|
7089
|
+
docs: {
|
|
7090
|
+
description: {
|
|
7091
|
+
story:
|
|
7092
|
+
'Opens an `any`-typed option (which always opens as a card), edits it, then cancels — the original value comes back. Cancel appears only once there is something to discard.',
|
|
7093
|
+
},
|
|
7094
|
+
},
|
|
7095
|
+
},
|
|
7096
|
+
args: {
|
|
7097
|
+
compact: true,
|
|
7098
|
+
minColumnWidth: '300px',
|
|
7099
|
+
options: {
|
|
7100
|
+
expected_value: {
|
|
7101
|
+
type: 'any',
|
|
7102
|
+
display_name: 'Expected Value',
|
|
7103
|
+
short_desc: 'What the checked value should be',
|
|
7104
|
+
},
|
|
7105
|
+
} as never,
|
|
7106
|
+
value: {
|
|
7107
|
+
expected_value: { type: 'string', value: 'original' },
|
|
7108
|
+
} as never,
|
|
7109
|
+
},
|
|
7110
|
+
play: async ({ canvasElement }) => {
|
|
7111
|
+
await _testsClickText('Expected Value');
|
|
7112
|
+
// the card, not the inline row - this is the shape the bug lived in
|
|
7113
|
+
await waitFor(() =>
|
|
7114
|
+
expect(canvasElement.querySelector('.options-readfirst-card')).toBeTruthy()
|
|
7115
|
+
);
|
|
7116
|
+
|
|
7117
|
+
// Nothing typed yet: Cancel would do exactly what Done does, so it is not
|
|
7118
|
+
// offered. Two buttons differing in name but not in effect are worse than one.
|
|
7119
|
+
expect(canvasElement.querySelector('.options-readfirst-cancel')).toBeNull();
|
|
7120
|
+
|
|
7121
|
+
await _testsChangeStringField({
|
|
7122
|
+
selector: '.options-readfirst-card .reqore-textarea',
|
|
7123
|
+
value: 'edited',
|
|
7124
|
+
});
|
|
7125
|
+
await waitFor(() =>
|
|
7126
|
+
expect(canvasElement.querySelector('.options-readfirst-cancel')).toBeTruthy()
|
|
7127
|
+
);
|
|
7128
|
+
|
|
7129
|
+
await _testsClickButton({ selector: '.options-readfirst-cancel' });
|
|
7130
|
+
await _testsWaitForText('original');
|
|
7131
|
+
await _testsWaitForTextToNotExist('edited');
|
|
7132
|
+
},
|
|
7133
|
+
};
|
|
7134
|
+
|
|
7135
|
+
/**
|
|
7136
|
+
* Escape discards from a card, exactly as it does from the inline row.
|
|
7137
|
+
*
|
|
7138
|
+
* Escape is the one keystroke every editor treats as "get me out of this without
|
|
7139
|
+
* saving". The card ignored it entirely, which made the keystroke a silent
|
|
7140
|
+
* commit — the quietest possible way to keep something the author was trying to
|
|
7141
|
+
* throw away.
|
|
7142
|
+
*/
|
|
7143
|
+
export const CompactCardEscapeDiscardsTheEdit: Story = {
|
|
7144
|
+
parameters: {
|
|
7145
|
+
docs: {
|
|
7146
|
+
description: {
|
|
7147
|
+
story:
|
|
7148
|
+
'Edits an `any`-typed option in its card and presses Escape — the edit is discarded rather than kept, matching the inline row.',
|
|
7149
|
+
},
|
|
7150
|
+
},
|
|
7151
|
+
},
|
|
7152
|
+
args: {
|
|
7153
|
+
compact: true,
|
|
7154
|
+
minColumnWidth: '300px',
|
|
7155
|
+
options: {
|
|
7156
|
+
expected_value: {
|
|
7157
|
+
type: 'any',
|
|
7158
|
+
display_name: 'Expected Value',
|
|
7159
|
+
short_desc: 'What the checked value should be',
|
|
7160
|
+
},
|
|
7161
|
+
} as never,
|
|
7162
|
+
value: {
|
|
7163
|
+
expected_value: { type: 'string', value: 'original' },
|
|
7164
|
+
} as never,
|
|
7165
|
+
},
|
|
7166
|
+
play: async ({ canvasElement }) => {
|
|
7167
|
+
await _testsClickText('Expected Value');
|
|
7168
|
+
await waitFor(() =>
|
|
7169
|
+
expect(canvasElement.querySelector('.options-readfirst-card')).toBeTruthy()
|
|
7170
|
+
);
|
|
7171
|
+
|
|
7172
|
+
await _testsChangeStringField({
|
|
7173
|
+
selector: '.options-readfirst-card .reqore-textarea',
|
|
7174
|
+
value: 'edited',
|
|
7175
|
+
});
|
|
7176
|
+
await _testsWaitForText('edited');
|
|
7177
|
+
|
|
7178
|
+
// Fired on the editor itself, which is where the key actually lands: the
|
|
7179
|
+
// handler sits on a wrapper around the editor, and events bubble UP.
|
|
7180
|
+
const editor = canvasElement.querySelector(
|
|
7181
|
+
'.options-readfirst-card .reqore-textarea'
|
|
7182
|
+
) as HTMLElement;
|
|
7183
|
+
fireEvent.keyDown(editor, { key: 'Escape' });
|
|
7184
|
+
|
|
7185
|
+
await _testsWaitForText('original');
|
|
7186
|
+
await _testsWaitForTextToNotExist('edited');
|
|
7187
|
+
},
|
|
7188
|
+
};
|
|
7189
|
+
|
|
7190
|
+
/**
|
|
7191
|
+
* An unset field shows the default the server will use, not an em-dash.
|
|
7192
|
+
*
|
|
7193
|
+
* A field with no value of its own but a declared `default_value` is not
|
|
7194
|
+
* unanswered — the default is what the server will apply if the field is left
|
|
7195
|
+
* alone. The row used to say the opposite, rendering the same em-dash as a
|
|
7196
|
+
* field with no default at all, so the only place a default surfaced was a
|
|
7197
|
+
* `Default: …` line behind the info toggle, and only when the schema also
|
|
7198
|
+
* supplied `default_value_desc`.
|
|
7199
|
+
*
|
|
7200
|
+
* Both halves have to hold together, which is why they share one frame here:
|
|
7201
|
+
* the default is VISIBLE, and the field is still NOT SET. Author keeps the
|
|
7202
|
+
* dimmed italic treatment of an unanswered row and stays in the Optional box —
|
|
7203
|
+
* if it ever moved to Set, the change would have achieved nothing.
|
|
7204
|
+
*
|
|
7205
|
+
* The motivating case is Qorus's `author` field, which every create form
|
|
7206
|
+
* carries. It was served the current username as its VALUE, so Author occupied
|
|
7207
|
+
* a row in the Set box of every create form to say only what the server was
|
|
7208
|
+
* going to do anyway. Serving it as a default fixes the bucketing; without this
|
|
7209
|
+
* change it would have made the information disappear instead.
|
|
7210
|
+
*/
|
|
7211
|
+
export const CompactUnsetFieldShowsItsDefault: Story = {
|
|
7212
|
+
parameters: {
|
|
7213
|
+
docs: {
|
|
7214
|
+
description: {
|
|
7215
|
+
story:
|
|
7216
|
+
'Renders a compact form holding the three value treatments side by side: Author and Mode are unset but declare a default, so each row shows the default it will contribute; Notes has no default and keeps the em-dash; Name carries a real value. The defaulted rows stay dimmed and stay in the Optional box — showing a default does not make a field count as answered.',
|
|
7217
|
+
},
|
|
7218
|
+
},
|
|
7219
|
+
},
|
|
7220
|
+
args: {
|
|
7221
|
+
compact: true,
|
|
7222
|
+
minColumnWidth: '300px',
|
|
7223
|
+
// This story is about what a rendered row SAYS, so every box is open.
|
|
7224
|
+
// `compactCollapsedGroups` is a default parameter, so passing a value
|
|
7225
|
+
// REPLACES the ['optional'] default rather than adding to it.
|
|
7226
|
+
compactCollapsedGroups: [],
|
|
7227
|
+
options: {
|
|
7228
|
+
name: { type: 'string', display_name: 'Name', required: true },
|
|
7229
|
+
// the shape the server sends: `default_value` UI-wrapped as {type, value}
|
|
7230
|
+
author: {
|
|
7231
|
+
type: 'string',
|
|
7232
|
+
display_name: 'Author',
|
|
7233
|
+
short_desc: 'Who the object will be attributed to',
|
|
7234
|
+
default_value: { type: 'string', value: 'dnichols' },
|
|
7235
|
+
},
|
|
7236
|
+
// a bare default, with no envelope, is equally valid
|
|
7237
|
+
mode: {
|
|
7238
|
+
type: 'string',
|
|
7239
|
+
display_name: 'Mode',
|
|
7240
|
+
short_desc: 'How the run treats side effects',
|
|
7241
|
+
default_value: 'simulate',
|
|
7242
|
+
},
|
|
7243
|
+
// no default at all — the em-dash is the honest answer for this one
|
|
7244
|
+
notes: { type: 'string', display_name: 'Notes' },
|
|
7245
|
+
} as never,
|
|
7246
|
+
value: {
|
|
7247
|
+
name: { type: 'string', value: 'my-test' },
|
|
7248
|
+
} as never,
|
|
7249
|
+
},
|
|
7250
|
+
play: async ({ canvasElement }) => {
|
|
7251
|
+
const rowText = (field: string) =>
|
|
7252
|
+
canvasElement
|
|
7253
|
+
.querySelector(`[data-field="${field}"] .options-readfirst-valuetext`)
|
|
7254
|
+
?.textContent?.trim();
|
|
7255
|
+
|
|
7256
|
+
await waitFor(() => expect(canvasElement.querySelector('[data-field="author"]')).toBeTruthy());
|
|
7257
|
+
|
|
7258
|
+
// The default is what the row shows, through the same formatter a real
|
|
7259
|
+
// value goes through
|
|
7260
|
+
await waitFor(() => expect(rowText('author')).toBe('dnichols'));
|
|
7261
|
+
expect(rowText('mode')).toBe('simulate');
|
|
7262
|
+
|
|
7263
|
+
// …and a field with nothing to fall back on still says so
|
|
7264
|
+
expect(rowText('notes')).toBe('—');
|
|
7265
|
+
|
|
7266
|
+
// The other half of the fix: visible, but still NOT answered. Author must
|
|
7267
|
+
// stay in Optional — landing in Set is the regression this guards.
|
|
7268
|
+
const authorBox = canvasElement
|
|
7269
|
+
.querySelector('[data-field="author"]')
|
|
7270
|
+
?.closest('.options-readfirst-group');
|
|
7271
|
+
expect(authorBox?.textContent).toContain('Optional');
|
|
7272
|
+
},
|
|
7273
|
+
};
|
|
7274
|
+
|
|
7275
|
+
/**
|
|
7276
|
+
* A capped box folds its remainder behind a row, not a scrap of text.
|
|
7277
|
+
*
|
|
7278
|
+
* `maxFieldsShown` caps how many rows any status box shows — Needs attention,
|
|
7279
|
+
* Set and Optional alike — so a long form stays scannable instead of making the
|
|
7280
|
+
* reader page past one box to reach the next.
|
|
7281
|
+
*
|
|
7282
|
+
* The control that reveals the rest IS a field row: same grid, same padding,
|
|
7283
|
+
* same hover, so it lines up with the rows above rather than floating in the
|
|
7284
|
+
* box's corner. A dashed edge and a chevron are what say it is not itself a
|
|
7285
|
+
* field — nothing on it can be set.
|
|
7286
|
+
*/
|
|
7287
|
+
export const CompactMaxFieldsShown: Story = {
|
|
7288
|
+
parameters: {
|
|
7289
|
+
docs: {
|
|
7290
|
+
description: {
|
|
7291
|
+
story:
|
|
7292
|
+
'Renders a compact form with `maxFieldsShown: 3`, so each status box shows three rows and folds the rest behind a dashed "N more fields" row. Clicking that row reveals the remainder and the label becomes "Show fewer".',
|
|
7293
|
+
},
|
|
7294
|
+
},
|
|
7295
|
+
},
|
|
7296
|
+
args: {
|
|
7297
|
+
compact: true,
|
|
7298
|
+
minColumnWidth: '300px',
|
|
7299
|
+
compactCollapsedGroups: [],
|
|
7300
|
+
maxFieldsShown: 3,
|
|
7301
|
+
options: {
|
|
7302
|
+
alpha: { type: 'string', display_name: 'Alpha' },
|
|
7303
|
+
bravo: { type: 'string', display_name: 'Bravo' },
|
|
7304
|
+
charlie: { type: 'string', display_name: 'Charlie' },
|
|
7305
|
+
delta: { type: 'string', display_name: 'Delta' },
|
|
7306
|
+
echo: { type: 'string', display_name: 'Echo' },
|
|
7307
|
+
foxtrot: { type: 'string', display_name: 'Foxtrot' },
|
|
7308
|
+
} as never,
|
|
7309
|
+
value: {} as never,
|
|
7310
|
+
},
|
|
7311
|
+
play: async ({ canvasElement }) => {
|
|
7312
|
+
// Three shown, three folded — the control counts what is not on screen
|
|
7313
|
+
await _testsWaitForText('3 more optional fields');
|
|
7314
|
+
await _testsWaitForText('Alpha');
|
|
7315
|
+
await _testsWaitForTextToNotExist('Foxtrot');
|
|
7316
|
+
|
|
7317
|
+
// It is a ROW, not a bare line of text: it carries the row class, so it
|
|
7318
|
+
// inherits the grid and padding the fields above it use
|
|
7319
|
+
const more = canvasElement.querySelector('.readfirst-more-row') as HTMLElement;
|
|
7320
|
+
expect(more).toBeTruthy();
|
|
7321
|
+
expect(more.classList.contains('readfirst-row')).toBe(true);
|
|
7322
|
+
expect(more.getAttribute('aria-expanded')).toBe('false');
|
|
7323
|
+
|
|
7324
|
+
// The dashed edge is the thing that says "not a field". Asserting the
|
|
7325
|
+
// COMPUTED style, because the first cut of this took its colour from the
|
|
7326
|
+
// inter-row divider tint (~8% of the text colour) and rendered an invisible
|
|
7327
|
+
// border — the rule was there, the affordance was not.
|
|
7328
|
+
const edge = getComputedStyle(more);
|
|
7329
|
+
expect(edge.borderStyle).toBe('dashed');
|
|
7330
|
+
expect(edge.borderTopWidth).toBe('1px');
|
|
7331
|
+
// …and it inherits the rows' geometry rather than inventing its own
|
|
7332
|
+
expect(edge.paddingLeft).toBe(getComputedStyle(
|
|
7333
|
+
canvasElement.querySelector('.readfirst-row:not(.readfirst-more-row)') as HTMLElement
|
|
7334
|
+
).paddingLeft);
|
|
7335
|
+
|
|
7336
|
+
fireEvent.click(more);
|
|
7337
|
+
|
|
7338
|
+
await _testsWaitForText('Foxtrot');
|
|
7339
|
+
await _testsWaitForText('Show fewer');
|
|
7340
|
+
expect(
|
|
7341
|
+
(canvasElement.querySelector('.readfirst-more-row') as HTMLElement).getAttribute(
|
|
7342
|
+
'aria-expanded'
|
|
7343
|
+
)
|
|
7344
|
+
).toBe('true');
|
|
7345
|
+
},
|
|
7346
|
+
};
|
|
7347
|
+
|
|
7348
|
+
/**
|
|
7349
|
+
* An uncapped form is unchanged.
|
|
7350
|
+
*
|
|
7351
|
+
* `maxFieldsShown` is opt-in: without it every box shows every row it holds and
|
|
7352
|
+
* no control renders at all. The story exists so that stays true — a default
|
|
7353
|
+
* that quietly started folding rows would hide fields from every consumer that
|
|
7354
|
+
* never asked for it.
|
|
7355
|
+
*/
|
|
7356
|
+
export const CompactNoMaxFieldsShown: Story = {
|
|
7357
|
+
parameters: {
|
|
7358
|
+
docs: {
|
|
7359
|
+
description: {
|
|
7360
|
+
story:
|
|
7361
|
+
'Renders the same form with no `maxFieldsShown` — every row shows and no "more fields" control appears.',
|
|
7362
|
+
},
|
|
7363
|
+
},
|
|
7364
|
+
},
|
|
7365
|
+
args: {
|
|
7366
|
+
...CompactMaxFieldsShown.args,
|
|
7367
|
+
maxFieldsShown: undefined,
|
|
7368
|
+
},
|
|
7369
|
+
play: async ({ canvasElement }) => {
|
|
7370
|
+
await _testsWaitForText('Foxtrot');
|
|
7371
|
+
expect(canvasElement.querySelector('.readfirst-more-row')).toBeNull();
|
|
7372
|
+
},
|
|
7373
|
+
};
|