@konfuzio/document-validation-ui 0.1.24-dev.0 → 0.1.24-dev.2

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.
Files changed (31) hide show
  1. package/dist/css/app.css +1 -1
  2. package/dist/index.html +1 -1
  3. package/dist/js/app.js +1 -1
  4. package/dist/js/app.js.map +1 -1
  5. package/dist/js/chunk-vendors.js +1 -1
  6. package/dist/js/chunk-vendors.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/assets/scss/document_annotations.scss +5 -1
  9. package/src/assets/scss/document_search_bar.scss +71 -0
  10. package/src/assets/scss/document_toolbar.scss +2 -1
  11. package/src/assets/scss/theme.scss +2 -1
  12. package/src/assets/scss/variables.scss +1 -0
  13. package/src/components/App.vue +16 -0
  14. package/src/components/DocumentAnnotations/AnnotationActionButtons.vue +10 -0
  15. package/src/components/DocumentAnnotations/AnnotationRow.vue +11 -6
  16. package/src/components/DocumentAnnotations/DocumentAnnotations.vue +4 -5
  17. package/src/components/DocumentPage/DocumentPage.cy.js +82 -1
  18. package/src/components/DocumentPage/DocumentPage.vue +41 -1
  19. package/src/components/DocumentPage/DocumentToolbar.cy.js +12 -0
  20. package/src/components/DocumentPage/DocumentToolbar.vue +9 -1
  21. package/src/components/DocumentPage/NewAnnotation.vue +6 -2
  22. package/src/components/DocumentPage/ScrollingDocument.vue +6 -0
  23. package/src/components/DocumentPage/ScrollingPage.vue +26 -4
  24. package/src/components/DocumentPage/SearchBar.vue +130 -0
  25. package/src/components/DocumentTopBar/DocumentName.vue +10 -2
  26. package/src/icons.js +3 -1
  27. package/src/locales/de.json +7 -1
  28. package/src/locales/en.json +8 -1
  29. package/src/locales/es.json +7 -1
  30. package/src/store/display.js +114 -0
  31. package/src/store/document.js +1 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konfuzio/document-validation-ui",
3
- "version": "0.1.24-dev.0",
3
+ "version": "0.1.24-dev.2",
4
4
  "repository": "git://github.com:konfuzio-ai/document-validation-ui.git",
5
5
  "main": "dist/app.js",
6
6
  "scripts": {
@@ -515,8 +515,12 @@
515
515
 
516
516
  .missing-button-container {
517
517
  background-color: transparent;
518
+ display: flex;
519
+ flex-direction: row;
518
520
 
519
- .missing-btn {
521
+ .missing-btn,
522
+ .search-btn {
523
+ padding: 6px;
520
524
  color: $grey-blue !important;
521
525
  font-size: 14px !important;
522
526
  font-weight: 500;
@@ -0,0 +1,71 @@
1
+ @import "./imports.scss";
2
+ #document-search {
3
+ position: absolute;
4
+ right: 8px;
5
+ min-width: 276px;
6
+ width: fit-content;
7
+ z-index: 95;
8
+
9
+ .search-container {
10
+ background-color: $searchbar;
11
+ color: $white;
12
+ height: 40px;
13
+ padding: 8px 4px;
14
+ border-radius: 8px;
15
+ display: flex;
16
+ flex-direction: row;
17
+ justify-content: space-between;
18
+ align-items: center;
19
+
20
+ .button {
21
+ background: none;
22
+ color: inherit;
23
+
24
+ &:disabled {
25
+ pointer-events: none;
26
+ }
27
+
28
+ &:hover {
29
+ background: $low-opacity-white;
30
+ border-radius: 4px;
31
+ color: inherit;
32
+ }
33
+ .icon {
34
+ vertical-align: middle;
35
+ }
36
+ }
37
+
38
+ .search-input {
39
+ margin-left: 8px;
40
+ flex: 1;
41
+ font-size: 14px;
42
+ background: inherit;
43
+ color: inherit;
44
+ border: 0;
45
+ outline: none;
46
+ }
47
+
48
+ .search-no-results {
49
+ color: $text-lighter;
50
+ font-size: 12px;
51
+ }
52
+
53
+ .search-navigation {
54
+ display: flex;
55
+ flex-direction: row;
56
+ align-items: center;
57
+
58
+ .search-counters {
59
+ font-size: 12px;
60
+ margin-right: 6px;
61
+ color: $text-lighter;
62
+ }
63
+ }
64
+
65
+ .search-loading {
66
+ display: flex;
67
+ font-size: 12px;
68
+ color: $text-lighter;
69
+ }
70
+ }
71
+ }
@@ -95,7 +95,8 @@
95
95
  }
96
96
  }
97
97
 
98
- .download-file {
98
+ .download-file,
99
+ .search-icon {
99
100
  color: $toolbar-elements;
100
101
 
101
102
  .is-active {
@@ -253,7 +253,8 @@
253
253
  width: 100%;
254
254
 
255
255
  .icon {
256
- &.download-file {
256
+ &.download-file,
257
+ &.search-icon {
257
258
  svg {
258
259
  height: 100% !important;
259
260
  width: 100% !important;
@@ -48,6 +48,7 @@ $font-family: "Inter", sans-serif;
48
48
  $background: $white;
49
49
  $top-bar-background: var(--top-bar-background);
50
50
  $toolbar: $text-color;
51
+ $searchbar: $text-color;
51
52
  $tooltip: $dark;
52
53
  $toolbar-elements: $white;
53
54
 
@@ -72,6 +72,12 @@ export default {
72
72
  required: false,
73
73
  default: "",
74
74
  },
75
+ // eslint-disable-next-line vue/prop-name-casing
76
+ details_url: {
77
+ type: String,
78
+ required: false,
79
+ default: "",
80
+ },
75
81
  },
76
82
  computed: {
77
83
  documentId() {
@@ -115,6 +121,15 @@ export default {
115
121
  return null;
116
122
  }
117
123
  },
124
+ detailsUrl() {
125
+ if (process.env.VUE_APP_DOCUMENT_DETAILS_URL) {
126
+ return process.env.VUE_APP_DOCUMENT_DETAILS_URL;
127
+ } else if (this.details_url) {
128
+ return this.details_url;
129
+ } else {
130
+ return null;
131
+ }
132
+ },
118
133
  },
119
134
  created() {
120
135
  // Sentry config
@@ -156,6 +171,7 @@ export default {
156
171
 
157
172
  // document and project config
158
173
  Promise.all([
174
+ this.$store.dispatch("display/setDetailsUrl", this.detailsUrl),
159
175
  this.$store.dispatch("document/setDocId", this.documentId),
160
176
  this.$store.dispatch("document/setPublicView", this.isPublicView),
161
177
  this.$store.dispatch(
@@ -59,6 +59,13 @@
59
59
  <b-button type="is-ghost" class="missing-btn" @click.stop="markAsMissing">
60
60
  {{ $t("missing_annotation") }}
61
61
  </b-button>
62
+ <b-button
63
+ type="is-ghost"
64
+ class="search-btn"
65
+ @click.stop="searchInDocument"
66
+ >
67
+ {{ $t("search_in_document") }}
68
+ </b-button>
62
69
  </div>
63
70
 
64
71
  <!-- Restore not found annotations -->
@@ -141,6 +148,9 @@ export default {
141
148
  restore() {
142
149
  this.$emit("restore");
143
150
  },
151
+ searchInDocument() {
152
+ this.$emit("search-label-in-document");
153
+ },
144
154
  },
145
155
  };
146
156
  </script>
@@ -137,12 +137,13 @@
137
137
  :save-btn="showSaveButton()"
138
138
  :restore-btn="showRestoreButton()"
139
139
  :is-loading="isLoading"
140
- @mark-as-missing="handleMissingAnnotation()"
141
- @save="handleSaveChanges()"
142
- @accept="handleSaveChanges()"
140
+ @mark-as-missing="handleMissingAnnotation"
141
+ @save="handleSaveChanges"
142
+ @accept="handleSaveChanges"
143
143
  @decline="handleSaveChanges(true)"
144
- @cancel="handleCancelButton()"
145
- @restore="handleRestore()"
144
+ @cancel="handleCancelButton"
145
+ @restore="handleRestore"
146
+ @search-label-in-document="searchLabelInDocument"
146
147
  />
147
148
  </div>
148
149
  </div>
@@ -511,7 +512,7 @@ export default {
511
512
  false
512
513
  );
513
514
  },
514
- handleSaveChanges(decline) {
515
+ handleSaveChanges(decline = false) {
515
516
  if (this.publicView || this.isDocumentReviewed) return;
516
517
 
517
518
  // Verify if we are editing a filled or empty annotation
@@ -732,6 +733,10 @@ export default {
732
733
 
733
734
  window.open(annotationDetailsUrl, "_blank");
734
735
  },
736
+ searchLabelInDocument() {
737
+ this.$store.dispatch("display/enableSearch", true);
738
+ this.$store.dispatch("display/setCurrentSearch", this.label.name);
739
+ },
735
740
  },
736
741
  };
737
742
  </script>
@@ -63,8 +63,7 @@
63
63
  :icon="
64
64
  annotationSetsAccordion[indexGroup] ? 'angle-up' : 'angle-down'
65
65
  "
66
- >
67
- </b-icon>
66
+ />
68
67
  {{
69
68
  `${annotationSet.label_set.name} ${numberOfAnnotationSetGroup(
70
69
  annotationSet
@@ -388,9 +387,6 @@ export default {
388
387
  // Not allow starting edit mode with ArrowUp key
389
388
  if (event.key === "ArrowUp" && !this.isAnnotationBeingEdited) return;
390
389
 
391
- // open accordions
392
- this.openAllAccordions();
393
-
394
390
  // Get all the annotation elements
395
391
  let annotations = this.createArray("keyboard-nav");
396
392
 
@@ -404,6 +400,9 @@ export default {
404
400
 
405
401
  // navigate with the arrow up or down keys
406
402
  if (event.key === "ArrowDown") {
403
+ // open accordions
404
+ this.openAllAccordions();
405
+
407
406
  // Check if we are focusing on the Finish Review button
408
407
  if (this.count >= annotations.length) {
409
408
  const finishBtn = this.createArray("finish-review-btn");
@@ -1,5 +1,5 @@
1
- import DocumentDashboard from "../DocumentDashboard.vue";
2
1
  import DocumentPage from "../DocumentPage/DocumentPage.vue";
2
+ import SearchBar from "../DocumentPage/SearchBar.vue";
3
3
 
4
4
  const viewport = {
5
5
  width: 1280,
@@ -13,6 +13,87 @@ describe("Document Page", () => {
13
13
  cy.viewport(viewport.width, viewport.height);
14
14
  });
15
15
 
16
+ it("Search for text in the document", () => {
17
+ cy.getStore("document").then(($document) => {
18
+ if ($document.selectedDocument.pages[0]) {
19
+ cy.setScale($document.selectedDocument.pages[0]);
20
+ cy.fetchBlob(
21
+ `${$document.selectedDocument.pages[0].image_url}?${$document.selectedDocument.downloaded_at}`
22
+ ).then((blob) => {
23
+ cy.mount(SearchBar).then(({ wrapper, component }) => {
24
+ if ($document.pages && $document.pages.length > 0) {
25
+ const entities = $document.pages.flatMap((page) => {
26
+ return page.entities;
27
+ });
28
+ let entity;
29
+ do {
30
+ entity = entities[Math.floor(Math.random() * entities.length)]; // get a random entity
31
+ } while (!entity || entity.offset_string.length < 3);
32
+
33
+ cy.dispatchAction("display", "enableSearch", true);
34
+ cy.dispatchAction(
35
+ "display",
36
+ "setCurrentSearch",
37
+ entity.offset_string
38
+ );
39
+ cy.wait(2000);
40
+ cy.getStore("display").then(($display) => {
41
+ expect($display.searchResults.length).to.be.greaterThan(0);
42
+ });
43
+ }
44
+ });
45
+ });
46
+ } else {
47
+ throw new Error("Document not loaded");
48
+ }
49
+ });
50
+ });
51
+
52
+ it("Navigate to next search result if exists", () => {
53
+ cy.getStore("document").then(($document) => {
54
+ if ($document.selectedDocument.pages[0]) {
55
+ cy.setScale($document.selectedDocument.pages[0]);
56
+ cy.fetchBlob(
57
+ `${$document.selectedDocument.pages[0].image_url}?${$document.selectedDocument.downloaded_at}`
58
+ ).then((blob) => {
59
+ cy.mount(SearchBar).then(({ wrapper, component }) => {
60
+ cy.wait(2000);
61
+ if ($document.pages && $document.pages.length > 0) {
62
+ const entities = $document.pages.flatMap((page) => {
63
+ return page.entities;
64
+ });
65
+ let entity;
66
+ do {
67
+ entity = entities[Math.floor(Math.random() * entities.length)]; // get a random entity
68
+ } while (!entity || entity.offset_string.length < 3);
69
+
70
+ cy.dispatchAction("display", "enableSearch", true);
71
+ cy.dispatchAction(
72
+ "display",
73
+ "setCurrentSearch",
74
+ entity.offset_string
75
+ );
76
+ cy.wait(2000);
77
+ cy.getStore("display").then(($display) => {
78
+ if ($display.searchResults.length > 1) {
79
+ expect(component.currentCounter).to.be.eql(1);
80
+ cy.get("#document-search .next-search").click();
81
+ cy.getStore("display").then(() => {
82
+ expect(component.currentCounter).to.be.eql(2);
83
+ });
84
+ } else {
85
+ cy.get("#document-search .next-search").should("be.disabled");
86
+ }
87
+ });
88
+ }
89
+ });
90
+ });
91
+ } else {
92
+ throw new Error("Document not loaded");
93
+ }
94
+ });
95
+ });
96
+
16
97
  it("All annotations appear at the right place", () => {
17
98
  cy.getStore("document").then(($document) => {
18
99
  if (
@@ -51,6 +51,15 @@
51
51
  }"
52
52
  />
53
53
  <template v-if="pageInVisibleRange && !editMode">
54
+ <template v-if="searchResults.length > 0">
55
+ <v-rect
56
+ v-for="(bbox, index) in searchResults"
57
+ :key="'sr' + index"
58
+ :config="{
59
+ ...selectionTextRect(bbox, bbox === currentSearchResultForPage),
60
+ }"
61
+ ></v-rect>
62
+ </template>
54
63
  <v-group v-if="!publicView || !isDocumentReviewed" ref="entities">
55
64
  <v-rect
56
65
  v-for="(entity, index) in scaledEntities"
@@ -266,8 +275,25 @@ export default {
266
275
  );
267
276
  },
268
277
 
278
+ searchResults() {
279
+ const results = this.$store.getters["display/searchResultsForPage"](
280
+ this.page.number
281
+ );
282
+ return results;
283
+ },
284
+
285
+ currentSearchResultForPage() {
286
+ return this.$store.getters["display/currentSearchResultForPage"](
287
+ this.page.number
288
+ );
289
+ },
290
+
269
291
  ...mapState("selection", ["isSelecting", "selectedEntities"]),
270
- ...mapState("display", ["scale", "categorizeModalIsActive"]),
292
+ ...mapState("display", [
293
+ "scale",
294
+ "categorizeModalIsActive",
295
+ "searchEnabled",
296
+ ]),
271
297
  ...mapState("document", [
272
298
  "documentAnnotationSelected",
273
299
  "recalculatingAnnotations",
@@ -308,6 +334,11 @@ export default {
308
334
  this.drawPage(true);
309
335
  }
310
336
  },
337
+ searchEnabled(isEnabled) {
338
+ if (isEnabled) {
339
+ this.closePopups(true);
340
+ }
341
+ },
311
342
  },
312
343
  mounted() {
313
344
  if (
@@ -495,6 +526,15 @@ export default {
495
526
  }
496
527
  },
497
528
 
529
+ selectionTextRect(bbox, isFocused) {
530
+ return {
531
+ fill: isFocused ? "orange" : "greenyellow",
532
+ stroke: isFocused ? "orange" : "",
533
+ globalCompositeOperation: "multiply",
534
+ ...this.bboxToRect(this.page, bbox),
535
+ };
536
+ },
537
+
498
538
  /**
499
539
  * Builds the konva config object for the entity.
500
540
  */
@@ -6,6 +6,18 @@ describe("Document Toolbar", () => {
6
6
  cy.setFullMode();
7
7
  });
8
8
 
9
+ it("open document search", () => {
10
+ cy.mount(DocumentDashboard).then(({ wrapper, component }) => {
11
+ component.onDocumentResize();
12
+ cy.get("#toolbar-container").find(".search-document").first().click();
13
+
14
+ cy.getStore("display").then(($display) => {
15
+ expect($display.searchEnabled).to.eql(true);
16
+ cy.get("#document-search").should("exist");
17
+ });
18
+ });
19
+ });
20
+
9
21
  it("downloads the original file", () => {
10
22
  cy.mount(DocumentDashboard).then(({ wrapper, component }) => {
11
23
  component.onDocumentResize();
@@ -24,6 +24,13 @@
24
24
  </div>
25
25
  </b-tooltip>
26
26
  <div v-if="isEditModeAvailable" class="toolbar-divider" />
27
+ <div
28
+ v-if="!publicView"
29
+ class="search-document icons"
30
+ @click="toggleSearch"
31
+ >
32
+ <b-icon icon="search" size="small" class="search-icon" />
33
+ </div>
27
34
 
28
35
  <div v-if="!publicView" class="download-file icons">
29
36
  <b-dropdown aria-role="list" position="is-top-right" scrollable>
@@ -80,7 +87,7 @@
80
87
  </template>
81
88
 
82
89
  <script>
83
- import { mapState, mapGetters } from "vuex";
90
+ import { mapActions, mapState, mapGetters } from "vuex";
84
91
  import FitZoomIcon from "../../assets/images/FitZoomIcon";
85
92
  import PlusIcon from "../../assets/images/PlusIcon";
86
93
  import MinusIcon from "../../assets/images/MinusIcon";
@@ -144,6 +151,7 @@ export default {
144
151
  }
145
152
  },
146
153
  methods: {
154
+ ...mapActions("display", ["toggleSearch"]),
147
155
  handleEdit() {
148
156
  if (this.editModeDisabled) return;
149
157
  this.$store.dispatch("selection/disableSelection");
@@ -1,8 +1,12 @@
1
1
  <template>
2
2
  <div class="annotation-popup" :style="{ left: `${left}px`, top: `${top}px` }">
3
- <input v-model="textFromEntities" class="popup-input" type="text" />
3
+ <div v-if="!textFromEntities" class="popup-input">
4
+ <b-icon icon="spinner" class="fa-spin loading-icon-size spinner" />
5
+ </div>
6
+ <input v-else v-model="textFromEntities" class="popup-input" type="text" />
4
7
  <b-dropdown
5
8
  v-model="selectedSet"
9
+ :disabled="!textFromEntities"
6
10
  aria-role="list"
7
11
  :class="[
8
12
  'annotation-dropdown',
@@ -70,7 +74,7 @@
70
74
  <b-dropdown
71
75
  v-model="selectedLabel"
72
76
  aria-role="list"
73
- :disabled="!labels || labels.length === 0"
77
+ :disabled="!textFromEntities || !labels || labels.length === 0"
74
78
  scrollable
75
79
  class="label-dropdown annotation-dropdown"
76
80
  >
@@ -5,6 +5,9 @@
5
5
  v-scroll.immediate="updateScrollBounds"
6
6
  class="scrolling-document"
7
7
  >
8
+ <transition :name="searchEnabled ? 'slide-down' : 'slide-up'">
9
+ <SearchBar v-if="searchEnabled" />
10
+ </transition>
8
11
  <div
9
12
  v-if="
10
13
  selectedDocument && scale && !loading && !recalculatingAnnotations
@@ -36,12 +39,14 @@ import scroll from "../../directives/scroll";
36
39
  import ScrollingPage from "./ScrollingPage";
37
40
  import Toolbar from "./DocumentToolbar";
38
41
  import ActionBar from "./ActionBar";
42
+ import SearchBar from "./SearchBar";
39
43
 
40
44
  export default {
41
45
  components: {
42
46
  ScrollingPage,
43
47
  Toolbar,
44
48
  ActionBar,
49
+ SearchBar,
45
50
  },
46
51
  directives: {
47
52
  scroll,
@@ -73,6 +78,7 @@ export default {
73
78
  "documentActionBar",
74
79
  "pageChangedFromThumbnail",
75
80
  "currentPage",
81
+ "searchEnabled",
76
82
  ]),
77
83
  ...mapGetters("display", ["visiblePageRange"]),
78
84
 
@@ -49,7 +49,9 @@ export default {
49
49
  },
50
50
 
51
51
  computed: {
52
- ...mapState("display", ["pageChangedFromThumbnail"]),
52
+ ...mapState("display", ["pageChangedFromThumbnail", "currentPage"]),
53
+ ...mapState("document", ["pages", "documentAnnotationSelected", "loading"]),
54
+ ...mapState("edit", ["editMode"]),
53
55
  ...mapGetters("display", ["visiblePageRange", "bboxToRect"]),
54
56
  ...mapGetters("document", ["scrollDocumentToAnnotation"]),
55
57
 
@@ -90,9 +92,11 @@ export default {
90
92
  return this.scrollTop + this.clientHeight;
91
93
  },
92
94
 
93
- ...mapState("display", ["currentPage"]),
94
- ...mapState("document", ["pages", "documentAnnotationSelected", "loading"]),
95
- ...mapState("edit", ["editMode"]),
95
+ currentSearchResultForPage() {
96
+ return this.$store.getters["display/currentSearchResultForPage"](
97
+ this.page.number
98
+ );
99
+ },
96
100
  },
97
101
 
98
102
  watch: {
@@ -128,6 +132,24 @@ export default {
128
132
  this.$emit("page-jump", this.elementTop, 0);
129
133
  }
130
134
  },
135
+ /**
136
+ * Scroll to the search result if the current one changes and it's on this page.
137
+ */
138
+ currentSearchResultForPage(res) {
139
+ // skip page jump if the result is null (the current search result is not on this page)
140
+ if (!res) {
141
+ return;
142
+ }
143
+ const y = this.getYForBbox(res); // y of the search result
144
+ const totalY = y + this.elementTop; // y of search result + page top
145
+ // skip page jump if the search result is already visible on this page
146
+ if (totalY < this.scrollBottom && totalY > this.scrollTop) {
147
+ return;
148
+ }
149
+ this.$nextTick(function () {
150
+ this.scrollTo(y);
151
+ });
152
+ },
131
153
  },
132
154
  mounted() {
133
155
  this.updateElementBounds();