testomatio-editor-blocks 0.4.34 → 0.4.36
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.js +0 -29
- package/package/editor/blocks/stepField.js +167 -34
- package/package/editor/customMarkdownConverter.js +47 -25
- package/package/styles.css +8 -1
- package/package.json +1 -1
- package/src/App.tsx +2 -7
- package/src/editor/blocks/step.tsx +0 -37
- package/src/editor/blocks/stepField.tsx +172 -34
- package/src/editor/customMarkdownConverter.test.ts +371 -0
- package/src/editor/customMarkdownConverter.ts +49 -25
- package/src/editor/styles.css +8 -1
|
@@ -212,32 +212,6 @@ export const stepBlock = createReactBlockSpec(
|
|
|
212
212
|
return count;
|
|
213
213
|
}, [block.id, documentVersion, editor.document]);
|
|
214
214
|
|
|
215
|
-
// Check if there is a preceding "Steps" heading
|
|
216
|
-
const hasStepsHeading = useMemo(() => {
|
|
217
|
-
const allBlocks = editor.document;
|
|
218
|
-
const blockIndex = allBlocks.findIndex((b) => b.id === block.id);
|
|
219
|
-
if (blockIndex < 0) return false;
|
|
220
|
-
|
|
221
|
-
for (let i = blockIndex - 1; i >= 0; i--) {
|
|
222
|
-
const b = allBlocks[i];
|
|
223
|
-
if (b.type === "testStep" || b.type === "snippet" || isEmptyParagraph(b)) {
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (b.type === "heading") {
|
|
227
|
-
const text = (Array.isArray(b.content) ? b.content : [])
|
|
228
|
-
.filter((n: any) => n.type === "text")
|
|
229
|
-
.map((n: any) => n.text ?? "")
|
|
230
|
-
.join("")
|
|
231
|
-
.trim()
|
|
232
|
-
.toLowerCase()
|
|
233
|
-
.replace(/[:\-–—]$/, "");
|
|
234
|
-
return isStepsHeading(text);
|
|
235
|
-
}
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
return false;
|
|
239
|
-
}, [block.id, documentVersion, editor.document]);
|
|
240
|
-
|
|
241
215
|
useEditorChange(() => {
|
|
242
216
|
setDocumentVersion((version) => version + 1);
|
|
243
217
|
}, editor);
|
|
@@ -417,17 +391,6 @@ export const stepBlock = createReactBlockSpec(
|
|
|
417
391
|
const canToggleData = !dataHasContent;
|
|
418
392
|
const canToggleExpected = !expectedHasContent;
|
|
419
393
|
|
|
420
|
-
// Render as plain text when not under a "Steps" heading
|
|
421
|
-
if (!hasStepsHeading) {
|
|
422
|
-
return (
|
|
423
|
-
<div className="bn-teststep-plain" data-block-id={block.id}>
|
|
424
|
-
<span>{stepTitle || "(empty step)"}</span>
|
|
425
|
-
{stepData ? <span className="bn-teststep-plain__data">{stepData}</span> : null}
|
|
426
|
-
{expectedResult ? <span className="bn-teststep-plain__expected">{expectedResult}</span> : null}
|
|
427
|
-
</div>
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
394
|
if (viewMode === "horizontal") {
|
|
432
395
|
return (
|
|
433
396
|
<StepHorizontalView
|
|
@@ -80,7 +80,7 @@ type ExtractedImage = {
|
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
type LinkMeta = { start: number; end: number; url: string };
|
|
83
|
-
type FormattingMeta = { start: number; end: number; type: "bold" | "italic" };
|
|
83
|
+
type FormattingMeta = { start: number; end: number; type: "bold" | "italic" | "code" };
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
function stripInlineMarkdown(markdown: string): {
|
|
@@ -207,6 +207,34 @@ function stripInlineMarkdown(markdown: string): {
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// Code block: ```\n...\n``` (triple backticks with newlines)
|
|
211
|
+
if (markdown[i] === "`" && markdown[i + 1] === "`" && markdown[i + 2] === "`") {
|
|
212
|
+
const contentStart = markdown[i + 3] === "\n" ? i + 4 : i + 3;
|
|
213
|
+
const closeIdx = markdown.indexOf("```", contentStart);
|
|
214
|
+
if (closeIdx !== -1) {
|
|
215
|
+
const contentEnd = markdown[closeIdx - 1] === "\n" ? closeIdx - 1 : closeIdx;
|
|
216
|
+
const inner = markdown.slice(contentStart, contentEnd);
|
|
217
|
+
const start = plainText.length;
|
|
218
|
+
plainText += inner;
|
|
219
|
+
formatting.push({ start, end: plainText.length, type: "code" });
|
|
220
|
+
i = closeIdx + 3;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Inline code: `text`
|
|
226
|
+
if (markdown[i] === "`") {
|
|
227
|
+
const closeIdx = markdown.indexOf("`", i + 1);
|
|
228
|
+
if (closeIdx !== -1) {
|
|
229
|
+
const inner = markdown.slice(i + 1, closeIdx);
|
|
230
|
+
const start = plainText.length;
|
|
231
|
+
plainText += inner;
|
|
232
|
+
formatting.push({ start, end: plainText.length, type: "code" });
|
|
233
|
+
i = closeIdx + 1;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
210
238
|
plainText += markdown[i];
|
|
211
239
|
i++;
|
|
212
240
|
}
|
|
@@ -224,13 +252,23 @@ function buildFullMarkdown(plainText: string, links: LinkMeta[], formatting: For
|
|
|
224
252
|
const markers: Marker[] = [];
|
|
225
253
|
|
|
226
254
|
for (const fmt of formatting) {
|
|
227
|
-
|
|
255
|
+
let openMarker: string;
|
|
256
|
+
let closeMarker: string;
|
|
257
|
+
if (fmt.type === "code") {
|
|
258
|
+
const content = plainText.slice(fmt.start, fmt.end);
|
|
259
|
+
const isMultiline = content.includes("\n");
|
|
260
|
+
openMarker = isMultiline ? "```\n" : "`";
|
|
261
|
+
closeMarker = isMultiline ? "\n```" : "`";
|
|
262
|
+
} else {
|
|
263
|
+
openMarker = fmt.type === "bold" ? "**" : "*";
|
|
264
|
+
closeMarker = openMarker;
|
|
265
|
+
}
|
|
228
266
|
// Opening: outer markers (bold) before inner (italic) → bold order=0, italic order=1
|
|
229
267
|
// Closing: inner markers (italic) before outer (bold) → italic order=0, bold order=1
|
|
230
|
-
const openOrder = fmt.type === "bold" ? 0 : 1;
|
|
231
|
-
const closeOrder = fmt.type === "bold" ? 1 : 0;
|
|
232
|
-
markers.push({ pos: fmt.start, text:
|
|
233
|
-
markers.push({ pos: fmt.end, text:
|
|
268
|
+
const openOrder = fmt.type === "bold" ? 0 : fmt.type === "code" ? 2 : 1;
|
|
269
|
+
const closeOrder = fmt.type === "bold" ? 1 : fmt.type === "code" ? -1 : 0;
|
|
270
|
+
markers.push({ pos: fmt.start, text: openMarker, order: openOrder });
|
|
271
|
+
markers.push({ pos: fmt.end, text: closeMarker, order: closeOrder });
|
|
234
272
|
}
|
|
235
273
|
|
|
236
274
|
for (const link of links) {
|
|
@@ -265,7 +303,16 @@ function adjustFormattingForEdit(formatting: FormattingMeta[], editPos: number,
|
|
|
265
303
|
.filter((fmt) => fmt.end > fmt.start);
|
|
266
304
|
}
|
|
267
305
|
|
|
268
|
-
function getCaretRectInPreview(preview: HTMLElement, offset: number): { top: number; left: number; height: number } | null {
|
|
306
|
+
function getCaretRectInPreview(preview: HTMLElement, offset: number, textareaValue?: string): { top: number; left: number; height: number } | null {
|
|
307
|
+
// Convert textarea-space offset to preview-space (strip newlines)
|
|
308
|
+
let nlCount = 0;
|
|
309
|
+
if (textareaValue) {
|
|
310
|
+
for (let i = 0; i < offset && i < textareaValue.length; i++) {
|
|
311
|
+
if (textareaValue[i] === "\n") nlCount++;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const previewOffset = offset - nlCount;
|
|
315
|
+
|
|
269
316
|
const walker = document.createTreeWalker(preview, NodeFilter.SHOW_TEXT);
|
|
270
317
|
let currentOffset = 0;
|
|
271
318
|
|
|
@@ -273,8 +320,8 @@ function getCaretRectInPreview(preview: HTMLElement, offset: number): { top: num
|
|
|
273
320
|
const textNode = walker.currentNode as Text;
|
|
274
321
|
const nodeLen = textNode.length;
|
|
275
322
|
|
|
276
|
-
if (
|
|
277
|
-
const localOffset =
|
|
323
|
+
if (previewOffset <= currentOffset + nodeLen) {
|
|
324
|
+
const localOffset = previewOffset - currentOffset;
|
|
278
325
|
try {
|
|
279
326
|
const range = document.createRange();
|
|
280
327
|
range.setStart(textNode, localOffset);
|
|
@@ -297,7 +344,7 @@ function getCaretRectInPreview(preview: HTMLElement, offset: number): { top: num
|
|
|
297
344
|
return null;
|
|
298
345
|
}
|
|
299
346
|
|
|
300
|
-
function applyFormattingHighlights(preview: HTMLElement, formatting: FormattingMeta[]) {
|
|
347
|
+
function applyFormattingHighlights(preview: HTMLElement, formatting: FormattingMeta[], textareaValue?: string) {
|
|
301
348
|
if (formatting.length === 0) return;
|
|
302
349
|
|
|
303
350
|
// Remove previous formatting highlights
|
|
@@ -323,10 +370,41 @@ function applyFormattingHighlights(preview: HTMLElement, formatting: FormattingM
|
|
|
323
370
|
parent.removeChild(el);
|
|
324
371
|
}
|
|
325
372
|
}
|
|
373
|
+
const existingCode = preview.querySelectorAll("code.step-preview-code");
|
|
374
|
+
for (let i = 0; i < existingCode.length; i++) {
|
|
375
|
+
const el = existingCode[i];
|
|
376
|
+
const parent = el.parentNode;
|
|
377
|
+
if (parent) {
|
|
378
|
+
while (el.firstChild) {
|
|
379
|
+
parent.insertBefore(el.firstChild, el);
|
|
380
|
+
}
|
|
381
|
+
parent.removeChild(el);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// After unwrapping formatting elements, merge adjacent/empty text nodes
|
|
386
|
+
// so the tree walker sees clean text nodes matching the original structure.
|
|
387
|
+
preview.normalize();
|
|
388
|
+
|
|
389
|
+
// OverType splits textarea lines into <div> elements, discarding the \n
|
|
390
|
+
// characters. Convert textarea-space positions (with \n) to preview-space
|
|
391
|
+
// positions (without \n) so we can find the correct text nodes.
|
|
392
|
+
function taToPreview(taPos: number): number {
|
|
393
|
+
if (!textareaValue) return taPos;
|
|
394
|
+
let nlCount = 0;
|
|
395
|
+
for (let i = 0; i < taPos && i < textareaValue.length; i++) {
|
|
396
|
+
if (textareaValue[i] === "\n") nlCount++;
|
|
397
|
+
}
|
|
398
|
+
return taPos - nlCount;
|
|
399
|
+
}
|
|
326
400
|
|
|
327
401
|
const sorted = [...formatting].sort((a, b) => b.start - a.start);
|
|
328
402
|
|
|
329
403
|
for (const fmt of sorted) {
|
|
404
|
+
const pStart = taToPreview(fmt.start);
|
|
405
|
+
const pEnd = taToPreview(fmt.end);
|
|
406
|
+
|
|
407
|
+
// Collect text nodes with their preview-space offsets
|
|
330
408
|
const walker = document.createTreeWalker(preview, NodeFilter.SHOW_TEXT);
|
|
331
409
|
let currentOffset = 0;
|
|
332
410
|
let startNode: Text | null = null;
|
|
@@ -339,13 +417,13 @@ function applyFormattingHighlights(preview: HTMLElement, formatting: FormattingM
|
|
|
339
417
|
const nodeStart = currentOffset;
|
|
340
418
|
const nodeEnd = currentOffset + textNode.length;
|
|
341
419
|
|
|
342
|
-
if (!startNode &&
|
|
420
|
+
if (!startNode && pStart >= nodeStart && pStart < nodeEnd) {
|
|
343
421
|
startNode = textNode;
|
|
344
|
-
startLocalOffset =
|
|
422
|
+
startLocalOffset = pStart - nodeStart;
|
|
345
423
|
}
|
|
346
|
-
if (!endNode &&
|
|
424
|
+
if (!endNode && pEnd > nodeStart && pEnd <= nodeEnd) {
|
|
347
425
|
endNode = textNode;
|
|
348
|
-
endLocalOffset =
|
|
426
|
+
endLocalOffset = pEnd - nodeStart;
|
|
349
427
|
}
|
|
350
428
|
|
|
351
429
|
currentOffset = nodeEnd;
|
|
@@ -354,19 +432,58 @@ function applyFormattingHighlights(preview: HTMLElement, formatting: FormattingM
|
|
|
354
432
|
|
|
355
433
|
if (!startNode || !endNode) continue;
|
|
356
434
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
range.setStart(startNode, startLocalOffset);
|
|
360
|
-
range.setEnd(endNode, endLocalOffset);
|
|
435
|
+
const tagName = fmt.type === "bold" ? "strong" : fmt.type === "code" ? "code" : "em";
|
|
436
|
+
const className = fmt.type === "bold" ? "step-preview-bold" : fmt.type === "code" ? "step-preview-code" : "step-preview-italic";
|
|
361
437
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
438
|
+
// If start and end are in the same text node, wrap directly
|
|
439
|
+
if (startNode === endNode) {
|
|
440
|
+
try {
|
|
441
|
+
const range = document.createRange();
|
|
442
|
+
range.setStart(startNode, startLocalOffset);
|
|
443
|
+
range.setEnd(endNode, endLocalOffset);
|
|
444
|
+
const wrapper = document.createElement(tagName);
|
|
445
|
+
wrapper.className = className;
|
|
446
|
+
const fragment = range.extractContents();
|
|
447
|
+
wrapper.appendChild(fragment);
|
|
448
|
+
range.insertNode(wrapper);
|
|
449
|
+
} catch {
|
|
450
|
+
// DOM manipulation can fail if range crosses element boundaries
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
// Multi-node range (e.g. code spanning multiple lines/divs):
|
|
454
|
+
// collect all text nodes in the range, then wrap each one individually
|
|
455
|
+
const textNodes: { node: Text; localStart: number; localEnd: number }[] = [];
|
|
456
|
+
const walker2 = document.createTreeWalker(preview, NodeFilter.SHOW_TEXT);
|
|
457
|
+
let collecting = false;
|
|
458
|
+
while (walker2.nextNode()) {
|
|
459
|
+
const tn = walker2.currentNode as Text;
|
|
460
|
+
if (tn === startNode) {
|
|
461
|
+
collecting = true;
|
|
462
|
+
textNodes.push({ node: tn, localStart: startLocalOffset, localEnd: tn.length });
|
|
463
|
+
} else if (tn === endNode) {
|
|
464
|
+
textNodes.push({ node: tn, localStart: 0, localEnd: endLocalOffset });
|
|
465
|
+
break;
|
|
466
|
+
} else if (collecting) {
|
|
467
|
+
textNodes.push({ node: tn, localStart: 0, localEnd: tn.length });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Wrap in reverse order to preserve offsets
|
|
471
|
+
for (let ti = textNodes.length - 1; ti >= 0; ti--) {
|
|
472
|
+
const { node, localStart, localEnd } = textNodes[ti];
|
|
473
|
+
if (localStart >= localEnd) continue;
|
|
474
|
+
try {
|
|
475
|
+
const range = document.createRange();
|
|
476
|
+
range.setStart(node, localStart);
|
|
477
|
+
range.setEnd(node, localEnd);
|
|
478
|
+
const wrapper = document.createElement(tagName);
|
|
479
|
+
wrapper.className = className;
|
|
480
|
+
const fragment = range.extractContents();
|
|
481
|
+
wrapper.appendChild(fragment);
|
|
482
|
+
range.insertNode(wrapper);
|
|
483
|
+
} catch {
|
|
484
|
+
// skip nodes that can't be wrapped
|
|
485
|
+
}
|
|
486
|
+
}
|
|
370
487
|
}
|
|
371
488
|
}
|
|
372
489
|
}
|
|
@@ -576,11 +693,11 @@ export function StepField({
|
|
|
576
693
|
const originalUpdatePreview = instance.updatePreview.bind(instance);
|
|
577
694
|
instance.updatePreview = function () {
|
|
578
695
|
originalUpdatePreview();
|
|
579
|
-
applyFormattingHighlights(this.preview, formattingRef.current);
|
|
696
|
+
applyFormattingHighlights(this.preview, formattingRef.current, this.textarea?.value);
|
|
580
697
|
applyLinkHighlights(this.preview, linksRef.current);
|
|
581
698
|
};
|
|
582
699
|
// Apply initial highlights
|
|
583
|
-
applyFormattingHighlights(instance.preview, formattingRef.current);
|
|
700
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, instance.textarea?.value);
|
|
584
701
|
applyLinkHighlights(instance.preview, linksRef.current);
|
|
585
702
|
|
|
586
703
|
// Create custom caret element inside the wrapper
|
|
@@ -633,7 +750,7 @@ export function StepField({
|
|
|
633
750
|
return;
|
|
634
751
|
}
|
|
635
752
|
|
|
636
|
-
const rect = getCaretRectInPreview(instance.preview, pos);
|
|
753
|
+
const rect = getCaretRectInPreview(instance.preview, pos, instance.textarea?.value);
|
|
637
754
|
if (rect) {
|
|
638
755
|
caret.style.display = "block";
|
|
639
756
|
caret.style.top = `${rect.top}px`;
|
|
@@ -712,7 +829,7 @@ export function StepField({
|
|
|
712
829
|
isSyncingRef.current = false;
|
|
713
830
|
} else {
|
|
714
831
|
// Even if text didn't change, formatting/links might have — re-apply highlights
|
|
715
|
-
applyFormattingHighlights(instance.preview, formatting);
|
|
832
|
+
applyFormattingHighlights(instance.preview, formatting, instance.textarea?.value);
|
|
716
833
|
applyLinkHighlights(instance.preview, links);
|
|
717
834
|
}
|
|
718
835
|
|
|
@@ -899,14 +1016,14 @@ export function StepField({
|
|
|
899
1016
|
}, [enableImageUpload, insertImageMarkdown, onImageFile, textareaNode, uploadImage]);
|
|
900
1017
|
|
|
901
1018
|
const handleToolbarAction = useCallback(
|
|
902
|
-
(action: "toggleBold" | "toggleItalic") => {
|
|
1019
|
+
(action: "toggleBold" | "toggleItalic" | "toggleCode") => {
|
|
903
1020
|
const instance = editorInstanceRef.current;
|
|
904
1021
|
if (!textareaNode || !instance) {
|
|
905
1022
|
return;
|
|
906
1023
|
}
|
|
907
1024
|
textareaNode.focus();
|
|
908
1025
|
|
|
909
|
-
const fmtType: "bold" | "italic" = action === "toggleBold" ? "bold" : "italic";
|
|
1026
|
+
const fmtType: "bold" | "italic" | "code" = action === "toggleBold" ? "bold" : action === "toggleCode" ? "code" : "italic";
|
|
910
1027
|
const start = textareaNode.selectionStart ?? 0;
|
|
911
1028
|
const end = textareaNode.selectionEnd ?? 0;
|
|
912
1029
|
|
|
@@ -934,7 +1051,7 @@ export function StepField({
|
|
|
934
1051
|
setPlainTextValue(markdownToPlainText(markdown));
|
|
935
1052
|
|
|
936
1053
|
// Re-apply highlights
|
|
937
|
-
applyFormattingHighlights(instance.preview, formattingRef.current);
|
|
1054
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, textareaNode?.value);
|
|
938
1055
|
applyLinkHighlights(instance.preview, linksRef.current);
|
|
939
1056
|
},
|
|
940
1057
|
[textareaNode],
|
|
@@ -1022,7 +1139,7 @@ export function StepField({
|
|
|
1022
1139
|
const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
|
|
1023
1140
|
onChangeRef.current?.(markdown);
|
|
1024
1141
|
// Re-apply highlights since links changed
|
|
1025
|
-
applyFormattingHighlights(instance.preview, formattingRef.current);
|
|
1142
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, instance.textarea?.value);
|
|
1026
1143
|
applyLinkHighlights(instance.preview, linksRef.current);
|
|
1027
1144
|
}
|
|
1028
1145
|
}, [cursorLink]);
|
|
@@ -1187,6 +1304,12 @@ export function StepField({
|
|
|
1187
1304
|
handleToolbarAction("toggleItalic");
|
|
1188
1305
|
return;
|
|
1189
1306
|
}
|
|
1307
|
+
if (event.key === "e" || event.key === "E") {
|
|
1308
|
+
event.preventDefault();
|
|
1309
|
+
event.stopImmediatePropagation();
|
|
1310
|
+
handleToolbarAction("toggleCode");
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1190
1313
|
}
|
|
1191
1314
|
|
|
1192
1315
|
if (enableAutocomplete && shouldShowAutocomplete) {
|
|
@@ -1394,6 +1517,21 @@ export function StepField({
|
|
|
1394
1517
|
<path d="M8.66699 13.3334H4.66699V12.0001H5.95166L8.69566 4.00008H7.33366V2.66675H11.3337V4.00008H10.049L7.30499 12.0001H8.66699V13.3334Z" fill="currentColor"/>
|
|
1395
1518
|
</svg>
|
|
1396
1519
|
</button>
|
|
1520
|
+
<button
|
|
1521
|
+
type="button"
|
|
1522
|
+
className="bn-step-toolbar__button"
|
|
1523
|
+
data-tooltip="Code"
|
|
1524
|
+
onMouseDown={(event) => {
|
|
1525
|
+
event.preventDefault();
|
|
1526
|
+
handleToolbarAction("toggleCode");
|
|
1527
|
+
}}
|
|
1528
|
+
aria-label="Code"
|
|
1529
|
+
tabIndex={-1}
|
|
1530
|
+
>
|
|
1531
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
1532
|
+
<path d="M10.333 12.6667L14 8.00008L10.333 3.33341L9.15833 4.28341L12.1583 8.00008L9.15833 11.7167L10.333 12.6667ZM5.66699 12.6667L6.84166 11.7167L3.84166 8.00008L6.84166 4.28341L5.66699 3.33341L2 8.00008L5.66699 12.6667Z" fill="currentColor"/>
|
|
1533
|
+
</svg>
|
|
1534
|
+
</button>
|
|
1397
1535
|
</>
|
|
1398
1536
|
)}
|
|
1399
1537
|
{enableImageUpload && uploadImage && showImageButton && (
|