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.

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