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.

588 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
  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. // 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('beforeunload', 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. * MD-to-HTML compiled file content
  86. */
  87. get compiledFileContent(): string {
  88. return marked(this.fileContent);
  89. }
  90. /**
  91. * MD-to-HTML compiled processed file content
  92. */
  93. get compiledProcessedFileContent(): string {
  94. return marked(this.processedFileContent);
  95. }
  96. /**
  97. * MD-to-HTML compiled processed file content with diff highlight
  98. */
  99. get compiledProcessedFileContentPreview(): string {
  100. return marked(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. },
  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.$confirm.require({
  167. message: 'You will lose any progress on the current uploaded document. Are you sure you want to proceed?',
  168. header: 'Confirmation',
  169. icon: 'pi pi-exclamation-triangle',
  170. accept: () => {
  171. this.fileContent = this.processedFileContent = '';
  172. let file = event.files[0];
  173. this.toggleUploadDialog(false);
  174. this.$api.discardFile(this.file.id);
  175. this.$emit('newFile', file);
  176. },
  177. reject: () => {
  178. // TODO: Show a message to the user that the action was cancelled.
  179. }
  180. });
  181. }
  182. /**
  183. * Wait for the file to be processed in ingest
  184. */
  185. private async waitForFile() {
  186. const response = await this.$api.getFileData(this.file.id);
  187. if (response.status === 'processing') {
  188. return;
  189. }
  190. clearInterval(this.intervalId);
  191. if (response.status === 'success') {
  192. this.fileContent = response.content ? response.content : '';
  193. this.$toast.add({
  194. severity: 'success',
  195. summary: 'File loaded',
  196. detail: 'The file has been processed by ingest.',
  197. life: 3000
  198. });
  199. }
  200. if (response.status === 'fail') {
  201. const error = 'There was an error processing the file in ingest';
  202. this.$toast.add({
  203. severity: 'error',
  204. summary: 'File error',
  205. detail: error,
  206. life: 3000
  207. });
  208. this.$emit('error', error);
  209. }
  210. }
  211. private async verifySdOnOriginalDocumentIsDone(id: string) {
  212. const response = await this.$api.verifySdOnOriginalDocumentIsDone(id);
  213. if (response.status === 'processing') {
  214. return;
  215. }
  216. clearInterval(this.intervalId);
  217. if (response.status === 'fail') {
  218. this.$toast.add({
  219. severity: 'error',
  220. summary: 'Something went wrong.',
  221. detail: 'Document could not have been processed.',
  222. life: 7000,
  223. });
  224. return;
  225. }
  226. this.$toast.add({
  227. severity: 'success',
  228. summary: 'File loaded',
  229. detail: 'The file has been processed by ingest.',
  230. life: 7000,
  231. });
  232. this.downloadFinishedOriginalDocument(id);
  233. }
  234. private downloadFinishedOriginalDocument(id: string) {
  235. window.open(`${window.location.origin}/search-and-displace/original-document/${id}/download`);
  236. }
  237. /**
  238. *
  239. * @param $event
  240. */
  241. private onSelectedSearchersReorder($event: any) {
  242. Object.assign({}, this.selectedSearchers, $event.value);
  243. }
  244. private confirmDeleteProduct(searcher: any) {
  245. this.$delete(this.selectedSearchers, searcher.id);
  246. }
  247. /**
  248. * Run the searchers
  249. */
  250. private async runSearchers() {
  251. this.processing = true;
  252. this.processedFileContent = '';
  253. let searchers: Array<{ key: string; type: string; value: string; }> = [];
  254. Object.values(this.selectedSearchers).forEach((searcher: any) => {
  255. searchers.push({
  256. key: searcher.id,
  257. type: this.searchersOptions[searcher.id].type,
  258. value: this.searchersOptions[searcher.id].value || '',
  259. });
  260. });
  261. try {
  262. const response = await this.$api.filterDocument(this.fileContent, searchers);
  263. this.processedFileContent = response.content;
  264. this.documentDiffIndexes = response.indexes;
  265. this.createDiffPreview();
  266. this.processing = false;
  267. } catch (e) {
  268. this.$emit('error', 'Server error.');
  269. // if (isServerError(e)) {
  270. // this.$emit('error', getServerErrorMessage(e));
  271. // }
  272. }
  273. }
  274. private async runSearchersWithoutDisplacing() {
  275. this.processing = true;
  276. this.processedFileContent = '';
  277. let searchers: Array<{ key: string; type: string; value: string; }> = [];
  278. Object.values(this.selectedSearchers).forEach((searcher: any) => {
  279. searchers.push({
  280. key: searcher.id,
  281. type: this.searchersOptions[searcher.id].type,
  282. value: this.searchersOptions[searcher.id].value || ''
  283. });
  284. });
  285. try {
  286. const response = await this.$api.filterDocument(this.fileContent, searchers, true);
  287. this.processedFileContent = this.fileContent;
  288. this.documentDiffIndexes = response;
  289. this.createDiffPreview();
  290. this.processing = false;
  291. } catch (e) {
  292. this.$emit('error', 'Server error.');
  293. // if (isServerError(e)) {
  294. // this.$emit('error', getServerErrorMessage(e));
  295. // }
  296. }
  297. }
  298. /**
  299. * Create the diff preview for the document
  300. */
  301. private createDiffPreview() {
  302. this.processedFileContentPreview = this.processedFileContent;
  303. let indexes: Array<{ start: number; end: number }> = [];
  304. for (let searcher in this.documentDiffIndexes) {
  305. const searcherIndexes = this.documentDiffIndexes[searcher];
  306. searcherIndexes.forEach(index => {
  307. indexes.push(index);
  308. });
  309. }
  310. indexes.sort((a, b) => {
  311. return b.start - a.start;
  312. });
  313. this.processedFileContentPreview = indexes.reduce(
  314. (r, a) => {
  315. r[a.start] = '<mark>' + r[a.start];
  316. r[a.end] += '</mark>';
  317. return r;
  318. },
  319. this.processedFileContent.split('')
  320. ).join('');
  321. }
  322. /**
  323. * Download the document in ODT format
  324. */
  325. private async downloadOdt() {
  326. let response = await this.$api.convertFile(this.processedFileContent, this.file.id);
  327. window.open(`${window.location.origin}/file/download/` + response.path);
  328. }
  329. private async downloadOriginal() {
  330. if ( ! this.document) {
  331. return;
  332. }
  333. this.applyingOnOriginalDocument = true;
  334. this.$toast.add({
  335. severity: 'info',
  336. summary: 'Processing...',
  337. detail: 'This operation may take a while..',
  338. life: 7000,
  339. });
  340. let searchers: Array<{ key: string; type: string; value: string; }> = [];
  341. Object.values(this.selectedSearchers).forEach((searcher: any) => {
  342. searchers.push({
  343. key: searcher.id,
  344. type: this.searchersOptions[searcher.id].type,
  345. value: this.searchersOptions[searcher.id].value || '',
  346. });
  347. });
  348. try {
  349. const data = await this.$api.sdOnOriginalDocument(this.document, searchers);
  350. this.intervalId = setInterval(() => {
  351. this.verifySdOnOriginalDocumentIsDone(data.id);
  352. }, 5000);
  353. } catch (e) {
  354. this.applyingOnOriginalDocument = false;
  355. if (isServerError(e)) {
  356. if (e.response.data.hasOwnProperty('errors')) {
  357. const errors = e.response.data.errors;
  358. if (errors.hasOwnProperty('file')) {
  359. this.$toast.add({
  360. severity: 'error',
  361. summary: errors.file[0],
  362. detail: 'There was an error processing your file. Please try again later.',
  363. life: 7000,
  364. });
  365. return;
  366. }
  367. }
  368. if (e.response.data.hasOwnProperty('message')) {
  369. this.$toast.add({
  370. severity: 'error',
  371. summary: e.response.data.message,
  372. detail: 'There was an error processing your file. Please try again later.',
  373. life: 7000,
  374. });
  375. return;
  376. }
  377. }
  378. this.$toast.add({
  379. severity: 'error',
  380. summary: 'Something went wrong.',
  381. detail: 'There was an error processing your file. Please try again later.',
  382. life: 7000,
  383. });
  384. }
  385. }
  386. private canRunSearchers(): boolean {
  387. if (this.fileContent == '' || Object.keys(this.selectedSearchers).length === 0) {
  388. return false;
  389. }
  390. for (let key of Object.keys(this.selectedSearchers)) {
  391. const searcher = this.selectedSearchers[key];
  392. if (!this.isValidParam(searcher.id, searcher.param)) {
  393. return false;
  394. }
  395. }
  396. return true;
  397. }
  398. /**
  399. * Check if a param is valid or not.
  400. *
  401. * @param {string} paramId
  402. * @param {string} paramType
  403. * @returns {boolean}
  404. */
  405. private isValidParam(paramId: string, paramType: string): boolean {
  406. if (
  407. paramType === 'required' &&
  408. (Object.keys(this.searchersOptions[paramId]).length === 0 || this.searchersOptions[paramId] === undefined)
  409. ) {
  410. return false;
  411. }
  412. return true;
  413. }
  414. private onDefineSearcher(): void {
  415. const selection = window.getSelection();
  416. const selectedText = selection ? selection.toString() : '';
  417. if ( ! selectedText) {
  418. this.$toast.add({
  419. severity: 'info',
  420. summary: 'No text selected.',
  421. detail: 'You need to select some text in order to define a new searcher.',
  422. life: 6000,
  423. });
  424. return;
  425. }
  426. this.showDefineSearcher = true;
  427. this.searcherToDefineText = selectedText;
  428. }
  429. private onAddNewSearcher(): void {
  430. this.showDefineSearcher = true;
  431. this.searcherToDefineText = '';
  432. }
  433. private onSearcherDefined(definedSearcher: Object): void {
  434. this.$toast.add({
  435. severity: 'success',
  436. summary: 'Searcher defined.',
  437. detail: 'You can use this newly defined searcher right away.',
  438. life: 6000,
  439. });
  440. this.$emit('newSearcher', definedSearcher);
  441. this.showDefineSearcher = false;
  442. }
  443. /**
  444. * Watch the `showDiffHighlight` property for changes
  445. *
  446. * @param {boolean} newValue
  447. * @param {boolean} oldValue
  448. */
  449. @Watch('showDiffHighlight')
  450. private onDiffHighlightChanged(newValue: boolean, oldValue: boolean): void {
  451. //
  452. }
  453. @Watch('selectedSearchers', { deep: true, })
  454. private onSelectedSearchersChanged(): void {
  455. const selectedIds = Object.keys(this.selectedSearchers);
  456. const optionsIds = Object.keys(this.searchersOptions);
  457. selectedIds.forEach((selectedId) => {
  458. if (optionsIds.includes(selectedId)) {
  459. return;
  460. }
  461. this.$set(this.searchersOptions, selectedId, {
  462. type: this.selectedSearchers[selectedId].tag ? 'displace' : 'replace',
  463. value: this.selectedSearchers[selectedId].tag ? this.selectedSearchers[selectedId].tag : '',
  464. });
  465. });
  466. optionsIds.forEach((optionId) => {
  467. if (selectedIds.includes(optionId)) {
  468. return;
  469. }
  470. this.$delete(this.selectedSearchers, optionId);
  471. });
  472. }
  473. }