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.

604 lines
18 KiB

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