@qoretechnologies/reqraft 0.10.61 → 0.10.62-pr.137.g86641aa
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/dpqlEditor/DpqlEditor.d.ts.map +1 -1
- package/dist/components/dpqlEditor/DpqlEditor.js +8 -3
- package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
- package/dist/components/dpqlEditor/types.d.ts +5 -0
- package/dist/components/dpqlEditor/types.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +17 -8
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +65 -15
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +12 -1
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/compactRowContext.d.ts +2 -0
- package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowContext.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +5 -1
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/expressions/ExpressionField.d.ts +21 -1
- package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
- package/dist/components/form/expressions/ExpressionField.js +72 -14
- package/dist/components/form/expressions/ExpressionField.js.map +1 -1
- package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/index.js +16 -3
- package/dist/components/form/expressions/builder/index.js.map +1 -1
- package/dist/components/form/expressions/expressionOperands.d.ts +16 -0
- package/dist/components/form/expressions/expressionOperands.d.ts.map +1 -0
- package/dist/components/form/expressions/expressionOperands.js +97 -0
- package/dist/components/form/expressions/expressionOperands.js.map +1 -0
- package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
- package/dist/components/form/expressions/mockExpressions.js +15 -0
- package/dist/components/form/expressions/mockExpressions.js.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts +2 -0
- package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.js +24 -5
- package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
- package/dist/components/form/expressions/textOfExpression.d.ts +18 -0
- package/dist/components/form/expressions/textOfExpression.d.ts.map +1 -0
- package/dist/components/form/expressions/textOfExpression.js +37 -0
- package/dist/components/form/expressions/textOfExpression.js.map +1 -0
- package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
- package/dist/components/form/expressions/useRenderExpression.js +7 -7
- package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts +8 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.js +10 -10
- package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts +6 -0
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +280 -55
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/fields/template/writtenAsText.d.ts +57 -0
- package/dist/components/form/fields/template/writtenAsText.d.ts.map +1 -0
- package/dist/components/form/fields/template/writtenAsText.js +203 -0
- package/dist/components/form/fields/template/writtenAsText.js.map +1 -0
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +7 -1
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/smartEditor/useLspSession.d.ts +6 -0
- package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
- package/dist/components/smartEditor/useLspSession.js +7 -6
- package/dist/components/smartEditor/useLspSession.js.map +1 -1
- package/dist/helpers/templates.d.ts +18 -0
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +30 -1
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +98 -18
- package/dist/helpers/validations.js.map +1 -1
- package/package.json +1 -1
- package/src/components/dpqlEditor/DpqlEditor.tsx +11 -18
- package/src/components/dpqlEditor/types.ts +5 -0
- package/src/components/form/engine/CompactRow.tsx +31 -10
- package/src/components/form/engine/FormEngine.stories.tsx +656 -18
- package/src/components/form/engine/FormEngine.tsx +64 -22
- package/src/components/form/engine/FormEngineRemote.stories.tsx +5 -0
- package/src/components/form/engine/OptionFieldMessages.tsx +12 -1
- package/src/components/form/engine/compactRowContext.ts +2 -0
- package/src/components/form/engine/readFirst.ts +7 -1
- package/src/components/form/expressions/ExpressionField.stories.tsx +57 -9
- package/src/components/form/expressions/ExpressionField.tsx +179 -60
- package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +8 -1
- package/src/components/form/expressions/builder/index.tsx +16 -3
- package/src/components/form/expressions/dpqlMockLanguage.ts +7 -2
- package/src/components/form/expressions/expressionOperands.ts +74 -0
- package/src/components/form/expressions/mockExpressions.ts +16 -4
- package/src/components/form/expressions/renderExpressionToText.ts +26 -9
- package/src/components/form/expressions/textOfExpression.ts +39 -0
- package/src/components/form/expressions/useRenderExpression.ts +9 -1
- package/src/components/form/fields/number/Number.stories.tsx +8 -7
- package/src/components/form/fields/rich-text/RichText.tsx +18 -10
- package/src/components/form/fields/template/TemplateField.stories.tsx +486 -18
- package/src/components/form/fields/template/TemplateField.tsx +506 -62
- package/src/components/form/fields/template/writtenAsText.ts +217 -0
- package/src/components/form/index.tsx +6 -0
- package/src/components/smartEditor/useLspSession.ts +15 -10
- package/src/helpers/templates.ts +35 -0
- package/src/helpers/validations.ts +83 -12
|
@@ -17,8 +17,6 @@ import {
|
|
|
17
17
|
_testsChangeStringField,
|
|
18
18
|
_testsClickButton,
|
|
19
19
|
_testsClickText,
|
|
20
|
-
_testsOpenTemplateMenu,
|
|
21
|
-
_testsOpenTemplates,
|
|
22
20
|
_testsWaitForInputValue,
|
|
23
21
|
_testsWaitForText,
|
|
24
22
|
_testsWaitForTextsCount,
|
|
@@ -662,7 +660,7 @@ export const OptionWithAnyType: Story = {
|
|
|
662
660
|
docs: {
|
|
663
661
|
description: {
|
|
664
662
|
story:
|
|
665
|
-
'Renders four options typed as any
|
|
663
|
+
'Renders four options typed as any that take templates and no expressions: each is entered on Value · Template. On Value, an untyped value is written as text (the number 1234 as typed); on Template, the template picker - a rich-text template picked there makes the value rich text, in its own editor. The operator can set a value of a type from the field menu - Boolean gives the yes / no control.',
|
|
666
664
|
},
|
|
667
665
|
},
|
|
668
666
|
},
|
|
@@ -708,27 +706,50 @@ export const OptionWithAnyType: Story = {
|
|
|
708
706
|
},
|
|
709
707
|
},
|
|
710
708
|
},
|
|
711
|
-
play: async () => {
|
|
712
|
-
|
|
713
|
-
|
|
709
|
+
play: async ({ canvasElement }) => {
|
|
710
|
+
const fields = () => [...canvasElement.querySelectorAll<HTMLElement>('.value-tabs-field')];
|
|
711
|
+
// each option is entered on Value · Template, and opens on Value
|
|
712
|
+
await waitFor(() => expect(fields()).toHaveLength(4), { timeout: 10000 });
|
|
713
|
+
for (const field of fields()) {
|
|
714
|
+
expect([...field.querySelectorAll('.value-tab')].map((t) => t.getAttribute('data-tab'))).toEqual([
|
|
715
|
+
'Value',
|
|
716
|
+
'Template',
|
|
717
|
+
]);
|
|
718
|
+
expect(field.querySelector('.value-tab[aria-pressed="true"]')?.getAttribute('data-tab')).toBe('Value');
|
|
719
|
+
}
|
|
714
720
|
|
|
715
|
-
// The
|
|
721
|
+
// The value a type was set for (number 1234) is written as text, as typed - and keeps its type.
|
|
716
722
|
// This guards against the bug where availableOptions overwrites the stored type with schema 'any'.
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
//
|
|
722
|
-
|
|
723
|
+
const textOf = (field: HTMLElement) => field.querySelector('.value-tab-text [data-slate-editor]')?.textContent ?? '';
|
|
724
|
+
await waitFor(() => expect(fields().filter((field) => textOf(field).includes('1234'))).toHaveLength(1));
|
|
725
|
+
const empty = () => fields().filter((field) => !textOf(field).includes('1234'));
|
|
726
|
+
|
|
727
|
+
// An empty option's Template tab is the template picker: a template picked there is the value.
|
|
728
|
+
const [, picking, last] = empty();
|
|
729
|
+
await userEvent.click(picking.querySelector('.value-tab[data-tab="Template"]') as HTMLElement);
|
|
730
|
+
const picker = await waitFor(() => {
|
|
731
|
+
const el = picking.querySelector<HTMLElement>('button.template-selector');
|
|
732
|
+
expect(el, 'the template picker').toBeTruthy();
|
|
733
|
+
return el as HTMLElement;
|
|
734
|
+
});
|
|
735
|
+
await userEvent.click(picker);
|
|
723
736
|
await _testsClickButton({ label: 'Testing Richtext' });
|
|
724
737
|
await _testsClickButton({ label: 'Richtext Template' });
|
|
738
|
+
await waitFor(() => expect(canvasElement.textContent).toContain('Richtext Template'));
|
|
739
|
+
// a rich-text template makes the value rich text: its own editor, which takes templates itself, holds
|
|
740
|
+
// the template as a chip - no tabs
|
|
741
|
+
await waitFor(() => expect(fields()).toHaveLength(3));
|
|
742
|
+
expect(
|
|
743
|
+
[...canvasElement.querySelectorAll<HTMLElement>('[data-slate-editor]')].some(
|
|
744
|
+
(editor) => !editor.closest('.value-tabs-field') && editor.textContent?.includes('Richtext Template')
|
|
745
|
+
)
|
|
746
|
+
).toBe(true);
|
|
725
747
|
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
// so that section opens itself rather than asking for a click first.
|
|
730
|
-
await _testsOpenTemplateMenu(4);
|
|
748
|
+
// Another's field menu sets a value of a type: Boolean gives the yes / no control.
|
|
749
|
+
// "Set Custom Value" is its only group, so that section opens itself rather than asking for a click first.
|
|
750
|
+
await userEvent.click(last.querySelector('.template-more') as HTMLElement);
|
|
731
751
|
await _testsClickButton({ label: 'Boolean' });
|
|
752
|
+
await waitFor(() => expect(canvasElement.querySelector('.value-tabs-field .reqore-checkbox')).toBeTruthy());
|
|
732
753
|
},
|
|
733
754
|
};
|
|
734
755
|
|
|
@@ -1078,6 +1099,73 @@ export const CompactRowCancelEdit: Story = {
|
|
|
1078
1099
|
},
|
|
1079
1100
|
};
|
|
1080
1101
|
|
|
1102
|
+
export const CompactRowSaysWhereItMoves: Story = {
|
|
1103
|
+
parameters: {
|
|
1104
|
+
/* Nothing to SEE in the end frame: the note this story asserts is shown only while the field is open,
|
|
1105
|
+
and the play closes it, so the capture is an ordinary filled row. Rejected on qlip build
|
|
1106
|
+
20261008-083118 ("if it says that during the play test, then remove the snapshot"); the story stays
|
|
1107
|
+
because the behaviour it pins is the point. */
|
|
1108
|
+
qlip: { skip: true },
|
|
1109
|
+
docs: {
|
|
1110
|
+
description: {
|
|
1111
|
+
story:
|
|
1112
|
+
'Renders a compact form with two required fields left empty, in Needs attention. The first, opened and filled in, stays where it is while it is open (moving it would take the focus), and says it moves to Set when it is closed; closed, it is in Set. The second, filled in, stays where it is too, and says nothing: the form says it once, on the first field edited.',
|
|
1113
|
+
},
|
|
1114
|
+
},
|
|
1115
|
+
},
|
|
1116
|
+
args: {
|
|
1117
|
+
compact: true,
|
|
1118
|
+
minColumnWidth: '360px',
|
|
1119
|
+
value: {},
|
|
1120
|
+
options: {
|
|
1121
|
+
cookie_name: {
|
|
1122
|
+
type: 'string',
|
|
1123
|
+
ui_type: 'string',
|
|
1124
|
+
display_name: 'Cookie Name',
|
|
1125
|
+
required: true,
|
|
1126
|
+
},
|
|
1127
|
+
cookie_path: {
|
|
1128
|
+
type: 'string',
|
|
1129
|
+
ui_type: 'string',
|
|
1130
|
+
display_name: 'Cookie Path',
|
|
1131
|
+
required: true,
|
|
1132
|
+
},
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
play: async ({ canvasElement }) => {
|
|
1136
|
+
const row = () => canvasElement.querySelector<HTMLElement>('[data-field="cookie_name"]');
|
|
1137
|
+
await waitFor(() => expect(row()).toBeTruthy());
|
|
1138
|
+
fireEvent.click(row()!);
|
|
1139
|
+
await waitFor(() => expect(canvasElement.querySelector('.readfirst-row-editing')).toBeTruthy());
|
|
1140
|
+
expect(canvasElement.textContent).not.toContain('Moves to');
|
|
1141
|
+
const input = canvasElement.querySelector<HTMLInputElement>(
|
|
1142
|
+
'[data-field="cookie_name"] input, [data-field="cookie_name"] textarea'
|
|
1143
|
+
);
|
|
1144
|
+
fireEvent.change(input!, { target: { value: 'session' } });
|
|
1145
|
+
// filled in, it says where it goes - and stays put, the editor still open
|
|
1146
|
+
await waitFor(() => expect(canvasElement.textContent).toContain('Moves to "Set" when you close it (✓)'), {
|
|
1147
|
+
timeout: 5000,
|
|
1148
|
+
});
|
|
1149
|
+
expect(canvasElement.querySelector('.readfirst-row-editing')).toBeTruthy();
|
|
1150
|
+
fireEvent.click(canvasElement.querySelector<HTMLElement>('.options-readfirst-done')!);
|
|
1151
|
+
await waitFor(() => expect(canvasElement.querySelector('.readfirst-row-editing')).toBeNull());
|
|
1152
|
+
expect(canvasElement.textContent).not.toContain('Moves to');
|
|
1153
|
+
// the second field edited is held as the first was, and does not say so again
|
|
1154
|
+
fireEvent.click(canvasElement.querySelector<HTMLElement>('[data-field="cookie_path"]')!);
|
|
1155
|
+
const path = await waitFor(() => {
|
|
1156
|
+
const el = canvasElement.querySelector<HTMLInputElement>(
|
|
1157
|
+
'[data-field="cookie_path"] input, [data-field="cookie_path"] textarea'
|
|
1158
|
+
);
|
|
1159
|
+
expect(el).toBeTruthy();
|
|
1160
|
+
return el!;
|
|
1161
|
+
});
|
|
1162
|
+
fireEvent.change(path, { target: { value: '/' } });
|
|
1163
|
+
await waitFor(() => expect(canvasElement.textContent).toContain('2/2 set'));
|
|
1164
|
+
expect(canvasElement.querySelector('.readfirst-row-editing')).toBeTruthy();
|
|
1165
|
+
expect(canvasElement.textContent).not.toContain('Moves to');
|
|
1166
|
+
},
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1081
1169
|
export const CompactRowReadOnlyIsAWayIn: Story = {
|
|
1082
1170
|
parameters: {
|
|
1083
1171
|
/* Nothing to SEE here: what this story asserts is a role, a focus stop, a
|
|
@@ -9723,3 +9811,553 @@ const stickyBoxesStory = (layout: 'own' | 'host' | 'padded-host'): Story => ({
|
|
|
9723
9811
|
export const BoxHeaderSlidesUnderToolbarOwnScroll: Story = stickyBoxesStory('own');
|
|
9724
9812
|
export const BoxHeaderSlidesUnderToolbarHostScroll: Story = stickyBoxesStory('host');
|
|
9725
9813
|
export const BoxHeaderSlidesUnderToolbarPaddedHost: Story = stickyBoxesStory('padded-host');
|
|
9814
|
+
|
|
9815
|
+
const ROW_FIELDS = {
|
|
9816
|
+
items: [
|
|
9817
|
+
{
|
|
9818
|
+
label: 'Fields of the row',
|
|
9819
|
+
items: [
|
|
9820
|
+
{ label: 'pos', value: '$record:{pos}', badge: 'int' },
|
|
9821
|
+
{ label: 'bezeichnung', value: '$record:{bezeichnung}', badge: 'string' },
|
|
9822
|
+
],
|
|
9823
|
+
},
|
|
9824
|
+
],
|
|
9825
|
+
};
|
|
9826
|
+
|
|
9827
|
+
/** A whole-number option holding a field of the row, and whether the form can be saved. */
|
|
9828
|
+
const TextAroundATemplateForm = ({ initial, type = 'int' }: { initial: unknown; type?: string }) => {
|
|
9829
|
+
const [value, setValue] = useState<any>({ quantity: { type, value: initial } });
|
|
9830
|
+
const [valid, setValid] = useState<boolean>();
|
|
9831
|
+
return (
|
|
9832
|
+
<>
|
|
9833
|
+
<FormEngine
|
|
9834
|
+
name='textAround'
|
|
9835
|
+
stringTemplates={ROW_FIELDS as any}
|
|
9836
|
+
options={
|
|
9837
|
+
{
|
|
9838
|
+
quantity: {
|
|
9839
|
+
type,
|
|
9840
|
+
display_name: 'Quantity',
|
|
9841
|
+
required: true,
|
|
9842
|
+
preselected: true,
|
|
9843
|
+
supports_templates: true,
|
|
9844
|
+
},
|
|
9845
|
+
} as unknown as IQorusFormSchema
|
|
9846
|
+
}
|
|
9847
|
+
value={value}
|
|
9848
|
+
onChange={(_n, v) => setValue(v)}
|
|
9849
|
+
onValidityChange={(isValid) => setValid(isValid)}
|
|
9850
|
+
/>
|
|
9851
|
+
<code className='form-validity' data-valid={String(valid)}>
|
|
9852
|
+
{valid ? 'can be saved' : 'cannot be saved'}
|
|
9853
|
+
</code>
|
|
9854
|
+
</>
|
|
9855
|
+
);
|
|
9856
|
+
};
|
|
9857
|
+
|
|
9858
|
+
const TEXT_AROUND = '"Stk." makes this text, not a whole number. Delete it to keep the template alone.';
|
|
9859
|
+
|
|
9860
|
+
export const WholeNumberWithTextAroundATemplate: Story = {
|
|
9861
|
+
parameters: {
|
|
9862
|
+
docs: {
|
|
9863
|
+
description: {
|
|
9864
|
+
story:
|
|
9865
|
+
'Renders a whole-number option holding the field pos, written as text. Text written after the field ("$record:{pos} Stk.") is not a whole number: while it is being typed the option says which text makes it text and that deleting it keeps the template alone, and the form cannot be saved. Deleting the text makes it the field alone again: the message goes and the form can be saved.',
|
|
9866
|
+
},
|
|
9867
|
+
},
|
|
9868
|
+
},
|
|
9869
|
+
render: () => <TextAroundATemplateForm initial='$record:{pos}' />,
|
|
9870
|
+
play: async ({ canvasElement }) => {
|
|
9871
|
+
const validity = () => canvasElement.querySelector('.form-validity')?.getAttribute('data-valid');
|
|
9872
|
+
const editor = await waitFor(() => {
|
|
9873
|
+
const el = canvasElement.querySelector<HTMLElement>('[data-slate-editor]');
|
|
9874
|
+
expect(el, 'the whole number, written as text').toBeTruthy();
|
|
9875
|
+
return el as HTMLElement;
|
|
9876
|
+
});
|
|
9877
|
+
await waitFor(() => expect(editor.textContent).toContain('pos'));
|
|
9878
|
+
await waitFor(() => expect(validity()).toBe('true'));
|
|
9879
|
+
expect(canvasElement.textContent).not.toContain(TEXT_AROUND);
|
|
9880
|
+
await userEvent.click(editor);
|
|
9881
|
+
await userEvent.keyboard(' Stk.');
|
|
9882
|
+
await waitFor(() => expect(canvasElement.textContent).toContain(TEXT_AROUND));
|
|
9883
|
+
await waitFor(() => expect(validity()).toBe('false'));
|
|
9884
|
+
await userEvent.keyboard('{Backspace}{Backspace}{Backspace}{Backspace}{Backspace}');
|
|
9885
|
+
await waitFor(() => expect(canvasElement.textContent).not.toContain(TEXT_AROUND));
|
|
9886
|
+
await waitFor(() => expect(validity()).toBe('true'));
|
|
9887
|
+
},
|
|
9888
|
+
};
|
|
9889
|
+
|
|
9890
|
+
export const WholeNumberLiteralAndExpressionStayValid: Story = {
|
|
9891
|
+
parameters: {
|
|
9892
|
+
docs: {
|
|
9893
|
+
description: {
|
|
9894
|
+
story:
|
|
9895
|
+
'Renders the same whole-number option holding 12, a literal of its type: valid, and no message. A template alone and an expression are valid too; only text around a template is not.',
|
|
9896
|
+
},
|
|
9897
|
+
},
|
|
9898
|
+
},
|
|
9899
|
+
render: () => <TextAroundATemplateForm initial={12} />,
|
|
9900
|
+
play: async ({ canvasElement }) => {
|
|
9901
|
+
const validity = () => canvasElement.querySelector('.form-validity')?.getAttribute('data-valid');
|
|
9902
|
+
await waitFor(() => expect(validity()).toBe('true'));
|
|
9903
|
+
expect(canvasElement.textContent).not.toContain(TEXT_AROUND);
|
|
9904
|
+
},
|
|
9905
|
+
};
|
|
9906
|
+
|
|
9907
|
+
const textAroundOf = (type: string, label: string, message: string): Story => ({
|
|
9908
|
+
parameters: {
|
|
9909
|
+
docs: {
|
|
9910
|
+
description: {
|
|
9911
|
+
story: `Renders a ${label} option holding "$record:{pos} Stk.", text around the field pos. It is not ${label === 'true or false' ? 'true or false' : `a ${label}`}: the option says which text makes it so and how to fix it - "${message}" - and the form cannot be saved.`,
|
|
9912
|
+
},
|
|
9913
|
+
},
|
|
9914
|
+
},
|
|
9915
|
+
render: () => <TextAroundATemplateForm type={type} initial='$record:{pos} Stk.' />,
|
|
9916
|
+
play: async ({ canvasElement }) => {
|
|
9917
|
+
await waitFor(() => expect(canvasElement.textContent).toContain(message));
|
|
9918
|
+
await waitFor(() => expect(canvasElement.querySelector('.form-validity')?.getAttribute('data-valid')).toBe('false'));
|
|
9919
|
+
},
|
|
9920
|
+
});
|
|
9921
|
+
|
|
9922
|
+
export const NumberWithTextAroundATemplate = textAroundOf(
|
|
9923
|
+
'number',
|
|
9924
|
+
'number',
|
|
9925
|
+
'"Stk." makes this text, not a number. Delete it to keep the template alone.'
|
|
9926
|
+
);
|
|
9927
|
+
export const TrueOrFalseWithTextAroundATemplate = textAroundOf(
|
|
9928
|
+
'bool',
|
|
9929
|
+
'true or false',
|
|
9930
|
+
'"Stk." makes this text, not true or false. Delete it to keep the template alone.'
|
|
9931
|
+
);
|
|
9932
|
+
export const DateWithTextAroundATemplate = textAroundOf(
|
|
9933
|
+
'date',
|
|
9934
|
+
'date',
|
|
9935
|
+
'"Stk." makes this text, not a date. Delete it to keep the template alone.'
|
|
9936
|
+
);
|
|
9937
|
+
|
|
9938
|
+
/** A whole-number option whose host says, in its own words, what is wrong with it. */
|
|
9939
|
+
const HostReasonForm = ({ initial, reason }: { initial: unknown; reason?: string }) => {
|
|
9940
|
+
const [value, setValue] = useState<any>({ quantity: { type: 'int', value: initial } });
|
|
9941
|
+
const [valid, setValid] = useState<boolean>();
|
|
9942
|
+
return (
|
|
9943
|
+
<>
|
|
9944
|
+
<FormEngine
|
|
9945
|
+
name='hostReason'
|
|
9946
|
+
stringTemplates={ROW_FIELDS as any}
|
|
9947
|
+
options={
|
|
9948
|
+
{
|
|
9949
|
+
quantity: {
|
|
9950
|
+
type: 'int',
|
|
9951
|
+
display_name: 'Quantity',
|
|
9952
|
+
required: true,
|
|
9953
|
+
preselected: true,
|
|
9954
|
+
supports_templates: true,
|
|
9955
|
+
...(reason ? { invalid_reason: reason } : {}),
|
|
9956
|
+
},
|
|
9957
|
+
} as unknown as IQorusFormSchema
|
|
9958
|
+
}
|
|
9959
|
+
value={value}
|
|
9960
|
+
onChange={(_n, v) => setValue(v)}
|
|
9961
|
+
onValidityChange={(isValid) => setValid(isValid)}
|
|
9962
|
+
/>
|
|
9963
|
+
<code className='form-validity' data-valid={String(valid)}>
|
|
9964
|
+
{valid ? 'can be saved' : 'cannot be saved'}
|
|
9965
|
+
</code>
|
|
9966
|
+
</>
|
|
9967
|
+
);
|
|
9968
|
+
};
|
|
9969
|
+
|
|
9970
|
+
/* The host's reason says, as a field's own does, why the value is wrong and how to fix it (qlip build
|
|
9971
|
+
20261008-083118: "How does the user fix this? Why has this happened?"). */
|
|
9972
|
+
const HOST_REASON = 'Quantity counts pieces: delete "Stk." so that it holds pos alone';
|
|
9973
|
+
|
|
9974
|
+
export const HostReasonInPlaceOfTheFieldsOwn: Story = {
|
|
9975
|
+
parameters: {
|
|
9976
|
+
docs: {
|
|
9977
|
+
description: {
|
|
9978
|
+
story:
|
|
9979
|
+
'Renders a whole-number option holding "$record:{pos} Stk." whose host gives its own reason (`invalid_reason`), in its own words, saying why and what to do: "Quantity counts pieces: delete "Stk." so that it holds pos alone". The field says the host\'s reason, once, in place of its own, and the form cannot be saved.',
|
|
9980
|
+
},
|
|
9981
|
+
},
|
|
9982
|
+
},
|
|
9983
|
+
render: () => (
|
|
9984
|
+
<HostReasonForm initial='$record:{pos} Stk.' reason={HOST_REASON} />
|
|
9985
|
+
),
|
|
9986
|
+
play: async ({ canvasElement }) => {
|
|
9987
|
+
await waitFor(() => expect(canvasElement.textContent).toContain(HOST_REASON));
|
|
9988
|
+
expect(canvasElement.textContent).not.toContain('makes this text, not a whole number');
|
|
9989
|
+
await waitFor(() => expect(canvasElement.querySelector('.form-validity')?.getAttribute('data-valid')).toBe('false'));
|
|
9990
|
+
},
|
|
9991
|
+
};
|
|
9992
|
+
|
|
9993
|
+
export const HostReasonInPlaceOfRequired: Story = {
|
|
9994
|
+
parameters: {
|
|
9995
|
+
docs: {
|
|
9996
|
+
description: {
|
|
9997
|
+
story:
|
|
9998
|
+
'Renders the same option empty, with the host\'s reason "Enter a value or an expression": said in place of "This field is required", not beside it.',
|
|
9999
|
+
},
|
|
10000
|
+
},
|
|
10001
|
+
},
|
|
10002
|
+
render: () => <HostReasonForm initial={undefined} reason='Enter a value or an expression' />,
|
|
10003
|
+
play: async ({ canvasElement }) => {
|
|
10004
|
+
await waitFor(() => expect(canvasElement.textContent).toContain('Enter a value or an expression'));
|
|
10005
|
+
expect(canvasElement.textContent).not.toContain('This field is required');
|
|
10006
|
+
},
|
|
10007
|
+
};
|
|
10008
|
+
|
|
10009
|
+
export const YesNoHoldingATemplateRead: Story = {
|
|
10010
|
+
parameters: {
|
|
10011
|
+
docs: {
|
|
10012
|
+
description: {
|
|
10013
|
+
story:
|
|
10014
|
+
'Renders a compact form whose yes/no option holds the field pos. Its collapsed row shows the field, pos, as a chip: it used to read "Yes", because the row read any value that is not empty as true.',
|
|
10015
|
+
},
|
|
10016
|
+
},
|
|
10017
|
+
},
|
|
10018
|
+
render: () => (
|
|
10019
|
+
<FormEngine
|
|
10020
|
+
compact
|
|
10021
|
+
name='yesNoTemplate'
|
|
10022
|
+
stringTemplates={ROW_FIELDS as any}
|
|
10023
|
+
options={{ flag: { type: 'bool', display_name: 'Flag', supports_templates: true } } as unknown as IQorusFormSchema}
|
|
10024
|
+
value={{ flag: { type: 'bool', value: '$record:{pos}' } } as any}
|
|
10025
|
+
onChange={fn()}
|
|
10026
|
+
/>
|
|
10027
|
+
),
|
|
10028
|
+
play: async ({ canvasElement }) => {
|
|
10029
|
+
const row = await waitFor(() => {
|
|
10030
|
+
const el = canvasElement.querySelector<HTMLElement>('.readfirst-row');
|
|
10031
|
+
expect(el).toBeTruthy();
|
|
10032
|
+
return el as HTMLElement;
|
|
10033
|
+
});
|
|
10034
|
+
await waitFor(() => expect(row.textContent).toContain('pos'));
|
|
10035
|
+
expect(row.textContent).not.toContain('Yes');
|
|
10036
|
+
},
|
|
10037
|
+
};
|
|
10038
|
+
|
|
10039
|
+
// --- Value · Expression · Visual (qorus#646, David) -----------------------------------------------------------
|
|
10040
|
+
// A form option's value is entered on tabs - one value, three views - or on Value · Template where the option
|
|
10041
|
+
// takes templates only. Text, numbers and whole numbers are written as text on Value (templates as chips,
|
|
10042
|
+
// expressions detected); a yes / no, a date and fixed choices keep their own control.
|
|
10043
|
+
|
|
10044
|
+
const TEMPLATE_OR_EXPRESSION_OPTIONS = {
|
|
10045
|
+
quantity: { type: 'int', display_name: 'Quantity', supports_templates: true, supports_expressions: true, expressions: mockExpressions },
|
|
10046
|
+
price: { type: 'number', display_name: 'Price', supports_templates: true, supports_expressions: true, expressions: mockExpressions },
|
|
10047
|
+
active: { type: 'bool', display_name: 'Active', supports_templates: true, supports_expressions: true, expressions: mockExpressions },
|
|
10048
|
+
due: { type: 'date', display_name: 'Due', supports_templates: true, supports_expressions: true, expressions: mockExpressions },
|
|
10049
|
+
status: {
|
|
10050
|
+
type: 'string',
|
|
10051
|
+
display_name: 'Status',
|
|
10052
|
+
supports_templates: true,
|
|
10053
|
+
supports_expressions: true,
|
|
10054
|
+
expressions: mockExpressions,
|
|
10055
|
+
allowed_values: [
|
|
10056
|
+
{ name: 'open', display_name: 'Open', value: { type: 'string', value: 'open' } },
|
|
10057
|
+
{ name: 'closed', display_name: 'Closed', value: { type: 'string', value: 'closed' } },
|
|
10058
|
+
],
|
|
10059
|
+
},
|
|
10060
|
+
note: { type: 'string', display_name: 'Note', supports_templates: true, supports_expressions: true, expressions: mockExpressions },
|
|
10061
|
+
saved_template: { type: 'int', display_name: 'Saved template', supports_templates: true, supports_expressions: true, expressions: mockExpressions },
|
|
10062
|
+
flag_template: { type: 'bool', display_name: 'Flag template', supports_templates: true, supports_expressions: true, expressions: mockExpressions },
|
|
10063
|
+
computed: { type: 'int', display_name: 'Computed', supports_templates: true, supports_expressions: true, expressions: mockExpressions },
|
|
10064
|
+
converted: { type: 'int', display_name: 'Converted', supports_templates: true, supports_expressions: true, expressions: mockExpressions },
|
|
10065
|
+
template_only: { type: 'int', display_name: 'Template only', supports_templates: true },
|
|
10066
|
+
} as unknown as IQorusFormSchema;
|
|
10067
|
+
|
|
10068
|
+
const TEMPLATE_OR_EXPRESSION_VALUE = {
|
|
10069
|
+
quantity: { type: 'int', value: 12 },
|
|
10070
|
+
price: { type: 'number', value: 3.5 },
|
|
10071
|
+
active: { type: 'bool', value: true },
|
|
10072
|
+
due: { type: 'date', value: '2026-11-02T00:00:00Z' },
|
|
10073
|
+
status: { type: 'string', value: 'open' },
|
|
10074
|
+
note: { type: 'string', value: 'SUP-$record:{pos}' },
|
|
10075
|
+
saved_template: { type: 'int', value: '$record:{pos}' },
|
|
10076
|
+
flag_template: { type: 'bool', value: '$record:{pos}' },
|
|
10077
|
+
computed: {
|
|
10078
|
+
type: 'int',
|
|
10079
|
+
is_expression: true,
|
|
10080
|
+
value: { exp: '+', args: [{ type: 'int', value: '$record:{pos}' }, { type: 'int', value: 1 }] },
|
|
10081
|
+
},
|
|
10082
|
+
converted: { type: 'int', value: '$record:{bezeichnung}' },
|
|
10083
|
+
template_only: { type: 'int', value: '$record:{pos}' },
|
|
10084
|
+
};
|
|
10085
|
+
|
|
10086
|
+
/** The form, and what each option holds - its value, and whether it is an expression. */
|
|
10087
|
+
const TemplateOrExpressionForm = () => {
|
|
10088
|
+
const [value, setValue] = useState<any>(TEMPLATE_OR_EXPRESSION_VALUE);
|
|
10089
|
+
return (
|
|
10090
|
+
<>
|
|
10091
|
+
<FormEngine
|
|
10092
|
+
compact
|
|
10093
|
+
name='templateOrExpression'
|
|
10094
|
+
stringTemplates={ROW_FIELDS as any}
|
|
10095
|
+
options={TEMPLATE_OR_EXPRESSION_OPTIONS}
|
|
10096
|
+
value={value}
|
|
10097
|
+
onChange={(_n, v) => setValue(v)}
|
|
10098
|
+
/>
|
|
10099
|
+
<code className='form-held' data-held={JSON.stringify(value)} style={{ display: 'none' }} />
|
|
10100
|
+
</>
|
|
10101
|
+
);
|
|
10102
|
+
};
|
|
10103
|
+
|
|
10104
|
+
/** What the form holds for an option. */
|
|
10105
|
+
const heldOption = (canvasElement: HTMLElement, field: string) =>
|
|
10106
|
+
JSON.parse(canvasElement.querySelector('.form-held')?.getAttribute('data-held') ?? '{}')[field];
|
|
10107
|
+
|
|
10108
|
+
/** An option's row, read or being edited. */
|
|
10109
|
+
const optionRow = (field: string) =>
|
|
10110
|
+
[...document.querySelectorAll<HTMLElement>(`[data-field="${field}"]`)].pop() as HTMLElement;
|
|
10111
|
+
|
|
10112
|
+
/** Open an option's editor, as a click on its row does. */
|
|
10113
|
+
const editOption = async (field: string) => {
|
|
10114
|
+
const row = await waitFor(() => {
|
|
10115
|
+
const el = optionRow(field);
|
|
10116
|
+
expect(el, field).toBeTruthy();
|
|
10117
|
+
return el;
|
|
10118
|
+
});
|
|
10119
|
+
const editing = () =>
|
|
10120
|
+
document.querySelector<HTMLElement>(
|
|
10121
|
+
`[data-field="${field}"].readfirst-row-editing, [data-field="${field}"].options-readfirst-card`
|
|
10122
|
+
);
|
|
10123
|
+
if (!editing()) await userEvent.click(row);
|
|
10124
|
+
return waitFor(() => {
|
|
10125
|
+
const el = editing();
|
|
10126
|
+
expect(el, `${field} being edited`).toBeTruthy();
|
|
10127
|
+
return el as HTMLElement;
|
|
10128
|
+
});
|
|
10129
|
+
};
|
|
10130
|
+
|
|
10131
|
+
/** An option's tabs, and the one it is on. */
|
|
10132
|
+
const optionTabs = (row: HTMLElement) =>
|
|
10133
|
+
[...row.querySelectorAll<HTMLElement>('.value-tab')].map((tab) => tab.getAttribute('data-tab'));
|
|
10134
|
+
const optionActiveTab = (row: HTMLElement) =>
|
|
10135
|
+
row.querySelector<HTMLElement>('.value-tab[aria-pressed="true"]')?.getAttribute('data-tab');
|
|
10136
|
+
const chooseTab = async (row: HTMLElement, tab: string) => {
|
|
10137
|
+
await userEvent.click(row.querySelector<HTMLElement>(`.value-tab[data-tab="${tab}"]`) as HTMLElement);
|
|
10138
|
+
await waitFor(() => expect(optionActiveTab(optionRow(row.getAttribute('data-field') as string))).toBe(tab));
|
|
10139
|
+
};
|
|
10140
|
+
|
|
10141
|
+
export const ValueTabPerOptionType: Story = {
|
|
10142
|
+
parameters: {
|
|
10143
|
+
docs: {
|
|
10144
|
+
description: {
|
|
10145
|
+
story:
|
|
10146
|
+
'Renders a form whose options take templates and expressions, and opens each on its Value tab: a whole number, a number and text are written as text (templates as chips, expressions detected); a yes / no, a date and fixed choices keep their own control. Each has the tabs Value · Expression · Visual, except the fixed choices, which take no expression - Value · Template.',
|
|
10147
|
+
},
|
|
10148
|
+
},
|
|
10149
|
+
},
|
|
10150
|
+
render: () => <TemplateOrExpressionForm />,
|
|
10151
|
+
play: async () => {
|
|
10152
|
+
for (const [field, asText, tabs] of [
|
|
10153
|
+
['quantity', true, ['Value', 'Expression', 'Visual']],
|
|
10154
|
+
['price', true, ['Value', 'Expression', 'Visual']],
|
|
10155
|
+
['note', true, ['Value', 'Expression', 'Visual']],
|
|
10156
|
+
['active', false, ['Value', 'Expression', 'Visual']],
|
|
10157
|
+
['due', false, ['Value', 'Expression', 'Visual']],
|
|
10158
|
+
// a value that must be one of the choices takes no expression: its templates are on Template
|
|
10159
|
+
['status', false, ['Value', 'Template']],
|
|
10160
|
+
] as const) {
|
|
10161
|
+
const row = await editOption(field);
|
|
10162
|
+
await waitFor(() => expect(optionTabs(row)).toEqual(tabs));
|
|
10163
|
+
expect(optionActiveTab(row)).toBe('Value');
|
|
10164
|
+
expect(!!row.querySelector('.value-tab-text [data-slate-editor]'), `${field} written as text`).toBe(asText);
|
|
10165
|
+
if (!asText) expect(row.querySelector('input, .reqore-checkbox'), `${field} has its own control`).toBeTruthy();
|
|
10166
|
+
}
|
|
10167
|
+
},
|
|
10168
|
+
};
|
|
10169
|
+
|
|
10170
|
+
export const ValueToExpressionAndVisual: Story = {
|
|
10171
|
+
parameters: {
|
|
10172
|
+
docs: {
|
|
10173
|
+
description: {
|
|
10174
|
+
story:
|
|
10175
|
+
'Renders the form and opens Quantity (12) on the Expression tab: the value written as expression text (12), with the form\'s templates offered beside it ($) and the server\'s $ completion; the form still holds 12. Visual shows the expression built from it - 12 its first operand.',
|
|
10176
|
+
},
|
|
10177
|
+
},
|
|
10178
|
+
},
|
|
10179
|
+
render: () => <TemplateOrExpressionForm />,
|
|
10180
|
+
play: async ({ canvasElement }) => {
|
|
10181
|
+
const row = await editOption('quantity');
|
|
10182
|
+
await chooseTab(row, 'Expression');
|
|
10183
|
+
const field = await waitFor(() => {
|
|
10184
|
+
const el = optionRow('quantity').querySelector<HTMLElement>('.expression-field');
|
|
10185
|
+
expect(el, 'the expression editor').toBeTruthy();
|
|
10186
|
+
return el as HTMLElement;
|
|
10187
|
+
});
|
|
10188
|
+
await waitFor(() =>
|
|
10189
|
+
expect(field.querySelector('[data-slate-editor]')?.textContent?.replace(/\uFEFF/g, '').trim()).toBe('12')
|
|
10190
|
+
);
|
|
10191
|
+
expect(field.querySelector('.expression-text-template-picker'), 'the templates offered').toBeTruthy();
|
|
10192
|
+
expect(heldOption(canvasElement, 'quantity')).toEqual({ type: 'int', value: 12 });
|
|
10193
|
+
await chooseTab(optionRow('quantity'), 'Visual');
|
|
10194
|
+
await waitFor(() => expect(optionRow('quantity').querySelector('.expression')).toBeTruthy());
|
|
10195
|
+
},
|
|
10196
|
+
};
|
|
10197
|
+
|
|
10198
|
+
export const SavedTemplateOpensOnValue: Story = {
|
|
10199
|
+
parameters: {
|
|
10200
|
+
docs: {
|
|
10201
|
+
description: {
|
|
10202
|
+
story:
|
|
10203
|
+
'Renders the form and opens "Saved template", a whole number holding the field pos: it opens on Value, the field a chip in the text it is written in; the form holds the bare template.',
|
|
10204
|
+
},
|
|
10205
|
+
},
|
|
10206
|
+
},
|
|
10207
|
+
render: () => <TemplateOrExpressionForm />,
|
|
10208
|
+
play: async ({ canvasElement }) => {
|
|
10209
|
+
const row = await editOption('saved_template');
|
|
10210
|
+
expect(optionActiveTab(row)).toBe('Value');
|
|
10211
|
+
await waitFor(() => expect(row.querySelector('.value-tab-text [data-slate-editor]')?.textContent).toContain('pos'));
|
|
10212
|
+
expect(heldOption(canvasElement, 'saved_template')).toEqual({ type: 'int', value: '$record:{pos}' });
|
|
10213
|
+
},
|
|
10214
|
+
};
|
|
10215
|
+
|
|
10216
|
+
export const SavedTemplateOnAYesNoOpensOnExpression: Story = {
|
|
10217
|
+
parameters: {
|
|
10218
|
+
docs: {
|
|
10219
|
+
description: {
|
|
10220
|
+
story:
|
|
10221
|
+
'Renders the form and opens "Flag template", a yes / no holding the field pos: a yes / no keeps its own control on Value, which cannot show a template, so it opens on Expression, as the template. Its Value tab says where the template is shown, and offers to replace it with a value.',
|
|
10222
|
+
},
|
|
10223
|
+
},
|
|
10224
|
+
},
|
|
10225
|
+
render: () => <TemplateOrExpressionForm />,
|
|
10226
|
+
play: async () => {
|
|
10227
|
+
const row = await editOption('flag_template');
|
|
10228
|
+
expect(optionActiveTab(row)).toBe('Expression');
|
|
10229
|
+
await waitFor(() => expect(row.querySelector('.expression-field [data-slate-editor]')?.textContent).toContain('pos'));
|
|
10230
|
+
await chooseTab(row, 'Value');
|
|
10231
|
+
await waitFor(() =>
|
|
10232
|
+
expect(optionRow('flag_template').querySelector('.value-tab-cannot-show')?.textContent).toContain('Expression tab')
|
|
10233
|
+
);
|
|
10234
|
+
},
|
|
10235
|
+
};
|
|
10236
|
+
|
|
10237
|
+
export const SavedTextWithTemplatesStaysText: Story = {
|
|
10238
|
+
parameters: {
|
|
10239
|
+
docs: {
|
|
10240
|
+
description: {
|
|
10241
|
+
story:
|
|
10242
|
+
'Renders the form and opens Note, text holding "SUP-" and the field pos: it opens on Value, the field a chip in the text. On Expression it reads concat("SUP-", pos).',
|
|
10243
|
+
},
|
|
10244
|
+
},
|
|
10245
|
+
},
|
|
10246
|
+
render: () => <TemplateOrExpressionForm />,
|
|
10247
|
+
play: async () => {
|
|
10248
|
+
const row = await editOption('note');
|
|
10249
|
+
expect(optionActiveTab(row)).toBe('Value');
|
|
10250
|
+
const editor = await waitFor(() => {
|
|
10251
|
+
const el = row.querySelector<HTMLElement>('.value-tab-text [data-slate-editor]');
|
|
10252
|
+
expect(el).toBeTruthy();
|
|
10253
|
+
return el as HTMLElement;
|
|
10254
|
+
});
|
|
10255
|
+
await waitFor(() => expect(editor.textContent).toContain('pos'));
|
|
10256
|
+
expect(editor.textContent).toContain('SUP-');
|
|
10257
|
+
await chooseTab(row, 'Expression');
|
|
10258
|
+
await waitFor(() =>
|
|
10259
|
+
expect(optionRow('note').querySelector('.expression-field [data-slate-editor]')?.textContent).toContain('concat')
|
|
10260
|
+
);
|
|
10261
|
+
},
|
|
10262
|
+
};
|
|
10263
|
+
|
|
10264
|
+
export const SavedExpressionOpensOnExpression: Story = {
|
|
10265
|
+
parameters: {
|
|
10266
|
+
docs: {
|
|
10267
|
+
description: {
|
|
10268
|
+
story:
|
|
10269
|
+
'Renders the form and opens Computed, a whole number holding the expression pos + 1: it opens on Expression, as DPQL text. On Value it says it cannot show this expression as a value and offers to replace it - the expression is kept until it is replaced.',
|
|
10270
|
+
},
|
|
10271
|
+
},
|
|
10272
|
+
},
|
|
10273
|
+
render: () => <TemplateOrExpressionForm />,
|
|
10274
|
+
play: async ({ canvasElement }) => {
|
|
10275
|
+
const row = await editOption('computed');
|
|
10276
|
+
expect(optionActiveTab(row)).toBe('Expression');
|
|
10277
|
+
await waitFor(() => expect(row.querySelector('.expression-field [data-slate-editor]')?.textContent).toContain('+'));
|
|
10278
|
+
await chooseTab(row, 'Value');
|
|
10279
|
+
await waitFor(() => expect(optionRow('computed').querySelector('.value-tab-cannot-show')).toBeTruthy());
|
|
10280
|
+
expect(heldOption(canvasElement, 'computed').is_expression).toBe(true);
|
|
10281
|
+
await userEvent.click(optionRow('computed').querySelector('.value-tab-replace') as HTMLElement);
|
|
10282
|
+
await waitFor(() => expect(heldOption(canvasElement, 'computed')?.value).toBeUndefined());
|
|
10283
|
+
},
|
|
10284
|
+
};
|
|
10285
|
+
|
|
10286
|
+
export const TemplateOnlyOptionHasValueAndTemplate: Story = {
|
|
10287
|
+
parameters: {
|
|
10288
|
+
docs: {
|
|
10289
|
+
description: {
|
|
10290
|
+
story:
|
|
10291
|
+
'Renders the form and opens "Template only", a whole number that takes templates and no expressions, holding pos: its tabs are Value · Template - no Visual - and its Template tab is the template picker, the bare template its value.',
|
|
10292
|
+
},
|
|
10293
|
+
},
|
|
10294
|
+
},
|
|
10295
|
+
render: () => <TemplateOrExpressionForm />,
|
|
10296
|
+
play: async ({ canvasElement }) => {
|
|
10297
|
+
const row = await editOption('template_only');
|
|
10298
|
+
await waitFor(() => expect(optionTabs(row)).toEqual(['Value', 'Template']));
|
|
10299
|
+
await chooseTab(row, 'Template');
|
|
10300
|
+
await waitFor(() => expect(optionRow('template_only').textContent).toContain('pos'));
|
|
10301
|
+
expect(optionRow('template_only').querySelector('.expression-field')).toBeNull();
|
|
10302
|
+
expect(heldOption(canvasElement, 'template_only')).toEqual({ type: 'int', value: '$record:{pos}' });
|
|
10303
|
+
},
|
|
10304
|
+
};
|
|
10305
|
+
|
|
10306
|
+
export const ExpressionTypedOnValueSwitchesWithUndo: Story = {
|
|
10307
|
+
parameters: {
|
|
10308
|
+
docs: {
|
|
10309
|
+
description: {
|
|
10310
|
+
story:
|
|
10311
|
+
'Renders the form, opens Quantity on Value and writes 1 + 2: the Value tab never holds an expression, so it moves to Expression, with an Undo that puts the text back on Value.',
|
|
10312
|
+
},
|
|
10313
|
+
},
|
|
10314
|
+
},
|
|
10315
|
+
render: () => <TemplateOrExpressionForm />,
|
|
10316
|
+
play: async ({ canvasElement }) => {
|
|
10317
|
+
const row = await editOption('quantity');
|
|
10318
|
+
const editor = await waitFor(() => {
|
|
10319
|
+
const el = row.querySelector<HTMLElement>('.value-tab-text [data-slate-editor]');
|
|
10320
|
+
expect(el).toBeTruthy();
|
|
10321
|
+
return el as HTMLElement;
|
|
10322
|
+
});
|
|
10323
|
+
await userEvent.click(editor);
|
|
10324
|
+
await userEvent.keyboard(' + 2');
|
|
10325
|
+
await waitFor(() => expect(optionActiveTab(optionRow('quantity'))).toBe('Expression'), { timeout: 10000 });
|
|
10326
|
+
await waitFor(() => expect(heldOption(canvasElement, 'quantity').is_expression).toBe(true));
|
|
10327
|
+
await userEvent.click(optionRow('quantity').querySelector('.dpql-detected-undo') as HTMLElement);
|
|
10328
|
+
await waitFor(() => expect(optionActiveTab(optionRow('quantity'))).toBe('Value'));
|
|
10329
|
+
},
|
|
10330
|
+
};
|
|
10331
|
+
|
|
10332
|
+
export const TemplateOfAnotherTypeWarns: Story = {
|
|
10333
|
+
parameters: {
|
|
10334
|
+
docs: {
|
|
10335
|
+
description: {
|
|
10336
|
+
story:
|
|
10337
|
+
'Renders the form and opens Converted, a whole number holding the text field bezeichnung: whether it converts depends on the data, so it is a warning - "Text is converted to a whole number when the value is used" - and the form can still be saved.',
|
|
10338
|
+
},
|
|
10339
|
+
},
|
|
10340
|
+
},
|
|
10341
|
+
render: () => <TemplateOrExpressionForm />,
|
|
10342
|
+
play: async () => {
|
|
10343
|
+
const row = await editOption('converted');
|
|
10344
|
+
await waitFor(() =>
|
|
10345
|
+
expect(row.querySelector('.value-tab-conversion')?.textContent).toContain('Text is converted to a whole number')
|
|
10346
|
+
);
|
|
10347
|
+
},
|
|
10348
|
+
};
|
|
10349
|
+
|
|
10350
|
+
export const ValueToExpressionAndVisualOnAPhone: Story = {
|
|
10351
|
+
...ValueToExpressionAndVisual,
|
|
10352
|
+
parameters: { ...ValueToExpressionAndVisual.parameters, qlip: { viewport: { width: 390, height: 844 } } },
|
|
10353
|
+
};
|
|
10354
|
+
|
|
10355
|
+
export const SavedExpressionOpensOnExpressionOnAPhone: Story = {
|
|
10356
|
+
...SavedExpressionOpensOnExpression,
|
|
10357
|
+
parameters: { ...SavedExpressionOpensOnExpression.parameters, qlip: { viewport: { width: 390, height: 844 } } },
|
|
10358
|
+
};
|
|
10359
|
+
|
|
10360
|
+
export const TemplateOnlyOptionHasValueAndTemplateOnAPhone: Story = {
|
|
10361
|
+
...TemplateOnlyOptionHasValueAndTemplate,
|
|
10362
|
+
parameters: { ...TemplateOnlyOptionHasValueAndTemplate.parameters, qlip: { viewport: { width: 390, height: 844 } } },
|
|
10363
|
+
};
|