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.
@@ -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-teststep__header", children: [_jsx("span", { className: "bn-teststep__title", children: "Step" }), viewToggle] }), _jsxs("div", { className: "bn-teststep__horizontal-fields", children: [_jsxs("div", { className: "bn-teststep__horizontal-col", children: [_jsx("div", { className: "bn-teststep__header", children: _jsx("span", { className: "bn-teststep__title", children: "Step" }) }), _jsx(StepField, { label: "Step", showLabel: false, 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 })] }), _jsxs("div", { className: "bn-teststep__horizontal-col", children: [_jsx("div", { className: "bn-teststep__header", children: _jsx("span", { className: "bn-teststep__title", children: "Expected result" }) }), _jsx(StepField, { label: "Expected result", showLabel: false, 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"] }) })] })] }));
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
- if (globalFetcher) {
14
- const result = globalFetcher();
15
- if (!result || typeof result.then !== "function") {
16
- const normalized = normalizeSnippetSuggestions(result);
17
- cachedSuggestions = normalized;
18
- return normalized;
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
- return [];
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
- Promise.resolve(globalFetcher())
32
- .then((result) => normalizeSnippetSuggestions(result))
33
- .then((items) => {
34
- if (cancelled)
35
- return;
36
- cachedSuggestions = items;
37
- setSuggestions(items);
38
- })
39
- .catch((error) => console.error("Failed to fetch snippet suggestions", error));
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
- if (globalFetcher) {
14
- const result = globalFetcher();
15
- if (!result || typeof result.then !== "function") {
16
- const normalized = normalizeStepSuggestions(result);
17
- cachedSuggestions = normalized;
18
- return normalized;
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
- return [];
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
- Promise.resolve(globalFetcher())
32
- .then((result) => normalizeStepSuggestions(result))
33
- .then((items) => {
34
- if (cancelled)
35
- return;
36
- cachedSuggestions = items;
37
- setSuggestions(items);
38
- })
39
- .catch((error) => console.error("Failed to fetch step suggestions", error));
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
  };
@@ -510,13 +510,17 @@ html.dark .bn-step-image-preview__content {
510
510
  }
511
511
 
512
512
  .bn-teststep__horizontal-col {
513
- flex: 1 0 0%;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.22",
3
+ "version": "0.4.24",
4
4
  "description": "Custom BlockNote schema, markdown conversion helpers, and UI for Testomatio-style test cases and steps.",
5
5
  "type": "module",
6
6
  "main": "./package/index.js",
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
- return filterSuggestionItems([...defaultItems, stepItem, snippetItem], query);
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
- showLabel={false}
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
- return cachedSuggestions;
46
- }
47
- if (globalFetcher) {
48
- const result = globalFetcher();
49
- if (!result || typeof (result as Promise<unknown>).then !== "function") {
50
- const normalized = normalizeSnippetSuggestions(result as SnippetInput);
51
- cachedSuggestions = normalized;
52
- return normalized;
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
- return [];
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
- Promise.resolve(globalFetcher())
68
- .then((result) => normalizeSnippetSuggestions(result))
69
- .then((items) => {
70
- if (cancelled) return;
71
- cachedSuggestions = items;
72
- setSuggestions(items);
73
- })
74
- .catch((error) => console.error("Failed to fetch snippet suggestions", error));
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
- return cachedSuggestions;
52
- }
53
- if (globalFetcher) {
54
- const result = globalFetcher();
55
- if (!result || typeof (result as Promise<unknown>).then !== "function") {
56
- const normalized = normalizeStepSuggestions(result as StepInput);
57
- cachedSuggestions = normalized;
58
- return normalized;
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
- return [];
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
- Promise.resolve(globalFetcher())
74
- .then((result) => normalizeStepSuggestions(result))
75
- .then((items) => {
76
- if (cancelled) return;
77
- cachedSuggestions = items;
78
- setSuggestions(items);
79
- })
80
- .catch((error) => console.error("Failed to fetch step suggestions", error));
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;
@@ -510,13 +510,17 @@ html.dark .bn-step-image-preview__content {
510
510
  }
511
511
 
512
512
  .bn-teststep__horizontal-col {
513
- flex: 1 0 0%;
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;