@qoretechnologies/reqraft 0.10.2 → 0.10.5
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 +5 -0
- package/design/COMPACT_ENGINE_REDESIGN.md +156 -0
- package/design/FORM_ENGINE_COMPACT_UX_PLAN.md +353 -0
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +158 -101
- 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 +122 -105
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +9 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +272 -82
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts +6 -3
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +76 -49
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +1 -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 +19 -0
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +22 -1
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/variants/VariantCalmTable.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantCalmTable.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantCalmTable.js +94 -0
- package/dist/components/form/engine/variants/VariantCalmTable.js.map +1 -0
- package/dist/components/form/engine/variants/VariantCards.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantCards.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantCards.js +80 -0
- package/dist/components/form/engine/variants/VariantCards.js.map +1 -0
- package/dist/components/form/engine/variants/VariantFocus.d.ts +7 -0
- package/dist/components/form/engine/variants/VariantFocus.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantFocus.js +138 -0
- package/dist/components/form/engine/variants/VariantFocus.js.map +1 -0
- package/dist/components/form/engine/variants/VariantMinimal.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantMinimal.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantMinimal.js +73 -0
- package/dist/components/form/engine/variants/VariantMinimal.js.map +1 -0
- package/dist/components/form/engine/variants/focusDemo.d.ts +13 -0
- package/dist/components/form/engine/variants/focusDemo.d.ts.map +1 -0
- package/dist/components/form/engine/variants/focusDemo.js +139 -0
- package/dist/components/form/engine/variants/focusDemo.js.map +1 -0
- package/dist/components/form/engine/variants/variantModel.d.ts +70 -0
- package/dist/components/form/engine/variants/variantModel.d.ts.map +1 -0
- package/dist/components/form/engine/variants/variantModel.js +133 -0
- package/dist/components/form/engine/variants/variantModel.js.map +1 -0
- package/dist/components/form/engine/variants/variantParts.d.ts +79 -0
- package/dist/components/form/engine/variants/variantParts.d.ts.map +1 -0
- package/dist/components/form/engine/variants/variantParts.js +191 -0
- package/dist/components/form/engine/variants/variantParts.js.map +1 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts +3 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +5 -2
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +273 -258
- package/src/components/form/engine/CompactToolbar.tsx +112 -85
- package/src/components/form/engine/FormEngine.stories.tsx +239 -115
- package/src/components/form/engine/FormEngine.tsx +332 -83
- package/src/components/form/engine/compactRowStyles.ts +221 -144
- package/src/components/form/engine/compactToolbarContext.ts +1 -0
- package/src/components/form/engine/readFirst.ts +35 -0
- package/src/components/form/engine/variants/FormEngineVariants.stories.tsx +119 -0
- package/src/components/form/engine/variants/VariantCalmTable.tsx +242 -0
- package/src/components/form/engine/variants/VariantCards.tsx +212 -0
- package/src/components/form/engine/variants/VariantFocus.tsx +382 -0
- package/src/components/form/engine/variants/VariantMinimal.tsx +170 -0
- package/src/components/form/engine/variants/focusDemo.ts +145 -0
- package/src/components/form/engine/variants/variantModel.ts +216 -0
- package/src/components/form/engine/variants/variantParts.tsx +313 -0
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +9 -2
- package/src/components/form/fields/auto/AutoFormField.tsx +8 -0
|
@@ -963,7 +963,7 @@ const CompactSchema: Record<string, TCompactField> = {
|
|
|
963
963
|
};
|
|
964
964
|
|
|
965
965
|
// `description` is intentionally left empty so the required-but-unset state is
|
|
966
|
-
// visible (a "
|
|
966
|
+
// visible (a "— Required" row and an invalid-field message).
|
|
967
967
|
const CompactValue: IOptions = {
|
|
968
968
|
name: { type: 'string', value: 'order-fulfilment' },
|
|
969
969
|
lang: { type: 'string', value: 'python' },
|
|
@@ -998,6 +998,32 @@ const clickFieldsMenuItem = async (text: string) => {
|
|
|
998
998
|
await fireEvent.click(item as Element);
|
|
999
999
|
};
|
|
1000
1000
|
|
|
1001
|
+
// The Optional status box now holds every not-yet-added field AND any added-but-
|
|
1002
|
+
// empty optional field, so it starts COLLAPSED (and ReqorePanel unmounts collapsed
|
|
1003
|
+
// content). Tests that assert on / interact with optional fields call this to open
|
|
1004
|
+
// it. The whole panel title bar toggles the collapse.
|
|
1005
|
+
const _expandOptionalBox = async () => {
|
|
1006
|
+
const findBox = () =>
|
|
1007
|
+
Array.from(document.querySelectorAll('.options-readfirst-group')).find((panel) =>
|
|
1008
|
+
panel.querySelector('.reqore-panel-title')?.textContent?.includes('Optional')
|
|
1009
|
+
);
|
|
1010
|
+
let box: Element | undefined;
|
|
1011
|
+
await waitFor(
|
|
1012
|
+
() => {
|
|
1013
|
+
box = findBox();
|
|
1014
|
+
expect(box).toBeTruthy();
|
|
1015
|
+
},
|
|
1016
|
+
{ timeout: 10000 }
|
|
1017
|
+
);
|
|
1018
|
+
// No `.reqore-panel-content` ⇒ the box is collapsed (content unmounted) — open it.
|
|
1019
|
+
if (box && !box.querySelector('.reqore-panel-content')) {
|
|
1020
|
+
await fireEvent.click(box.querySelector('.reqore-panel-title') as HTMLElement);
|
|
1021
|
+
await waitFor(() => expect(box!.querySelector('.reqore-panel-content')).toBeTruthy(), {
|
|
1022
|
+
timeout: 10000,
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1001
1027
|
// CompactSchema plus one optional (non-preselected) field, to exercise the
|
|
1002
1028
|
// "Fields" menu add / select-all / reset actions.
|
|
1003
1029
|
const CompactFieldsMenuSchema: Record<string, TCompactField> = {
|
|
@@ -1022,10 +1048,16 @@ export const Compact: Story = {
|
|
|
1022
1048
|
play: async () => {
|
|
1023
1049
|
// Groups render with their display metadata; rows show formatted values.
|
|
1024
1050
|
await _testsWaitForText('Identity and core settings');
|
|
1051
|
+
// Regression: `general` is a REAL consumer-defined group here (it's in
|
|
1052
|
+
// CompactGroups, and `description`/`tags` set `group: 'general'`), so its
|
|
1053
|
+
// sub-label MUST render. It must NOT be suppressed as the synthetic "no
|
|
1054
|
+
// group" catch-all — doing so visually merged its rows (e.g. Tags) into the
|
|
1055
|
+
// group above them.
|
|
1056
|
+
await _testsWaitForText('General');
|
|
1025
1057
|
await _testsWaitForText('order-fulfilment');
|
|
1026
1058
|
await _testsWaitForText('orders, batch');
|
|
1027
1059
|
await _testsWaitForText('Yes');
|
|
1028
|
-
await _testsWaitForText('
|
|
1060
|
+
await _testsWaitForText('—');
|
|
1029
1061
|
},
|
|
1030
1062
|
};
|
|
1031
1063
|
|
|
@@ -1039,11 +1071,12 @@ export const CompactReadOnly: Story = {
|
|
|
1039
1071
|
await _testsWaitForText('order-fulfilment');
|
|
1040
1072
|
// Read-only hides the Draft/Ready badge (the meter itself stays)…
|
|
1041
1073
|
await _testsWaitForTextToNotExist('Draft');
|
|
1042
|
-
// …and rows open in view mode:
|
|
1074
|
+
// …and rows open in view mode: the card's done (✓/close) button collapses
|
|
1075
|
+
// back. The button is icon-only now, so assert it by class, not text.
|
|
1043
1076
|
await _testsClickText('order-fulfilment');
|
|
1044
|
-
await
|
|
1077
|
+
await waitFor(() => expect(document.querySelector('.options-readfirst-done')).toBeTruthy());
|
|
1045
1078
|
await _testsClickButton({ selector: '.options-readfirst-done' });
|
|
1046
|
-
await
|
|
1079
|
+
await waitFor(() => expect(document.querySelector('.options-readfirst-done')).toBeNull());
|
|
1047
1080
|
},
|
|
1048
1081
|
};
|
|
1049
1082
|
|
|
@@ -1056,9 +1089,12 @@ export const CompactEmpty: Story = {
|
|
|
1056
1089
|
groups: CompactGroups,
|
|
1057
1090
|
},
|
|
1058
1091
|
play: async () => {
|
|
1092
|
+
// The four empty OPTIONAL fields live in the (collapsed) Optional box — open
|
|
1093
|
+
// it so all six empty fields are on screen.
|
|
1094
|
+
await _expandOptionalBox();
|
|
1059
1095
|
// Both required fields read as unset; the four optional ones as "Not set".
|
|
1060
|
-
|
|
1061
|
-
await _testsWaitForTextsCount('
|
|
1096
|
+
// All six empty fields read as a calm dash (the red asterisk marks required).
|
|
1097
|
+
await _testsWaitForTextsCount('—', undefined, 6);
|
|
1062
1098
|
},
|
|
1063
1099
|
};
|
|
1064
1100
|
|
|
@@ -1076,6 +1112,9 @@ export const CompactBasic: Story = {
|
|
|
1076
1112
|
// Unresolved required/invalid fields → the header shows the Draft badge
|
|
1077
1113
|
// (the IDE restyled-hero convention).
|
|
1078
1114
|
await _testsWaitForText('Draft');
|
|
1115
|
+
// Several asserted/clicked fields (Disabled option, …) are empty optionals in
|
|
1116
|
+
// the collapsed Optional box — open it so they're on screen.
|
|
1117
|
+
await _expandOptionalBox();
|
|
1079
1118
|
// Values resolve in read-first rows: a template shows its display name (from
|
|
1080
1119
|
// the supplied templates list), colour as hex, hash as a field-count summary.
|
|
1081
1120
|
await _testsWaitForText('Test (local)');
|
|
@@ -1539,11 +1578,10 @@ export const CompactValidIdentifierRule: Story = {
|
|
|
1539
1578
|
play: async () => {
|
|
1540
1579
|
await _testsWaitForText('Variable name');
|
|
1541
1580
|
await _testsWaitForText('1-bad-identifier');
|
|
1542
|
-
// The rules-driven validation marks the form as needing attention
|
|
1581
|
+
// The rules-driven validation marks the form as needing attention — the
|
|
1582
|
+
// dedicated "Needs attention" box (and the header link) signal it.
|
|
1543
1583
|
await _testsWaitForText('Draft');
|
|
1544
|
-
await _testsWaitForText(
|
|
1545
|
-
'A field is not valid and requires attention. Click here to only show invalid fields.'
|
|
1546
|
-
);
|
|
1584
|
+
await _testsWaitForText('Needs attention');
|
|
1547
1585
|
},
|
|
1548
1586
|
};
|
|
1549
1587
|
|
|
@@ -1608,7 +1646,19 @@ export const CompactFocusedEditing: Story = {
|
|
|
1608
1646
|
await waitFor(() => expect(document.querySelector('.options-readfirst-card')).toBeTruthy(), {
|
|
1609
1647
|
timeout: 10000,
|
|
1610
1648
|
});
|
|
1611
|
-
|
|
1649
|
+
// Fullscreen now lives in the card's "More" (⋮) menu, before the Done ✓.
|
|
1650
|
+
await _testsClickButton({ selector: '.options-readfirst-more' });
|
|
1651
|
+
let fsItem: Element | undefined;
|
|
1652
|
+
await waitFor(
|
|
1653
|
+
() => {
|
|
1654
|
+
fsItem = Array.from(document.querySelectorAll('.reqore-menu-item')).find((element) =>
|
|
1655
|
+
element.textContent?.includes('Edit fullscreen')
|
|
1656
|
+
);
|
|
1657
|
+
expect(fsItem).toBeTruthy();
|
|
1658
|
+
},
|
|
1659
|
+
{ timeout: 10000 }
|
|
1660
|
+
);
|
|
1661
|
+
await fireEvent.click(fsItem as Element);
|
|
1612
1662
|
await waitFor(() => expect(document.querySelector('.reqore-modal')).toBeTruthy(), {
|
|
1613
1663
|
timeout: 10000,
|
|
1614
1664
|
});
|
|
@@ -1684,6 +1734,9 @@ export const CompactSortOrder: Story = {
|
|
|
1684
1734
|
value: {} as IOptions,
|
|
1685
1735
|
},
|
|
1686
1736
|
play: async () => {
|
|
1737
|
+
// All three fields are empty + optional, so they sit in the (collapsed)
|
|
1738
|
+
// Optional box — open it to read their order.
|
|
1739
|
+
await _expandOptionalBox();
|
|
1687
1740
|
await _testsWaitForText('First');
|
|
1688
1741
|
const order = Array.from(document.querySelectorAll('.readfirst-row[data-field]')).map(
|
|
1689
1742
|
(element) => element.getAttribute('data-field')
|
|
@@ -1709,7 +1762,7 @@ export const CompactReadFirstEditing: Story = {
|
|
|
1709
1762
|
await _testsWaitForText('Yes');
|
|
1710
1763
|
await _testsWaitForText('Python');
|
|
1711
1764
|
// The required-but-empty field shows its placeholder instead of an editor.
|
|
1712
|
-
await _testsWaitForText('
|
|
1765
|
+
await _testsWaitForText('—');
|
|
1713
1766
|
// No field editor (textarea) is mounted while everything is collapsed.
|
|
1714
1767
|
await expect(document.querySelectorAll('.reqore-textarea')).toHaveLength(0);
|
|
1715
1768
|
|
|
@@ -1739,6 +1792,61 @@ export const CompactReadFirstEditing: Story = {
|
|
|
1739
1792
|
},
|
|
1740
1793
|
};
|
|
1741
1794
|
|
|
1795
|
+
// Following a field across panels: filling an empty optional field moves it from
|
|
1796
|
+
// the Optional box to Set, and the engine scrolls to + flashes its new row so it's
|
|
1797
|
+
// easy to keep track of. The flash is the observable signal that the panel-change
|
|
1798
|
+
// locate fired (the scroll itself, scrollIntoView, isn't assertable in the runner).
|
|
1799
|
+
export const CompactPanelChangeScroll: Story = {
|
|
1800
|
+
parameters: { chromatic: { disable: true } },
|
|
1801
|
+
args: {
|
|
1802
|
+
compact: true,
|
|
1803
|
+
minColumnWidth: '300px',
|
|
1804
|
+
options: {
|
|
1805
|
+
req: {
|
|
1806
|
+
type: 'string',
|
|
1807
|
+
ui_type: 'string',
|
|
1808
|
+
display_name: 'Req',
|
|
1809
|
+
required: true,
|
|
1810
|
+
preselected: true,
|
|
1811
|
+
},
|
|
1812
|
+
opt: { type: 'string', ui_type: 'string', display_name: 'Opt', preselected: true },
|
|
1813
|
+
} as IOptionsSchema,
|
|
1814
|
+
value: {} as IOptions,
|
|
1815
|
+
},
|
|
1816
|
+
play: async () => {
|
|
1817
|
+
await _testsWaitForText('Req');
|
|
1818
|
+
// 'opt' is an empty optional → the (collapsed) Optional box. Open it, fill the
|
|
1819
|
+
// field, collapse — it jumps to Set.
|
|
1820
|
+
await _expandOptionalBox();
|
|
1821
|
+
await _testsClickText('Opt');
|
|
1822
|
+
await _testsChangeStringField({
|
|
1823
|
+
selector: '.options-readfirst-inline .reqore-textarea',
|
|
1824
|
+
value: 'hello',
|
|
1825
|
+
});
|
|
1826
|
+
await sleep(300);
|
|
1827
|
+
await _testsClickButton({ selector: '.options-readfirst-done' });
|
|
1828
|
+
// It now lives in the Set box…
|
|
1829
|
+
await waitFor(
|
|
1830
|
+
() =>
|
|
1831
|
+
expect(
|
|
1832
|
+
document
|
|
1833
|
+
.querySelector('.readfirst-row[data-field="opt"]')
|
|
1834
|
+
?.closest('.options-readfirst-group')
|
|
1835
|
+
?.querySelector('.reqore-panel-title')?.textContent
|
|
1836
|
+
).toContain('Set'),
|
|
1837
|
+
{ timeout: 5000 }
|
|
1838
|
+
);
|
|
1839
|
+
// …and flashed, signalling the engine located/scrolled to its new panel.
|
|
1840
|
+
await waitFor(
|
|
1841
|
+
() =>
|
|
1842
|
+
expect(
|
|
1843
|
+
document.querySelector('.readfirst-row[data-field="opt"]')?.className
|
|
1844
|
+
).toContain('readfirst-row-flash'),
|
|
1845
|
+
{ timeout: 4000 }
|
|
1846
|
+
);
|
|
1847
|
+
},
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1742
1850
|
export const CompactRequiredOnlyAndSearch: Story = {
|
|
1743
1851
|
parameters: { chromatic: { disable: true } },
|
|
1744
1852
|
args: {
|
|
@@ -1787,27 +1895,61 @@ export const CompactFieldsMenu: Story = {
|
|
|
1787
1895
|
},
|
|
1788
1896
|
play: async () => {
|
|
1789
1897
|
await _testsWaitForText('Tags');
|
|
1790
|
-
// 'Notes' is optional
|
|
1898
|
+
// 'Notes' is optional + unset → it lives (collapsed) in the Optional box, so
|
|
1899
|
+
// it isn't in the DOM yet.
|
|
1791
1900
|
await _testsWaitForTextToNotExist('Notes');
|
|
1792
1901
|
|
|
1793
|
-
// "Select all" adds every optional field
|
|
1902
|
+
// "Select all" adds every optional field. Reveal the (collapsed) Optional box
|
|
1903
|
+
// — Notes is now an ADDED row (the normal variant, not the hidden/addable one).
|
|
1794
1904
|
await clickFieldsMenuItem('Select all');
|
|
1905
|
+
await _expandOptionalBox();
|
|
1795
1906
|
await _testsWaitForText('Notes');
|
|
1907
|
+
await waitFor(() =>
|
|
1908
|
+
expect(
|
|
1909
|
+
document.querySelector('.readfirst-row[data-field="notes"]:not(.readfirst-row-hidden)')
|
|
1910
|
+
).toBeTruthy()
|
|
1911
|
+
);
|
|
1796
1912
|
|
|
1797
|
-
// "Default fields" drops the user-added optional fields — Notes
|
|
1913
|
+
// "Default fields" drops the user-added optional fields — Notes reverts to a
|
|
1914
|
+
// HIDDEN (addable) row in the still-open Optional box (it's always browsable
|
|
1915
|
+
// now, just not added).
|
|
1798
1916
|
await clickFieldsMenuItem('Default fields');
|
|
1799
|
-
await
|
|
1917
|
+
await waitFor(() =>
|
|
1918
|
+
expect(document.querySelector('.readfirst-row-hidden[data-field="notes"]')).toBeTruthy()
|
|
1919
|
+
);
|
|
1800
1920
|
|
|
1801
|
-
// The
|
|
1802
|
-
//
|
|
1921
|
+
// The delete affordance now lives in the expanded editor's "More" (⋮) menu:
|
|
1922
|
+
// re-add Notes, open it, then Remove field via More → the confirm modal →
|
|
1923
|
+
// Confirm.
|
|
1803
1924
|
await clickFieldsMenuItem('Select all');
|
|
1804
1925
|
await _testsWaitForText('Notes');
|
|
1805
|
-
await
|
|
1806
|
-
|
|
1926
|
+
await _testsClickText('Notes');
|
|
1927
|
+
// Only Notes is expanded, so the single More (⋮) menu in the DOM is its own.
|
|
1928
|
+
// (ReqoreDropdown's trigger isn't a DOM descendant of the row, so don't scope
|
|
1929
|
+
// the selector to [data-field].)
|
|
1930
|
+
await waitFor(
|
|
1931
|
+
() => expect(document.querySelector('.options-readfirst-more')).toBeTruthy(),
|
|
1932
|
+
{ timeout: 10000 }
|
|
1933
|
+
);
|
|
1934
|
+
await _testsClickButton({ selector: '.options-readfirst-more' });
|
|
1935
|
+
let removeItem: Element | undefined;
|
|
1936
|
+
await waitFor(
|
|
1937
|
+
() => {
|
|
1938
|
+
removeItem = Array.from(document.querySelectorAll('.reqore-menu-item')).find((element) =>
|
|
1939
|
+
element.textContent?.includes('Remove field')
|
|
1940
|
+
);
|
|
1941
|
+
expect(removeItem).toBeTruthy();
|
|
1942
|
+
},
|
|
1943
|
+
{ timeout: 10000 }
|
|
1807
1944
|
);
|
|
1808
|
-
await
|
|
1945
|
+
await fireEvent.click(removeItem as Element);
|
|
1809
1946
|
await _testsClickButton({ label: 'Confirm' });
|
|
1810
|
-
|
|
1947
|
+
// Removing the field reverts it to a HIDDEN (addable) row in the still-open
|
|
1948
|
+
// Optional box (it's always browsable now, just no longer added) — and its
|
|
1949
|
+
// editor closes.
|
|
1950
|
+
await waitFor(() =>
|
|
1951
|
+
expect(document.querySelector('.readfirst-row-hidden[data-field="notes"]')).toBeTruthy()
|
|
1952
|
+
);
|
|
1811
1953
|
},
|
|
1812
1954
|
};
|
|
1813
1955
|
|
|
@@ -1847,6 +1989,31 @@ export const CompactDescriptionsToggle: Story = {
|
|
|
1847
1989
|
// One toggle reveals the short_desc on every field that has one.
|
|
1848
1990
|
await _testsWaitForText('The server hostname or IP address');
|
|
1849
1991
|
await _testsWaitForText('TCP port to connect on');
|
|
1992
|
+
|
|
1993
|
+
// Regression: opening a field for INLINE editing must keep its description
|
|
1994
|
+
// visible while the global toggle is on — it used to vanish because the
|
|
1995
|
+
// inline editor's label dropped the short_desc.
|
|
1996
|
+
await _testsClickText('Host');
|
|
1997
|
+
await waitFor(
|
|
1998
|
+
() =>
|
|
1999
|
+
expect(
|
|
2000
|
+
document.querySelector(
|
|
2001
|
+
'.readfirst-row-editing[data-field="host"] .options-readfirst-label-desc'
|
|
2002
|
+
)
|
|
2003
|
+
).toBeTruthy(),
|
|
2004
|
+
{ timeout: 10000 }
|
|
2005
|
+
);
|
|
2006
|
+
await _testsWaitForText('The server hostname or IP address');
|
|
2007
|
+
// Collapse back to the read row (Done) before toggling descriptions off.
|
|
2008
|
+
await _testsClickButton({ selector: '[data-field="host"] .options-readfirst-done' });
|
|
2009
|
+
await waitFor(
|
|
2010
|
+
() =>
|
|
2011
|
+
expect(
|
|
2012
|
+
document.querySelector('.readfirst-row-editing[data-field="host"]')
|
|
2013
|
+
).toBeFalsy(),
|
|
2014
|
+
{ timeout: 10000 }
|
|
2015
|
+
);
|
|
2016
|
+
|
|
1850
2017
|
// Toggling off hides them again.
|
|
1851
2018
|
await _testsClickButton({ selector: '.options-readfirst-descriptions' });
|
|
1852
2019
|
await _testsWaitForTextToNotExist('The server hostname or IP address');
|
|
@@ -1874,7 +2041,9 @@ export const CompactSearchHidden: Story = {
|
|
|
1874
2041
|
value: 'notes',
|
|
1875
2042
|
});
|
|
1876
2043
|
await _testsWaitForText('Notes');
|
|
1877
|
-
await
|
|
2044
|
+
await waitFor(() =>
|
|
2045
|
+
expect(document.querySelector('.readfirst-row-hidden[data-field="notes"]')).toBeTruthy()
|
|
2046
|
+
);
|
|
1878
2047
|
|
|
1879
2048
|
// Rows are keyboard-operable (role=button + Enter): focusing the hidden row
|
|
1880
2049
|
// and pressing Enter adds the field and opens its inline editor.
|
|
@@ -2928,29 +3097,17 @@ export const CompactFieldTypes: Story = {
|
|
|
2928
3097
|
// fresh nodes per click.
|
|
2929
3098
|
await _testsWaitForText('This value fails validation upstream.');
|
|
2930
3099
|
await _testsWaitForText('Deprecated — migrate before 2026-09.');
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
);
|
|
2935
|
-
await expect(catalogPanel('infoMsgQuiet')).toBeNull();
|
|
2936
|
-
await fireEvent.click(
|
|
2937
|
-
document.querySelector(
|
|
2938
|
-
'.readfirst-row[data-field="infoMsgQuiet"] .options-readfirst-info-slot .options-readfirst-info-toggle'
|
|
2939
|
-
) as HTMLElement
|
|
2940
|
-
);
|
|
3100
|
+
// Dedicated schema messages render as panels, visible WITHOUT interaction
|
|
3101
|
+
// (the per-row ⓘ is gone — descriptions are revealed by the global toggle or
|
|
3102
|
+
// by expanding the field).
|
|
2941
3103
|
await _testsWaitForText('Requests are signed automatically.');
|
|
2942
3104
|
await _testsWaitForText('Connection verified.');
|
|
2943
|
-
//
|
|
2944
|
-
|
|
2945
|
-
await expect(catalogPanel('infoShortDesc')).toBeNull();
|
|
2946
|
-
await fireEvent.click(
|
|
2947
|
-
document.querySelector(
|
|
2948
|
-
'.readfirst-row[data-field="infoShortDesc"] .options-readfirst-info-slot .options-readfirst-info-toggle'
|
|
2949
|
-
) as HTMLElement
|
|
2950
|
-
);
|
|
3105
|
+
// The global descriptions toggle reveals each field's short_desc under its name.
|
|
3106
|
+
await fireEvent.click(document.querySelector('.options-readfirst-descriptions') as HTMLElement);
|
|
2951
3107
|
await _testsWaitForText(
|
|
2952
3108
|
'A one-line summary shown under the field name and in the hover title.'
|
|
2953
3109
|
);
|
|
3110
|
+
// A field with a long desc still exposes the ? help affordance.
|
|
2954
3111
|
await expect(
|
|
2955
3112
|
document.querySelector('.readfirst-row[data-field="infoLongDesc"] .options-readfirst-help')
|
|
2956
3113
|
).toBeTruthy();
|
|
@@ -2988,7 +3145,10 @@ const _compactExpandAllRows = async () => {
|
|
|
2988
3145
|
document.querySelectorAll<HTMLElement>(
|
|
2989
3146
|
'.readfirst-row:not(.readfirst-row-editing):not(.readfirst-row-disabled):not(.readfirst-row-hidden)'
|
|
2990
3147
|
)
|
|
2991
|
-
|
|
3148
|
+
// An arg_schema field opens a NESTED compact sub-form (recursive compact);
|
|
3149
|
+
// its rows live inside the parent's edit card — don't count those as
|
|
3150
|
+
// top-level read rows to expand.
|
|
3151
|
+
).filter((r) => !r.closest('.options-readfirst-card'));
|
|
2992
3152
|
// Generous guard: the catalog has ~70 fields.
|
|
2993
3153
|
for (let guard = 0; guard < 120; guard++) {
|
|
2994
3154
|
const remaining = readRows();
|
|
@@ -3090,7 +3250,7 @@ export const CompactRequiredGroups: Story = {
|
|
|
3090
3250
|
// contiguous members (byHost/byFile in Connection) cluster into a rail, which
|
|
3091
3251
|
// carries the grouping in place of a chip; only the lone member (byUrl in
|
|
3092
3252
|
// General) keeps a "One of" chip — so exactly one chip, not three.
|
|
3093
|
-
await _testsWaitForTextsCount('
|
|
3253
|
+
await _testsWaitForTextsCount('—', undefined, 3);
|
|
3094
3254
|
await _testsWaitForText('Draft');
|
|
3095
3255
|
await _testsWaitForTextsCount('One of', undefined, 1);
|
|
3096
3256
|
|
|
@@ -3147,13 +3307,14 @@ export const CompactRequiredGroups: Story = {
|
|
|
3147
3307
|
await _testsWaitForText('https://example.com');
|
|
3148
3308
|
|
|
3149
3309
|
// One fulfilled member satisfies the group → the badge flips to Ready and the
|
|
3150
|
-
//
|
|
3151
|
-
//
|
|
3310
|
+
// Once satisfied: the filled member keeps a "Covers" chip; the empty siblings
|
|
3311
|
+
// show their "Covered by 'By URL'" note INLINE (not a chip), and no "One of"
|
|
3312
|
+
// remains. So exactly one required-group chip stays (the coverer's).
|
|
3152
3313
|
await _testsWaitForText('Ready');
|
|
3153
3314
|
await _testsWaitForTextsCount('Covered by “By URL”', undefined, 2);
|
|
3154
3315
|
await _testsWaitForText('Covers');
|
|
3155
3316
|
await _testsWaitForTextToNotExist('One of');
|
|
3156
|
-
await expect(document.querySelectorAll('.options-readfirst-required-group')).toHaveLength(
|
|
3317
|
+
await expect(document.querySelectorAll('.options-readfirst-required-group')).toHaveLength(1);
|
|
3157
3318
|
},
|
|
3158
3319
|
};
|
|
3159
3320
|
|
|
@@ -3682,57 +3843,41 @@ export const CompactShowcase: Story = {
|
|
|
3682
3843
|
).toBeTruthy();
|
|
3683
3844
|
await waitFor(() => {
|
|
3684
3845
|
// The intent stripe rides the value surface's left border, fed by
|
|
3685
|
-
// --readfirst-stripe on the field's BLOCK root.
|
|
3686
|
-
//
|
|
3846
|
+
// --readfirst-stripe on the field's BLOCK root. Schema messages now render
|
|
3847
|
+
// inside the value cell, so the block root is the row itself.
|
|
3687
3848
|
const intentRow = document.querySelector(
|
|
3688
|
-
'.
|
|
3849
|
+
'.readfirst-row[data-field="chromeIntent"]'
|
|
3689
3850
|
) as HTMLElement;
|
|
3690
3851
|
expect(intentRow?.style?.getPropertyValue('--readfirst-stripe')).toBeTruthy();
|
|
3691
3852
|
});
|
|
3692
3853
|
await _testsWaitForText('••••••');
|
|
3693
3854
|
await _testsWaitForText('This field also carries a warning message.');
|
|
3694
|
-
// The
|
|
3695
|
-
//
|
|
3696
|
-
// alongside the info affordances.
|
|
3855
|
+
// The unmet auth one-of group (authToken/authCertFile) renders the
|
|
3856
|
+
// "One of the below is required" cluster box.
|
|
3697
3857
|
await waitFor(() =>
|
|
3698
|
-
expect(document.
|
|
3858
|
+
expect(document.querySelector('.options-readfirst-required-cluster')).toBeTruthy()
|
|
3699
3859
|
);
|
|
3700
3860
|
|
|
3701
|
-
//
|
|
3702
|
-
//
|
|
3703
|
-
// wrapper element.
|
|
3704
|
-
const infoToggle = (field: string) =>
|
|
3705
|
-
document.querySelector(
|
|
3706
|
-
`.readfirst-row[data-field="${field}"] .options-readfirst-info-slot .options-readfirst-info-toggle`
|
|
3707
|
-
) as HTMLElement;
|
|
3861
|
+
// Schema message panels render inside the value cell of the row itself,
|
|
3862
|
+
// directly beneath the value.
|
|
3708
3863
|
const infoPanel = (field: string) =>
|
|
3709
3864
|
document.querySelector(
|
|
3710
|
-
`.
|
|
3865
|
+
`.readfirst-row[data-field="${field}"] .options-readfirst-info-panel`
|
|
3711
3866
|
);
|
|
3712
3867
|
|
|
3713
|
-
//
|
|
3714
|
-
//
|
|
3715
|
-
// default-value note; toggling again hides it.
|
|
3716
|
-
await expect(infoPanel('metaDefault')).toBeNull();
|
|
3717
|
-
await expect(infoToggle('metaDefault')).toBeTruthy();
|
|
3718
|
-
await fireEvent.click(infoToggle('metaDefault'));
|
|
3868
|
+
// Default-value notes and validation/dependency hints now render as a compact
|
|
3869
|
+
// INLINE reason (no ⓘ, no panel) — visible without any interaction.
|
|
3719
3870
|
await _testsWaitForText('Default: thirty — Falls back to 30 seconds when unset.');
|
|
3720
|
-
|
|
3721
|
-
await waitFor(() => expect(infoPanel('metaDefault')).toBeNull());
|
|
3722
|
-
|
|
3723
|
-
// Auto-open panels can be dismissed the same way (override sticks).
|
|
3871
|
+
// Dedicated schema messages stay prominent PANELS, also always visible.
|
|
3724
3872
|
await expect(infoPanel('apiEndpoint')).toBeTruthy();
|
|
3725
|
-
await fireEvent.click(infoToggle('apiEndpoint'));
|
|
3726
|
-
await waitFor(() => expect(infoPanel('apiEndpoint')).toBeNull());
|
|
3727
|
-
await fireEvent.click(infoToggle('apiEndpoint'));
|
|
3728
3873
|
await _testsWaitForText('v1 endpoints are deprecated — migrate to /v2 before 2026-09.');
|
|
3729
3874
|
|
|
3730
|
-
// short_desc renders UNDER the field name
|
|
3731
|
-
//
|
|
3875
|
+
// short_desc renders UNDER the field name when the global descriptions toggle
|
|
3876
|
+
// is engaged (the per-row ⓘ is gone).
|
|
3732
3877
|
const labelDesc = (field: string) =>
|
|
3733
3878
|
document.querySelector(`.readfirst-row[data-field="${field}"] .options-readfirst-label-desc`);
|
|
3734
3879
|
await expect(labelDesc('chromeIcon')).toBeNull();
|
|
3735
|
-
await fireEvent.click(
|
|
3880
|
+
await fireEvent.click(document.querySelector('.options-readfirst-descriptions') as HTMLElement);
|
|
3736
3881
|
await waitFor(() => expect(labelDesc('chromeIcon')).toBeTruthy());
|
|
3737
3882
|
},
|
|
3738
3883
|
};
|
|
@@ -3817,38 +3962,14 @@ export const CompactRequiredGroupRails: Story = {
|
|
|
3817
3962
|
},
|
|
3818
3963
|
play: async () => {
|
|
3819
3964
|
await _testsWaitForText('API key');
|
|
3820
|
-
//
|
|
3965
|
+
// The unmet `target` group (email/slack/webhook/sms, none set) renders the
|
|
3966
|
+
// "One of the below is required" cluster box.
|
|
3821
3967
|
await waitFor(() =>
|
|
3822
|
-
expect(document.
|
|
3968
|
+
expect(document.querySelector('.options-readfirst-required-cluster')).toBeTruthy()
|
|
3823
3969
|
);
|
|
3824
|
-
// The
|
|
3825
|
-
//
|
|
3826
|
-
await
|
|
3827
|
-
const apiNode = document.querySelector(
|
|
3828
|
-
'.readfirst-row[data-field="apiKey"] .options-readfirst-node'
|
|
3829
|
-
) as HTMLElement;
|
|
3830
|
-
const oauthNode = document.querySelector(
|
|
3831
|
-
'.readfirst-row[data-field="oauthToken"] .options-readfirst-node'
|
|
3832
|
-
) as HTMLElement;
|
|
3833
|
-
// email is in the `target` group, which has NO member set — still unmet.
|
|
3834
|
-
const emailNode = document.querySelector(
|
|
3835
|
-
'.readfirst-row[data-field="email"] .options-readfirst-node'
|
|
3836
|
-
) as HTMLElement;
|
|
3837
|
-
// Fill marks the member carrying the value: apiKey filled (bg = its border
|
|
3838
|
-
// colour), oauthToken hollow (bg = the form background).
|
|
3839
|
-
expect(getComputedStyle(apiNode).backgroundColor).not.toBe(
|
|
3840
|
-
getComputedStyle(oauthNode).backgroundColor
|
|
3841
|
-
);
|
|
3842
|
-
// Colour follows the GROUP, not the member. The credential group is satisfied
|
|
3843
|
-
// by apiKey, so its empty alternative (oauthToken) reads the SAME colour as
|
|
3844
|
-
// the filled node — not warning. The still-unmet target group's node (email)
|
|
3845
|
-
// keeps the warning colour, so it differs.
|
|
3846
|
-
const apiBorder = getComputedStyle(apiNode).borderTopColor;
|
|
3847
|
-
const oauthBorder = getComputedStyle(oauthNode).borderTopColor;
|
|
3848
|
-
const emailBorder = getComputedStyle(emailNode).borderTopColor;
|
|
3849
|
-
expect(oauthBorder).toBe(apiBorder);
|
|
3850
|
-
expect(oauthBorder).not.toBe(emailBorder);
|
|
3851
|
-
});
|
|
3970
|
+
// The met `credential` group needs no box: apiKey satisfies it, so its empty
|
|
3971
|
+
// alternative oauthToken reads as covered by its sibling.
|
|
3972
|
+
await _testsWaitForText('Covered by “API key”');
|
|
3852
3973
|
},
|
|
3853
3974
|
};
|
|
3854
3975
|
|
|
@@ -3867,9 +3988,12 @@ export const CompactFieldSortWithinGroups: Story = {
|
|
|
3867
3988
|
)
|
|
3868
3989
|
).map((row) => row.getAttribute('data-field'));
|
|
3869
3990
|
|
|
3870
|
-
//
|
|
3991
|
+
// Rows are bucketed into status boxes (Needs attention → Set → Optional);
|
|
3992
|
+
// within a box, schema order holds. The `target` group is unmet (attention),
|
|
3993
|
+
// the `credential` group is met by apiKey (set), so the attention members
|
|
3994
|
+
// come first, then the set members.
|
|
3871
3995
|
await waitFor(() =>
|
|
3872
|
-
expect(order()).toEqual(['
|
|
3996
|
+
expect(order()).toEqual(['email', 'slack', 'webhook', 'sms', 'apiKey', 'oauthToken'])
|
|
3873
3997
|
);
|
|
3874
3998
|
|
|
3875
3999
|
// Open the Fields menu, drill into the (collapsed) "Sort by" submenu, then
|
|
@@ -3887,13 +4011,13 @@ export const CompactFieldSortWithinGroups: Story = {
|
|
|
3887
4011
|
await waitFor(() => expect(menuItem('Name A→Z')).toBeTruthy());
|
|
3888
4012
|
await fireEvent.click(menuItem('Name A→Z') as HTMLElement);
|
|
3889
4013
|
|
|
3890
|
-
// Sorted by display name inside each group; the
|
|
3891
|
-
// (
|
|
4014
|
+
// Sorted by display name inside each group; the status boxes stay separate
|
|
4015
|
+
// (attention: email/slack/sms/webhook | set: apiKey/oauthToken).
|
|
3892
4016
|
await waitFor(() =>
|
|
3893
|
-
expect(order()).toEqual(['
|
|
4017
|
+
expect(order()).toEqual(['email', 'slack', 'sms', 'webhook', 'apiKey', 'oauthToken'])
|
|
3894
4018
|
);
|
|
3895
|
-
// The required-group clusters survive the re-sort
|
|
3896
|
-
|
|
4019
|
+
// The required-group clusters survive the re-sort (both groups keep their
|
|
4020
|
+
// first-member marker, used to anchor the cluster box).
|
|
3897
4021
|
await expect(document.querySelectorAll('.readfirst-cluster-first')).toHaveLength(2);
|
|
3898
4022
|
},
|
|
3899
4023
|
};
|