testomatio-editor-blocks 0.1.1 → 0.2.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/README.md +13 -11
- package/package/editor/blocks/markdown.d.ts +5 -0
- package/package/editor/blocks/markdown.js +160 -0
- package/package/editor/blocks/snippet.d.ts +38 -0
- package/package/editor/blocks/snippet.js +65 -0
- package/package/editor/blocks/step.d.ts +32 -0
- package/package/editor/blocks/step.js +97 -0
- package/package/editor/blocks/stepField.d.ts +26 -0
- package/package/editor/blocks/stepField.js +316 -0
- package/package/editor/customMarkdownConverter.js +111 -80
- package/package/editor/customSchema.d.ts +31 -45
- package/package/editor/customSchema.js +6 -616
- package/package/editor/snippetAutocomplete.d.ts +28 -0
- package/package/editor/snippetAutocomplete.js +94 -0
- package/package/editor/stepAutocomplete.d.ts +1 -1
- package/package/editor/stepAutocomplete.js +15 -2
- package/package/editor/stepImageUpload.d.ts +1 -1
- package/package/editor/stepImageUpload.js +4 -9
- package/package/index.d.ts +2 -2
- package/package/index.js +2 -2
- package/package/styles.css +57 -0
- package/package.json +1 -1
- package/src/App.tsx +161 -45
- package/src/editor/blocks/blocks.test.ts +22 -0
- package/src/editor/blocks/markdown.ts +199 -0
- package/src/editor/blocks/snippet.tsx +109 -0
- package/src/editor/blocks/step.tsx +175 -0
- package/src/editor/blocks/stepField.tsx +487 -0
- package/src/editor/customMarkdownConverter.test.ts +121 -36
- package/src/editor/customMarkdownConverter.ts +128 -85
- package/src/editor/customSchema.tsx +6 -935
- package/src/editor/snippetAutocomplete.test.ts +54 -0
- package/src/editor/snippetAutocomplete.ts +133 -0
- package/src/editor/stepAutocomplete.test.ts +20 -0
- package/src/editor/stepAutocomplete.tsx +15 -2
- package/src/editor/stepImageUpload.test.ts +25 -0
- package/src/editor/stepImageUpload.ts +11 -0
- package/src/editor/styles.css +57 -0
- package/src/index.ts +2 -2
- package/src/editor/customSchema.test.ts +0 -47
- package/src/editor/stepImageUpload.tsx +0 -19
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
2
|
+
const MARKDOWN_ESCAPE_REGEX = /([*_\\])/g;
|
|
3
|
+
|
|
4
|
+
export function escapeHtml(text: string): string {
|
|
5
|
+
return text
|
|
6
|
+
.replace(/&/g, "&")
|
|
7
|
+
.replace(/</g, "<")
|
|
8
|
+
.replace(/>/g, ">")
|
|
9
|
+
.replace(/\"/g, """)
|
|
10
|
+
.replace(/'/g, "'");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type InlineSegment = {
|
|
14
|
+
text: string;
|
|
15
|
+
styles: {
|
|
16
|
+
bold: boolean;
|
|
17
|
+
italic: boolean;
|
|
18
|
+
underline: boolean;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function restoreEscapes(text: string): string {
|
|
23
|
+
return text.replace(/\uE000/g, "\\");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseInlineMarkdown(text: string): InlineSegment[] {
|
|
27
|
+
if (!text) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const normalized = text.replace(/\\([*_`~])/g, "\uE000$1");
|
|
32
|
+
const rawSegments = normalized
|
|
33
|
+
.split(/(\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/)
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
|
|
36
|
+
return rawSegments.map((segment) => {
|
|
37
|
+
const baseStyles = { bold: false, italic: false, underline: false };
|
|
38
|
+
|
|
39
|
+
if (/^\*\*(.+)\*\*$/.test(segment) || /^__(.+)__$/.test(segment)) {
|
|
40
|
+
const content = segment.slice(2, -2);
|
|
41
|
+
return {
|
|
42
|
+
text: restoreEscapes(content),
|
|
43
|
+
styles: { ...baseStyles, bold: true },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (/^\*(.+)\*$/.test(segment) || /^_(.+)_$/.test(segment)) {
|
|
48
|
+
const content = segment.slice(1, -1);
|
|
49
|
+
return {
|
|
50
|
+
text: restoreEscapes(content),
|
|
51
|
+
styles: { ...baseStyles, italic: true },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (/^<u>(.+)<\/u>$/.test(segment)) {
|
|
56
|
+
const content = segment.slice(3, -4);
|
|
57
|
+
return {
|
|
58
|
+
text: restoreEscapes(content),
|
|
59
|
+
styles: { ...baseStyles, underline: true },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
text: restoreEscapes(segment),
|
|
65
|
+
styles: { ...baseStyles },
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function inlineToHtml(inline: InlineSegment[]): string {
|
|
71
|
+
return inline
|
|
72
|
+
.map(({ text, styles }) => {
|
|
73
|
+
let html = escapeHtml(text);
|
|
74
|
+
if (styles.bold) {
|
|
75
|
+
html = `<strong>${html}</strong>`;
|
|
76
|
+
}
|
|
77
|
+
if (styles.italic) {
|
|
78
|
+
html = `<em>${html}</em>`;
|
|
79
|
+
}
|
|
80
|
+
if (styles.underline) {
|
|
81
|
+
html = `<u>${html}</u>`;
|
|
82
|
+
}
|
|
83
|
+
return html;
|
|
84
|
+
})
|
|
85
|
+
.join("");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function markdownToHtml(markdown: string): string {
|
|
89
|
+
if (!markdown) {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const lines = markdown.split(/\n/);
|
|
94
|
+
const htmlLines = lines.map((line) => {
|
|
95
|
+
const inline = parseInlineMarkdown(line);
|
|
96
|
+
const html = inlineToHtml(inline);
|
|
97
|
+
if (!html) {
|
|
98
|
+
return html;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return html.replace(
|
|
102
|
+
IMAGE_MARKDOWN_REGEX,
|
|
103
|
+
(_match, alt = "", src = "") =>
|
|
104
|
+
`<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" class="bn-inline-image" contenteditable="false" draggable="false" />`,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
return htmlLines.join("<br />");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function escapeMarkdownText(text: string): string {
|
|
111
|
+
return text.replace(MARKDOWN_ESCAPE_REGEX, "\\$1");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function normalizePlainText(text: string): string {
|
|
115
|
+
return text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function fallbackHtmlToMarkdown(html: string): string {
|
|
119
|
+
if (!html) {
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let result = html;
|
|
124
|
+
|
|
125
|
+
result = result.replace(/<img[^>]*>/gi, (match) => {
|
|
126
|
+
const src = match.match(/src="([^"]*)"/i)?.[1] ?? "";
|
|
127
|
+
const alt = match.match(/alt="([^"]*)"/i)?.[1] ?? "";
|
|
128
|
+
return ``;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
result = result
|
|
132
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
133
|
+
.replace(/<\/?(div|p)>/gi, "\n")
|
|
134
|
+
.replace(/<strong>(.*?)<\/strong>/gis, (_m, content) => `**${content}**`)
|
|
135
|
+
.replace(/<(em|i)>(.*?)<\/(em|i)>/gis, (_m, _tag, content) => `*${content}*`)
|
|
136
|
+
.replace(/<span[^>]*>/gi, "")
|
|
137
|
+
.replace(/<\/span>/gi, "")
|
|
138
|
+
.replace(/<u>(.*?)<\/u>/gis, (_m, content) => `<u>${content}</u>`);
|
|
139
|
+
|
|
140
|
+
result = result.replace(/<\/?[^>]+>/g, "");
|
|
141
|
+
|
|
142
|
+
return result
|
|
143
|
+
.split("\n")
|
|
144
|
+
.map((line) => line.trimEnd())
|
|
145
|
+
.join("\n")
|
|
146
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
147
|
+
.trim();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function htmlToMarkdown(html: string): string {
|
|
151
|
+
if (typeof document === "undefined") {
|
|
152
|
+
return fallbackHtmlToMarkdown(html);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const temp = document.createElement("div");
|
|
156
|
+
temp.innerHTML = html;
|
|
157
|
+
|
|
158
|
+
const traverse = (node: Node): string => {
|
|
159
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
160
|
+
const text = node.textContent ?? "";
|
|
161
|
+
return escapeMarkdownText(text);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const element = node as HTMLElement;
|
|
169
|
+
const children = Array.from(element.childNodes)
|
|
170
|
+
.map(traverse)
|
|
171
|
+
.join("");
|
|
172
|
+
|
|
173
|
+
switch (element.tagName.toLowerCase()) {
|
|
174
|
+
case "strong":
|
|
175
|
+
case "b":
|
|
176
|
+
return children ? `**${children}**` : children;
|
|
177
|
+
case "em":
|
|
178
|
+
case "i":
|
|
179
|
+
return children ? `*${children}*` : children;
|
|
180
|
+
case "u":
|
|
181
|
+
return children ? `<u>${children}</u>` : children;
|
|
182
|
+
case "br":
|
|
183
|
+
return "\n";
|
|
184
|
+
case "div":
|
|
185
|
+
case "p":
|
|
186
|
+
return children + "\n";
|
|
187
|
+
case "img": {
|
|
188
|
+
const src = element.getAttribute("src") ?? "";
|
|
189
|
+
const alt = element.getAttribute("alt") ?? "";
|
|
190
|
+
return ``;
|
|
191
|
+
}
|
|
192
|
+
default:
|
|
193
|
+
return children;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const markdown = Array.from(temp.childNodes).map(traverse).join("");
|
|
198
|
+
return markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
199
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createReactBlockSpec } from "@blocknote/react";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
import { StepField } from "./stepField";
|
|
4
|
+
import { useSnippetAutocomplete, type SnippetSuggestion } from "../snippetAutocomplete";
|
|
5
|
+
import type { StepSuggestion } from "../stepAutocomplete";
|
|
6
|
+
|
|
7
|
+
export const snippetBlock = createReactBlockSpec(
|
|
8
|
+
{
|
|
9
|
+
type: "snippet",
|
|
10
|
+
content: "none",
|
|
11
|
+
propSchema: {
|
|
12
|
+
snippetId: {
|
|
13
|
+
default: "",
|
|
14
|
+
},
|
|
15
|
+
snippetTitle: {
|
|
16
|
+
default: "",
|
|
17
|
+
},
|
|
18
|
+
snippetData: {
|
|
19
|
+
default: "",
|
|
20
|
+
},
|
|
21
|
+
snippetExpectedResult: {
|
|
22
|
+
default: "",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
render: ({ block, editor }) => {
|
|
28
|
+
const snippetTitle = (block.props.snippetTitle as string) || "";
|
|
29
|
+
const snippetData = (block.props.snippetData as string) || "";
|
|
30
|
+
const snippetSuggestions = useSnippetAutocomplete();
|
|
31
|
+
|
|
32
|
+
const handleSnippetChange = useCallback(
|
|
33
|
+
(nextTitle: string) => {
|
|
34
|
+
if (nextTitle === snippetTitle) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
editor.updateBlock(block.id, {
|
|
39
|
+
props: {
|
|
40
|
+
snippetTitle: nextTitle,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
[block.id, editor, snippetTitle],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const handleSnippetDataChange = useCallback(
|
|
48
|
+
(next: string) => {
|
|
49
|
+
if (next === snippetData) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
editor.updateBlock(block.id, {
|
|
54
|
+
props: {
|
|
55
|
+
snippetData: next,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
[editor, block.id, snippetData],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const handleSnippetSelect = useCallback(
|
|
63
|
+
(suggestion: SnippetSuggestion | StepSuggestion) => {
|
|
64
|
+
const rawBody = (suggestion as SnippetSuggestion).body ?? "";
|
|
65
|
+
const sanitizedBody = rawBody
|
|
66
|
+
.split(/\r?\n/)
|
|
67
|
+
.filter((line) => !/^<!--\s*(begin|end)\s+snippet/i.test(line.trim()))
|
|
68
|
+
.join("\n");
|
|
69
|
+
editor.updateBlock(block.id, {
|
|
70
|
+
props: {
|
|
71
|
+
snippetId: suggestion.id,
|
|
72
|
+
snippetData: sanitizedBody,
|
|
73
|
+
snippetTitle: suggestion.title,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
[block.id, editor],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="bn-teststep bn-snippet" data-block-id={block.id}>
|
|
82
|
+
<StepField
|
|
83
|
+
label="Snippet Title"
|
|
84
|
+
value={snippetTitle}
|
|
85
|
+
placeholder="Describe the reusable action"
|
|
86
|
+
onChange={handleSnippetChange}
|
|
87
|
+
autoFocus={snippetTitle.length === 0}
|
|
88
|
+
enableAutocomplete
|
|
89
|
+
suggestionFilter={(suggestion) => (suggestion as SnippetSuggestion).isSnippet === true}
|
|
90
|
+
suggestionsOverride={snippetSuggestions as unknown as StepSuggestion[]}
|
|
91
|
+
onSuggestionSelect={handleSnippetSelect}
|
|
92
|
+
fieldName="snippet-title"
|
|
93
|
+
showSuggestionsOnFocus
|
|
94
|
+
enableImageUpload={false}
|
|
95
|
+
/>
|
|
96
|
+
<StepField
|
|
97
|
+
label="Snippet Data"
|
|
98
|
+
value={snippetData}
|
|
99
|
+
placeholder="Add optional data or assets for the snippet"
|
|
100
|
+
onChange={handleSnippetDataChange}
|
|
101
|
+
multiline
|
|
102
|
+
fieldName="snippet-data"
|
|
103
|
+
enableImageUpload
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
);
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createReactBlockSpec } from "@blocknote/react";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { StepField } from "./stepField";
|
|
4
|
+
import { useStepImageUpload } from "../stepImageUpload";
|
|
5
|
+
import type { StepSuggestion } from "../stepAutocomplete";
|
|
6
|
+
|
|
7
|
+
export const stepBlock = createReactBlockSpec(
|
|
8
|
+
{
|
|
9
|
+
type: "testStep",
|
|
10
|
+
content: "none",
|
|
11
|
+
propSchema: {
|
|
12
|
+
stepTitle: {
|
|
13
|
+
default: "",
|
|
14
|
+
},
|
|
15
|
+
stepData: {
|
|
16
|
+
default: "",
|
|
17
|
+
},
|
|
18
|
+
expectedResult: {
|
|
19
|
+
default: "",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
render: ({ block, editor }) => {
|
|
25
|
+
const stepTitle = (block.props.stepTitle as string) || "";
|
|
26
|
+
const stepData = (block.props.stepData as string) || "";
|
|
27
|
+
const expectedResult = (block.props.expectedResult as string) || "";
|
|
28
|
+
const showExpectedField =
|
|
29
|
+
stepTitle.trim().length > 0 || stepData.trim().length > 0 || expectedResult.trim().length > 0;
|
|
30
|
+
const [isDataVisible, setIsDataVisible] = useState(() => stepData.trim().length > 0);
|
|
31
|
+
const [shouldFocusDataField, setShouldFocusDataField] = useState(false);
|
|
32
|
+
const uploadImage = useStepImageUpload();
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (stepData.trim().length > 0 && !isDataVisible) {
|
|
36
|
+
setIsDataVisible(true);
|
|
37
|
+
}
|
|
38
|
+
}, [isDataVisible, stepData]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (shouldFocusDataField && isDataVisible) {
|
|
42
|
+
const timer = setTimeout(() => setShouldFocusDataField(false), 0);
|
|
43
|
+
return () => clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}, [isDataVisible, shouldFocusDataField]);
|
|
47
|
+
|
|
48
|
+
const handleStepTitleChange = useCallback(
|
|
49
|
+
(next: string) => {
|
|
50
|
+
if (next === stepTitle) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
editor.updateBlock(block.id, {
|
|
55
|
+
props: {
|
|
56
|
+
stepTitle: next,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
[editor, block.id, stepTitle],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const handleStepDataChange = useCallback(
|
|
64
|
+
(next: string) => {
|
|
65
|
+
if (next === stepData) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
editor.updateBlock(block.id, {
|
|
70
|
+
props: {
|
|
71
|
+
stepData: next,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
[editor, block.id, stepData],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const handleShowDataField = useCallback(() => {
|
|
79
|
+
setIsDataVisible(true);
|
|
80
|
+
setShouldFocusDataField(true);
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const handleExpectedChange = useCallback(
|
|
84
|
+
(next: string) => {
|
|
85
|
+
if (next === expectedResult) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
editor.updateBlock(block.id, {
|
|
90
|
+
props: {
|
|
91
|
+
expectedResult: next,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
[editor, block.id, expectedResult],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="bn-teststep" data-block-id={block.id}>
|
|
100
|
+
<StepField
|
|
101
|
+
label="Step Title"
|
|
102
|
+
value={stepTitle}
|
|
103
|
+
placeholder="Describe the action to perform"
|
|
104
|
+
onChange={handleStepTitleChange}
|
|
105
|
+
autoFocus={stepTitle.length === 0}
|
|
106
|
+
enableAutocomplete
|
|
107
|
+
fieldName="title"
|
|
108
|
+
suggestionFilter={(suggestion) => (suggestion as StepSuggestion).isSnippet !== true}
|
|
109
|
+
rightAction={
|
|
110
|
+
!isDataVisible ? (
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
className="bn-teststep__toggle"
|
|
114
|
+
onClick={handleShowDataField}
|
|
115
|
+
aria-expanded="false"
|
|
116
|
+
tabIndex={-1}
|
|
117
|
+
>
|
|
118
|
+
+ Step Data
|
|
119
|
+
</button>
|
|
120
|
+
) : null
|
|
121
|
+
}
|
|
122
|
+
enableImageUpload={false}
|
|
123
|
+
showFormattingButtons
|
|
124
|
+
onImageFile={async (file) => {
|
|
125
|
+
if (!uploadImage) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setIsDataVisible(true);
|
|
130
|
+
setShouldFocusDataField(true);
|
|
131
|
+
try {
|
|
132
|
+
const result = await uploadImage(file);
|
|
133
|
+
if (result?.url) {
|
|
134
|
+
const nextValue = stepData.trim().length > 0 ? `${stepData}\n` : ``;
|
|
135
|
+
editor.updateBlock(block.id, {
|
|
136
|
+
props: {
|
|
137
|
+
stepData: nextValue,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error("Failed to upload image to Step Data", error);
|
|
143
|
+
}
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
{isDataVisible && (
|
|
147
|
+
<StepField
|
|
148
|
+
label="Step Data"
|
|
149
|
+
value={stepData}
|
|
150
|
+
placeholder="Provide additional data about the step"
|
|
151
|
+
onChange={handleStepDataChange}
|
|
152
|
+
autoFocus={shouldFocusDataField}
|
|
153
|
+
multiline
|
|
154
|
+
enableImageUpload
|
|
155
|
+
showFormattingButtons
|
|
156
|
+
showImageButton
|
|
157
|
+
/>
|
|
158
|
+
)}
|
|
159
|
+
{showExpectedField && (
|
|
160
|
+
<StepField
|
|
161
|
+
label="Expected Result"
|
|
162
|
+
value={expectedResult}
|
|
163
|
+
placeholder="What should happen?"
|
|
164
|
+
onChange={handleExpectedChange}
|
|
165
|
+
multiline
|
|
166
|
+
enableImageUpload
|
|
167
|
+
showFormattingButtons
|
|
168
|
+
showImageButton
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
);
|