testomatio-editor-blocks 0.4.22 → 0.4.24
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/package/editor/blocks/step.d.ts +9 -0
- package/package/editor/blocks/step.js +30 -0
- package/package/editor/blocks/stepHorizontalView.js +1 -1
- package/package/editor/snippetAutocomplete.js +34 -18
- package/package/editor/stepAutocomplete.js +34 -18
- package/package/styles.css +11 -1
- package/package.json +1 -1
- package/src/App.tsx +7 -2
- package/src/editor/blocks/step.tsx +34 -0
- package/src/editor/blocks/stepHorizontalView.tsx +1 -12
- package/src/editor/snippetAutocomplete.ts +32 -18
- package/src/editor/stepAutocomplete.tsx +32 -18
- package/src/editor/styles.css +11 -1
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export declare const isEmptyParagraph: (b: any) => boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Check whether a step or snippet can be inserted at / after the given block.
|
|
4
|
+
* Returns true only when walking backwards from `referenceBlockId` (skipping
|
|
5
|
+
* other steps, snippets, and empty paragraphs) reaches a heading whose text
|
|
6
|
+
* is "steps".
|
|
7
|
+
*/
|
|
8
|
+
export declare function canInsertStepOrSnippet(editor: {
|
|
9
|
+
document: any[];
|
|
10
|
+
}, referenceBlockId: string): boolean;
|
|
2
11
|
export declare const stepBlock: {
|
|
3
12
|
config: {
|
|
4
13
|
readonly type: "testStep";
|
|
@@ -47,6 +47,36 @@ export const isEmptyParagraph = (b) => b.type === "paragraph" &&
|
|
|
47
47
|
(!Array.isArray(b.content) ||
|
|
48
48
|
b.content.length === 0 ||
|
|
49
49
|
b.content.every((n) => { var _a; return n.type === "text" && !((_a = n.text) === null || _a === void 0 ? void 0 : _a.trim()); }));
|
|
50
|
+
/**
|
|
51
|
+
* Check whether a step or snippet can be inserted at / after the given block.
|
|
52
|
+
* Returns true only when walking backwards from `referenceBlockId` (skipping
|
|
53
|
+
* other steps, snippets, and empty paragraphs) reaches a heading whose text
|
|
54
|
+
* is "steps".
|
|
55
|
+
*/
|
|
56
|
+
export function canInsertStepOrSnippet(editor, referenceBlockId) {
|
|
57
|
+
const allBlocks = editor.document;
|
|
58
|
+
const blockIndex = allBlocks.findIndex((b) => b.id === referenceBlockId);
|
|
59
|
+
if (blockIndex < 0)
|
|
60
|
+
return false;
|
|
61
|
+
for (let i = blockIndex; i >= 0; i--) {
|
|
62
|
+
const b = allBlocks[i];
|
|
63
|
+
if (b.type === "testStep" || b.type === "snippet" || isEmptyParagraph(b)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (b.type === "heading") {
|
|
67
|
+
const text = (Array.isArray(b.content) ? b.content : [])
|
|
68
|
+
.filter((n) => n.type === "text")
|
|
69
|
+
.map((n) => { var _a; return (_a = n.text) !== null && _a !== void 0 ? _a : ""; })
|
|
70
|
+
.join("")
|
|
71
|
+
.trim()
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.replace(/[:\-–—]$/, "");
|
|
74
|
+
return text === "steps";
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
50
80
|
export const stepBlock = createReactBlockSpec({
|
|
51
81
|
type: "testStep",
|
|
52
82
|
content: "none",
|
|
@@ -3,5 +3,5 @@ import { StepField } from "./stepField";
|
|
|
3
3
|
const STEP_PLACEHOLDER = "Enter step name";
|
|
4
4
|
const EXPECTED_RESULT_PLACEHOLDER = "Enter expected result";
|
|
5
5
|
export function StepHorizontalView({ blockId, stepNumber, stepValue, expectedResult, onStepChange, onExpectedChange, onInsertNextStep, onFieldFocus, viewToggle, }) {
|
|
6
|
-
return (_jsxs("div", { className: "bn-teststep bn-teststep--horizontal", "data-block-id": blockId, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-
|
|
6
|
+
return (_jsxs("div", { className: "bn-teststep bn-teststep--horizontal", "data-block-id": blockId, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-teststep__horizontal-fields", children: [_jsx("div", { className: "bn-teststep__horizontal-col", children: _jsx(StepField, { label: "Step", value: stepValue, onChange: onStepChange, placeholder: STEP_PLACEHOLDER, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: onFieldFocus, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true }) }), _jsx("div", { className: "bn-teststep__horizontal-col", children: _jsx(StepField, { label: "Expected result", labelAction: viewToggle, value: expectedResult, onChange: onExpectedChange, placeholder: EXPECTED_RESULT_PLACEHOLDER, multiline: true, enableAutocomplete: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: onFieldFocus }) })] }), _jsx("div", { className: "bn-step-actions", children: _jsxs("button", { type: "button", className: "bn-step-action-btn", onClick: onInsertNextStep, children: [_jsx("svg", { className: "bn-step-action-btn__icon", width: "16", height: "16", viewBox: "0 0 13.334 13.334", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M6.667 0a6.667 6.667 0 1 1 0 13.334A6.667 6.667 0 0 1 6.667 0Zm0 1.334a5.333 5.333 0 1 0 0 10.666 5.333 5.333 0 0 0 0-10.666ZM7.334 3.334V6H10v1.334H7.334V10H6V7.334H3.334V6H6V3.334h1.334Z", fill: "currentColor" }) }), "Add new step"] }) })] })] }));
|
|
7
7
|
}
|
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
let globalFetcher = null;
|
|
3
3
|
let cachedSuggestions = [];
|
|
4
|
+
let inflightPromise = null;
|
|
4
5
|
export function setSnippetFetcher(fetcher) {
|
|
5
6
|
globalFetcher = fetcher;
|
|
6
7
|
cachedSuggestions = [];
|
|
8
|
+
inflightPromise = null;
|
|
7
9
|
}
|
|
8
10
|
export function useSnippetAutocomplete() {
|
|
9
11
|
const [suggestions, setSuggestions] = useState(() => {
|
|
10
|
-
if (cachedSuggestions.length > 0)
|
|
12
|
+
if (cachedSuggestions.length > 0)
|
|
11
13
|
return cachedSuggestions;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
if (!globalFetcher)
|
|
15
|
+
return [];
|
|
16
|
+
const result = globalFetcher();
|
|
17
|
+
if (result && typeof result.then === "function") {
|
|
18
|
+
if (!inflightPromise) {
|
|
19
|
+
inflightPromise = result
|
|
20
|
+
.then((r) => normalizeSnippetSuggestions(r))
|
|
21
|
+
.then((items) => { cachedSuggestions = items; inflightPromise = null; return items; })
|
|
22
|
+
.catch((error) => { inflightPromise = null; console.error("Failed to fetch snippet suggestions", error); return []; });
|
|
19
23
|
}
|
|
24
|
+
return [];
|
|
20
25
|
}
|
|
21
|
-
|
|
26
|
+
const normalized = normalizeSnippetSuggestions(result);
|
|
27
|
+
cachedSuggestions = normalized;
|
|
28
|
+
return normalized;
|
|
22
29
|
});
|
|
23
30
|
useEffect(() => {
|
|
24
31
|
if (suggestions.length > 0) {
|
|
@@ -28,15 +35,24 @@ export function useSnippetAutocomplete() {
|
|
|
28
35
|
return;
|
|
29
36
|
}
|
|
30
37
|
let cancelled = false;
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
if (!inflightPromise) {
|
|
39
|
+
inflightPromise = Promise.resolve(globalFetcher())
|
|
40
|
+
.then((result) => normalizeSnippetSuggestions(result))
|
|
41
|
+
.then((items) => {
|
|
42
|
+
cachedSuggestions = items;
|
|
43
|
+
inflightPromise = null;
|
|
44
|
+
return items;
|
|
45
|
+
})
|
|
46
|
+
.catch((error) => {
|
|
47
|
+
inflightPromise = null;
|
|
48
|
+
console.error("Failed to fetch snippet suggestions", error);
|
|
49
|
+
return [];
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
inflightPromise.then((items) => {
|
|
53
|
+
if (!cancelled)
|
|
54
|
+
setSuggestions(items);
|
|
55
|
+
});
|
|
40
56
|
return () => {
|
|
41
57
|
cancelled = true;
|
|
42
58
|
};
|
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
let globalFetcher = null;
|
|
3
3
|
let cachedSuggestions = [];
|
|
4
|
+
let inflightPromise = null;
|
|
4
5
|
export function setStepsFetcher(fetcher) {
|
|
5
6
|
globalFetcher = fetcher;
|
|
6
7
|
cachedSuggestions = [];
|
|
8
|
+
inflightPromise = null;
|
|
7
9
|
}
|
|
8
10
|
export function useStepAutocomplete() {
|
|
9
11
|
const [suggestions, setSuggestions] = useState(() => {
|
|
10
|
-
if (cachedSuggestions.length > 0)
|
|
12
|
+
if (cachedSuggestions.length > 0)
|
|
11
13
|
return cachedSuggestions;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
if (!globalFetcher)
|
|
15
|
+
return [];
|
|
16
|
+
const result = globalFetcher();
|
|
17
|
+
if (result && typeof result.then === "function") {
|
|
18
|
+
if (!inflightPromise) {
|
|
19
|
+
inflightPromise = result
|
|
20
|
+
.then((r) => normalizeStepSuggestions(r))
|
|
21
|
+
.then((items) => { cachedSuggestions = items; inflightPromise = null; return items; })
|
|
22
|
+
.catch((error) => { inflightPromise = null; console.error("Failed to fetch step suggestions", error); return []; });
|
|
19
23
|
}
|
|
24
|
+
return [];
|
|
20
25
|
}
|
|
21
|
-
|
|
26
|
+
const normalized = normalizeStepSuggestions(result);
|
|
27
|
+
cachedSuggestions = normalized;
|
|
28
|
+
return normalized;
|
|
22
29
|
});
|
|
23
30
|
useEffect(() => {
|
|
24
31
|
if (suggestions.length > 0) {
|
|
@@ -28,15 +35,24 @@ export function useStepAutocomplete() {
|
|
|
28
35
|
return;
|
|
29
36
|
}
|
|
30
37
|
let cancelled = false;
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
if (!inflightPromise) {
|
|
39
|
+
inflightPromise = Promise.resolve(globalFetcher())
|
|
40
|
+
.then((result) => normalizeStepSuggestions(result))
|
|
41
|
+
.then((items) => {
|
|
42
|
+
cachedSuggestions = items;
|
|
43
|
+
inflightPromise = null;
|
|
44
|
+
return items;
|
|
45
|
+
})
|
|
46
|
+
.catch((error) => {
|
|
47
|
+
inflightPromise = null;
|
|
48
|
+
console.error("Failed to fetch step suggestions", error);
|
|
49
|
+
return [];
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
inflightPromise.then((items) => {
|
|
53
|
+
if (!cancelled)
|
|
54
|
+
setSuggestions(items);
|
|
55
|
+
});
|
|
40
56
|
return () => {
|
|
41
57
|
cancelled = true;
|
|
42
58
|
};
|
package/package/styles.css
CHANGED
|
@@ -510,13 +510,17 @@ html.dark .bn-step-image-preview__content {
|
|
|
510
510
|
}
|
|
511
511
|
|
|
512
512
|
.bn-teststep__horizontal-col {
|
|
513
|
-
flex: 1
|
|
513
|
+
flex: 1 1 0%;
|
|
514
514
|
min-width: 0;
|
|
515
515
|
display: flex;
|
|
516
516
|
flex-direction: column;
|
|
517
517
|
gap: 4px;
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
+
.bn-teststep__horizontal-col .bn-step-field__top {
|
|
521
|
+
min-height: 28px;
|
|
522
|
+
}
|
|
523
|
+
|
|
520
524
|
@media (max-width: 860px) {
|
|
521
525
|
.bn-teststep__horizontal-fields {
|
|
522
526
|
flex-direction: column;
|
|
@@ -1207,6 +1211,12 @@ html.dark .bn-step-image-preview__content {
|
|
|
1207
1211
|
border-radius: 999px;
|
|
1208
1212
|
width: 1.35rem;
|
|
1209
1213
|
height: 1.35rem;
|
|
1214
|
+
display: flex;
|
|
1215
|
+
align-items: center;
|
|
1216
|
+
justify-content: center;
|
|
1217
|
+
padding: 0;
|
|
1218
|
+
font-size: 0.85rem;
|
|
1219
|
+
line-height: 1;
|
|
1210
1220
|
background: var(--overlay-dark);
|
|
1211
1221
|
color: var(--color-white);
|
|
1212
1222
|
cursor: pointer;
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -21,6 +21,7 @@ import { customSchema, type CustomEditor } from "./editor/customSchema";
|
|
|
21
21
|
import { setStepsFetcher, type StepJsonApiDocument } from "./editor/stepAutocomplete";
|
|
22
22
|
import { setSnippetFetcher, type SnippetJsonApiDocument } from "./editor/snippetAutocomplete";
|
|
23
23
|
import { setImageUploadHandler } from "./editor/stepImageUpload";
|
|
24
|
+
import { canInsertStepOrSnippet } from "./editor/blocks/step";
|
|
24
25
|
import "./App.css";
|
|
25
26
|
|
|
26
27
|
const focusStepField = (
|
|
@@ -333,7 +334,11 @@ function CustomSlashMenu() {
|
|
|
333
334
|
},
|
|
334
335
|
};
|
|
335
336
|
|
|
336
|
-
|
|
337
|
+
const cursorBlock = editor.getTextCursorPosition()?.block;
|
|
338
|
+
const canInsert = cursorBlock ? canInsertStepOrSnippet(editor, cursorBlock.id) : false;
|
|
339
|
+
const customItems = canInsert ? [stepItem, snippetItem] : [];
|
|
340
|
+
|
|
341
|
+
return filterSuggestionItems([...defaultItems, ...customItems], query);
|
|
337
342
|
};
|
|
338
343
|
|
|
339
344
|
return <SuggestionMenuController triggerCharacter="/" getItems={getItems} />;
|
|
@@ -519,7 +524,7 @@ function App() {
|
|
|
519
524
|
const fallbackBlock = documentBlocks[documentBlocks.length - 1];
|
|
520
525
|
const referenceId = selectedBlock?.id ?? fallbackBlock?.id;
|
|
521
526
|
|
|
522
|
-
if (!referenceId) {
|
|
527
|
+
if (!referenceId || !canInsertStepOrSnippet(editor, referenceId)) {
|
|
523
528
|
return;
|
|
524
529
|
}
|
|
525
530
|
|
|
@@ -53,6 +53,40 @@ export const isEmptyParagraph = (b: any): boolean =>
|
|
|
53
53
|
b.content.length === 0 ||
|
|
54
54
|
b.content.every((n: any) => n.type === "text" && !n.text?.trim()));
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Check whether a step or snippet can be inserted at / after the given block.
|
|
58
|
+
* Returns true only when walking backwards from `referenceBlockId` (skipping
|
|
59
|
+
* other steps, snippets, and empty paragraphs) reaches a heading whose text
|
|
60
|
+
* is "steps".
|
|
61
|
+
*/
|
|
62
|
+
export function canInsertStepOrSnippet(
|
|
63
|
+
editor: { document: any[] },
|
|
64
|
+
referenceBlockId: string,
|
|
65
|
+
): boolean {
|
|
66
|
+
const allBlocks = editor.document;
|
|
67
|
+
const blockIndex = allBlocks.findIndex((b: any) => b.id === referenceBlockId);
|
|
68
|
+
if (blockIndex < 0) return false;
|
|
69
|
+
|
|
70
|
+
for (let i = blockIndex; i >= 0; i--) {
|
|
71
|
+
const b = allBlocks[i];
|
|
72
|
+
if (b.type === "testStep" || b.type === "snippet" || isEmptyParagraph(b)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (b.type === "heading") {
|
|
76
|
+
const text = (Array.isArray(b.content) ? b.content : [])
|
|
77
|
+
.filter((n: any) => n.type === "text")
|
|
78
|
+
.map((n: any) => n.text ?? "")
|
|
79
|
+
.join("")
|
|
80
|
+
.trim()
|
|
81
|
+
.toLowerCase()
|
|
82
|
+
.replace(/[:\-–—]$/, "");
|
|
83
|
+
return text === "steps";
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
56
90
|
export const stepBlock = createReactBlockSpec(
|
|
57
91
|
{
|
|
58
92
|
type: "testStep",
|
|
@@ -35,18 +35,10 @@ export function StepHorizontalView({
|
|
|
35
35
|
<div className="bn-teststep__line" />
|
|
36
36
|
</div>
|
|
37
37
|
<div className="bn-teststep__content">
|
|
38
|
-
<div className="bn-teststep__header">
|
|
39
|
-
<span className="bn-teststep__title">Step</span>
|
|
40
|
-
{viewToggle}
|
|
41
|
-
</div>
|
|
42
38
|
<div className="bn-teststep__horizontal-fields">
|
|
43
39
|
<div className="bn-teststep__horizontal-col">
|
|
44
|
-
<div className="bn-teststep__header">
|
|
45
|
-
<span className="bn-teststep__title">Step</span>
|
|
46
|
-
</div>
|
|
47
40
|
<StepField
|
|
48
41
|
label="Step"
|
|
49
|
-
showLabel={false}
|
|
50
42
|
value={stepValue}
|
|
51
43
|
onChange={onStepChange}
|
|
52
44
|
placeholder={STEP_PLACEHOLDER}
|
|
@@ -61,12 +53,9 @@ export function StepHorizontalView({
|
|
|
61
53
|
/>
|
|
62
54
|
</div>
|
|
63
55
|
<div className="bn-teststep__horizontal-col">
|
|
64
|
-
<div className="bn-teststep__header">
|
|
65
|
-
<span className="bn-teststep__title">Expected result</span>
|
|
66
|
-
</div>
|
|
67
56
|
<StepField
|
|
68
57
|
label="Expected result"
|
|
69
|
-
|
|
58
|
+
labelAction={viewToggle}
|
|
70
59
|
value={expectedResult}
|
|
71
60
|
onChange={onExpectedChange}
|
|
72
61
|
placeholder={EXPECTED_RESULT_PLACEHOLDER}
|
|
@@ -33,26 +33,31 @@ type SnippetInput = SnippetSuggestion[] | SnippetJsonApiDocument | SnippetJsonAp
|
|
|
33
33
|
|
|
34
34
|
let globalFetcher: SnippetSuggestionsFetcher | null = null;
|
|
35
35
|
let cachedSuggestions: SnippetSuggestion[] = [];
|
|
36
|
+
let inflightPromise: Promise<SnippetSuggestion[]> | null = null;
|
|
36
37
|
|
|
37
38
|
export function setSnippetFetcher(fetcher: SnippetSuggestionsFetcher | null) {
|
|
38
39
|
globalFetcher = fetcher;
|
|
39
40
|
cachedSuggestions = [];
|
|
41
|
+
inflightPromise = null;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export function useSnippetAutocomplete(): SnippetSuggestion[] {
|
|
43
45
|
const [suggestions, setSuggestions] = useState<SnippetSuggestion[]>(() => {
|
|
44
|
-
if (cachedSuggestions.length > 0)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
if (cachedSuggestions.length > 0) return cachedSuggestions;
|
|
47
|
+
if (!globalFetcher) return [];
|
|
48
|
+
const result = globalFetcher();
|
|
49
|
+
if (result && typeof (result as Promise<unknown>).then === "function") {
|
|
50
|
+
if (!inflightPromise) {
|
|
51
|
+
inflightPromise = (result as Promise<SnippetInput>)
|
|
52
|
+
.then((r) => normalizeSnippetSuggestions(r))
|
|
53
|
+
.then((items) => { cachedSuggestions = items; inflightPromise = null; return items; })
|
|
54
|
+
.catch((error) => { inflightPromise = null; console.error("Failed to fetch snippet suggestions", error); return [] as SnippetSuggestion[]; });
|
|
53
55
|
}
|
|
56
|
+
return [];
|
|
54
57
|
}
|
|
55
|
-
|
|
58
|
+
const normalized = normalizeSnippetSuggestions(result as SnippetInput);
|
|
59
|
+
cachedSuggestions = normalized;
|
|
60
|
+
return normalized;
|
|
56
61
|
});
|
|
57
62
|
|
|
58
63
|
useEffect(() => {
|
|
@@ -64,14 +69,23 @@ export function useSnippetAutocomplete(): SnippetSuggestion[] {
|
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
let cancelled = false;
|
|
67
|
-
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
if (!inflightPromise) {
|
|
73
|
+
inflightPromise = Promise.resolve(globalFetcher())
|
|
74
|
+
.then((result) => normalizeSnippetSuggestions(result))
|
|
75
|
+
.then((items) => {
|
|
76
|
+
cachedSuggestions = items;
|
|
77
|
+
inflightPromise = null;
|
|
78
|
+
return items;
|
|
79
|
+
})
|
|
80
|
+
.catch((error) => {
|
|
81
|
+
inflightPromise = null;
|
|
82
|
+
console.error("Failed to fetch snippet suggestions", error);
|
|
83
|
+
return [] as SnippetSuggestion[];
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
inflightPromise.then((items) => {
|
|
87
|
+
if (!cancelled) setSuggestions(items);
|
|
88
|
+
});
|
|
75
89
|
|
|
76
90
|
return () => {
|
|
77
91
|
cancelled = true;
|
|
@@ -39,26 +39,31 @@ type StepInput = StepSuggestion[] | StepJsonApiDocument | StepJsonApiResource[]
|
|
|
39
39
|
|
|
40
40
|
let globalFetcher: StepSuggestionsFetcher | null = null;
|
|
41
41
|
let cachedSuggestions: StepSuggestion[] = [];
|
|
42
|
+
let inflightPromise: Promise<StepSuggestion[]> | null = null;
|
|
42
43
|
|
|
43
44
|
export function setStepsFetcher(fetcher: StepSuggestionsFetcher | null) {
|
|
44
45
|
globalFetcher = fetcher;
|
|
45
46
|
cachedSuggestions = [];
|
|
47
|
+
inflightPromise = null;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export function useStepAutocomplete(): StepSuggestion[] {
|
|
49
51
|
const [suggestions, setSuggestions] = useState<StepSuggestion[]>(() => {
|
|
50
|
-
if (cachedSuggestions.length > 0)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
if (cachedSuggestions.length > 0) return cachedSuggestions;
|
|
53
|
+
if (!globalFetcher) return [];
|
|
54
|
+
const result = globalFetcher();
|
|
55
|
+
if (result && typeof (result as Promise<unknown>).then === "function") {
|
|
56
|
+
if (!inflightPromise) {
|
|
57
|
+
inflightPromise = (result as Promise<StepInput>)
|
|
58
|
+
.then((r) => normalizeStepSuggestions(r))
|
|
59
|
+
.then((items) => { cachedSuggestions = items; inflightPromise = null; return items; })
|
|
60
|
+
.catch((error) => { inflightPromise = null; console.error("Failed to fetch step suggestions", error); return [] as StepSuggestion[]; });
|
|
59
61
|
}
|
|
62
|
+
return [];
|
|
60
63
|
}
|
|
61
|
-
|
|
64
|
+
const normalized = normalizeStepSuggestions(result as StepInput);
|
|
65
|
+
cachedSuggestions = normalized;
|
|
66
|
+
return normalized;
|
|
62
67
|
});
|
|
63
68
|
|
|
64
69
|
useEffect(() => {
|
|
@@ -70,14 +75,23 @@ export function useStepAutocomplete(): StepSuggestion[] {
|
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
let cancelled = false;
|
|
73
|
-
|
|
74
|
-
.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
if (!inflightPromise) {
|
|
79
|
+
inflightPromise = Promise.resolve(globalFetcher())
|
|
80
|
+
.then((result) => normalizeStepSuggestions(result))
|
|
81
|
+
.then((items) => {
|
|
82
|
+
cachedSuggestions = items;
|
|
83
|
+
inflightPromise = null;
|
|
84
|
+
return items;
|
|
85
|
+
})
|
|
86
|
+
.catch((error) => {
|
|
87
|
+
inflightPromise = null;
|
|
88
|
+
console.error("Failed to fetch step suggestions", error);
|
|
89
|
+
return [] as StepSuggestion[];
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
inflightPromise.then((items) => {
|
|
93
|
+
if (!cancelled) setSuggestions(items);
|
|
94
|
+
});
|
|
81
95
|
|
|
82
96
|
return () => {
|
|
83
97
|
cancelled = true;
|
package/src/editor/styles.css
CHANGED
|
@@ -510,13 +510,17 @@ html.dark .bn-step-image-preview__content {
|
|
|
510
510
|
}
|
|
511
511
|
|
|
512
512
|
.bn-teststep__horizontal-col {
|
|
513
|
-
flex: 1
|
|
513
|
+
flex: 1 1 0%;
|
|
514
514
|
min-width: 0;
|
|
515
515
|
display: flex;
|
|
516
516
|
flex-direction: column;
|
|
517
517
|
gap: 4px;
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
+
.bn-teststep__horizontal-col .bn-step-field__top {
|
|
521
|
+
min-height: 28px;
|
|
522
|
+
}
|
|
523
|
+
|
|
520
524
|
@media (max-width: 860px) {
|
|
521
525
|
.bn-teststep__horizontal-fields {
|
|
522
526
|
flex-direction: column;
|
|
@@ -1207,6 +1211,12 @@ html.dark .bn-step-image-preview__content {
|
|
|
1207
1211
|
border-radius: 999px;
|
|
1208
1212
|
width: 1.35rem;
|
|
1209
1213
|
height: 1.35rem;
|
|
1214
|
+
display: flex;
|
|
1215
|
+
align-items: center;
|
|
1216
|
+
justify-content: center;
|
|
1217
|
+
padding: 0;
|
|
1218
|
+
font-size: 0.85rem;
|
|
1219
|
+
line-height: 1;
|
|
1210
1220
|
background: var(--overlay-dark);
|
|
1211
1221
|
color: var(--color-white);
|
|
1212
1222
|
cursor: pointer;
|