testomatio-editor-blocks 0.2.3 → 0.4.0
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/markdown.d.ts +5 -0
- package/package/editor/blocks/markdown.js +30 -5
- package/package/editor/blocks/snippet.js +127 -2
- package/package/editor/blocks/step.js +82 -15
- package/package/editor/blocks/stepField.d.ts +7 -3
- package/package/editor/blocks/stepField.js +395 -230
- package/package/editor/blocks/useAutoResize.d.ts +8 -0
- package/package/editor/blocks/useAutoResize.js +31 -0
- package/package/editor/customMarkdownConverter.js +150 -21
- package/package/styles.css +327 -71
- package/package.json +5 -2
- package/src/App.tsx +1 -1
- package/src/editor/blocks/markdown.ts +35 -5
- package/src/editor/blocks/snippet.tsx +202 -26
- package/src/editor/blocks/step.tsx +132 -36
- package/src/editor/blocks/stepField.tsx +552 -267
- package/src/editor/blocks/useAutoResize.ts +44 -0
- package/src/editor/customMarkdownConverter.test.ts +114 -2
- package/src/editor/customMarkdownConverter.ts +166 -19
- package/src/editor/customSchema.test.ts +35 -0
- package/src/editor/markdownToBlocks.test.ts +119 -0
- package/src/editor/styles.css +342 -71
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
2
2
|
const MARKDOWN_ESCAPE_REGEX = /([*_\\])/g;
|
|
3
|
+
const INLINE_SEGMENT_REGEX =
|
|
4
|
+
/(\*\*\*[^*]+\*\*\*|___[^_]+___|\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/;
|
|
3
5
|
|
|
4
6
|
export function escapeHtml(text: string): string {
|
|
5
7
|
return text
|
|
@@ -29,13 +31,19 @@ function parseInlineMarkdown(text: string): InlineSegment[] {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
const normalized = text.replace(/\\([*_`~])/g, "\uE000$1");
|
|
32
|
-
const rawSegments = normalized
|
|
33
|
-
.split(/(\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/)
|
|
34
|
-
.filter(Boolean);
|
|
34
|
+
const rawSegments = normalized.split(INLINE_SEGMENT_REGEX).filter(Boolean);
|
|
35
35
|
|
|
36
36
|
return rawSegments.map((segment) => {
|
|
37
37
|
const baseStyles = { bold: false, italic: false, underline: false };
|
|
38
38
|
|
|
39
|
+
if (/^\*\*\*(.+)\*\*\*$/.test(segment) || /^___(.+)___$/.test(segment)) {
|
|
40
|
+
const content = segment.slice(3, -3);
|
|
41
|
+
return {
|
|
42
|
+
text: restoreEscapes(content),
|
|
43
|
+
styles: { bold: true, italic: true, underline: false },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
if (/^\*\*(.+)\*\*$/.test(segment) || /^__(.+)__$/.test(segment)) {
|
|
40
48
|
const content = segment.slice(2, -2);
|
|
41
49
|
return {
|
|
@@ -139,12 +147,13 @@ function fallbackHtmlToMarkdown(html: string): string {
|
|
|
139
147
|
|
|
140
148
|
result = result.replace(/<\/?[^>]+>/g, "");
|
|
141
149
|
|
|
142
|
-
|
|
150
|
+
const markdown = result
|
|
143
151
|
.split("\n")
|
|
144
152
|
.map((line) => line.trimEnd())
|
|
145
153
|
.join("\n")
|
|
146
154
|
.replace(/\n{3,}/g, "\n\n")
|
|
147
155
|
.trim();
|
|
156
|
+
return cleanupEscapedFormatting(markdown);
|
|
148
157
|
}
|
|
149
158
|
|
|
150
159
|
export function htmlToMarkdown(html: string): string {
|
|
@@ -195,5 +204,26 @@ export function htmlToMarkdown(html: string): string {
|
|
|
195
204
|
};
|
|
196
205
|
|
|
197
206
|
const markdown = Array.from(temp.childNodes).map(traverse).join("");
|
|
198
|
-
return markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
207
|
+
return cleanupEscapedFormatting(markdown).replace(/\n{3,}/g, "\n\n").trim();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function cleanupEscapedFormatting(markdown: string): string {
|
|
211
|
+
return markdown.replace(/(\\+)([*_]+)/g, (_match, slashes, markers) => {
|
|
212
|
+
if (markers.length === 0) {
|
|
213
|
+
return slashes + markers;
|
|
214
|
+
}
|
|
215
|
+
const shouldClean =
|
|
216
|
+
markers.length === 3 ||
|
|
217
|
+
markers.length === 2 ||
|
|
218
|
+
markers.length === 1;
|
|
219
|
+
if (!shouldClean) {
|
|
220
|
+
return slashes + markers;
|
|
221
|
+
}
|
|
222
|
+
const hasPrintable = slashes.length % 2 === 0;
|
|
223
|
+
return hasPrintable ? markers : slashes + markers;
|
|
224
|
+
});
|
|
199
225
|
}
|
|
226
|
+
|
|
227
|
+
export const __markdownStringUtils = {
|
|
228
|
+
cleanupEscapedFormatting,
|
|
229
|
+
};
|
|
@@ -1,8 +1,180 @@
|
|
|
1
1
|
import { createReactBlockSpec } from "@blocknote/react";
|
|
2
|
-
import {
|
|
2
|
+
import OverType, { type OverType as OverTypeInstance } from "overtype";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
4
|
import { StepField } from "./stepField";
|
|
4
5
|
import { useSnippetAutocomplete, type SnippetSuggestion } from "../snippetAutocomplete";
|
|
5
6
|
import type { StepSuggestion } from "../stepAutocomplete";
|
|
7
|
+
import { useStepImageUpload } from "../stepImageUpload";
|
|
8
|
+
|
|
9
|
+
type SnippetDataFieldProps = {
|
|
10
|
+
label: string;
|
|
11
|
+
value: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
onChange: (value: string) => void;
|
|
14
|
+
onFieldFocus?: () => void;
|
|
15
|
+
fieldName?: string;
|
|
16
|
+
enableImageUpload?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function SnippetDataField({
|
|
20
|
+
label,
|
|
21
|
+
value,
|
|
22
|
+
placeholder,
|
|
23
|
+
onChange,
|
|
24
|
+
onFieldFocus,
|
|
25
|
+
fieldName,
|
|
26
|
+
enableImageUpload = false,
|
|
27
|
+
}: SnippetDataFieldProps) {
|
|
28
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
29
|
+
const instanceRef = useRef<OverTypeInstance | null>(null);
|
|
30
|
+
const uploadImage = useStepImageUpload();
|
|
31
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
32
|
+
const onChangeRef = useRef(onChange);
|
|
33
|
+
const initialValueRef = useRef(value);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
onChangeRef.current = onChange;
|
|
37
|
+
}, [onChange]);
|
|
38
|
+
|
|
39
|
+
const insertImageMarkdown = useCallback(
|
|
40
|
+
(url: string) => {
|
|
41
|
+
const instance = instanceRef.current;
|
|
42
|
+
const textarea = instance?.textarea;
|
|
43
|
+
if (!instance || !textarea) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const currentValue = instance.getValue();
|
|
48
|
+
const selectionStart = textarea.selectionStart ?? currentValue.length;
|
|
49
|
+
const selectionEnd = textarea.selectionEnd ?? currentValue.length;
|
|
50
|
+
const before = currentValue.slice(0, selectionStart);
|
|
51
|
+
const after = currentValue.slice(selectionEnd);
|
|
52
|
+
const needsNewlineBefore = before.length > 0 && !before.endsWith("\n");
|
|
53
|
+
const needsNewlineAfter = after.length > 0 && !after.startsWith("\n");
|
|
54
|
+
const markdown = `${needsNewlineBefore ? "\n" : ""}${needsNewlineAfter ? "\n" : ""}`;
|
|
55
|
+
const nextValue = `${before}${markdown}${after}`;
|
|
56
|
+
|
|
57
|
+
instance.setValue(nextValue);
|
|
58
|
+
onChangeRef.current?.(nextValue);
|
|
59
|
+
|
|
60
|
+
requestAnimationFrame(() => {
|
|
61
|
+
const cursor = selectionStart + markdown.length;
|
|
62
|
+
textarea.selectionStart = cursor;
|
|
63
|
+
textarea.selectionEnd = cursor;
|
|
64
|
+
textarea.focus();
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
[],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const container = containerRef.current;
|
|
72
|
+
if (!container) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const [instance] = OverType.init(container, {
|
|
77
|
+
value: initialValueRef.current,
|
|
78
|
+
placeholder,
|
|
79
|
+
autoResize: true,
|
|
80
|
+
minHeight: "5rem",
|
|
81
|
+
toolbar: false,
|
|
82
|
+
onChange: (nextValue) => {
|
|
83
|
+
onChangeRef.current?.(nextValue);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
instanceRef.current = instance;
|
|
88
|
+
const textarea = instance.textarea;
|
|
89
|
+
if (fieldName) {
|
|
90
|
+
textarea.dataset.stepField = fieldName;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const handleFocus = () => {
|
|
94
|
+
setIsFocused(true);
|
|
95
|
+
onFieldFocus?.();
|
|
96
|
+
};
|
|
97
|
+
const handleBlur = () => setIsFocused(false);
|
|
98
|
+
|
|
99
|
+
textarea.addEventListener("focus", handleFocus);
|
|
100
|
+
textarea.addEventListener("blur", handleBlur);
|
|
101
|
+
|
|
102
|
+
return () => {
|
|
103
|
+
textarea.removeEventListener("focus", handleFocus);
|
|
104
|
+
textarea.removeEventListener("blur", handleBlur);
|
|
105
|
+
instance.destroy();
|
|
106
|
+
instanceRef.current = null;
|
|
107
|
+
};
|
|
108
|
+
}, [fieldName, onFieldFocus, placeholder]);
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const instance = instanceRef.current;
|
|
112
|
+
if (!instance) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (instance.getValue() !== value) {
|
|
117
|
+
instance.setValue(value);
|
|
118
|
+
}
|
|
119
|
+
}, [value]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!enableImageUpload || !uploadImage) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const textarea = instanceRef.current?.textarea;
|
|
127
|
+
if (!textarea) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const handlePaste = async (event: ClipboardEvent) => {
|
|
132
|
+
const items = Array.from(event.clipboardData?.items ?? []);
|
|
133
|
+
const imageItem = items.find((item) => item.kind === "file" && item.type.startsWith("image/"));
|
|
134
|
+
const file = imageItem?.getAsFile();
|
|
135
|
+
if (!file) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const response = await uploadImage(file);
|
|
143
|
+
if (response?.url) {
|
|
144
|
+
insertImageMarkdown(response.url);
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error("Failed to upload pasted image", error);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
textarea.addEventListener("paste", handlePaste as unknown as EventListener);
|
|
152
|
+
return () => {
|
|
153
|
+
textarea.removeEventListener("paste", handlePaste as unknown as EventListener);
|
|
154
|
+
};
|
|
155
|
+
}, [enableImageUpload, insertImageMarkdown, uploadImage]);
|
|
156
|
+
|
|
157
|
+
const editorClassName = useMemo(
|
|
158
|
+
() =>
|
|
159
|
+
[
|
|
160
|
+
"bn-step-editor",
|
|
161
|
+
"bn-step-editor--multiline",
|
|
162
|
+
isFocused ? "bn-step-editor--focused" : "",
|
|
163
|
+
]
|
|
164
|
+
.filter(Boolean)
|
|
165
|
+
.join(" "),
|
|
166
|
+
[isFocused],
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className="bn-step-field">
|
|
171
|
+
<div className="bn-step-field__top">
|
|
172
|
+
<span className="bn-step-field__label">{label}</span>
|
|
173
|
+
</div>
|
|
174
|
+
<div ref={containerRef} className={editorClassName} data-step-field={fieldName} />
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
6
178
|
|
|
7
179
|
export const snippetBlock = createReactBlockSpec(
|
|
8
180
|
{
|
|
@@ -27,8 +199,10 @@ export const snippetBlock = createReactBlockSpec(
|
|
|
27
199
|
render: ({ block, editor }) => {
|
|
28
200
|
const snippetTitle = (block.props.snippetTitle as string) || "";
|
|
29
201
|
const snippetData = (block.props.snippetData as string) || "";
|
|
202
|
+
const snippetId = (block.props.snippetId as string) || "";
|
|
30
203
|
const snippetSuggestions = useSnippetAutocomplete();
|
|
31
204
|
const hasSnippets = snippetSuggestions.length > 0;
|
|
205
|
+
const isSnippetSelected = snippetId.length > 0;
|
|
32
206
|
|
|
33
207
|
const handleSnippetChange = useCallback(
|
|
34
208
|
(nextTitle: string) => {
|
|
@@ -92,31 +266,33 @@ export const snippetBlock = createReactBlockSpec(
|
|
|
92
266
|
|
|
93
267
|
return (
|
|
94
268
|
<div className="bn-teststep bn-snippet" data-block-id={block.id}>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
269
|
+
{!isSnippetSelected ? (
|
|
270
|
+
<StepField
|
|
271
|
+
label="Snippet Title"
|
|
272
|
+
value={snippetTitle}
|
|
273
|
+
onChange={handleSnippetChange}
|
|
274
|
+
autoFocus={snippetTitle.length === 0}
|
|
275
|
+
enableAutocomplete={true}
|
|
276
|
+
suggestionFilter={(suggestion) => (suggestion as SnippetSuggestion).isSnippet === true}
|
|
277
|
+
suggestionsOverride={snippetSuggestions as unknown as StepSuggestion[]}
|
|
278
|
+
onSuggestionSelect={handleSnippetSelect}
|
|
279
|
+
fieldName="snippet-title"
|
|
280
|
+
showSuggestionsOnFocus={true}
|
|
281
|
+
enableImageUpload={false}
|
|
282
|
+
onFieldFocus={handleFieldFocus}
|
|
283
|
+
readOnly={false}
|
|
284
|
+
/>
|
|
285
|
+
) : (
|
|
286
|
+
<SnippetDataField
|
|
287
|
+
label={`Snippet: ${snippetTitle}`}
|
|
288
|
+
value={snippetData}
|
|
289
|
+
onChange={handleSnippetDataChange}
|
|
290
|
+
fieldName="snippet-data"
|
|
291
|
+
enableImageUpload
|
|
292
|
+
onFieldFocus={handleFieldFocus}
|
|
293
|
+
placeholder="Snippet data will appear here..."
|
|
294
|
+
/>
|
|
295
|
+
)}
|
|
120
296
|
</div>
|
|
121
297
|
);
|
|
122
298
|
},
|
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
import { createReactBlockSpec } from "@blocknote/react";
|
|
2
|
-
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { StepField } from "./stepField";
|
|
4
4
|
import { useStepImageUpload } from "../stepImageUpload";
|
|
5
5
|
import type { StepSuggestion } from "../stepAutocomplete";
|
|
6
6
|
|
|
7
|
+
const EXPECTED_COLLAPSED_KEY = "bn-expected-collapsed";
|
|
8
|
+
|
|
9
|
+
const readExpectedCollapsedPreference = (): boolean => {
|
|
10
|
+
if (typeof window === "undefined") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return window.localStorage.getItem(EXPECTED_COLLAPSED_KEY) === "true";
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const writeExpectedCollapsedPreference = (collapsed: boolean) => {
|
|
21
|
+
if (typeof window === "undefined") {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
window.localStorage.setItem(EXPECTED_COLLAPSED_KEY, collapsed ? "true" : "false");
|
|
26
|
+
} catch {
|
|
27
|
+
//
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
7
31
|
export const stepBlock = createReactBlockSpec(
|
|
8
32
|
{
|
|
9
33
|
type: "testStep",
|
|
@@ -25,25 +49,32 @@ export const stepBlock = createReactBlockSpec(
|
|
|
25
49
|
const stepTitle = (block.props.stepTitle as string) || "";
|
|
26
50
|
const stepData = (block.props.stepData as string) || "";
|
|
27
51
|
const expectedResult = (block.props.expectedResult as string) || "";
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
52
|
+
const expectedHasContent = expectedResult.trim().length > 0;
|
|
53
|
+
const storedExpectedCollapsed = useMemo(
|
|
54
|
+
() => readExpectedCollapsedPreference(),
|
|
55
|
+
[],
|
|
56
|
+
);
|
|
57
|
+
const dataHasContent = stepData.trim().length > 0;
|
|
58
|
+
const [isExpectedVisible, setIsExpectedVisible] = useState(
|
|
59
|
+
expectedHasContent ? true : !storedExpectedCollapsed,
|
|
60
|
+
);
|
|
61
|
+
const [isDataVisible, setIsDataVisible] = useState(dataHasContent);
|
|
31
62
|
const [shouldFocusDataField, setShouldFocusDataField] = useState(false);
|
|
32
63
|
const uploadImage = useStepImageUpload();
|
|
33
64
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
65
|
+
// Calculate step number based on position in document
|
|
66
|
+
const stepNumber = useMemo(() => {
|
|
67
|
+
const allBlocks = editor.document;
|
|
68
|
+
const stepBlocks = allBlocks.filter((b) => b.type === "testStep");
|
|
69
|
+
const index = stepBlocks.findIndex((b) => b.id === block.id);
|
|
70
|
+
return index >= 0 ? index + 1 : 1;
|
|
71
|
+
}, [editor.document, block.id]);
|
|
39
72
|
|
|
40
73
|
useEffect(() => {
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
return () => clearTimeout(timer);
|
|
74
|
+
if (dataHasContent && !isDataVisible) {
|
|
75
|
+
setIsDataVisible(true);
|
|
44
76
|
}
|
|
45
|
-
|
|
46
|
-
}, [isDataVisible, shouldFocusDataField]);
|
|
77
|
+
}, [dataHasContent, isDataVisible]);
|
|
47
78
|
|
|
48
79
|
const handleStepTitleChange = useCallback(
|
|
49
80
|
(next: string) => {
|
|
@@ -75,11 +106,15 @@ export const stepBlock = createReactBlockSpec(
|
|
|
75
106
|
[editor, block.id, stepData],
|
|
76
107
|
);
|
|
77
108
|
|
|
78
|
-
const
|
|
109
|
+
const handleShowData = useCallback(() => {
|
|
79
110
|
setIsDataVisible(true);
|
|
80
111
|
setShouldFocusDataField(true);
|
|
81
112
|
}, []);
|
|
82
113
|
|
|
114
|
+
const handleHideData = useCallback(() => {
|
|
115
|
+
setIsDataVisible(false);
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
83
118
|
const handleExpectedChange = useCallback(
|
|
84
119
|
(next: string) => {
|
|
85
120
|
if (next === expectedResult) {
|
|
@@ -117,31 +152,40 @@ export const stepBlock = createReactBlockSpec(
|
|
|
117
152
|
editor.setSelection(block.id, block.id);
|
|
118
153
|
}, [editor, block.id]);
|
|
119
154
|
|
|
155
|
+
const [dataFocusSignal] = useState(0);
|
|
156
|
+
const [expectedFocusSignal, setExpectedFocusSignal] = useState(0);
|
|
157
|
+
|
|
158
|
+
const handleShowExpected = useCallback(() => {
|
|
159
|
+
setIsExpectedVisible(true);
|
|
160
|
+
setExpectedFocusSignal((value) => value + 1);
|
|
161
|
+
writeExpectedCollapsedPreference(false);
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
const handleHideExpected = useCallback(() => {
|
|
165
|
+
setIsExpectedVisible(false);
|
|
166
|
+
writeExpectedCollapsedPreference(true);
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (expectedHasContent && !isExpectedVisible) {
|
|
171
|
+
setIsExpectedVisible(true);
|
|
172
|
+
}
|
|
173
|
+
}, [expectedHasContent, isExpectedVisible]);
|
|
174
|
+
|
|
175
|
+
const canToggleData = !dataHasContent;
|
|
176
|
+
const canToggleExpected = !expectedHasContent;
|
|
177
|
+
|
|
120
178
|
return (
|
|
121
179
|
<div className="bn-teststep" data-block-id={block.id}>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
placeholder="Describe the action to perform"
|
|
180
|
+
<StepField
|
|
181
|
+
label={`Step ${stepNumber}`}
|
|
182
|
+
value={stepTitle}
|
|
126
183
|
onChange={handleStepTitleChange}
|
|
127
184
|
autoFocus={stepTitle.length === 0}
|
|
128
185
|
enableAutocomplete
|
|
129
186
|
fieldName="title"
|
|
130
187
|
suggestionFilter={(suggestion) => (suggestion as StepSuggestion).isSnippet !== true}
|
|
131
188
|
onFieldFocus={handleFieldFocus}
|
|
132
|
-
rightAction={
|
|
133
|
-
!isDataVisible ? (
|
|
134
|
-
<button
|
|
135
|
-
type="button"
|
|
136
|
-
className="bn-teststep__toggle"
|
|
137
|
-
onClick={handleShowDataField}
|
|
138
|
-
aria-expanded="false"
|
|
139
|
-
tabIndex={-1}
|
|
140
|
-
>
|
|
141
|
-
+ Step Data
|
|
142
|
-
</button>
|
|
143
|
-
) : null
|
|
144
|
-
}
|
|
145
189
|
enableImageUpload={false}
|
|
146
190
|
showFormattingButtons
|
|
147
191
|
onImageFile={async (file) => {
|
|
@@ -166,32 +210,84 @@ export const stepBlock = createReactBlockSpec(
|
|
|
166
210
|
}
|
|
167
211
|
}}
|
|
168
212
|
/>
|
|
169
|
-
{isDataVisible
|
|
213
|
+
{isDataVisible ? (
|
|
170
214
|
<StepField
|
|
171
215
|
label="Step Data"
|
|
216
|
+
labelToggle={
|
|
217
|
+
canToggleData
|
|
218
|
+
? {
|
|
219
|
+
onClick: handleHideData,
|
|
220
|
+
expanded: true,
|
|
221
|
+
}
|
|
222
|
+
: undefined
|
|
223
|
+
}
|
|
172
224
|
value={stepData}
|
|
173
|
-
placeholder="Provide additional data about the step"
|
|
174
225
|
onChange={handleStepDataChange}
|
|
175
226
|
autoFocus={shouldFocusDataField}
|
|
227
|
+
focusSignal={dataFocusSignal}
|
|
176
228
|
multiline
|
|
177
229
|
enableImageUpload
|
|
178
230
|
showFormattingButtons
|
|
179
231
|
showImageButton
|
|
180
232
|
onFieldFocus={handleFieldFocus}
|
|
181
233
|
/>
|
|
234
|
+
) : (
|
|
235
|
+
<div className="bn-step-field bn-step-field--collapsed">
|
|
236
|
+
<span
|
|
237
|
+
className="bn-step-field__label bn-step-field__label--toggle"
|
|
238
|
+
role="button"
|
|
239
|
+
tabIndex={-1}
|
|
240
|
+
onClick={handleShowData}
|
|
241
|
+
onKeyDown={(event) => {
|
|
242
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
handleShowData();
|
|
245
|
+
}
|
|
246
|
+
}}
|
|
247
|
+
aria-expanded="false"
|
|
248
|
+
>
|
|
249
|
+
Step Data
|
|
250
|
+
</span>
|
|
251
|
+
</div>
|
|
182
252
|
)}
|
|
183
|
-
{
|
|
253
|
+
{isExpectedVisible ? (
|
|
184
254
|
<StepField
|
|
185
255
|
label="Expected Result"
|
|
256
|
+
labelToggle={
|
|
257
|
+
canToggleExpected
|
|
258
|
+
? {
|
|
259
|
+
onClick: handleHideExpected,
|
|
260
|
+
expanded: true,
|
|
261
|
+
}
|
|
262
|
+
: undefined
|
|
263
|
+
}
|
|
186
264
|
value={expectedResult}
|
|
187
|
-
placeholder="What should happen?"
|
|
188
265
|
onChange={handleExpectedChange}
|
|
189
266
|
multiline
|
|
267
|
+
focusSignal={expectedFocusSignal}
|
|
190
268
|
enableImageUpload
|
|
191
269
|
showFormattingButtons
|
|
192
270
|
showImageButton
|
|
193
271
|
onFieldFocus={handleFieldFocus}
|
|
194
272
|
/>
|
|
273
|
+
) : (
|
|
274
|
+
<div className="bn-step-field bn-step-field--collapsed">
|
|
275
|
+
<span
|
|
276
|
+
className="bn-step-field__label bn-step-field__label--toggle"
|
|
277
|
+
role="button"
|
|
278
|
+
tabIndex={-1}
|
|
279
|
+
onClick={handleShowExpected}
|
|
280
|
+
onKeyDown={(event) => {
|
|
281
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
282
|
+
event.preventDefault();
|
|
283
|
+
handleShowExpected();
|
|
284
|
+
}
|
|
285
|
+
}}
|
|
286
|
+
aria-expanded="false"
|
|
287
|
+
>
|
|
288
|
+
Expected Result
|
|
289
|
+
</span>
|
|
290
|
+
</div>
|
|
195
291
|
)}
|
|
196
292
|
<button type="button" className="bn-step-add" onClick={handleInsertNextStep}>
|
|
197
293
|
+ Step
|