You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

352 lines
8.5 KiB

5 years ago
  1. import * as marked from 'marked';
  2. import VueJsonPretty from 'vue-json-pretty';
  3. import isEmpty from 'lodash.isempty';
  4. import HTTP from './http';
  5. import Preview from './preview.vue';
  6. const getOffsetFromUrl = (url) => {
  7. const offsetMatch = url.match(/[?#].*offset=(\d+)/);
  8. if (offsetMatch == null) {
  9. return 0;
  10. }
  11. return parseInt(offsetMatch[1], 10);
  12. };
  13. const removeHost = (url) => {
  14. if (!url) {
  15. return url;
  16. }
  17. const hostMatch = url.match(/^https?:\/\/[^/]*\/(.*)$/);
  18. if (hostMatch == null) {
  19. return url;
  20. }
  21. return `${window.location.origin}/${hostMatch[1]}`;
  22. };
  23. const storeOffsetInUrl = (offset) => {
  24. let href = window.location.href;
  25. const fragmentStart = href.indexOf('#') + 1;
  26. if (fragmentStart === 0) {
  27. href += '#offset=' + offset;
  28. } else {
  29. const prefix = href.substring(0, fragmentStart);
  30. const fragment = href.substring(fragmentStart);
  31. const newFragment = fragment.split('&').map((fragmentPart) => {
  32. const keyValue = fragmentPart.split('=');
  33. return keyValue[0] === 'offset'
  34. ? 'offset=' + offset
  35. : fragmentPart;
  36. }).join('&');
  37. href = prefix + newFragment;
  38. }
  39. window.location.href = href;
  40. };
  41. const getLimitFromUrl = (url, prevLimit) => {
  42. try {
  43. const limitMatch = url.match(/[?#].*limit=(\d+)/);
  44. return parseInt(limitMatch[1], 10);
  45. } catch (err) {
  46. return prevLimit;
  47. }
  48. };
  49. const getSidebarTotal = (count, limit) => (
  50. count !== 0 && limit !== 0
  51. ? Math.ceil(count / limit)
  52. : 0
  53. );
  54. const getSidebarPage = (offset, limit) => (
  55. limit !== 0
  56. ? Math.ceil(offset / limit) + 1
  57. : 0
  58. );
  59. export default {
  60. components: { VueJsonPretty, Preview },
  61. data() {
  62. return {
  63. pageNumber: 0,
  64. docs: [],
  65. annotations: [],
  66. labels: [],
  67. guideline: '',
  68. total: 0,
  69. remaining: 0,
  70. searchQuery: '',
  71. url: '',
  72. offset: getOffsetFromUrl(window.location.href),
  73. picked: 'all',
  74. ordering: '',
  75. count: 0,
  76. prevLimit: 0,
  77. paginationPages: 0,
  78. paginationPage: 0,
  79. singleClassClassification: false,
  80. isAnnotationApprover: false,
  81. isMetadataActive: false,
  82. isAnnotationGuidelineActive: false,
  83. };
  84. },
  85. methods: {
  86. resetScrollbar() {
  87. const textbox = this.$refs.textbox;
  88. if (textbox) {
  89. textbox.scrollTop = 0;
  90. }
  91. },
  92. async nextPage() {
  93. this.pageNumber += 1;
  94. if (this.pageNumber === this.docs.length) {
  95. if (this.next) {
  96. this.url = this.next;
  97. await this.search();
  98. this.pageNumber = 0;
  99. } else {
  100. this.pageNumber = this.docs.length - 1;
  101. }
  102. } else {
  103. this.resetScrollbar();
  104. }
  105. },
  106. async prevPage() {
  107. this.pageNumber -= 1;
  108. if (this.pageNumber === -1) {
  109. if (this.prev) {
  110. this.url = this.prev;
  111. await this.search();
  112. this.pageNumber = this.docs.length - 1;
  113. } else {
  114. this.pageNumber = 0;
  115. }
  116. } else {
  117. this.resetScrollbar();
  118. }
  119. },
  120. async nextPagination() {
  121. if (this.next) {
  122. this.url = this.next;
  123. await this.search();
  124. this.pageNumber = 0;
  125. } else {
  126. this.pageNumber = this.docs.length - 1;
  127. }
  128. this.resetScrollbar();
  129. },
  130. async prevPagination() {
  131. if (this.prev) {
  132. this.url = this.prev;
  133. await this.search();
  134. this.pageNumber = this.docs.length - this.limit;
  135. } else {
  136. this.pageNumber = 0;
  137. }
  138. this.resetScrollbar();
  139. },
  140. async search() {
  141. await HTTP.get(this.url).then((response) => {
  142. this.docs = response.data.results;
  143. this.next = removeHost(response.data.next);
  144. this.prev = removeHost(response.data.previous);
  145. this.count = response.data.count;
  146. this.annotations = this.docs.map(doc => doc.annotations);
  147. this.offset = getOffsetFromUrl(this.url);
  148. this.prevLimit = this.limit;
  149. if (this.next || this.prevLimit) {
  150. this.limit = getLimitFromUrl(this.next, this.prevLimit);
  151. } else {
  152. this.limit = this.count;
  153. }
  154. this.paginationPages = getSidebarTotal(this.count, this.limit);
  155. this.paginationPage = getSidebarPage(this.offset, this.limit);
  156. });
  157. },
  158. documentMetadataFor(i) {
  159. const document = this.docs[i];
  160. if (document == null || document.meta == null) {
  161. return null;
  162. }
  163. const metadata = JSON.parse(document.meta);
  164. if (isEmpty(metadata)) {
  165. return null;
  166. }
  167. return metadata;
  168. },
  169. getState() {
  170. if (this.picked === 'all') {
  171. return '';
  172. }
  173. if (this.picked === 'active') {
  174. return 'true';
  175. }
  176. return 'false';
  177. },
  178. async submit() {
  179. const state = this.getState();
  180. this.url = `docs?q=${this.searchQuery}&is_checked=${state}&offset=${this.offset}&ordering=${this.ordering}`;
  181. await this.search();
  182. this.pageNumber = 0;
  183. },
  184. removeLabel(annotation) {
  185. const docId = this.docs[this.pageNumber].id;
  186. return HTTP.delete(`docs/${docId}/annotations/${annotation.id}`).then(() => {
  187. const index = this.annotations[this.pageNumber].indexOf(annotation);
  188. this.annotations[this.pageNumber].splice(index, 1);
  189. });
  190. },
  191. replaceNull(shortcut) {
  192. if (shortcut == null) {
  193. shortcut = '';
  194. }
  195. shortcut = shortcut.split(' ');
  196. return shortcut;
  197. },
  198. shortcutKey(label) {
  199. let shortcut = label.suffix_key;
  200. if (label.prefix_key) {
  201. shortcut = `${label.prefix_key} ${shortcut}`;
  202. }
  203. return shortcut;
  204. },
  205. approveDocumentAnnotations() {
  206. const document = this.docs[this.pageNumber];
  207. const approved = !this.documentAnnotationsAreApproved;
  208. HTTP.post(`docs/${document.id}/approve-labels`, { approved }).then((response) => {
  209. Object.assign(this.docs[this.pageNumber], response.data);
  210. });
  211. },
  212. },
  213. watch: {
  214. picked() {
  215. this.submit();
  216. },
  217. ordering() {
  218. this.offset = 0;
  219. this.submit();
  220. },
  221. annotations() {
  222. // fetch progress info.
  223. HTTP.get('statistics?include=total&include=remaining').then((response) => {
  224. this.total = response.data.total;
  225. this.remaining = response.data.remaining;
  226. });
  227. },
  228. offset() {
  229. storeOffsetInUrl(this.offset);
  230. },
  231. },
  232. created() {
  233. HTTP.get('labels').then((response) => {
  234. this.labels = response.data;
  235. });
  236. HTTP.get().then((response) => {
  237. this.singleClassClassification = response.data.single_class_classification;
  238. this.guideline = response.data.guideline;
  239. const roles = response.data.current_users_role;
  240. this.isAnnotationApprover = roles.is_annotation_approver || roles.is_project_admin;
  241. });
  242. this.submit();
  243. },
  244. computed: {
  245. achievement() {
  246. const done = this.total - this.remaining;
  247. const percentage = Math.round(done / this.total * 100);
  248. return this.total > 0 ? percentage : 0;
  249. },
  250. compiledMarkdown() {
  251. const documentMetadata = this.documentMetadata;
  252. const guideline = documentMetadata && documentMetadata.guideline
  253. ? documentMetadata.guideline
  254. : this.guideline;
  255. return marked(guideline, {
  256. sanitize: true,
  257. });
  258. },
  259. documentAnnotationsAreApproved() {
  260. const document = this.docs[this.pageNumber];
  261. return document != null && document.annotation_approver != null;
  262. },
  263. documentAnnotationsApprovalTooltip() {
  264. const document = this.docs[this.pageNumber];
  265. return this.documentAnnotationsAreApproved
  266. ? `Annotations approved by ${document.annotation_approver}, click to reject annotations`
  267. : 'Click to approve annotations';
  268. },
  269. displayDocumentMetadata() {
  270. let documentMetadata = this.documentMetadata;
  271. if (documentMetadata == null) {
  272. return null;
  273. }
  274. documentMetadata = { ...documentMetadata };
  275. delete documentMetadata.guideline;
  276. delete documentMetadata.documentSourceUrl;
  277. return documentMetadata;
  278. },
  279. documentMetadata() {
  280. return this.documentMetadataFor(this.pageNumber);
  281. },
  282. id2label() {
  283. const id2label = {};
  284. for (let i = 0; i < this.labels.length; i++) {
  285. const label = this.labels[i];
  286. id2label[label.id] = label;
  287. }
  288. return id2label;
  289. },
  290. progressColor() {
  291. if (this.achievement < 30) {
  292. return 'is-danger';
  293. }
  294. if (this.achievement < 70) {
  295. return 'is-warning';
  296. }
  297. return 'is-primary';
  298. },
  299. },
  300. };