testomatio-editor-blocks 0.4.64 → 0.4.66
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/testMeta.d.ts +37 -0
- package/package/editor/blocks/testMeta.js +111 -0
- package/package/editor/customMarkdownConverter.js +129 -22
- package/package/editor/customSchema.d.ts +32 -0
- package/package/editor/customSchema.js +2 -0
- package/package/editor/testMetaFields.d.ts +17 -0
- package/package/editor/testMetaFields.js +33 -0
- package/package/index.d.ts +2 -0
- package/package/index.js +2 -0
- package/package/styles.css +201 -0
- package/package.json +1 -1
- package/src/App.tsx +21 -2
- package/src/editor/blocks/testMeta.tsx +242 -0
- package/src/editor/customMarkdownConverter.test.ts +135 -84
- package/src/editor/customMarkdownConverter.ts +127 -11
- package/src/editor/customSchema.tsx +2 -0
- package/src/editor/styles.css +201 -0
- package/src/editor/testMetaFields.ts +53 -0
- package/src/index.ts +7 -0
package/package/styles.css
CHANGED
|
@@ -595,6 +595,207 @@ html.dark .bn-step-editor .overtype-wrapper .overtype-preview a.step-preview-lin
|
|
|
595
595
|
flex-shrink: 0;
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
+
/* ============================================
|
|
599
|
+
HEADING SIZES INSIDE THE EDITOR
|
|
600
|
+
Override BlockNote's default heading scale (3em / 2em / 1.3em) with a
|
|
601
|
+
compact 22px → 14px range. BlockNote derives heading font-size from the
|
|
602
|
+
`--level` custom property, so we just redefine it per level.
|
|
603
|
+
============================================ */
|
|
604
|
+
.testomatio-editor [data-content-type="heading"] {
|
|
605
|
+
--level: 22px; /* h1 (level 1 has no [data-level] rule of its own) */
|
|
606
|
+
}
|
|
607
|
+
.testomatio-editor [data-content-type="heading"][data-level="2"] {
|
|
608
|
+
--level: 20px;
|
|
609
|
+
}
|
|
610
|
+
.testomatio-editor [data-content-type="heading"][data-level="3"] {
|
|
611
|
+
--level: 18px;
|
|
612
|
+
}
|
|
613
|
+
.testomatio-editor [data-content-type="heading"][data-level="4"] {
|
|
614
|
+
--level: 16px;
|
|
615
|
+
}
|
|
616
|
+
.testomatio-editor [data-content-type="heading"][data-level="5"] {
|
|
617
|
+
--level: 14px;
|
|
618
|
+
}
|
|
619
|
+
.testomatio-editor [data-content-type="heading"][data-level="6"] {
|
|
620
|
+
--level: 14px;
|
|
621
|
+
}
|
|
622
|
+
/* Keep size stable during BlockNote's heading-transition animation. */
|
|
623
|
+
.testomatio-editor [data-prev-level="1"] {
|
|
624
|
+
--prev-level: 22px;
|
|
625
|
+
}
|
|
626
|
+
.testomatio-editor [data-prev-level="2"] {
|
|
627
|
+
--prev-level: 20px;
|
|
628
|
+
}
|
|
629
|
+
.testomatio-editor [data-prev-level="3"] {
|
|
630
|
+
--prev-level: 18px;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/* ============================================
|
|
634
|
+
TEST / SUITE METADATA BLOCK
|
|
635
|
+
Dimmed card for `<!-- test ... -->` / `<!-- suite ... -->` comments.
|
|
636
|
+
============================================ */
|
|
637
|
+
.bn-testmeta {
|
|
638
|
+
display: flex;
|
|
639
|
+
flex-direction: column;
|
|
640
|
+
gap: 4px;
|
|
641
|
+
width: 100%;
|
|
642
|
+
box-sizing: border-box;
|
|
643
|
+
padding: 6px 10px;
|
|
644
|
+
background: var(--bg-muted);
|
|
645
|
+
/*border: 1px solid var(--border-default);*/
|
|
646
|
+
/* Stronger top edge signals that the test case begins below this line. */
|
|
647
|
+
border-top: 3px solid var(--color-slate-400);
|
|
648
|
+
/*border-radius: 8px;*/
|
|
649
|
+
margin-top: 2rem;
|
|
650
|
+
opacity: 0.5;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/* Header line: `TEST @T1233456 ............ [+]` — label, id, and add button
|
|
654
|
+
always share one row. */
|
|
655
|
+
.bn-testmeta__header {
|
|
656
|
+
display: flex;
|
|
657
|
+
align-items: center;
|
|
658
|
+
gap: 8px;
|
|
659
|
+
margin-left: 8px;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.bn-testmeta__header .bn-testmeta__add-wrap {
|
|
663
|
+
margin-left: auto;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.bn-testmeta__label {
|
|
667
|
+
font-size: 11px;
|
|
668
|
+
font-weight: 600;
|
|
669
|
+
letter-spacing: 0.04em;
|
|
670
|
+
text-transform: uppercase;
|
|
671
|
+
color: var(--text-muted);
|
|
672
|
+
flex-shrink: 0;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.bn-testmeta__id {
|
|
676
|
+
font-size: 13px;
|
|
677
|
+
font-weight: 600;
|
|
678
|
+
color: var(--text-primary);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.bn-testmeta__rows {
|
|
682
|
+
display: flex;
|
|
683
|
+
flex-direction: column;
|
|
684
|
+
gap: 2px;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.bn-testmeta__row {
|
|
688
|
+
display: grid;
|
|
689
|
+
grid-template-columns: 140px minmax(0, 1fr) 24px;
|
|
690
|
+
align-items: center;
|
|
691
|
+
gap: 8px;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.bn-testmeta__key {
|
|
695
|
+
min-width: 0;
|
|
696
|
+
font-size: 13px;
|
|
697
|
+
color: var(--text-muted);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/* Defined values blend into the block like normal text, and only reveal the
|
|
701
|
+
input affordance on hover/focus ("activate on click"). */
|
|
702
|
+
.bn-testmeta__key--input,
|
|
703
|
+
.bn-testmeta__value {
|
|
704
|
+
width: 100%;
|
|
705
|
+
height: 26px;
|
|
706
|
+
padding: 0 8px;
|
|
707
|
+
box-sizing: border-box;
|
|
708
|
+
font-family: inherit;
|
|
709
|
+
font-size: 13px;
|
|
710
|
+
color: var(--text-primary);
|
|
711
|
+
background: transparent;
|
|
712
|
+
border: 1px solid transparent;
|
|
713
|
+
border-radius: 6px;
|
|
714
|
+
cursor: text;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.bn-testmeta__key--input:hover,
|
|
718
|
+
.bn-testmeta__value:hover {
|
|
719
|
+
border-color: var(--border-light);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.bn-testmeta__key--input:focus,
|
|
723
|
+
.bn-testmeta__value:focus {
|
|
724
|
+
outline: none;
|
|
725
|
+
background: var(--bg-white);
|
|
726
|
+
border-color: var(--step-input-border-focus);
|
|
727
|
+
box-shadow: 0 0 0 2px var(--step-input-shadow);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.bn-testmeta__remove,
|
|
731
|
+
.bn-testmeta__add {
|
|
732
|
+
width: 24px;
|
|
733
|
+
height: 24px;
|
|
734
|
+
display: inline-flex;
|
|
735
|
+
align-items: center;
|
|
736
|
+
justify-content: center;
|
|
737
|
+
font-size: 18px;
|
|
738
|
+
line-height: 1;
|
|
739
|
+
color: var(--text-muted);
|
|
740
|
+
background: transparent;
|
|
741
|
+
border: none;
|
|
742
|
+
border-radius: 6px;
|
|
743
|
+
cursor: pointer;
|
|
744
|
+
padding: 0;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.bn-testmeta__remove:hover,
|
|
748
|
+
.bn-testmeta__add:hover {
|
|
749
|
+
background: var(--step-bg-button-hover);
|
|
750
|
+
color: var(--text-primary);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.bn-testmeta__add-wrap {
|
|
754
|
+
position: relative;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.bn-testmeta__menu {
|
|
758
|
+
position: absolute;
|
|
759
|
+
top: calc(100% + 4px);
|
|
760
|
+
right: 0;
|
|
761
|
+
z-index: 100;
|
|
762
|
+
min-width: 160px;
|
|
763
|
+
max-height: 240px;
|
|
764
|
+
overflow-y: auto;
|
|
765
|
+
display: flex;
|
|
766
|
+
flex-direction: column;
|
|
767
|
+
padding: 4px;
|
|
768
|
+
background: var(--bg-white-opaque);
|
|
769
|
+
border: 1px solid var(--border-default);
|
|
770
|
+
border-radius: 8px;
|
|
771
|
+
box-shadow: 0 8px 24px var(--shadow-medium);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.bn-testmeta__menu-item {
|
|
775
|
+
display: block;
|
|
776
|
+
width: 100%;
|
|
777
|
+
padding: 6px 8px;
|
|
778
|
+
text-align: left;
|
|
779
|
+
font-family: inherit;
|
|
780
|
+
font-size: 13px;
|
|
781
|
+
color: var(--text-primary);
|
|
782
|
+
background: transparent;
|
|
783
|
+
border: none;
|
|
784
|
+
border-radius: 6px;
|
|
785
|
+
cursor: pointer;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.bn-testmeta__menu-item:hover {
|
|
789
|
+
background: var(--bg-muted);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.bn-testmeta__menu-item--custom {
|
|
793
|
+
margin-top: 2px;
|
|
794
|
+
border-top: 1px solid var(--border-light);
|
|
795
|
+
border-radius: 0 0 6px 6px;
|
|
796
|
+
color: var(--text-muted);
|
|
797
|
+
}
|
|
798
|
+
|
|
598
799
|
.bn-snippet-dropdown {
|
|
599
800
|
position: relative;
|
|
600
801
|
}
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -347,11 +347,30 @@ function CustomSlashMenu() {
|
|
|
347
347
|
},
|
|
348
348
|
};
|
|
349
349
|
|
|
350
|
+
const addTestItem = {
|
|
351
|
+
key: "add_test" as any,
|
|
352
|
+
title: "Add Test",
|
|
353
|
+
subtext: "Insert test metadata (id, priority, tags…)",
|
|
354
|
+
group: "Test documentation",
|
|
355
|
+
icon: <span className="bn-suggestion-icon">@T</span>,
|
|
356
|
+
aliases: ["test", "metadata", "meta", "test id"],
|
|
357
|
+
onItemClick: () => {
|
|
358
|
+
insertOrUpdateBlock(editor, {
|
|
359
|
+
type: "testMeta",
|
|
360
|
+
props: {
|
|
361
|
+
metaKind: "test",
|
|
362
|
+
metaFields: "[]",
|
|
363
|
+
metaInline: false,
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
|
|
350
369
|
const currentBlock = editor.getTextCursorPosition().block;
|
|
351
370
|
const canInsert = canInsertStepOrSnippet(editor, currentBlock.id);
|
|
352
371
|
const items = canInsert
|
|
353
|
-
? [...defaultItems, stepItem, snippetItem]
|
|
354
|
-
: defaultItems;
|
|
372
|
+
? [...defaultItems, stepItem, snippetItem, addTestItem]
|
|
373
|
+
: [...defaultItems, addTestItem];
|
|
355
374
|
return filterSuggestionItems(items, query);
|
|
356
375
|
};
|
|
357
376
|
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { createReactBlockSpec } from "@blocknote/react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import type { ChangeEvent } from "react";
|
|
4
|
+
import { getMetaFieldSuggestions } from "../testMetaFields";
|
|
5
|
+
|
|
6
|
+
export type MetaField = { key: string; value: string };
|
|
7
|
+
|
|
8
|
+
const ID_KEYS = new Set(["id"]);
|
|
9
|
+
|
|
10
|
+
function parseMetaFields(raw: unknown): MetaField[] {
|
|
11
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
if (!Array.isArray(parsed)) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
return parsed
|
|
20
|
+
.filter((item) => item && typeof item === "object")
|
|
21
|
+
.map((item) => ({
|
|
22
|
+
key: typeof item.key === "string" ? item.key : "",
|
|
23
|
+
value: typeof item.value === "string" ? item.value : "",
|
|
24
|
+
}));
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function serializeMetaFields(fields: MetaField[]): string {
|
|
31
|
+
return JSON.stringify(fields);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type AddFieldMenuProps = {
|
|
35
|
+
kind: "test" | "suite";
|
|
36
|
+
usedKeys: string[];
|
|
37
|
+
onPick: (key: string) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function AddFieldMenu({ kind, usedKeys, onPick }: AddFieldMenuProps) {
|
|
41
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
42
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!isOpen) return;
|
|
46
|
+
const handleMouseDown = (event: MouseEvent) => {
|
|
47
|
+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
48
|
+
setIsOpen(false);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
document.addEventListener("mousedown", handleMouseDown);
|
|
52
|
+
return () => document.removeEventListener("mousedown", handleMouseDown);
|
|
53
|
+
}, [isOpen]);
|
|
54
|
+
|
|
55
|
+
const available = useMemo(() => {
|
|
56
|
+
const used = new Set(usedKeys.map((k) => k.trim().toLowerCase()));
|
|
57
|
+
return getMetaFieldSuggestions(kind).filter((s) => !used.has(s.key.trim().toLowerCase()));
|
|
58
|
+
}, [kind, usedKeys]);
|
|
59
|
+
|
|
60
|
+
const pick = useCallback(
|
|
61
|
+
(key: string) => {
|
|
62
|
+
onPick(key);
|
|
63
|
+
setIsOpen(false);
|
|
64
|
+
},
|
|
65
|
+
[onPick],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="bn-testmeta__add-wrap" ref={containerRef}>
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
className="bn-testmeta__add"
|
|
73
|
+
aria-label="Add field"
|
|
74
|
+
title="Add field"
|
|
75
|
+
onClick={() => setIsOpen((prev) => !prev)}
|
|
76
|
+
>
|
|
77
|
+
+
|
|
78
|
+
</button>
|
|
79
|
+
{isOpen && (
|
|
80
|
+
<div className="bn-testmeta__menu" role="listbox">
|
|
81
|
+
{available.map((suggestion) => (
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
key={suggestion.key}
|
|
85
|
+
role="option"
|
|
86
|
+
className="bn-testmeta__menu-item"
|
|
87
|
+
onMouseDown={(event) => {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
pick(suggestion.key);
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{suggestion.label ?? suggestion.key}
|
|
93
|
+
</button>
|
|
94
|
+
))}
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
className="bn-testmeta__menu-item bn-testmeta__menu-item--custom"
|
|
98
|
+
onMouseDown={(event) => {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
pick("");
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
Custom field…
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const testMetaBlock = createReactBlockSpec(
|
|
112
|
+
{
|
|
113
|
+
type: "testMeta",
|
|
114
|
+
content: "none",
|
|
115
|
+
propSchema: {
|
|
116
|
+
// "test" | "suite" — which keyword the comment opened with.
|
|
117
|
+
metaKind: {
|
|
118
|
+
default: "test",
|
|
119
|
+
},
|
|
120
|
+
// JSON-encoded MetaField[] so insertion order is preserved.
|
|
121
|
+
metaFields: {
|
|
122
|
+
default: "[]",
|
|
123
|
+
},
|
|
124
|
+
// true when the source comment was a one-liner (`<!-- test id: @T.. -->`).
|
|
125
|
+
metaInline: {
|
|
126
|
+
default: false,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
render: ({ block, editor }) => {
|
|
132
|
+
const kind = (block.props.metaKind as string) === "suite" ? "suite" : "test";
|
|
133
|
+
const fields = useMemo(
|
|
134
|
+
() => parseMetaFields(block.props.metaFields),
|
|
135
|
+
[block.props.metaFields],
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const commitFields = useCallback(
|
|
139
|
+
(next: MetaField[]) => {
|
|
140
|
+
editor.updateBlock(block.id, {
|
|
141
|
+
props: { metaFields: serializeMetaFields(next) } as any,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
[block.id, editor],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const handleValueChange = useCallback(
|
|
148
|
+
(index: number, value: string) => {
|
|
149
|
+
const next = fields.map((field, i) =>
|
|
150
|
+
i === index ? { ...field, value } : field,
|
|
151
|
+
);
|
|
152
|
+
commitFields(next);
|
|
153
|
+
},
|
|
154
|
+
[fields, commitFields],
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const handleKeyChange = useCallback(
|
|
158
|
+
(index: number, key: string) => {
|
|
159
|
+
const next = fields.map((field, i) =>
|
|
160
|
+
i === index ? { ...field, key } : field,
|
|
161
|
+
);
|
|
162
|
+
commitFields(next);
|
|
163
|
+
},
|
|
164
|
+
[fields, commitFields],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const handleRemove = useCallback(
|
|
168
|
+
(index: number) => {
|
|
169
|
+
commitFields(fields.filter((_, i) => i !== index));
|
|
170
|
+
},
|
|
171
|
+
[fields, commitFields],
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const handleAddField = useCallback(
|
|
175
|
+
(key: string) => {
|
|
176
|
+
commitFields([...fields, { key, value: "" }]);
|
|
177
|
+
},
|
|
178
|
+
[fields, commitFields],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const usedKeys = useMemo(() => fields.map((f) => f.key), [fields]);
|
|
182
|
+
|
|
183
|
+
// The read-only `id` is shown inline on the header line next to the kind
|
|
184
|
+
// label; every other field renders as an editable row below.
|
|
185
|
+
const idField = fields.find((f) => ID_KEYS.has(f.key.trim().toLowerCase()));
|
|
186
|
+
const editableFields = fields
|
|
187
|
+
.map((field, index) => ({ field, index }))
|
|
188
|
+
.filter(({ field }) => !ID_KEYS.has(field.key.trim().toLowerCase()));
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div
|
|
192
|
+
className="bn-testmeta"
|
|
193
|
+
data-block-id={block.id}
|
|
194
|
+
data-kind={kind}
|
|
195
|
+
contentEditable={false}
|
|
196
|
+
suppressContentEditableWarning
|
|
197
|
+
draggable={false}
|
|
198
|
+
>
|
|
199
|
+
<div className="bn-testmeta__header">
|
|
200
|
+
<span className="bn-testmeta__label">{kind.toUpperCase()}</span>
|
|
201
|
+
{idField?.value && <span className="bn-testmeta__id">{idField.value}</span>}
|
|
202
|
+
<AddFieldMenu kind={kind} usedKeys={usedKeys} onPick={handleAddField} />
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{editableFields.length > 0 && (
|
|
206
|
+
<div className="bn-testmeta__rows">
|
|
207
|
+
{editableFields.map(({ field, index }) => (
|
|
208
|
+
<div className="bn-testmeta__row" key={index}>
|
|
209
|
+
<input
|
|
210
|
+
className="bn-testmeta__key bn-testmeta__key--input"
|
|
211
|
+
type="text"
|
|
212
|
+
value={field.key}
|
|
213
|
+
placeholder="key"
|
|
214
|
+
spellCheck={false}
|
|
215
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) => handleKeyChange(index, e.target.value)}
|
|
216
|
+
/>
|
|
217
|
+
<input
|
|
218
|
+
className="bn-testmeta__value"
|
|
219
|
+
type="text"
|
|
220
|
+
value={field.value}
|
|
221
|
+
placeholder="value"
|
|
222
|
+
spellCheck={false}
|
|
223
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) => handleValueChange(index, e.target.value)}
|
|
224
|
+
/>
|
|
225
|
+
<button
|
|
226
|
+
type="button"
|
|
227
|
+
className="bn-testmeta__remove"
|
|
228
|
+
aria-label="Remove field"
|
|
229
|
+
title="Remove field"
|
|
230
|
+
onClick={() => handleRemove(index)}
|
|
231
|
+
>
|
|
232
|
+
×
|
|
233
|
+
</button>
|
|
234
|
+
</div>
|
|
235
|
+
))}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
);
|