@qoretechnologies/reqraft 0.10.36 → 0.10.37-pr.95.g4e65c57
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/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 +259 -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 +313 -24
- package/src/components/form/engine/FormEngine.stories.tsx +487 -14
- 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/index.tsx +1 -0
|
@@ -107,6 +107,7 @@ import {
|
|
|
107
107
|
import { OptionsHelpDialog } from './OptionsHelpDialog';
|
|
108
108
|
import {
|
|
109
109
|
TReadFirstStatus,
|
|
110
|
+
findAllowedValueOption,
|
|
110
111
|
getFirstAttentionOptionName,
|
|
111
112
|
getOptionGroup,
|
|
112
113
|
getOptionGroupLabel,
|
|
@@ -429,13 +430,18 @@ export const fixOptions = (
|
|
|
429
430
|
};
|
|
430
431
|
}
|
|
431
432
|
|
|
433
|
+
// A value that is not one of the declared choices is dropped. What counts
|
|
434
|
+
// as "one of the choices" is `findAllowedValueOption` — the same predicate
|
|
435
|
+
// the read-first row uses to LABEL a value — because a value the row can
|
|
436
|
+
// name is by definition a value the form must keep. Inlining a narrower
|
|
437
|
+
// test here (envelope and `name`, but not a bare `value`) silently erased
|
|
438
|
+
// every value declared the bare way: the collapsed row still showed its
|
|
439
|
+
// display name while the editor showed "—" and the value never reached
|
|
440
|
+
// the submitted data.
|
|
432
441
|
if (
|
|
433
442
|
newOption.value !== undefined &&
|
|
434
443
|
options?.[optionName]?.allowed_values &&
|
|
435
|
-
!options?.[optionName]
|
|
436
|
-
(allowedValue: any) =>
|
|
437
|
-
allowedValue.value?.value === newOption.value || allowedValue.name === newOption.value
|
|
438
|
-
) &&
|
|
444
|
+
!findAllowedValueOption(newOption.value, options?.[optionName]) &&
|
|
439
445
|
!isValueTemplate(newOption.value) &&
|
|
440
446
|
!options?.[optionName]?.multiselect &&
|
|
441
447
|
!options?.[optionName]?.allowed_values_creatable
|
|
@@ -715,6 +721,27 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
715
721
|
*/
|
|
716
722
|
autoFocusFirstRequired?: boolean;
|
|
717
723
|
|
|
724
|
+
/**
|
|
725
|
+
* Opt-in: on mount, OPEN the first row that needs attention — without taking
|
|
726
|
+
* focus.
|
|
727
|
+
*
|
|
728
|
+
* `autoFocusFirstRequired` fuses two decisions that are not the same: which
|
|
729
|
+
* row is open (layout) and where the caret is (focus). Focus is the half that
|
|
730
|
+
* can be stolen, so that flag waits for focus to be free — and a form mounted
|
|
731
|
+
* BY a click never sees free focus, because the button that mounted it still
|
|
732
|
+
* has it. Adding a list item is exactly that case: the row the author must
|
|
733
|
+
* fill stayed shut behind a second click, and the flag that exists to prevent
|
|
734
|
+
* it could not fire.
|
|
735
|
+
*
|
|
736
|
+
* Opening a row takes nothing from the user, so this half needs no guard. Use
|
|
737
|
+
* it where the mount is already the answer to a deliberate action; use
|
|
738
|
+
* `autoFocusFirstRequired` where the form is the destination and the caret
|
|
739
|
+
* should land in it. Setting both keeps the focusing behaviour. Same one-shot
|
|
740
|
+
* contract: it fires on the first render with focusable content and then never
|
|
741
|
+
* again for the life of the instance. No-op in classic mode. Default: off.
|
|
742
|
+
*/
|
|
743
|
+
expandFirstRequired?: boolean;
|
|
744
|
+
|
|
718
745
|
/**
|
|
719
746
|
* Names of read-first rows to open on mount, in addition to whatever the
|
|
720
747
|
* user opens afterwards.
|
|
@@ -783,6 +810,7 @@ const FormEngineImpl = ({
|
|
|
783
810
|
rendererOnlyUiTypes,
|
|
784
811
|
inheritedFromParent,
|
|
785
812
|
autoFocusFirstRequired,
|
|
813
|
+
expandFirstRequired,
|
|
786
814
|
initialExpandedOptions,
|
|
787
815
|
...rest
|
|
788
816
|
}: IFormEngineProps) => {
|
|
@@ -831,6 +859,47 @@ const FormEngineImpl = ({
|
|
|
831
859
|
const [showInvalidOptionsOnly, setShowInvalidOptionsOnly] = useState<boolean>(false);
|
|
832
860
|
// Which options are expanded into their editor (several can be open at once).
|
|
833
861
|
const [expandedOptions, setExpandedOptions] = useState<string[]>([]);
|
|
862
|
+
|
|
863
|
+
// --- Reveal a status box's content when it is opened ----------------------
|
|
864
|
+
//
|
|
865
|
+
// The status boxes stack, so the last one ("Optional", which holds every
|
|
866
|
+
// not-yet-added field) sits at the bottom of the form — frequently at the
|
|
867
|
+
// bottom of the scroll container too. Opening it mounts its rows BELOW the
|
|
868
|
+
// fold: the box's own header is all that stays on screen, the click appears to
|
|
869
|
+
// have done nothing, and the fields it just revealed have to be hunted for by
|
|
870
|
+
// scrolling.
|
|
871
|
+
//
|
|
872
|
+
// The box that was just opened, read by the layout effect below. Held as state
|
|
873
|
+
// rather than acted on inside the collapse handler because the handler runs
|
|
874
|
+
// BEFORE React re-renders — at that moment the rows do not exist yet, so the
|
|
875
|
+
// box has not grown and there is nothing to scroll to. A layout effect runs
|
|
876
|
+
// after the DOM is updated and before paint, which makes the reveal
|
|
877
|
+
// deterministic instead of a guessed delay.
|
|
878
|
+
const [openedStatusBox, setOpenedStatusBox] = useState<string | undefined>(undefined);
|
|
879
|
+
const statusBoxRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
880
|
+
useLayoutEffect(() => {
|
|
881
|
+
if (!openedStatusBox) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const element = statusBoxRefs.current[openedStatusBox];
|
|
885
|
+
// Consume it either way: a box that has gone (a search narrowed it out of
|
|
886
|
+
// existence) must not leave a pending reveal armed for a later render.
|
|
887
|
+
setOpenedStatusBox(undefined);
|
|
888
|
+
// `scrollIntoView` is not implemented in jsdom, and a missing reveal must
|
|
889
|
+
// never break a form.
|
|
890
|
+
if (!element?.scrollIntoView) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
// 'nearest' scrolls the MINIMUM needed: nothing at all when the box is
|
|
894
|
+
// already fully visible, and a top-alignment when it is taller than the
|
|
895
|
+
// viewport (header plus the first rows — the right answer for a long
|
|
896
|
+
// Optional list). Anything else moves the page when it did not need to.
|
|
897
|
+
element.scrollIntoView({
|
|
898
|
+
block: 'nearest',
|
|
899
|
+
behavior:
|
|
900
|
+
window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ? 'auto' : 'smooth',
|
|
901
|
+
});
|
|
902
|
+
}, [openedStatusBox]);
|
|
834
903
|
// Remembers each row's last settled status box, so an actively-edited field
|
|
835
904
|
// stays put when its status flips (e.g. becomes valid) instead of jumping to
|
|
836
905
|
// another box mid-edit and stealing focus. Keyed by option name.
|
|
@@ -1863,7 +1932,11 @@ const FormEngineImpl = ({
|
|
|
1863
1932
|
// (or below-the-fold) form is never scrolled into view on mount.
|
|
1864
1933
|
const autoFocusNameRef = useRef<string | undefined>(undefined);
|
|
1865
1934
|
useEffect(() => {
|
|
1866
|
-
|
|
1935
|
+
// Two decisions, one scan. `autoFocusFirstRequired` opens the row AND puts
|
|
1936
|
+
// the caret in it; `expandFirstRequired` only opens it. Setting both keeps
|
|
1937
|
+
// the focusing behaviour, since focusing implies opening.
|
|
1938
|
+
const wantsFocus = !!autoFocusFirstRequired;
|
|
1939
|
+
if ((!wantsFocus && !expandFirstRequired) || !compact || !options) {
|
|
1867
1940
|
return;
|
|
1868
1941
|
}
|
|
1869
1942
|
if (hasAutoFocusedRef.current) {
|
|
@@ -1881,8 +1954,14 @@ const FormEngineImpl = ({
|
|
|
1881
1954
|
// clean mount focus rests on the body, so the intended "drop into the first
|
|
1882
1955
|
// field" still fires. Bailing here leaves `hasAutoFocusedRef` false, so it
|
|
1883
1956
|
// retries once focus is free.
|
|
1957
|
+
//
|
|
1958
|
+
// Only the FOCUSING variant waits: opening a row moves no caret, so there is
|
|
1959
|
+
// nothing to steal and nothing to wait for. Making the expand-only variant
|
|
1960
|
+
// wait here would make it a no-op in its main use — a sub-form mounted by a
|
|
1961
|
+
// click, where the button that mounted it still holds focus and never gives
|
|
1962
|
+
// it up.
|
|
1884
1963
|
const active = document.activeElement as HTMLElement | null;
|
|
1885
|
-
if (active && active !== document.body) {
|
|
1964
|
+
if (wantsFocus && active && active !== document.body) {
|
|
1886
1965
|
return;
|
|
1887
1966
|
}
|
|
1888
1967
|
|
|
@@ -1910,7 +1989,10 @@ const FormEngineImpl = ({
|
|
|
1910
1989
|
|
|
1911
1990
|
if (target) {
|
|
1912
1991
|
// Set the ref before the state update so CompactRow's focus timer sees it.
|
|
1913
|
-
|
|
1992
|
+
// Expand-only leaves it unset, which is what keeps the caret where it is.
|
|
1993
|
+
if (wantsFocus) {
|
|
1994
|
+
autoFocusNameRef.current = target;
|
|
1995
|
+
}
|
|
1914
1996
|
setExpandedOptions((prev) =>
|
|
1915
1997
|
prev.includes(target) ? prev
|
|
1916
1998
|
: expandMode === 'multi' ? [...prev, target]
|
|
@@ -1919,6 +2001,7 @@ const FormEngineImpl = ({
|
|
|
1919
2001
|
}
|
|
1920
2002
|
}, [
|
|
1921
2003
|
autoFocusFirstRequired,
|
|
2004
|
+
expandFirstRequired,
|
|
1922
2005
|
compact,
|
|
1923
2006
|
options,
|
|
1924
2007
|
availableOptions,
|
|
@@ -2494,9 +2577,40 @@ const FormEngineImpl = ({
|
|
|
2494
2577
|
if (requiredOnly && !(schema?.required || schema?.required_groups)) {
|
|
2495
2578
|
return false;
|
|
2496
2579
|
}
|
|
2497
|
-
|
|
2580
|
+
// A field that absorbs a sibling also answers for it: searching
|
|
2581
|
+
// "language" has to surface the row that now holds the language control,
|
|
2582
|
+
// or the field becomes unreachable through the filter.
|
|
2583
|
+
if (matchesQuery(optionName)) return true;
|
|
2584
|
+
const absorb = (schema as { absorb_fields?: string[] } | undefined)?.absorb_fields;
|
|
2585
|
+
return !!absorb?.some((absorbedName) => matchesQuery(absorbedName));
|
|
2498
2586
|
};
|
|
2499
2587
|
|
|
2588
|
+
// Fields a sibling absorbs into its own container.
|
|
2589
|
+
//
|
|
2590
|
+
// A field declaring `absorb_fields: ['language']` renders that sibling's
|
|
2591
|
+
// control inside its own row instead of letting it have a row of its own.
|
|
2592
|
+
// The pair this exists for is a code editor and its language: they are one
|
|
2593
|
+
// decision — "what is this code, and in what language" — and reading them
|
|
2594
|
+
// as two unrelated rows one above the other is what the layout used to say.
|
|
2595
|
+
//
|
|
2596
|
+
// Absorbed fields stay in `grouped`, and therefore in the Set / Optional /
|
|
2597
|
+
// Needs-attention counts and in the completion meter. Only the ROW is
|
|
2598
|
+
// skipped. Dropping them from the buckets instead would leave the meter
|
|
2599
|
+
// ("13/18 set", computed from `availableOptions`) disagreeing with the box
|
|
2600
|
+
// badge beside it, which counts row entries.
|
|
2601
|
+
const absorbedBy: Record<string, string> = {};
|
|
2602
|
+
forEach(options, (option, optionName) => {
|
|
2603
|
+
const absorb = (option as { absorb_fields?: string[] } | undefined)?.absorb_fields;
|
|
2604
|
+
if (!absorb?.length) return;
|
|
2605
|
+
absorb.forEach((absorbedName) => {
|
|
2606
|
+
// Only absorb a sibling that exists and is not already spoken for; a
|
|
2607
|
+
// schema naming a missing field must not make the field disappear.
|
|
2608
|
+
if (options?.[absorbedName] && !absorbedBy[absorbedName]) {
|
|
2609
|
+
absorbedBy[absorbedName] = optionName;
|
|
2610
|
+
}
|
|
2611
|
+
});
|
|
2612
|
+
});
|
|
2613
|
+
|
|
2500
2614
|
// Group the options by their raw `group` key, remembering the order groups
|
|
2501
2615
|
// first appear in the schema. Each row carries a `hidden` flag.
|
|
2502
2616
|
const groupOrder: string[] = [];
|
|
@@ -2649,6 +2763,11 @@ const FormEngineImpl = ({
|
|
|
2649
2763
|
// together at the first member's slot and rendered as a connected rail (flat
|
|
2650
2764
|
// rows — no wrapper — so the value surface applies normally; the rail + nodes
|
|
2651
2765
|
// are drawn per member). Narrow stacks fall back to flat rows.
|
|
2766
|
+
// The absorbed siblings a host row must render, in schema order. Empty for
|
|
2767
|
+
// every row that absorbs nothing, which is all of them by default.
|
|
2768
|
+
const absorbedNamesFor = (hostName: string): string[] =>
|
|
2769
|
+
Object.keys(absorbedBy).filter((absorbedName) => absorbedBy[absorbedName] === hostName);
|
|
2770
|
+
|
|
2652
2771
|
const renderGroupRows = (names: Array<{ name: string; hidden: boolean }>) => {
|
|
2653
2772
|
const renderRow = (
|
|
2654
2773
|
entry: { name: string; hidden?: boolean },
|
|
@@ -2671,6 +2790,9 @@ const FormEngineImpl = ({
|
|
|
2671
2790
|
clustered={clustered}
|
|
2672
2791
|
clusterFirst={clusterFirst}
|
|
2673
2792
|
clusterLast={clusterLast}
|
|
2793
|
+
// The siblings this row renders inside itself instead of leaving
|
|
2794
|
+
// them a row of their own — see `absorbedBy`.
|
|
2795
|
+
absorbedFields={absorbedNamesFor(entry.name)}
|
|
2674
2796
|
/>
|
|
2675
2797
|
);
|
|
2676
2798
|
// (Clustering runs in narrow mode too now — the "One of the below is
|
|
@@ -2679,6 +2801,10 @@ const FormEngineImpl = ({
|
|
|
2679
2801
|
const emitted = new Set<string>();
|
|
2680
2802
|
const groupOf = (name: string) =>
|
|
2681
2803
|
(options?.[name]?.required_groups as string[] | undefined)?.[0];
|
|
2804
|
+
// An absorbed field keeps its place in the buckets — so the Set /
|
|
2805
|
+
// Optional counts and the completion meter still see it — but its host
|
|
2806
|
+
// renders its control, so it gets no row of its own here.
|
|
2807
|
+
names = names.filter((entry) => !absorbedBy[entry.name]);
|
|
2682
2808
|
return names.map((entry) => {
|
|
2683
2809
|
const grp = groupOf(entry.name);
|
|
2684
2810
|
if (!grp) return renderRow(entry, false);
|
|
@@ -2783,6 +2909,14 @@ const FormEngineImpl = ({
|
|
|
2783
2909
|
$accent={accent}
|
|
2784
2910
|
$bg={boxBg}
|
|
2785
2911
|
key={box.key}
|
|
2912
|
+
ref={(node: HTMLDivElement | null) => {
|
|
2913
|
+
statusBoxRefs.current[box.key] = node;
|
|
2914
|
+
}}
|
|
2915
|
+
// Opening arms the reveal; collapsing disarms it, so a
|
|
2916
|
+
// close never scrolls.
|
|
2917
|
+
onCollapseChange={(collapsed) =>
|
|
2918
|
+
setOpenedStatusBox(collapsed ? undefined : box.key)
|
|
2919
|
+
}
|
|
2786
2920
|
flat
|
|
2787
2921
|
minimal
|
|
2788
2922
|
collapseButtonProps={{ flat: true, minimal: true, size: 'small' }}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { expect } from 'storybook/test';
|
|
3
|
+
import { SchemaDataView } from './SchemaDataView';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The read-only rendering of a value described by an `arg_schema`.
|
|
7
|
+
*
|
|
8
|
+
* Rendered directly here rather than through a whole FormEngine: these stories
|
|
9
|
+
* are about what the VIEW does with a described value, and mounting a form
|
|
10
|
+
* around it only adds chrome that the form's own stories already cover.
|
|
11
|
+
*/
|
|
12
|
+
const meta: Meta<typeof SchemaDataView> = {
|
|
13
|
+
title: 'Form/Engine/SchemaDataView',
|
|
14
|
+
component: SchemaDataView,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
/** Auth-profile schemes: the first field is a CHOICE, so its heading is prose. */
|
|
21
|
+
const SchemeSchema = {
|
|
22
|
+
type: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
display_name: 'Scheme Type',
|
|
25
|
+
allowed_values: [
|
|
26
|
+
{ value: 'default', display_name: 'Default RBAC' },
|
|
27
|
+
{ value: 'cookie', display_name: 'Cookie' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
cookie_name: { type: 'string', display_name: 'Session Cookie Name' },
|
|
31
|
+
redirect_url: { type: 'string', display_name: 'Redirect URL' },
|
|
32
|
+
} as never;
|
|
33
|
+
|
|
34
|
+
const schemeValue = [
|
|
35
|
+
{ type: 'hash', value: { type: 'default' } },
|
|
36
|
+
{
|
|
37
|
+
type: 'hash',
|
|
38
|
+
value: {
|
|
39
|
+
type: 'cookie',
|
|
40
|
+
cookie_name: 'qorus-session',
|
|
41
|
+
redirect_url: 'https://example.com/login',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/** Service methods: the first field is a LITERAL, so its heading is mono. */
|
|
47
|
+
const MethodSchema = {
|
|
48
|
+
name: { type: 'string', display_name: 'Method Name' },
|
|
49
|
+
desc: { type: 'string', display_name: 'Description' },
|
|
50
|
+
} as never;
|
|
51
|
+
|
|
52
|
+
const methodValue = [
|
|
53
|
+
{ type: 'hash', value: { name: 'init', desc: 'init method' } },
|
|
54
|
+
{ type: 'hash', value: { name: 'onOrderStatus', desc: 'returns the status of a single order' } },
|
|
55
|
+
{ type: 'hash', value: { name: 'onConnect', desc: 'connection lifecycle hook' } },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const colors = { key: '#f1f0ee', muted: '#a0a0a0', border: '#a0a0a066', accent: '#3b8eea' };
|
|
59
|
+
|
|
60
|
+
export const Schemes: Story = {
|
|
61
|
+
parameters: {
|
|
62
|
+
docs: {
|
|
63
|
+
description: {
|
|
64
|
+
story:
|
|
65
|
+
'A described list whose identifying field is a choice — each item is headed by the display name of that choice, in prose.',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
args: { value: schemeValue, schema: SchemeSchema, colors },
|
|
70
|
+
play: async ({ canvasElement }) => {
|
|
71
|
+
const titles = [...canvasElement.querySelectorAll('.schema-view-item-title')].map((element) =>
|
|
72
|
+
(element.textContent ?? '').trim()
|
|
73
|
+
);
|
|
74
|
+
await expect(titles).toEqual(['Default RBAC', 'Cookie']);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const ServiceMethods: Story = {
|
|
79
|
+
parameters: {
|
|
80
|
+
docs: {
|
|
81
|
+
description: {
|
|
82
|
+
story:
|
|
83
|
+
"A described list whose identifying field is a literal — each item is headed by the method's name, which keeps the mono face it has as a value.",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
args: { value: methodValue, schema: MethodSchema, colors },
|
|
88
|
+
play: async ({ canvasElement }) => {
|
|
89
|
+
await expect(canvasElement.querySelectorAll('.schema-view-item-title')).toHaveLength(3);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const Undescribed: Story = {
|
|
94
|
+
parameters: {
|
|
95
|
+
docs: {
|
|
96
|
+
description: {
|
|
97
|
+
story:
|
|
98
|
+
'Keys the schema does not describe are still shown, under their raw name, after the described ones — a preview that dropped data would be worse than a raw tree.',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
args: {
|
|
103
|
+
value: [{ type: 'hash', value: { type: 'cookie', legacy_flag: 'kept' } }],
|
|
104
|
+
schema: SchemeSchema,
|
|
105
|
+
colors,
|
|
106
|
+
},
|
|
107
|
+
play: async ({ canvasElement }) => {
|
|
108
|
+
await expect(canvasElement.textContent).toContain('legacy_flag');
|
|
109
|
+
|
|
110
|
+
// A field name renders on ONE line. The label column is measured from the
|
|
111
|
+
// longest name, and the label carries `overflow-wrap: anywhere` — so an
|
|
112
|
+
// under-measured column does not ellipsise, it breaks the name in half.
|
|
113
|
+
// That is exactly what happened when the labels became uppercase and the
|
|
114
|
+
// measurement was still in `ch`, which knows nothing about the tracking:
|
|
115
|
+
// `LEGACY_FLAG` came out over two lines.
|
|
116
|
+
//
|
|
117
|
+
// Asserted as geometry rather than as a width, because the width is the
|
|
118
|
+
// thing that keeps being wrong — the invariant is that the name fits.
|
|
119
|
+
const label = [...canvasElement.querySelectorAll<HTMLElement>('.schema-view-fields > *')].find(
|
|
120
|
+
(element) => (element.textContent ?? '').trim().toLowerCase() === 'legacy_flag'
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
await expect(label).toBeTruthy();
|
|
124
|
+
|
|
125
|
+
const lineHeight = parseFloat(getComputedStyle(label!).lineHeight) || 16;
|
|
126
|
+
|
|
127
|
+
await expect(label!.offsetHeight).toBeLessThan(lineHeight * 1.6);
|
|
128
|
+
},
|
|
129
|
+
};
|