Repo for the search and displace core module including the interface to select files and search and displace operations to run on them. https://searchanddisplace.com
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.

625 lines
19 KiB

3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. import {Vue, Component, Prop, Watch} from 'vue-property-decorator';
  2. import {FileData} from '@/interfaces/FileData';
  3. import { eventBus } from '@/app';
  4. import DefineSearcher from '../Searchers/DefineSearcher.vue';
  5. import RadioButton from "primevue/radiobutton";
  6. import {isServerError} from "@/SearchDisplace/helpers";
  7. @Component({
  8. components: {
  9. RadioButton,
  10. DefineSearcher,
  11. },
  12. })
  13. export default class ProcessFile extends Vue {
  14. /**
  15. * Props
  16. */
  17. // The data for the file we are processing
  18. @Prop({default: {id: -1, file: '', path: ''}})
  19. public readonly file!: FileData;
  20. // The list of available searchers
  21. @Prop({default: []})
  22. public readonly searchers!: [];
  23. @Prop({default: null})
  24. public readonly document!: File | null;
  25. /**
  26. * Class members
  27. */
  28. // The id of the interval used to query the file status
  29. private intervalId!: any;
  30. // The content of the file we are processing
  31. private fileContent: string = '';
  32. // The processed document content
  33. private processedFileContent: string = '';
  34. private processedFileContentPreview: string = '';
  35. private documentDiffIndexes: { [key: string]: Array<{ start: number; end: number; }>; } = {};
  36. // Flag to determine whether the text is processing or not
  37. private processing: boolean = false;
  38. // Toggles the visibility of the selected searchers sidebar
  39. private searchersSidebarVisible: boolean = false;
  40. // Toggles the visibility of the available searchers dialog
  41. private searchersDialogVisible: boolean = false;
  42. // Toggles the visibility of the document upload dialog
  43. private uploadDialogVisible: boolean = false;
  44. // The list of filters/searchers in a format usable by the datatable
  45. // private searchersData: Array<{ id: string; name: string; type: string; }> = [];
  46. // The list of filters applied to the selected searchers
  47. private searchersFilters: any = {};
  48. // The list of selected filters/searchers
  49. private selectedSearchers: any = {};
  50. //The list of expanded rows in the selected filters/searchers table
  51. private expandedRows: Array<any> = [];
  52. // The list of options applied to the searchers (for the moment, only replace_with)
  53. private searchersOptions: { [key: string]: { type: string, value: string, } } = {};
  54. // Flag to determine whether or not we will show the diff highlights
  55. private showDiffHighlight: boolean = false;
  56. private newlySelectedSearchers: Array<{ id: string; name: string; }> = [];
  57. private showDefineSearcher: boolean = false;
  58. private searcherToDefineText: string = '';
  59. private applyingOnOriginalDocument: boolean = false;
  60. /**
  61. *
  62. */
  63. created() {
  64. let storedSearchers = localStorage.getItem('searchers');
  65. if (storedSearchers !== null) {
  66. this.selectedSearchers = JSON.parse(storedSearchers);
  67. localStorage.removeItem('searchers');
  68. let searchersOptions = localStorage.getItem('searchersOptions');
  69. if (searchersOptions !== null) {
  70. this.searchersOptions = JSON.parse(searchersOptions);
  71. localStorage.removeItem('searchersOptions');
  72. }
  73. }
  74. this.intervalId = setInterval(this.waitForFile, 3000);
  75. window.addEventListener('onbeforeunload', async (event) => {
  76. // Cancel the event as stated by the standard.
  77. event.preventDefault();
  78. const response = await this.$api.discardFile(this.file.id);
  79. return event;
  80. });
  81. eventBus.$on('changeRoute', this.changeRoute);
  82. }
  83. /**
  84. * HTML compiled file content
  85. */
  86. get compiledFileContent(): string {
  87. return this.fileContent;
  88. }
  89. /**
  90. * HTML compiled processed file content
  91. */
  92. get compiledProcessedFileContent(): string {
  93. return this.processedFileContent;
  94. }
  95. /**
  96. * MD-to-HTML compiled processed file content with diff highlight
  97. */
  98. get compiledProcessedFileContentPreview(): string {
  99. return this.processedFileContentPreview;
  100. }
  101. public changeRoute(url: string) {
  102. const el = document.body;
  103. setTimeout(() => {
  104. el.classList.remove('p-overflow-hidden');
  105. }, 10);
  106. this.$confirm.require({
  107. message: 'You will lose any progress on the current uploaded document. Are you sure you want to proceed?',
  108. header: 'Confirmation',
  109. icon: 'pi pi-exclamation-triangle',
  110. blockScroll: false,
  111. accept: () => {
  112. window.location.href = url;
  113. this.$api.discardFile(this.file.id);
  114. },
  115. reject: () => {
  116. // TODO: Show a message to the user that the action was cancelled.
  117. }
  118. });
  119. }
  120. /**
  121. * Toggle the sidebar containing the searchers
  122. */
  123. private toggleSearchersSidebar() {
  124. this.searchersSidebarVisible = !this.searchersSidebarVisible;
  125. }
  126. /**
  127. * Toggle the menu containing the list of available searchers
  128. *
  129. * @param {string} newValue The new value for the dialog visibility
  130. */
  131. private toggleSearchersDialog(newValue?: boolean) {
  132. if (typeof newValue !== 'undefined') {
  133. this.searchersDialogVisible = newValue;
  134. } else {
  135. this.searchersDialogVisible = !this.searchersDialogVisible;
  136. }
  137. if ( ! this.searchersDialogVisible) {
  138. for (let selectedSearcher of this.newlySelectedSearchers) {
  139. // this.selectedSearchers[selectedSearcher.id] = selectedSearcher;
  140. this.$set(this.selectedSearchers, selectedSearcher.id, selectedSearcher);
  141. this.expandedRows = Object.values(this.selectedSearchers).filter((p: any) => p.id);
  142. }
  143. this.newlySelectedSearchers = [];
  144. }
  145. }
  146. /**
  147. * Toggle the dialog which lets the user upload a new document
  148. *
  149. * @param {boolean} newValue
  150. */
  151. toggleUploadDialog(newValue?: boolean) {
  152. if (typeof newValue !== 'undefined') {
  153. this.uploadDialogVisible = newValue;
  154. } else {
  155. this.uploadDialogVisible = !this.uploadDialogVisible;
  156. }
  157. }
  158. /**
  159. * A method which uploads the files to the server for processing
  160. *
  161. * @param event The event containing the uploaded files information
  162. */
  163. public async uploadFile(event: any): Promise<void> {
  164. localStorage.setItem('searchers', JSON.stringify(this.selectedSearchers));
  165. localStorage.setItem('searchersOptions', JSON.stringify(this.searchersOptions));
  166. this.toggleUploadDialog(false);
  167. this.$confirm.require({
  168. message: 'You will lose any progress on the current uploaded document. Are you sure you want to proceed?',
  169. header: 'Confirmation',
  170. icon: 'pi pi-exclamation-triangle',
  171. accept: () => {
  172. this.fileContent = this.processedFileContent = '';
  173. let file = event.files[0];
  174. this.toggleUploadDialog(false);
  175. this.$api.discardFile(this.file.id);
  176. this.$emit('newFile', file);
  177. },
  178. reject: () => {
  179. // TODO: Show a message to the user that the action was cancelled.
  180. }
  181. });
  182. }
  183. /**
  184. * Wait for the file to be processed in ingest
  185. */
  186. private async waitForFile() {
  187. const response = await this.$api.getFileData(this.file.id);
  188. if (response.status === 'processing') {
  189. return;
  190. }
  191. clearInterval(this.intervalId);
  192. if (response.status === 'success') {
  193. this.fileContent = response.content ? response.content : '';
  194. this.$toast.add({
  195. severity: 'success',
  196. summary: 'File loaded',
  197. detail: 'The file has been processed by ingest.',
  198. life: 3000
  199. });
  200. }
  201. if (response.status === 'fail') {
  202. const error = 'There was an error processing the file in ingest';
  203. this.$toast.add({
  204. severity: 'error',
  205. summary: 'File error',
  206. detail: error,
  207. life: 3000
  208. });
  209. this.$emit('error', error);
  210. }
  211. }
  212. private async verifySdOnOriginalDocumentIsDone(id: string) {
  213. const response = await this.$api.verifySdOnOriginalDocumentIsDone(id);
  214. if (response.status === 'processing') {
  215. return;
  216. }
  217. clearInterval(this.intervalId);
  218. if (response.status === 'fail') {
  219. this.$toast.add({
  220. severity: 'error',
  221. summary: 'Something went wrong.',
  222. detail: 'Document could not have been processed.',
  223. life: 7000,
  224. });
  225. this.applyingOnOriginalDocument = false;
  226. return;
  227. }
  228. this.$toast.add({
  229. severity: 'success',
  230. summary: 'File loaded',
  231. detail: 'The file has been processed by ingest.',
  232. life: 7000,
  233. });
  234. this.downloadFinishedOriginalDocument(id);
  235. this.applyingOnOriginalDocument = false;
  236. // @TODO Send request to backend to delete file if no other way..
  237. }
  238. private downloadFinishedOriginalDocument(id: string) {
  239. const url = `${window.location.origin}/search-and-displace/original-document/${id}/download`;
  240. const link = window.document.createElement('a');
  241. link.setAttribute('download', 'Document.doxc');
  242. link.href = url;
  243. window.document.body.appendChild(link);
  244. link.click();
  245. link.remove();
  246. }
  247. /**
  248. *
  249. * @param $event
  250. */
  251. private onSelectedSearchersReorder($event: any) {
  252. Object.assign({}, this.selectedSearchers, $event.value);
  253. }
  254. private confirmDeleteProduct(searcher: any) {
  255. this.$delete(this.selectedSearchers, searcher.id);
  256. }
  257. /**
  258. * Run the searchers
  259. */
  260. private async runSearchers() {
  261. this.processing = true;
  262. this.processedFileContent = '';
  263. let searchers: Array<{ key: string; type: string; value: string; }> = [];
  264. Object.values(this.selectedSearchers).forEach((searcher: any) => {
  265. searchers.push({
  266. key: searcher.id,
  267. type: this.searchersOptions[searcher.id].type,
  268. value: this.searchersOptions[searcher.id].value || '',
  269. });
  270. });
  271. try {
  272. const response = await this.$api.filterDocument(this.file.id, searchers);
  273. this.processedFileContent = response.content;
  274. this.documentDiffIndexes = response.indexes;
  275. this.createDiffPreview();
  276. this.processing = false;
  277. } catch (e) {
  278. this.$emit('error', 'Server error.');
  279. // if (isServerError(e)) {
  280. // this.$emit('error', getServerErrorMessage(e));
  281. // }
  282. }
  283. }
  284. private async runSearchersWithoutDisplacing() {
  285. this.processing = true;
  286. let searchers: Array<{ key: string; type: string; value: string; }> = [];
  287. Object.values(this.selectedSearchers).forEach((searcher: any) => {
  288. searchers.push({
  289. key: searcher.id,
  290. type: this.searchersOptions[searcher.id].type,
  291. value: this.searchersOptions[searcher.id].value || ''
  292. });
  293. });
  294. try {
  295. const response = await this.$api.filterDocument(this.file.id, searchers, true);
  296. this.processedFileContent = response.content;
  297. // this.documentDiffIndexes = response;
  298. this.createDiffPreview();
  299. this.processing = false;
  300. } catch (e) {
  301. this.$emit('error', 'Server error.');
  302. // if (isServerError(e)) {
  303. // this.$emit('error', getServerErrorMessage(e));
  304. // }
  305. }
  306. }
  307. /**
  308. * Create the diff preview for the document
  309. */
  310. private createDiffPreview() {
  311. this.processedFileContentPreview = this.processedFileContent;
  312. let indexes: Array<{ start: number; end: number }> = [];
  313. for (let searcher in this.documentDiffIndexes) {
  314. const searcherIndexes = this.documentDiffIndexes[searcher];
  315. searcherIndexes.forEach(index => {
  316. indexes.push(index);
  317. });
  318. }
  319. indexes.sort((a, b) => {
  320. return b.start - a.start;
  321. });
  322. this.processedFileContentPreview = indexes.reduce(
  323. (r, a) => {
  324. r[a.start] = '<mark>' + r[a.start];
  325. r[a.end] += '</mark>';
  326. return r;
  327. },
  328. this.processedFileContent.split('')
  329. ).join('');
  330. }
  331. /**
  332. * Download the document in ODT format
  333. */
  334. private async downloadOdt() {
  335. let searchers: Array<{ key: string; type: string; value: string; }> = [];
  336. Object.values(this.selectedSearchers).forEach((searcher: any) => {
  337. searchers.push({
  338. key: searcher.id,
  339. type: this.searchersOptions[searcher.id].type,
  340. value: this.searchersOptions[searcher.id].value || '',
  341. });
  342. });
  343. let response = await this.$api.convertFile({id: this.file.id, name: this.file.file_name || 'filename.odt'}, searchers);
  344. window.open(`${window.location.origin}/file/download/` + response.path);
  345. }
  346. private async downloadOriginal() {
  347. if ( ! this.document) {
  348. return;
  349. }
  350. this.applyingOnOriginalDocument = true;
  351. this.$toast.add({
  352. severity: 'info',
  353. summary: 'Processing...',
  354. detail: 'This operation may take a while..',
  355. life: 7000,
  356. });
  357. let searchers: Array<{ key: string; type: string; value: string; }> = [];
  358. Object.values(this.selectedSearchers).forEach((searcher: any) => {
  359. searchers.push({
  360. key: searcher.id,
  361. type: this.searchersOptions[searcher.id].type,
  362. value: this.searchersOptions[searcher.id].value || '',
  363. });
  364. });
  365. try {
  366. const data = await this.$api.sdOnOriginalDocument(this.document, searchers);
  367. this.intervalId = setInterval(() => {
  368. this.verifySdOnOriginalDocumentIsDone(data.id);
  369. }, 5000);
  370. } catch (e) {
  371. this.applyingOnOriginalDocument = false;
  372. if (isServerError(e)) {
  373. if (e.response.data.hasOwnProperty('errors')) {
  374. const errors = e.response.data.errors;
  375. if (errors.hasOwnProperty('file')) {
  376. this.$toast.add({
  377. severity: 'error',
  378. summary: errors.file[0],
  379. detail: 'There was an error processing your file. Please try again later.',
  380. life: 7000,
  381. });
  382. return;
  383. }
  384. }
  385. if (e.response.data.hasOwnProperty('message')) {
  386. this.$toast.add({
  387. severity: 'error',
  388. summary: e.response.data.message,
  389. detail: 'There was an error processing your file. Please try again later.',
  390. life: 7000,
  391. });
  392. return;
  393. }
  394. }
  395. this.$toast.add({
  396. severity: 'error',
  397. summary: 'Something went wrong.',
  398. detail: 'There was an error processing your file. Please try again later.',
  399. life: 7000,
  400. });
  401. }
  402. }
  403. private canRunSearchers(): boolean {
  404. if (this.fileContent == '' || Object.keys(this.selectedSearchers).length === 0) {
  405. return false;
  406. }
  407. for (let key of Object.keys(this.selectedSearchers)) {
  408. const searcher = this.selectedSearchers[key];
  409. if (!this.isValidParam(searcher.id, searcher.param)) {
  410. return false;
  411. }
  412. }
  413. return true;
  414. }
  415. /**
  416. * Check if a param is valid or not.
  417. *
  418. * @param {string} paramId
  419. * @param {string} paramType
  420. * @returns {boolean}
  421. */
  422. private isValidParam(paramId: string, paramType: string): boolean {
  423. if (
  424. paramType === 'required' &&
  425. (Object.keys(this.searchersOptions[paramId]).length === 0 || this.searchersOptions[paramId] === undefined)
  426. ) {
  427. return false;
  428. }
  429. return true;
  430. }
  431. private onDefineSearcher(): void {
  432. const selection = window.getSelection();
  433. const selectedText = selection ? selection.toString() : '';
  434. if ( ! selectedText) {
  435. this.$toast.add({
  436. severity: 'info',
  437. summary: 'No text selected.',
  438. detail: 'You need to select some text in order to define a new searcher.',
  439. life: 6000,
  440. });
  441. return;
  442. }
  443. this.showDefineSearcher = true;
  444. this.searcherToDefineText = selectedText;
  445. }
  446. private onAddNewSearcher(): void {
  447. this.showDefineSearcher = true;
  448. this.searcherToDefineText = '';
  449. }
  450. private onSearcherDefined(definedSearcher: Object): void {
  451. this.$toast.add({
  452. severity: 'success',
  453. summary: 'Searcher defined.',
  454. detail: 'You can use this newly defined searcher right away.',
  455. life: 6000,
  456. });
  457. this.$emit('newSearcher', definedSearcher);
  458. this.showDefineSearcher = false;
  459. }
  460. /**
  461. * Watch the filters for any changes. When the search value is empty, remove it
  462. * This is needed due to a bug in PrimeVue where the pagination doesn't work when there is a active filter
  463. *
  464. * @param newValue
  465. * @param oldValue
  466. */
  467. @Watch('searchersFilters', { deep: true })
  468. private onFiltersChanged(newValue: any, oldValue: object): void {
  469. if (newValue.global === '') {
  470. this.searchersFilters = {};
  471. }
  472. }
  473. /**
  474. * Watch the `showDiffHighlight` property for changes
  475. *
  476. * @param {boolean} newValue
  477. * @param {boolean} oldValue
  478. */
  479. @Watch('showDiffHighlight')
  480. private onDiffHighlightChanged(newValue: boolean, oldValue: boolean): void {
  481. //
  482. }
  483. @Watch('selectedSearchers', { deep: true, })
  484. private onSelectedSearchersChanged(): void {
  485. const selectedIds = Object.keys(this.selectedSearchers);
  486. const optionsIds = Object.keys(this.searchersOptions);
  487. selectedIds.forEach((selectedId) => {
  488. if (optionsIds.includes(selectedId)) {
  489. return;
  490. }
  491. this.$set(this.searchersOptions, selectedId, {
  492. type: this.selectedSearchers[selectedId].tag ? 'displace' : 'replace',
  493. value: this.selectedSearchers[selectedId].tag ? this.selectedSearchers[selectedId].tag : '',
  494. });
  495. });
  496. optionsIds.forEach((optionId) => {
  497. if (selectedIds.includes(optionId)) {
  498. return;
  499. }
  500. this.$delete(this.selectedSearchers, optionId);
  501. });
  502. }
  503. }