@qoretechnologies/reqraft 0.10.37 → 0.10.38-pr.95.gc0ba509
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/dist/components/codeSize/index.d.ts +37 -0
- package/dist/components/codeSize/index.d.ts.map +1 -0
- package/dist/components/codeSize/index.js +87 -0
- package/dist/components/codeSize/index.js.map +1 -0
- package/dist/components/form/engine/CompactRow.d.ts +6 -1
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +261 -19
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +20 -0
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +137 -24
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts +37 -0
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -0
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +307 -0
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -0
- package/dist/components/form/engine/compactRowStyles.d.ts +13 -0
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +30 -5
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +66 -1
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +175 -5
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.js +51 -21
- package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.js +42 -25
- package/dist/components/form/fields/array/ArrayAutoField.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts +4 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +8 -1
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/helpers/validations.d.ts +14 -0
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +58 -3
- package/dist/helpers/validations.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/codeSize/index.tsx +79 -0
- package/src/components/form/engine/CompactRow.tsx +330 -24
- package/src/components/form/engine/FormEngine.stories.tsx +614 -15
- package/src/components/form/engine/FormEngine.tsx +142 -8
- package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +129 -0
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +594 -0
- package/src/components/form/engine/compactRowStyles.ts +44 -2
- package/src/components/form/engine/readFirst.ts +201 -5
- package/src/components/form/fields/array/ArrayAuto.tsx +35 -4
- package/src/components/form/fields/array/ArrayAutoField.stories.tsx +53 -0
- package/src/components/form/fields/array/ArrayAutoField.tsx +22 -4
- package/src/components/form/fields/auto/AutoFormField.tsx +11 -0
- package/src/helpers/validations.ts +59 -2
- 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';
|
|
@@ -912,6 +912,169 @@ export const OptionInheritsRenderPropFromSiblingCompact: Story = {
|
|
|
912
912
|
// cell couldn't provide on its own. Locks the compact preview so a future
|
|
913
913
|
// CompactRow refactor can't silently reduce a Qorus source-code field to an
|
|
914
914
|
// ellipsised one-liner again.
|
|
915
|
+
// An open field used to have exactly one way out — the green Done check — so
|
|
916
|
+
// every exit committed, Escape included. This locks the Cancel affordance in
|
|
917
|
+
// place: it appears only once there is something to discard, and it puts the
|
|
918
|
+
// value back to what the field was opened with.
|
|
919
|
+
export const CompactRowCancelEdit: Story = {
|
|
920
|
+
parameters: {
|
|
921
|
+
docs: {
|
|
922
|
+
description: {
|
|
923
|
+
story:
|
|
924
|
+
'Renders a compact row opened for editing with a changed value — the Cancel action appears beside Done, and clicking it puts the field back to the value it was opened with and closes it.',
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
args: {
|
|
929
|
+
compact: true,
|
|
930
|
+
minColumnWidth: '360px',
|
|
931
|
+
value: { cookie_name: { type: 'string', value: 'my-cookie' } },
|
|
932
|
+
options: {
|
|
933
|
+
cookie_name: {
|
|
934
|
+
type: 'string',
|
|
935
|
+
ui_type: 'string',
|
|
936
|
+
display_name: 'Cookie Name',
|
|
937
|
+
short_desc: 'Cookie name for cookie authentication',
|
|
938
|
+
},
|
|
939
|
+
},
|
|
940
|
+
},
|
|
941
|
+
play: async ({ canvasElement }) => {
|
|
942
|
+
// Open the field.
|
|
943
|
+
const row = canvasElement.querySelector<HTMLElement>('[data-field="cookie_name"]');
|
|
944
|
+
expect(row).toBeTruthy();
|
|
945
|
+
fireEvent.click(row!);
|
|
946
|
+
await waitFor(() =>
|
|
947
|
+
expect(canvasElement.querySelector('.readfirst-row-editing')).toBeTruthy()
|
|
948
|
+
);
|
|
949
|
+
|
|
950
|
+
// An untouched field offers no Cancel — it would do exactly what Done does.
|
|
951
|
+
expect(canvasElement.querySelector('.options-readfirst-cancel')).toBeNull();
|
|
952
|
+
|
|
953
|
+
const input = canvasElement.querySelector<HTMLInputElement>(
|
|
954
|
+
'[data-field="cookie_name"] input, [data-field="cookie_name"] textarea'
|
|
955
|
+
);
|
|
956
|
+
expect(input).toBeTruthy();
|
|
957
|
+
fireEvent.change(input!, { target: { value: 'changed-cookie' } });
|
|
958
|
+
|
|
959
|
+
// Cancel appears the moment the value differs from what was opened.
|
|
960
|
+
await waitFor(
|
|
961
|
+
() => expect(canvasElement.querySelector('.options-readfirst-cancel')).toBeTruthy(),
|
|
962
|
+
{ timeout: 5000 }
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
fireEvent.click(canvasElement.querySelector<HTMLElement>('.options-readfirst-cancel')!);
|
|
966
|
+
|
|
967
|
+
// The row closes and the opened-with value is back.
|
|
968
|
+
await waitFor(() => expect(canvasElement.querySelector('.readfirst-row-editing')).toBeNull(), {
|
|
969
|
+
timeout: 5000,
|
|
970
|
+
});
|
|
971
|
+
await waitFor(
|
|
972
|
+
() =>
|
|
973
|
+
expect(
|
|
974
|
+
canvasElement.querySelector('[data-field="cookie_name"]')?.textContent ?? ''
|
|
975
|
+
).toContain('my-cookie'),
|
|
976
|
+
{ timeout: 5000 }
|
|
977
|
+
);
|
|
978
|
+
},
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
// Language and Source code are one decision — "what is this code, and in what
|
|
982
|
+
// language" — and the form used to ask it as two unrelated rows one above the
|
|
983
|
+
// other. `absorb_fields` lets the editor take the language into its own
|
|
984
|
+
// container so the pair reads as the single element it is.
|
|
985
|
+
export const CompactRowAbsorbsLanguage: Story = {
|
|
986
|
+
parameters: {
|
|
987
|
+
docs: {
|
|
988
|
+
description: {
|
|
989
|
+
story:
|
|
990
|
+
'Renders a code-editor row that absorbs its Language sibling — the language control sits inside the Source Code container above the editor instead of on a row of its own, and the collapsed row summarises it as a chip beside the code size.',
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
args: {
|
|
995
|
+
compact: true,
|
|
996
|
+
minColumnWidth: '360px',
|
|
997
|
+
componentOverrides: { 'code-editor': CodeEditorStandin },
|
|
998
|
+
value: {
|
|
999
|
+
language: { type: 'string', value: 'qore' },
|
|
1000
|
+
source: { type: 'string', value: '%new-style\nclass Example {\n}\n' },
|
|
1001
|
+
},
|
|
1002
|
+
options: {
|
|
1003
|
+
language: {
|
|
1004
|
+
type: 'string',
|
|
1005
|
+
ui_type: 'string',
|
|
1006
|
+
display_name: 'Language',
|
|
1007
|
+
allowed_values: [
|
|
1008
|
+
{ display_name: 'Qore', value: { type: 'string', value: 'qore' } },
|
|
1009
|
+
{ display_name: 'Python', value: { type: 'string', value: 'python' } },
|
|
1010
|
+
],
|
|
1011
|
+
},
|
|
1012
|
+
source: {
|
|
1013
|
+
type: 'string',
|
|
1014
|
+
ui_type: 'code-editor',
|
|
1015
|
+
display_name: 'Source Code',
|
|
1016
|
+
inherit_props: { language: 'language' },
|
|
1017
|
+
absorb_fields: ['language'],
|
|
1018
|
+
},
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
play: async ({ canvasElement }) => {
|
|
1022
|
+
// The absorbed field has no row of its own.
|
|
1023
|
+
await waitFor(() => expect(canvasElement.querySelector('[data-field="source"]')).toBeTruthy());
|
|
1024
|
+
expect(canvasElement.querySelector('[data-field="language"]')).toBeNull();
|
|
1025
|
+
|
|
1026
|
+
// Collapsed, the host still reports the absorbed value — otherwise the
|
|
1027
|
+
// language would vanish from the form whenever the editor is closed.
|
|
1028
|
+
const sourceRow = canvasElement.querySelector('[data-field="source"]');
|
|
1029
|
+
expect(sourceRow?.textContent ?? '').toContain('qore');
|
|
1030
|
+
|
|
1031
|
+
// Opening the host renders the language control inside its container.
|
|
1032
|
+
fireEvent.click(sourceRow as HTMLElement);
|
|
1033
|
+
await waitFor(() =>
|
|
1034
|
+
expect(canvasElement.querySelector('.readfirst-row-editing')).toBeTruthy()
|
|
1035
|
+
);
|
|
1036
|
+
await waitFor(() =>
|
|
1037
|
+
expect(canvasElement.querySelector('.options-readfirst-absorbed')).toBeTruthy()
|
|
1038
|
+
);
|
|
1039
|
+
// It is still identifiable as its own field, not a property of the editor.
|
|
1040
|
+
expect(canvasElement.querySelector('.options-readfirst-absorbed')?.textContent ?? '').toContain(
|
|
1041
|
+
'Language'
|
|
1042
|
+
);
|
|
1043
|
+
},
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
export const CompactRowCancelEditAffordance: Story = {
|
|
1047
|
+
...CompactRowCancelEdit,
|
|
1048
|
+
parameters: {
|
|
1049
|
+
docs: {
|
|
1050
|
+
description: {
|
|
1051
|
+
story:
|
|
1052
|
+
'Renders a compact row open for editing with a changed value, stopping while it is open — Cancel edit sits beside the green Done check, which is what an open field used to offer on its own.',
|
|
1053
|
+
},
|
|
1054
|
+
},
|
|
1055
|
+
},
|
|
1056
|
+
play: async ({ canvasElement }) => {
|
|
1057
|
+
const row = canvasElement.querySelector<HTMLElement>('[data-field="cookie_name"]');
|
|
1058
|
+
expect(row).toBeTruthy();
|
|
1059
|
+
fireEvent.click(row!);
|
|
1060
|
+
await waitFor(() =>
|
|
1061
|
+
expect(canvasElement.querySelector('.readfirst-row-editing')).toBeTruthy()
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
const input = canvasElement.querySelector<HTMLInputElement>(
|
|
1065
|
+
'[data-field="cookie_name"] input, [data-field="cookie_name"] textarea'
|
|
1066
|
+
);
|
|
1067
|
+
fireEvent.change(input!, { target: { value: 'changed-cookie' } });
|
|
1068
|
+
|
|
1069
|
+
await waitFor(
|
|
1070
|
+
() => expect(canvasElement.querySelector('.options-readfirst-cancel')).toBeTruthy(),
|
|
1071
|
+
{ timeout: 5000 }
|
|
1072
|
+
);
|
|
1073
|
+
// Both ways out are on screen together, which is the point of the story.
|
|
1074
|
+
expect(canvasElement.querySelector('.options-readfirst-done')).toBeTruthy();
|
|
1075
|
+
},
|
|
1076
|
+
};
|
|
1077
|
+
|
|
915
1078
|
export const CompactRowCodeEditorPreview: Story = {
|
|
916
1079
|
parameters: {
|
|
917
1080
|
docs: {
|
|
@@ -980,6 +1143,132 @@ export const CompactRowCodeEditorPreview: Story = {
|
|
|
980
1143
|
},
|
|
981
1144
|
};
|
|
982
1145
|
|
|
1146
|
+
export const CompactRowCodeEditorMessage: Story = {
|
|
1147
|
+
parameters: {
|
|
1148
|
+
docs: {
|
|
1149
|
+
description: {
|
|
1150
|
+
story:
|
|
1151
|
+
"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.",
|
|
1152
|
+
},
|
|
1153
|
+
},
|
|
1154
|
+
},
|
|
1155
|
+
args: {
|
|
1156
|
+
...CompactRowCodeEditorPreview.args,
|
|
1157
|
+
options: {
|
|
1158
|
+
...(CompactRowCodeEditorPreview.args as any).options,
|
|
1159
|
+
source: {
|
|
1160
|
+
...(CompactRowCodeEditorPreview.args as any).options.source,
|
|
1161
|
+
// The field ABSORBS its language sibling, which is how Qorus ships it:
|
|
1162
|
+
// the language control renders inside the Source Code container. That
|
|
1163
|
+
// is the shape the message has to survive.
|
|
1164
|
+
absorb_fields: ['language'],
|
|
1165
|
+
// Qorus groups its fields (INFO / SCALING), so the row renders inside a
|
|
1166
|
+
// group rather than in a flat list.
|
|
1167
|
+
group: 'info',
|
|
1168
|
+
messages: [
|
|
1169
|
+
{
|
|
1170
|
+
intent: 'danger',
|
|
1171
|
+
title: 'Line 4',
|
|
1172
|
+
content: "syntax error, unexpected '}'",
|
|
1173
|
+
},
|
|
1174
|
+
],
|
|
1175
|
+
},
|
|
1176
|
+
},
|
|
1177
|
+
},
|
|
1178
|
+
play: async ({ canvasElement }) => {
|
|
1179
|
+
// The message renders on the collapsed row, beside the code preview rather
|
|
1180
|
+
// than instead of it.
|
|
1181
|
+
await waitFor(
|
|
1182
|
+
() => {
|
|
1183
|
+
const row = canvasElement.querySelector('[data-field="source"]');
|
|
1184
|
+
expect(row?.textContent ?? '').toContain('Line 4');
|
|
1185
|
+
expect(row?.textContent ?? '').toContain("syntax error, unexpected '}'");
|
|
1186
|
+
},
|
|
1187
|
+
{ timeout: 5000 }
|
|
1188
|
+
);
|
|
1189
|
+
|
|
1190
|
+
const row = canvasElement.querySelector('[data-field="source"]');
|
|
1191
|
+
// ... and the preview it sits beside is still there.
|
|
1192
|
+
expect(row?.textContent ?? '').toContain('class ExampleJob inherits QorusJob');
|
|
1193
|
+
},
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Adds a schema message to a field a moment AFTER the form has mounted, the way
|
|
1198
|
+
* a host that validates asynchronously does.
|
|
1199
|
+
*/
|
|
1200
|
+
const LateMessageHarness = (args: any) => {
|
|
1201
|
+
const [options, setOptions] = useState(args.options);
|
|
1202
|
+
|
|
1203
|
+
useEffect(() => {
|
|
1204
|
+
const timer = setTimeout(
|
|
1205
|
+
() =>
|
|
1206
|
+
setOptions((current: any) => ({
|
|
1207
|
+
...current,
|
|
1208
|
+
source: {
|
|
1209
|
+
...current.source,
|
|
1210
|
+
messages: [
|
|
1211
|
+
{ intent: 'danger', title: 'Line 4', content: "syntax error, unexpected '}'" },
|
|
1212
|
+
],
|
|
1213
|
+
},
|
|
1214
|
+
})),
|
|
1215
|
+
100
|
|
1216
|
+
);
|
|
1217
|
+
return () => clearTimeout(timer);
|
|
1218
|
+
}, []);
|
|
1219
|
+
|
|
1220
|
+
return <FormEngine {...args} options={options} />;
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
export const CompactRowCodeEditorLateMessage: Story = {
|
|
1224
|
+
parameters: {
|
|
1225
|
+
docs: {
|
|
1226
|
+
description: {
|
|
1227
|
+
story:
|
|
1228
|
+
'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.',
|
|
1229
|
+
},
|
|
1230
|
+
},
|
|
1231
|
+
},
|
|
1232
|
+
args: { ...CompactRowCodeEditorPreview.args },
|
|
1233
|
+
render: (args) => <LateMessageHarness {...args} />,
|
|
1234
|
+
play: async ({ canvasElement }) => {
|
|
1235
|
+
await waitFor(
|
|
1236
|
+
() => {
|
|
1237
|
+
const row = canvasElement.querySelector('[data-field="source"]');
|
|
1238
|
+
expect(row?.textContent ?? '').toContain('Line 4');
|
|
1239
|
+
},
|
|
1240
|
+
{ timeout: 10000 }
|
|
1241
|
+
);
|
|
1242
|
+
},
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
export const CompactRowMessageWhileEditing: Story = {
|
|
1246
|
+
parameters: {
|
|
1247
|
+
docs: {
|
|
1248
|
+
description: {
|
|
1249
|
+
story:
|
|
1250
|
+
"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.",
|
|
1251
|
+
},
|
|
1252
|
+
},
|
|
1253
|
+
},
|
|
1254
|
+
args: { ...CompactRowCodeEditorMessage.args },
|
|
1255
|
+
play: async ({ canvasElement }) => {
|
|
1256
|
+
// Open the row, the way an author does before fixing what the message says.
|
|
1257
|
+
await waitFor(() => {
|
|
1258
|
+
expect(canvasElement.querySelector('[data-field="source"]')).toBeTruthy();
|
|
1259
|
+
});
|
|
1260
|
+
await fireEvent.click(canvasElement.querySelector('[data-field="source"]') as HTMLElement);
|
|
1261
|
+
|
|
1262
|
+
await waitFor(() => {
|
|
1263
|
+
expect(canvasElement.querySelector('.readfirst-row-editing')).toBeTruthy();
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
const editingRow = canvasElement.querySelector('.readfirst-row-editing') as HTMLElement;
|
|
1267
|
+
expect(editingRow?.textContent ?? '').toContain('Line 4');
|
|
1268
|
+
expect(editingRow?.textContent ?? '').toContain("syntax error, unexpected '}'");
|
|
1269
|
+
},
|
|
1270
|
+
};
|
|
1271
|
+
|
|
983
1272
|
export const CompactRowMarkdownPreview: Story = {
|
|
984
1273
|
parameters: {
|
|
985
1274
|
docs: {
|
|
@@ -1164,7 +1453,7 @@ export const NestedOptionInheritsRenderPropFromAncestorCompact: Story = {
|
|
|
1164
1453
|
docs: {
|
|
1165
1454
|
description: {
|
|
1166
1455
|
story:
|
|
1167
|
-
"Renders the NestedOptionInheritsRenderPropFromAncestor schema with compact=true — the compact renderer
|
|
1456
|
+
"Renders the NestedOptionInheritsRenderPropFromAncestor schema with compact=true — the compact renderer previews the list-of-hash rows through their arg_schema, naming each method, rather than printing [object Object].",
|
|
1168
1457
|
},
|
|
1169
1458
|
},
|
|
1170
1459
|
},
|
|
@@ -1229,11 +1518,13 @@ export const NestedOptionInheritsRenderPropFromAncestorCompact: Story = {
|
|
|
1229
1518
|
await waitFor(() => expect(canvas.getAllByText('Methods').length).toBeGreaterThan(0), {
|
|
1230
1519
|
timeout: 5000,
|
|
1231
1520
|
});
|
|
1232
|
-
// The list-of-hashes value
|
|
1233
|
-
//
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1521
|
+
// The list-of-hashes value names its items — never a raw "[object Object]"
|
|
1522
|
+
// (regression: it used to stringify each hash envelope). The names now come
|
|
1523
|
+
// from the schema preview rather than a joined summary line above it, so the
|
|
1524
|
+
// assertion moved with them; what must never come back is the stringified
|
|
1525
|
+
// envelope.
|
|
1526
|
+
await expect(await canvas.findByText('init', undefined, { timeout: 5000 })).toBeInTheDocument();
|
|
1527
|
+
await expect(await canvas.findByText('run')).toBeInTheDocument();
|
|
1237
1528
|
await expect(canvasElement.textContent ?? '').not.toContain('[object Object]');
|
|
1238
1529
|
},
|
|
1239
1530
|
};
|
|
@@ -1290,12 +1581,16 @@ export const CompactNestedListRowsStayCompact: Story = {
|
|
|
1290
1581
|
} as unknown as IOptionsSchema,
|
|
1291
1582
|
},
|
|
1292
1583
|
play: async ({ canvasElement }) => {
|
|
1293
|
-
// Collapsed, the row
|
|
1294
|
-
|
|
1584
|
+
// Collapsed, the row previews its items through their schema — one numbered
|
|
1585
|
+
// entry per method, each naming itself.
|
|
1586
|
+
// The method name heads its item now, so that is what identifies the row.
|
|
1587
|
+
await _testsWaitForText('init');
|
|
1588
|
+
await _testsWaitForText('run');
|
|
1295
1589
|
|
|
1296
1590
|
// Open the Methods row — this mounts ArrayAuto and, with it, one
|
|
1297
|
-
// arg_schema sub-form per row.
|
|
1298
|
-
|
|
1591
|
+
// arg_schema sub-form per row. Clicked by the field's NAME: the joined
|
|
1592
|
+
// summary that used to sit on this row is gone, replaced by the preview.
|
|
1593
|
+
await _testsClickText('Methods');
|
|
1299
1594
|
|
|
1300
1595
|
await waitFor(
|
|
1301
1596
|
() => expect(canvasElement.querySelectorAll('.array-auto-item')).toHaveLength(2),
|
|
@@ -1347,8 +1642,8 @@ export const CompactNestedListRowsStayCompactMobile: Story = {
|
|
|
1347
1642
|
),
|
|
1348
1643
|
],
|
|
1349
1644
|
play: async ({ canvasElement }) => {
|
|
1350
|
-
await _testsWaitForText('init
|
|
1351
|
-
await _testsClickText('
|
|
1645
|
+
await _testsWaitForText('init');
|
|
1646
|
+
await _testsClickText('Methods');
|
|
1352
1647
|
|
|
1353
1648
|
await waitFor(
|
|
1354
1649
|
() => expect(canvasElement.querySelectorAll('.array-auto-item')).toHaveLength(2),
|
|
@@ -4122,6 +4417,29 @@ export const CompactFieldTypes: Story = {
|
|
|
4122
4417
|
secure: { type: 'bool', display_name: 'Secure' },
|
|
4123
4418
|
},
|
|
4124
4419
|
},
|
|
4420
|
+
// The list counterpart of `connectionInfo`: a list of hashes whose fields
|
|
4421
|
+
// are declared. This is the shape the schema preview was built for, and the
|
|
4422
|
+
// catalogue was missing it — it covered the described HASH but not the
|
|
4423
|
+
// described LIST, which is the case that was actually reported.
|
|
4424
|
+
routes: {
|
|
4425
|
+
type: 'list',
|
|
4426
|
+
ui_type: 'list',
|
|
4427
|
+
element_type: 'hash',
|
|
4428
|
+
display_name: 'List of hashes (arg_schema)',
|
|
4429
|
+
group: 'structured',
|
|
4430
|
+
arg_schema: {
|
|
4431
|
+
method: {
|
|
4432
|
+
type: 'string',
|
|
4433
|
+
display_name: 'Method',
|
|
4434
|
+
allowed_values: [
|
|
4435
|
+
{ value: 'get', display_name: 'GET' },
|
|
4436
|
+
{ value: 'post', display_name: 'POST' },
|
|
4437
|
+
],
|
|
4438
|
+
},
|
|
4439
|
+
path: { type: 'string', display_name: 'Path' },
|
|
4440
|
+
handler: { type: 'string', ui_type: 'code-editor', display_name: 'Handler' },
|
|
4441
|
+
},
|
|
4442
|
+
},
|
|
4125
4443
|
freeConfig: {
|
|
4126
4444
|
type: 'free-hash',
|
|
4127
4445
|
ui_type: 'free-hash',
|
|
@@ -4330,6 +4648,22 @@ export const CompactFieldTypes: Story = {
|
|
|
4330
4648
|
secure: { type: 'bool', value: true },
|
|
4331
4649
|
},
|
|
4332
4650
|
},
|
|
4651
|
+
routes: {
|
|
4652
|
+
type: 'list',
|
|
4653
|
+
value: [
|
|
4654
|
+
// One item complete, one with the optional field unset — the empty
|
|
4655
|
+
// state stays exercised rather than being filled in everywhere.
|
|
4656
|
+
{
|
|
4657
|
+
type: 'hash',
|
|
4658
|
+
value: {
|
|
4659
|
+
method: 'get',
|
|
4660
|
+
path: '/orders',
|
|
4661
|
+
handler: 'sub get_orders() {\n return orders.all();\n}',
|
|
4662
|
+
},
|
|
4663
|
+
},
|
|
4664
|
+
{ type: 'hash', value: { method: 'post', path: '/orders' } },
|
|
4665
|
+
],
|
|
4666
|
+
},
|
|
4333
4667
|
freeConfig: { type: 'free-hash', value: '%YAML 1.2\n---\nretries: 3\ntimeout: 30\n' },
|
|
4334
4668
|
bigConfig: {
|
|
4335
4669
|
type: 'hash',
|
|
@@ -4412,7 +4746,23 @@ export const CompactFieldTypes: Story = {
|
|
|
4412
4746
|
await _testsWaitForText('#0000FF'); // colour → uppercase hex
|
|
4413
4747
|
await _testsWaitForText('rgba(255, 0, 0, 0.5)'); // colour with alpha → rgba()
|
|
4414
4748
|
await _testsWaitForText('config.txt'); // file → filename
|
|
4415
|
-
|
|
4749
|
+
// A hash the schema DESCRIBES needs no count: its preview names every field,
|
|
4750
|
+
// so "3 fields" above "Host / Port / Secure" would be the same fact twice.
|
|
4751
|
+
// A hash it does not describe keeps the count — the untyped tree beneath it
|
|
4752
|
+
// says nothing about what the value means, so the count is the only line
|
|
4753
|
+
// that does. Both halves asserted here: the distinction IS the behaviour.
|
|
4754
|
+
await _testsWaitForText('2 fields'); // undescribed hash → field count summary
|
|
4755
|
+
await _testsWaitForText('Host'); // described hash → its fields, by name
|
|
4756
|
+
await _testsWaitForTextToNotExist('3 fields');
|
|
4757
|
+
|
|
4758
|
+
// Described LIST of hashes: numbered items, labels by display_name, and the
|
|
4759
|
+
// allowed value read back as its label rather than the stored `get`.
|
|
4760
|
+
await _testsWaitForText('Path');
|
|
4761
|
+
await _testsWaitForText('/orders');
|
|
4762
|
+
await _testsWaitForText('GET');
|
|
4763
|
+
// A code sub-field renders AS code, not as a flattened mono line.
|
|
4764
|
+
await _testsWaitForText('Handler');
|
|
4765
|
+
await _testsWaitForText(/sub get_orders/);
|
|
4416
4766
|
await _testsWaitForText('order-to-invoice'); // interface reference → raw value
|
|
4417
4767
|
|
|
4418
4768
|
// Field stack (merged from dpql): byte-size shows its value string; the
|
|
@@ -4582,7 +4932,12 @@ export const CompactFieldTypesEditing: Story = {
|
|
|
4582
4932
|
await userEvent.click(within(confirmationModal!).getByRole('button', { name: 'Clear value' }));
|
|
4583
4933
|
await waitFor(() => {
|
|
4584
4934
|
expect(editRow('enabled').querySelector('.options-readfirst-clear')).not.toBeInTheDocument();
|
|
4585
|
-
|
|
4935
|
+
// Clear swaps for the undo affordance. In an OPEN field that is
|
|
4936
|
+
// "Cancel edit" — the field was opened holding its form-load value, so
|
|
4937
|
+
// undoing this edit and reverting the field are the same thing and only
|
|
4938
|
+
// one button is offered. The form-load revert reappears here only when
|
|
4939
|
+
// the field was already modified before it was opened.
|
|
4940
|
+
expect(editRow('enabled').querySelector('.options-readfirst-cancel')).toBeInTheDocument();
|
|
4586
4941
|
});
|
|
4587
4942
|
},
|
|
4588
4943
|
};
|
|
@@ -5915,3 +6270,247 @@ export const CompactRowMarkdownWithoutRenderer: Story = {
|
|
|
5915
6270
|
expect(descText).not.toContain('**');
|
|
5916
6271
|
},
|
|
5917
6272
|
};
|
|
6273
|
+
|
|
6274
|
+
/**
|
|
6275
|
+
* The auth-profile scheme sub-schema, which is where all three of the list-row
|
|
6276
|
+
* affordances below were reported. One required choice with named allowed
|
|
6277
|
+
* values, one field that belongs to a single choice.
|
|
6278
|
+
*/
|
|
6279
|
+
const AuthSchemeArgSchema = {
|
|
6280
|
+
type: {
|
|
6281
|
+
type: 'string',
|
|
6282
|
+
ui_type: 'string',
|
|
6283
|
+
display_name: 'Scheme Type',
|
|
6284
|
+
short_desc: 'Authentication scheme type',
|
|
6285
|
+
required: true,
|
|
6286
|
+
allowed_values: [
|
|
6287
|
+
{ value: 'default', display_name: 'Default RBAC' },
|
|
6288
|
+
{ value: 'cookie', display_name: 'Cookie' },
|
|
6289
|
+
{ value: 'oauth2', display_name: 'OAuth2' },
|
|
6290
|
+
],
|
|
6291
|
+
},
|
|
6292
|
+
cookie_name: {
|
|
6293
|
+
type: 'string',
|
|
6294
|
+
ui_type: 'string',
|
|
6295
|
+
display_name: 'Session Cookie Name',
|
|
6296
|
+
short_desc: 'Applies to the Cookie scheme alone',
|
|
6297
|
+
depends_on: ['type=cookie'],
|
|
6298
|
+
},
|
|
6299
|
+
} as unknown as IOptionsSchema;
|
|
6300
|
+
|
|
6301
|
+
const AuthSchemeOptions = {
|
|
6302
|
+
schemes: {
|
|
6303
|
+
type: 'list',
|
|
6304
|
+
ui_type: 'list',
|
|
6305
|
+
element_type: 'hash',
|
|
6306
|
+
display_name: 'Authentication Schemes',
|
|
6307
|
+
short_desc: 'Schemes tried in order, first match wins',
|
|
6308
|
+
required: true,
|
|
6309
|
+
arg_schema: AuthSchemeArgSchema,
|
|
6310
|
+
},
|
|
6311
|
+
} as unknown as IOptionsSchema;
|
|
6312
|
+
|
|
6313
|
+
/**
|
|
6314
|
+
* A list-of-hash row reads back in the words the form asked for the value.
|
|
6315
|
+
*
|
|
6316
|
+
* Reported on an auth profile: the row summarised as "2 items" and its preview
|
|
6317
|
+
* printed a data tree — "Object · 1 field" over `type: default`. Both are
|
|
6318
|
+
* exactly what is stored, and neither is a string the author has ever seen: the
|
|
6319
|
+
* form calls that key "Scheme Type" and that value "Default RBAC". A row meant
|
|
6320
|
+
* to confirm a choice showed a value nobody had chosen, in a shape nobody had
|
|
6321
|
+
* asked about.
|
|
6322
|
+
*
|
|
6323
|
+
* A generic data view has to announce what it found, because inference is all it
|
|
6324
|
+
* has. A field with an `arg_schema` needs none of that — the shape, the names and
|
|
6325
|
+
* the choices are known before the value arrives — so the preview renders
|
|
6326
|
+
* THROUGH the schema (`SchemaDataView`) and the summary resolves through the
|
|
6327
|
+
* same one. The row, the preview and the editor cannot describe a value three
|
|
6328
|
+
* ways.
|
|
6329
|
+
*/
|
|
6330
|
+
export const CompactListOfHashReadsInSchemaWords: Story = {
|
|
6331
|
+
parameters: {
|
|
6332
|
+
docs: {
|
|
6333
|
+
description: {
|
|
6334
|
+
story:
|
|
6335
|
+
'A list-of-hash option whose sub-schema names its fields and values. The collapsed row summarises by those names ("Default RBAC, Cookie") and the preview renders as labelled schema rows — "Scheme Type · Default RBAC" per numbered item — instead of an untyped data tree over the stored `type: default`.',
|
|
6336
|
+
},
|
|
6337
|
+
},
|
|
6338
|
+
},
|
|
6339
|
+
args: {
|
|
6340
|
+
compact: true,
|
|
6341
|
+
minColumnWidth: '300px',
|
|
6342
|
+
options: AuthSchemeOptions,
|
|
6343
|
+
value: {
|
|
6344
|
+
schemes: {
|
|
6345
|
+
type: 'list',
|
|
6346
|
+
value: [
|
|
6347
|
+
{ type: 'hash', value: { type: 'default' } },
|
|
6348
|
+
{ type: 'hash', value: { type: 'cookie', cookie_name: 'qorus-session' } },
|
|
6349
|
+
],
|
|
6350
|
+
},
|
|
6351
|
+
},
|
|
6352
|
+
},
|
|
6353
|
+
play: async ({ canvasElement }) => {
|
|
6354
|
+
// The preview IS the value here — the row prints no summary line above it.
|
|
6355
|
+
// A joined "Default RBAC, Cookie" directly over a preview that names both
|
|
6356
|
+
// items is the same facts twice, the lossy version first.
|
|
6357
|
+
const preview = await waitFor(() => {
|
|
6358
|
+
const element = canvasElement.querySelector('.schema-data-view');
|
|
6359
|
+
expect(element).toBeTruthy();
|
|
6360
|
+
return element!;
|
|
6361
|
+
});
|
|
6362
|
+
const text = preview.textContent ?? '';
|
|
6363
|
+
// Each item is headed by its first value, so the list reads by name; the
|
|
6364
|
+
// remaining fields keep their labels.
|
|
6365
|
+
const titles = [...preview.querySelectorAll('.schema-view-item-title')].map((element) =>
|
|
6366
|
+
(element.textContent ?? '').trim()
|
|
6367
|
+
);
|
|
6368
|
+
expect(titles).toEqual(['Default RBAC', 'Cookie']);
|
|
6369
|
+
expect(text).toContain('Session Cookie Name');
|
|
6370
|
+
|
|
6371
|
+
// Nothing announces the container's shape — that is the tell of a renderer
|
|
6372
|
+
// guessing at data it has not been told about.
|
|
6373
|
+
expect(text).not.toMatch(/Object\b/);
|
|
6374
|
+
expect(text).not.toMatch(/\d+ fields?\b/);
|
|
6375
|
+
|
|
6376
|
+
// The joined summary is gone from the row, and it is the ROW that has to be
|
|
6377
|
+
// checked: the same words still exist inside the preview, so asserting on
|
|
6378
|
+
// the whole canvas would pass whether or not the line was removed.
|
|
6379
|
+
const valueLine = canvasElement.querySelector(
|
|
6380
|
+
'[data-field="schemes"] .options-readfirst-valuetext'
|
|
6381
|
+
);
|
|
6382
|
+
expect(valueLine?.textContent ?? '').not.toBe('Default RBAC, Cookie');
|
|
6383
|
+
|
|
6384
|
+
// A literal the author typed is set in mono; a chosen label is not.
|
|
6385
|
+
const mono = [...preview.querySelectorAll('.schema-view-data')].map((el) =>
|
|
6386
|
+
(el.textContent ?? '').trim()
|
|
6387
|
+
);
|
|
6388
|
+
expect(mono).toContain('qorus-session');
|
|
6389
|
+
expect(mono).not.toContain('Default RBAC');
|
|
6390
|
+
|
|
6391
|
+
// The stored spellings are what the author never chose, so they must not be
|
|
6392
|
+
// in the preview at all. Substring checks cannot say this — "Scheme Type"
|
|
6393
|
+
// contains "type" and "Default RBAC" contains "default" — so the assertion
|
|
6394
|
+
// is on whole leaf elements: no chip or cell reads exactly `type`,
|
|
6395
|
+
// `cookie_name` or `default`. A substring assertion here passed against a
|
|
6396
|
+
// preview that was still rendering the raw pair.
|
|
6397
|
+
const leafTexts = [...preview.querySelectorAll('*')]
|
|
6398
|
+
.filter((element) => element.children.length === 0)
|
|
6399
|
+
.map((element) => (element.textContent ?? '').trim());
|
|
6400
|
+
expect(leafTexts).toContain('Default RBAC');
|
|
6401
|
+
for (const stored of ['type', 'cookie_name', 'default']) {
|
|
6402
|
+
expect(leafTexts).not.toContain(stored);
|
|
6403
|
+
}
|
|
6404
|
+
expect(canvasElement.textContent ?? '').not.toContain('[object Object]');
|
|
6405
|
+
},
|
|
6406
|
+
};
|
|
6407
|
+
|
|
6408
|
+
/**
|
|
6409
|
+
* Adding a list item opens the field the item cannot be saved without.
|
|
6410
|
+
*
|
|
6411
|
+
* `+ Add new item for "Authentication Scheme"` added a row whose one required
|
|
6412
|
+
* field sat collapsed, so the author had to find it and click it before the
|
|
6413
|
+
* form could be completed — a second click to reach the only thing the first
|
|
6414
|
+
* click could have meant.
|
|
6415
|
+
*
|
|
6416
|
+
* The engine already had `autoFocusFirstRequired`, and it could not fire here:
|
|
6417
|
+
* it waits for focus to be free so it never steals a caret, and a form mounted
|
|
6418
|
+
* BY a click never sees free focus — the button that mounted it still holds it.
|
|
6419
|
+
* Opening a row and moving the caret are separate decisions, so they are now
|
|
6420
|
+
* separate flags; this one only opens.
|
|
6421
|
+
*/
|
|
6422
|
+
export const CompactAddedListRowOpensItsRequiredField: Story = {
|
|
6423
|
+
parameters: {
|
|
6424
|
+
docs: {
|
|
6425
|
+
description: {
|
|
6426
|
+
story:
|
|
6427
|
+
'Clicks "Add new item" on a list-of-hash option and shows the new row with its required "Scheme Type" field already open. Rows that were already in the value stay collapsed — only the row just added is opened.',
|
|
6428
|
+
},
|
|
6429
|
+
},
|
|
6430
|
+
},
|
|
6431
|
+
args: {
|
|
6432
|
+
compact: true,
|
|
6433
|
+
minColumnWidth: '300px',
|
|
6434
|
+
options: AuthSchemeOptions,
|
|
6435
|
+
value: {
|
|
6436
|
+
schemes: {
|
|
6437
|
+
type: 'list',
|
|
6438
|
+
value: [{ type: 'hash', value: { type: 'default' } }],
|
|
6439
|
+
},
|
|
6440
|
+
},
|
|
6441
|
+
},
|
|
6442
|
+
play: async ({ canvasElement }) => {
|
|
6443
|
+
// Open the list itself, which mounts the rows and the Add button. Clicked by
|
|
6444
|
+
// FIELD, not by its text: the row no longer prints a summary line, and the
|
|
6445
|
+
// words that remain live inside the preview.
|
|
6446
|
+
await _testsClickText('Authentication Schemes');
|
|
6447
|
+
await waitFor(() => expect(canvasElement.querySelectorAll('.array-auto-item')).toHaveLength(1));
|
|
6448
|
+
|
|
6449
|
+
// The row that was already there is collapsed — this is the state the new
|
|
6450
|
+
// row must NOT be confused with.
|
|
6451
|
+
const isOpen = (element: Element | null | undefined) =>
|
|
6452
|
+
!!element &&
|
|
6453
|
+
(element.classList.contains('readfirst-row-editing') ||
|
|
6454
|
+
element.classList.contains('options-readfirst-card'));
|
|
6455
|
+
const typeRows = () => canvasElement.querySelectorAll('[data-field="type"]');
|
|
6456
|
+
expect([...typeRows()].some(isOpen)).toBe(false);
|
|
6457
|
+
|
|
6458
|
+
await _testsClickButton({ label: 'Add new item for "Authentication Schemes"' });
|
|
6459
|
+
|
|
6460
|
+
await waitFor(() => expect(canvasElement.querySelectorAll('.array-auto-item')).toHaveLength(2));
|
|
6461
|
+
// Exactly one open required field: the one belonging to the row just added.
|
|
6462
|
+
await waitFor(() => expect([...typeRows()].filter(isOpen)).toHaveLength(1));
|
|
6463
|
+
},
|
|
6464
|
+
};
|
|
6465
|
+
|
|
6466
|
+
/**
|
|
6467
|
+
* The same schema-worded row at a phone-class width.
|
|
6468
|
+
*
|
|
6469
|
+
* The narrow branch is what makes this worth its own story: below 480px the row
|
|
6470
|
+
* stacks the value under the label, so a summary and a preview that were both
|
|
6471
|
+
* rewritten to be READ (rather than decoded) have to survive the stacking
|
|
6472
|
+
* without wrapping into an unreadable column. `compactNarrow` comes from
|
|
6473
|
+
* `useMeasure` on the form's own wrapper, not from a media query, so a narrow
|
|
6474
|
+
* container is the honest way to reach the branch here — the viewport-parameter
|
|
6475
|
+
* rule applies to media-query components, which this is not.
|
|
6476
|
+
*/
|
|
6477
|
+
export const CompactListOfHashReadsInSchemaWordsMobile: Story = {
|
|
6478
|
+
parameters: {
|
|
6479
|
+
docs: {
|
|
6480
|
+
description: {
|
|
6481
|
+
story:
|
|
6482
|
+
'The schema-worded list-of-hash row at a ~360px width: the summary and the renamed preview stack under the field name and stay legible in one column.',
|
|
6483
|
+
},
|
|
6484
|
+
},
|
|
6485
|
+
},
|
|
6486
|
+
args: CompactListOfHashReadsInSchemaWords.args,
|
|
6487
|
+
decorators: [
|
|
6488
|
+
(StoryComponent: React.ComponentType) => (
|
|
6489
|
+
<div style={{ maxWidth: 360, margin: '0 auto', border: '1px dashed #ffffff22' }}>
|
|
6490
|
+
<StoryComponent />
|
|
6491
|
+
</div>
|
|
6492
|
+
),
|
|
6493
|
+
],
|
|
6494
|
+
play: async ({ canvasElement }) => {
|
|
6495
|
+
await _testsWaitForText('Default RBAC');
|
|
6496
|
+
|
|
6497
|
+
// The narrow branch is actually engaged — otherwise this is the desktop
|
|
6498
|
+
// story with a border round it, and it would pass while proving nothing.
|
|
6499
|
+
await waitFor(() =>
|
|
6500
|
+
expect(canvasElement.querySelector('.readfirst-narrow')).toBeTruthy()
|
|
6501
|
+
);
|
|
6502
|
+
|
|
6503
|
+
const preview = await waitFor(() => {
|
|
6504
|
+
const element = canvasElement.querySelector('.schema-data-view');
|
|
6505
|
+
expect(element).toBeTruthy();
|
|
6506
|
+
return element!;
|
|
6507
|
+
});
|
|
6508
|
+
const leafTexts = [...preview.querySelectorAll('*')]
|
|
6509
|
+
.filter((element) => element.children.length === 0)
|
|
6510
|
+
.map((element) => (element.textContent ?? '').trim());
|
|
6511
|
+
expect(leafTexts).toContain('Session Cookie Name');
|
|
6512
|
+
for (const stored of ['type', 'cookie_name', 'default']) {
|
|
6513
|
+
expect(leafTexts).not.toContain(stored);
|
|
6514
|
+
}
|
|
6515
|
+
},
|
|
6516
|
+
};
|