@konfuzio/document-validation-ui 0.1.39-dev.0 → 0.1.39
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/dist/css/app.css +1 -1
- package/dist/index.html +1 -1
- package/dist/js/app.js +1 -1
- package/dist/js/app.js.map +1 -1
- package/package.json +1 -1
- package/src/assets/scss/document_annotations.scss +15 -7
- package/src/assets/scss/theme.scss +22 -0
- package/src/components/DocumentAnnotations/AnnotationFilters.vue +66 -12
- package/src/components/DocumentAnnotations/DocumentAnnotations.vue +47 -7
- package/src/components/DocumentAnnotations/EmptyState.vue +9 -2
- package/src/store/display.js +2 -10
- package/src/store/document.js +97 -7
- package/src/utils/utils.js +23 -0
package/package.json
CHANGED
|
@@ -49,14 +49,22 @@
|
|
|
49
49
|
overflow: auto;
|
|
50
50
|
height: 100vh;
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
padding: 16px 16px 0px 16px;
|
|
52
|
+
.annotation-options {
|
|
54
53
|
display: flex;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
justify-content: space-between;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
padding: 16px 16px 0px 16px;
|
|
57
|
+
gap: 16px;
|
|
58
|
+
|
|
59
|
+
#annotation-filters {
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-direction: row;
|
|
62
|
+
width: 100%;
|
|
63
|
+
justify-content: space-around;
|
|
64
|
+
gap: 12px;
|
|
65
|
+
span {
|
|
66
|
+
font-size: 14px;
|
|
67
|
+
}
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
70
|
|
|
@@ -309,6 +309,28 @@
|
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
|
+
.taginput-container {
|
|
313
|
+
&.is-focused {
|
|
314
|
+
box-shadow: none !important;
|
|
315
|
+
border-color: $primary !important;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
.field {
|
|
319
|
+
input {
|
|
320
|
+
box-shadow: none !important;
|
|
321
|
+
&:focus {
|
|
322
|
+
border-color: $primary;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
.control.has-icons-left {
|
|
326
|
+
.icon {
|
|
327
|
+
svg {
|
|
328
|
+
height: 20px;
|
|
329
|
+
width: 20px;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
312
334
|
|
|
313
335
|
.b-checkbox.checkbox {
|
|
314
336
|
input[type="checkbox"] + .check {
|
|
@@ -1,22 +1,76 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
<div class="annotation-options">
|
|
3
|
+
<div id="annotation-search">
|
|
4
|
+
<b-field>
|
|
5
|
+
<b-taginput
|
|
6
|
+
v-model="search"
|
|
7
|
+
ellipsis
|
|
8
|
+
icon="search"
|
|
9
|
+
:placeholder="$t('search')"
|
|
10
|
+
>
|
|
11
|
+
<template #tag="props">
|
|
12
|
+
<span>{{ labelNameForAnnotationId(props.tag) || props.tag }}</span>
|
|
13
|
+
</template>
|
|
14
|
+
</b-taginput>
|
|
15
|
+
</b-field>
|
|
16
|
+
</div>
|
|
17
|
+
<div id="annotation-filters">
|
|
18
|
+
<b-switch
|
|
19
|
+
v-model="annotationFilters.showFeedbackNeeded"
|
|
20
|
+
class="is-small"
|
|
21
|
+
>{{ $t("human_feedback_needed") }}</b-switch
|
|
22
|
+
>
|
|
23
|
+
<b-switch v-model="annotationFilters.showEmpty" class="is-small">{{
|
|
24
|
+
$t("label_missing_annotations")
|
|
25
|
+
}}</b-switch>
|
|
26
|
+
<b-switch v-model="annotationFilters.showAccepted" class="is-small">{{
|
|
27
|
+
$t("accepted_annotations")
|
|
28
|
+
}}</b-switch>
|
|
29
|
+
</div>
|
|
12
30
|
</div>
|
|
13
31
|
</template>
|
|
14
32
|
<script>
|
|
15
|
-
import { mapState } from "vuex";
|
|
33
|
+
import { mapGetters, mapState } from "vuex";
|
|
16
34
|
export default {
|
|
17
35
|
name: "AnnotationFilters",
|
|
36
|
+
data() {
|
|
37
|
+
return {
|
|
38
|
+
search: [],
|
|
39
|
+
};
|
|
40
|
+
},
|
|
18
41
|
computed: {
|
|
19
|
-
...mapState("document", [
|
|
42
|
+
...mapState("document", [
|
|
43
|
+
"annotationSets",
|
|
44
|
+
"annotationFilters",
|
|
45
|
+
"annotationSearch",
|
|
46
|
+
]),
|
|
47
|
+
...mapGetters("document", ["annotationById", "labelOfAnnotation"]),
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
watch: {
|
|
51
|
+
search() {
|
|
52
|
+
if (this.search.length > 0) {
|
|
53
|
+
this.$emit("openAll");
|
|
54
|
+
}
|
|
55
|
+
if (this.search != this.annotationSearch) {
|
|
56
|
+
this.$store.dispatch("document/setAnnotationSearch", this.search);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
mounted() {
|
|
61
|
+
this.search = this.annotationSearch;
|
|
62
|
+
},
|
|
63
|
+
methods: {
|
|
64
|
+
labelNameForAnnotationId(annotationId) {
|
|
65
|
+
const annotation = this.annotationById(Number(annotationId));
|
|
66
|
+
if (annotation) {
|
|
67
|
+
const label = this.labelOfAnnotation(annotation);
|
|
68
|
+
if (label) {
|
|
69
|
+
return label.name;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
},
|
|
20
74
|
},
|
|
21
75
|
};
|
|
22
76
|
</script>
|
|
@@ -21,14 +21,30 @@
|
|
|
21
21
|
|
|
22
22
|
<!-- When there's no annotation sets -->
|
|
23
23
|
<div
|
|
24
|
-
v-else-if="
|
|
24
|
+
v-else-if="
|
|
25
|
+
getAnnotationsFiltered.annotationSets.length === 0 &&
|
|
26
|
+
!isSearchingAnnotationList
|
|
27
|
+
"
|
|
25
28
|
class="empty-annotation-sets"
|
|
26
29
|
>
|
|
27
30
|
<EmptyState />
|
|
28
31
|
</div>
|
|
29
32
|
|
|
30
33
|
<div v-else ref="annotationList" :class="['annotation-set-list']">
|
|
31
|
-
<AnnotationFilters
|
|
34
|
+
<AnnotationFilters
|
|
35
|
+
v-if="isDocumentEditable"
|
|
36
|
+
@openAll="openAllAccordions"
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<div
|
|
40
|
+
v-if="
|
|
41
|
+
getAnnotationsFiltered.annotationSets.length === 0 &&
|
|
42
|
+
isSearchingAnnotationList
|
|
43
|
+
"
|
|
44
|
+
class="empty-annotation-sets"
|
|
45
|
+
>
|
|
46
|
+
<EmptyState :is-search="true" />
|
|
47
|
+
</div>
|
|
32
48
|
|
|
33
49
|
<div
|
|
34
50
|
v-if="Object.entries(annotationSetsInTable()).length > 0"
|
|
@@ -131,10 +147,19 @@
|
|
|
131
147
|
</div>
|
|
132
148
|
</div>
|
|
133
149
|
|
|
134
|
-
<div v-if="annotationSet.labels.length === 0" class="no-labels">
|
|
135
|
-
<span>
|
|
136
|
-
|
|
137
|
-
|
|
150
|
+
<div v-else-if="annotationSet.labels.length === 0" class="no-labels">
|
|
151
|
+
<span>
|
|
152
|
+
{{
|
|
153
|
+
isSearchingAnnotationList
|
|
154
|
+
? $t("no_results")
|
|
155
|
+
: $t("no_labels_in_set")
|
|
156
|
+
}}</span
|
|
157
|
+
>
|
|
158
|
+
<!-- eslint-disable vue/no-v-html -->
|
|
159
|
+
<span
|
|
160
|
+
v-if="isDocumentEditable && !isSearchingAnnotationList"
|
|
161
|
+
v-html="$t('link_to_add_labels')"
|
|
162
|
+
/>
|
|
138
163
|
</div>
|
|
139
164
|
|
|
140
165
|
<div
|
|
@@ -205,6 +230,7 @@ export default {
|
|
|
205
230
|
"isDocumentReviewed",
|
|
206
231
|
"annotationSetOfAnnotation",
|
|
207
232
|
"isAnnotationInAnnotationSet",
|
|
233
|
+
"isSearchingAnnotationList",
|
|
208
234
|
]),
|
|
209
235
|
isAnnotationBeingEdited() {
|
|
210
236
|
return this.editAnnotation && this.editAnnotation.id;
|
|
@@ -231,6 +257,12 @@ export default {
|
|
|
231
257
|
oldAnnotationSets
|
|
232
258
|
);
|
|
233
259
|
},
|
|
260
|
+
getAnnotationsFiltered(newFiltered, oldFiltered) {
|
|
261
|
+
this.loadAccordions(
|
|
262
|
+
newFiltered.annotationSets,
|
|
263
|
+
oldFiltered.annotationSets
|
|
264
|
+
);
|
|
265
|
+
},
|
|
234
266
|
annotationId(newAnnotationId) {
|
|
235
267
|
if (newAnnotationId) {
|
|
236
268
|
const annotationSet = this.annotationSetOfAnnotation(newAnnotationId);
|
|
@@ -255,6 +287,11 @@ export default {
|
|
|
255
287
|
window.removeEventListener("keydown", this.keyDownHandler);
|
|
256
288
|
},
|
|
257
289
|
methods: {
|
|
290
|
+
annotationSetShouldAppear(annotationSet) {
|
|
291
|
+
return !(
|
|
292
|
+
annotationSet.labels.length === 0 && this.isSearchingAnnotationList
|
|
293
|
+
);
|
|
294
|
+
},
|
|
258
295
|
toggleAccordion(index) {
|
|
259
296
|
const newAnnotationSetsAccordion = [...this.annotationSetsAccordion];
|
|
260
297
|
newAnnotationSetsAccordion[index] = !newAnnotationSetsAccordion[index];
|
|
@@ -288,11 +325,13 @@ export default {
|
|
|
288
325
|
newAnnotationSets.forEach((newAnnotationSet) => {
|
|
289
326
|
const existed = oldAnnotationSets.find(
|
|
290
327
|
(oldAnnotationSet) =>
|
|
328
|
+
oldAnnotationSet &&
|
|
329
|
+
newAnnotationSet &&
|
|
291
330
|
oldAnnotationSet.id &&
|
|
292
331
|
newAnnotationSet.id &&
|
|
293
332
|
oldAnnotationSet.id === newAnnotationSet.id
|
|
294
333
|
);
|
|
295
|
-
if (!existed && newAnnotationSet.id !== null) {
|
|
334
|
+
if (!existed && newAnnotationSet && newAnnotationSet.id !== null) {
|
|
296
335
|
annotationSetsCreated.push(newAnnotationSet);
|
|
297
336
|
}
|
|
298
337
|
});
|
|
@@ -301,6 +340,7 @@ export default {
|
|
|
301
340
|
newAnnotationSets.forEach((newAnnotationSet, index) => {
|
|
302
341
|
const wasOpen = annotationSetsOpened.find(
|
|
303
342
|
(annotationSetOpened) =>
|
|
343
|
+
annotationSetOpened &&
|
|
304
344
|
annotationSetOpened.id &&
|
|
305
345
|
newAnnotationSet.id &&
|
|
306
346
|
newAnnotationSet.id === annotationSetOpened.id
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<p class="title">
|
|
6
6
|
{{ $t("no_label_sets_found") }}
|
|
7
7
|
</p>
|
|
8
|
-
<p class="description">
|
|
8
|
+
<p v-if="!isSearch" class="description">
|
|
9
9
|
{{ $t("no_label_sets_found_description") }}
|
|
10
10
|
</p>
|
|
11
11
|
</div>
|
|
@@ -15,7 +15,14 @@
|
|
|
15
15
|
import EmptyStateImg from "../../assets/images/EmptyStateImg";
|
|
16
16
|
export default {
|
|
17
17
|
name: "EmptyState",
|
|
18
|
-
components: { EmptyStateImg }
|
|
18
|
+
components: { EmptyStateImg },
|
|
19
|
+
props: {
|
|
20
|
+
isSearch: {
|
|
21
|
+
type: Boolean,
|
|
22
|
+
required: false,
|
|
23
|
+
default: false,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
19
26
|
};
|
|
20
27
|
</script>
|
|
21
28
|
<style scoped lang="scss" src="../../assets/scss/empty_state.scss"></style>
|
package/src/store/display.js
CHANGED
|
@@ -7,17 +7,9 @@ import {
|
|
|
7
7
|
MINIMUM_OPTIMIZED_APP_WIDTH,
|
|
8
8
|
} from "../constants";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
import { debounce } from "../utils/utils";
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
let timer;
|
|
14
|
-
return (...args) => {
|
|
15
|
-
clearTimeout(timer);
|
|
16
|
-
timer = setTimeout(() => {
|
|
17
|
-
cb(...args);
|
|
18
|
-
}, duration);
|
|
19
|
-
};
|
|
20
|
-
};
|
|
12
|
+
const HTTP = myImports.HTTP;
|
|
21
13
|
|
|
22
14
|
const floor = (value, precision) => {
|
|
23
15
|
const multiplier = Math.pow(10, precision || 0);
|
package/src/store/document.js
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
navigateToNewDocumentURL,
|
|
7
7
|
getURLPath,
|
|
8
8
|
setURLAnnotationHash,
|
|
9
|
+
setURLQueryParam,
|
|
10
|
+
debounce,
|
|
9
11
|
} from "../utils/utils";
|
|
10
12
|
|
|
11
13
|
const HTTP = myImports.HTTP;
|
|
@@ -53,6 +55,8 @@ const state = {
|
|
|
53
55
|
? false
|
|
54
56
|
: true,
|
|
55
57
|
},
|
|
58
|
+
annotationSearch:
|
|
59
|
+
(getURLQueryParam("search") && getURLQueryParam("search").split(",")) || [],
|
|
56
60
|
};
|
|
57
61
|
const getters = {
|
|
58
62
|
/**
|
|
@@ -302,12 +306,60 @@ const getters = {
|
|
|
302
306
|
let processedAnnotationSets = [];
|
|
303
307
|
let processedLabels = [];
|
|
304
308
|
|
|
309
|
+
// search feature
|
|
310
|
+
const addAnnotation = (listToAdd, annotation, force) => {
|
|
311
|
+
if (force) {
|
|
312
|
+
listToAdd.push(annotation);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
if (state.annotationSearch.length > 0) {
|
|
316
|
+
if (
|
|
317
|
+
annotation.offset_string &&
|
|
318
|
+
state.annotationSearch.find((search) =>
|
|
319
|
+
annotation.offset_string
|
|
320
|
+
.toLowerCase()
|
|
321
|
+
.includes(search.toLowerCase())
|
|
322
|
+
)
|
|
323
|
+
) {
|
|
324
|
+
listToAdd.push(annotation);
|
|
325
|
+
return true;
|
|
326
|
+
} else if (
|
|
327
|
+
annotation.id &&
|
|
328
|
+
state.annotationSearch.find((search) => `${annotation.id}` === search)
|
|
329
|
+
) {
|
|
330
|
+
listToAdd.push(annotation);
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
listToAdd.push(annotation);
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const labelHasSearchText = (label) => {
|
|
341
|
+
if (state.annotationSearch.length > 0) {
|
|
342
|
+
if (
|
|
343
|
+
label.name &&
|
|
344
|
+
state.annotationSearch.find((search) =>
|
|
345
|
+
label.name.toLowerCase().includes(search.toLowerCase())
|
|
346
|
+
)
|
|
347
|
+
) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
};
|
|
355
|
+
|
|
305
356
|
if (state.annotationSets) {
|
|
306
357
|
state.annotationSets.forEach((annotationSet) => {
|
|
307
358
|
labels = [];
|
|
308
359
|
annotationSet.labels.forEach((label) => {
|
|
309
360
|
const labelAnnotations = [];
|
|
310
361
|
let addLabel = false;
|
|
362
|
+
const labelHasSearch = labelHasSearchText(label);
|
|
311
363
|
if (
|
|
312
364
|
!state.annotationFilters.showEmpty ||
|
|
313
365
|
!state.annotationFilters.showFeedbackNeeded ||
|
|
@@ -323,22 +375,42 @@ const getters = {
|
|
|
323
375
|
state.annotationFilters.showFeedbackNeeded &&
|
|
324
376
|
annotation.revised === false
|
|
325
377
|
) {
|
|
326
|
-
|
|
327
|
-
|
|
378
|
+
const added = addAnnotation(
|
|
379
|
+
labelAnnotations,
|
|
380
|
+
annotation,
|
|
381
|
+
labelHasSearch
|
|
382
|
+
);
|
|
383
|
+
if (added) {
|
|
384
|
+
addLabel = true;
|
|
385
|
+
}
|
|
328
386
|
}
|
|
329
387
|
if (
|
|
330
388
|
state.annotationFilters.showAccepted &&
|
|
331
389
|
annotation.revised === true
|
|
332
390
|
) {
|
|
333
|
-
|
|
334
|
-
|
|
391
|
+
const added = addAnnotation(
|
|
392
|
+
labelAnnotations,
|
|
393
|
+
annotation,
|
|
394
|
+
labelHasSearch
|
|
395
|
+
);
|
|
396
|
+
if (added) {
|
|
397
|
+
addLabel = true;
|
|
398
|
+
}
|
|
335
399
|
}
|
|
336
400
|
});
|
|
337
401
|
}
|
|
338
402
|
} else {
|
|
339
403
|
// add annotations to the document array
|
|
340
|
-
|
|
341
|
-
|
|
404
|
+
label.annotations.forEach((annotation) => {
|
|
405
|
+
const added = addAnnotation(
|
|
406
|
+
labelAnnotations,
|
|
407
|
+
annotation,
|
|
408
|
+
labelHasSearch
|
|
409
|
+
);
|
|
410
|
+
if (added) {
|
|
411
|
+
addLabel = true;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
342
414
|
}
|
|
343
415
|
if (addLabel) {
|
|
344
416
|
labels.push({ ...label, annotations: labelAnnotations });
|
|
@@ -346,7 +418,11 @@ const getters = {
|
|
|
346
418
|
}
|
|
347
419
|
annotations.push(...labelAnnotations);
|
|
348
420
|
});
|
|
349
|
-
|
|
421
|
+
|
|
422
|
+
// if in search do not add the annotation set
|
|
423
|
+
if (!(state.annotationSearch.length > 0 && labels.length === 0)) {
|
|
424
|
+
processedAnnotationSets.push({ ...annotationSet, labels });
|
|
425
|
+
}
|
|
350
426
|
});
|
|
351
427
|
}
|
|
352
428
|
|
|
@@ -484,6 +560,13 @@ const getters = {
|
|
|
484
560
|
}
|
|
485
561
|
},
|
|
486
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Checks if it's currently searching for annotations
|
|
565
|
+
*/
|
|
566
|
+
isSearchingAnnotationList: (state) => {
|
|
567
|
+
return state.annotationSearch && state.annotationSearch.length > 0;
|
|
568
|
+
},
|
|
569
|
+
|
|
487
570
|
/**
|
|
488
571
|
* Get number of empty labels per annotation set
|
|
489
572
|
*/
|
|
@@ -873,6 +956,9 @@ const actions = {
|
|
|
873
956
|
setSplittingSuggestions: ({ commit }, value) => {
|
|
874
957
|
commit("SET_SPLITTING_SUGGESTIONS", value);
|
|
875
958
|
},
|
|
959
|
+
setAnnotationSearch: ({ commit }, value) => {
|
|
960
|
+
commit("SET_ANNOTATION_SEARCH", value);
|
|
961
|
+
},
|
|
876
962
|
|
|
877
963
|
/**
|
|
878
964
|
* Actions that use HTTP requests always return the axios promise,
|
|
@@ -1587,6 +1673,10 @@ const mutations = {
|
|
|
1587
1673
|
SET_SPLITTING_SUGGESTIONS: (state, array) => {
|
|
1588
1674
|
state.splittingSuggestions = array;
|
|
1589
1675
|
},
|
|
1676
|
+
SET_ANNOTATION_SEARCH: (state, search) => {
|
|
1677
|
+
state.annotationSearch = search;
|
|
1678
|
+
setURLQueryParam("search", search);
|
|
1679
|
+
},
|
|
1590
1680
|
};
|
|
1591
1681
|
|
|
1592
1682
|
export default {
|
package/src/utils/utils.js
CHANGED
|
@@ -35,6 +35,19 @@ export function getURLValueFromHash(value) {
|
|
|
35
35
|
return id;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export function setURLQueryParam(query, value, deleteParam = "") {
|
|
39
|
+
const url = new URL(window.location.href);
|
|
40
|
+
if (value != "") {
|
|
41
|
+
if (deleteParam != "") {
|
|
42
|
+
url.searchParams.delete(deleteParam);
|
|
43
|
+
}
|
|
44
|
+
url.searchParams.set(query, value);
|
|
45
|
+
} else {
|
|
46
|
+
url.searchParams.delete(query);
|
|
47
|
+
}
|
|
48
|
+
window.history.pushState(null, "", url.toString());
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
export function setURLAnnotationHash(annotationId) {
|
|
39
52
|
if (annotationId) {
|
|
40
53
|
window.location.hash = `ann${annotationId}`;
|
|
@@ -69,3 +82,13 @@ export function navigateToDocumentsList(path, projectId, userId) {
|
|
|
69
82
|
export function isElementArray(element) {
|
|
70
83
|
return Array.isArray(element);
|
|
71
84
|
}
|
|
85
|
+
|
|
86
|
+
export function debounce(cb, duration) {
|
|
87
|
+
let timer;
|
|
88
|
+
return (...args) => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
timer = setTimeout(() => {
|
|
91
|
+
cb(...args);
|
|
92
|
+
}, duration);
|
|
93
|
+
};
|
|
94
|
+
}
|