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

import marked from 'marked';
import {Vue, Component, Prop, Watch} from 'vue-property-decorator';
import {FileData} from '@/interfaces/FileData';
import { eventBus } from '@/app';
import DefineSearcher from '../Searchers/DefineSearcher.vue';
import RadioButton from "primevue/radiobutton";
import {isServerError} from "@/SearchDisplace/helpers";
@Component({
components: {
RadioButton,
DefineSearcher,
},
})
export default class ProcessFile extends Vue {
/**
* Props
*/
// The data for the file we are processing
@Prop({default: {id: -1, file: '', path: ''}})
public readonly file!: FileData;
// The list of available searchers
@Prop({default: []})
public readonly searchers!: [];
@Prop({default: null})
public readonly document!: File | null;
/**
* Class members
*/
// The id of the interval used to query the file status
private intervalId!: any;
// The content of the file we are processing
private fileContent: string = '';
// The processed document content
private processedFileContent: string = '';
private processedFileContentPreview: string = '';
private documentDiffIndexes: { [key: string]: Array<{ start: number; end: number; }>; } = {};
// Flag to determine whether the text is processing or not
private processing: boolean = false;
// Toggles the visibility of the selected searchers sidebar
private searchersSidebarVisible: boolean = false;
// Toggles the visibility of the available searchers dialog
private searchersDialogVisible: boolean = false;
// Toggles the visibility of the document upload dialog
private uploadDialogVisible: boolean = false;
// The list of filters/searchers in a format usable by the datatable
// private searchersData: Array<{ id: string; name: string; type: string; }> = [];
// The list of filters applied to the selected searchers
private searchersFilters: any = {
name: '',
global: ''
};
// The list of selected filters/searchers
private selectedSearchers: any = {};
//The list of expanded rows in the selected filters/searchers table
private expandedRows: Array<any> = [];
// The list of options applied to the searchers (for the moment, only replace_with)
private searchersOptions: { [key: string]: { type: string, value: string, } } = {};
// Flag to determine whether or not we will show the diff highlights
private showDiffHighlight: boolean = false;
private newlySelectedSearchers: Array<{ id: string; name: string; }> = [];
private showDefineSearcher: boolean = false;
private searcherToDefineText: string = '';
private applyingOnOriginalDocument: boolean = false;
/**
*
*/
created() {
let storedSearchers = localStorage.getItem('searchers');
if (storedSearchers !== null) {
this.selectedSearchers = JSON.parse(storedSearchers);
localStorage.removeItem('searchers');
let searchersOptions = localStorage.getItem('searchersOptions');
if (searchersOptions !== null) {
this.searchersOptions = JSON.parse(searchersOptions);
localStorage.removeItem('searchersOptions');
}
}
this.intervalId = setInterval(this.waitForFile, 3000);
window.addEventListener('beforeunload', async (event) => {
// Cancel the event as stated by the standard.
event.preventDefault();
const response = await this.$api.discardFile(this.file.id);
return event;
});
eventBus.$on('changeRoute', this.changeRoute);
}
/**
* MD-to-HTML compiled file content
*/
get compiledFileContent(): string {
return marked(this.fileContent);
}
/**
* MD-to-HTML compiled processed file content
*/
get compiledProcessedFileContent(): string {
return marked(this.processedFileContent);
}
/**
* MD-to-HTML compiled processed file content with diff highlight
*/
get compiledProcessedFileContentPreview(): string {
return marked(this.processedFileContentPreview);
}
public changeRoute(url: string) {
const el = document.body;
setTimeout(() => {
el.classList.remove('p-overflow-hidden');
}, 10);
this.$confirm.require({
message: 'You will lose any progress on the current uploaded document. Are you sure you want to proceed?',
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle',
blockScroll: false,
accept: () => {
window.location.href = url;
},
reject: () => {
// TODO: Show a message to the user that the action was cancelled.
}
});
}
/**
* Toggle the sidebar containing the searchers
*/
private toggleSearchersSidebar() {
this.searchersSidebarVisible = !this.searchersSidebarVisible;
}
/**
* Toggle the menu containing the list of available searchers
*
* @param {string} newValue The new value for the dialog visibility
*/
private toggleSearchersDialog(newValue?: boolean) {
if (typeof newValue !== 'undefined') {
this.searchersDialogVisible = newValue;
} else {
this.searchersDialogVisible = !this.searchersDialogVisible;
}
if ( ! this.searchersDialogVisible) {
for (let selectedSearcher of this.newlySelectedSearchers) {
// this.selectedSearchers[selectedSearcher.id] = selectedSearcher;
this.$set(this.selectedSearchers, selectedSearcher.id, selectedSearcher);
this.expandedRows = Object.values(this.selectedSearchers).filter((p: any) => p.id);
}
this.newlySelectedSearchers = [];
}
}
/**
* Toggle the dialog which lets the user upload a new document
*
* @param {boolean} newValue
*/
toggleUploadDialog(newValue?: boolean) {
if (typeof newValue !== 'undefined') {
this.uploadDialogVisible = newValue;
} else {
this.uploadDialogVisible = !this.uploadDialogVisible;
}
}
/**
* A method which uploads the files to the server for processing
*
* @param event The event containing the uploaded files information
*/
public async uploadFile(event: any): Promise<void> {
localStorage.setItem('searchers', JSON.stringify(this.selectedSearchers));
localStorage.setItem('searchersOptions', JSON.stringify(this.searchersOptions));
this.$confirm.require({
message: 'You will lose any progress on the current uploaded document. Are you sure you want to proceed?',
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle',
accept: () => {
this.fileContent = this.processedFileContent = '';
let file = event.files[0];
this.toggleUploadDialog(false);
this.$api.discardFile(this.file.id);
this.$emit('newFile', file);
},
reject: () => {
// TODO: Show a message to the user that the action was cancelled.
}
});
}
/**
* Wait for the file to be processed in ingest
*/
private async waitForFile() {
const response = await this.$api.getFileData(this.file.id);
if (response.status === 'processing') {
return;
}
clearInterval(this.intervalId);
if (response.status === 'success') {
this.fileContent = response.content ? response.content : '';
this.$toast.add({
severity: 'success',
summary: 'File loaded',
detail: 'The file has been processed by ingest.',
life: 3000
});
}
if (response.status === 'fail') {
const error = 'There was an error processing the file in ingest';
this.$toast.add({
severity: 'error',
summary: 'File error',
detail: error,
life: 3000
});
this.$emit('error', error);
}
}
private async verifySdOnOriginalDocumentIsDone(id: string) {
const response = await this.$api.verifySdOnOriginalDocumentIsDone(id);
if (response.status === 'processing') {
return;
}
clearInterval(this.intervalId);
if (response.status === 'fail') {
this.$toast.add({
severity: 'error',
summary: 'Something went wrong.',
detail: 'Document could not have been processed.',
life: 7000,
});
this.applyingOnOriginalDocument = false;
return;
}
this.$toast.add({
severity: 'success',
summary: 'File loaded',
detail: 'The file has been processed by ingest.',
life: 7000,
});
this.downloadFinishedOriginalDocument(id);
this.applyingOnOriginalDocument = false;
// @TODO Send request to backend to delete file if no other way..
}
private downloadFinishedOriginalDocument(id: string) {
const url = `${window.location.origin}/search-and-displace/original-document/${id}/download`;
const link = window.document.createElement('a');
link.setAttribute('download', 'Document.doxc');
link.href = url;
window.document.body.appendChild(link);
link.click();
link.remove();
}
/**
*
* @param $event
*/
private onSelectedSearchersReorder($event: any) {
Object.assign({}, this.selectedSearchers, $event.value);
}
private confirmDeleteProduct(searcher: any) {
this.$delete(this.selectedSearchers, searcher.id);
}
/**
* Run the searchers
*/
private async runSearchers() {
this.processing = true;
this.processedFileContent = '';
let searchers: Array<{ key: string; type: string; value: string; }> = [];
Object.values(this.selectedSearchers).forEach((searcher: any) => {
searchers.push({
key: searcher.id,
type: this.searchersOptions[searcher.id].type,
value: this.searchersOptions[searcher.id].value || '',
});
});
try {
const response = await this.$api.filterDocument(this.fileContent, searchers);
this.processedFileContent = response.content;
this.documentDiffIndexes = response.indexes;
this.createDiffPreview();
this.processing = false;
} catch (e) {
this.$emit('error', 'Server error.');
// if (isServerError(e)) {
// this.$emit('error', getServerErrorMessage(e));
// }
}
}
private async runSearchersWithoutDisplacing() {
this.processing = true;
this.processedFileContent = '';
let searchers: Array<{ key: string; type: string; value: string; }> = [];
Object.values(this.selectedSearchers).forEach((searcher: any) => {
searchers.push({
key: searcher.id,
type: this.searchersOptions[searcher.id].type,
value: this.searchersOptions[searcher.id].value || ''
});
});
try {
const response = await this.$api.filterDocument(this.fileContent, searchers, true);
this.processedFileContent = this.fileContent;
this.documentDiffIndexes = response;
this.createDiffPreview();
this.processing = false;
} catch (e) {
this.$emit('error', 'Server error.');
// if (isServerError(e)) {
// this.$emit('error', getServerErrorMessage(e));
// }
}
}
/**
* Create the diff preview for the document
*/
private createDiffPreview() {
this.processedFileContentPreview = this.processedFileContent;
let indexes: Array<{ start: number; end: number }> = [];
for (let searcher in this.documentDiffIndexes) {
const searcherIndexes = this.documentDiffIndexes[searcher];
searcherIndexes.forEach(index => {
indexes.push(index);
});
}
indexes.sort((a, b) => {
return b.start - a.start;
});
this.processedFileContentPreview = indexes.reduce(
(r, a) => {
r[a.start] = '<mark>' + r[a.start];
r[a.end] += '</mark>';
return r;
},
this.processedFileContent.split('')
).join('');
}
/**
* Download the document in ODT format
*/
private async downloadOdt() {
let response = await this.$api.convertFile(this.processedFileContent, this.file.id);
window.open(`${window.location.origin}/file/download/` + response.path);
}
private async downloadOriginal() {
if ( ! this.document) {
return;
}
this.applyingOnOriginalDocument = true;
this.$toast.add({
severity: 'info',
summary: 'Processing...',
detail: 'This operation may take a while..',
life: 7000,
});
let searchers: Array<{ key: string; type: string; value: string; }> = [];
Object.values(this.selectedSearchers).forEach((searcher: any) => {
searchers.push({
key: searcher.id,
type: this.searchersOptions[searcher.id].type,
value: this.searchersOptions[searcher.id].value || '',
});
});
try {
const data = await this.$api.sdOnOriginalDocument(this.document, searchers);
this.intervalId = setInterval(() => {
this.verifySdOnOriginalDocumentIsDone(data.id);
}, 5000);
} catch (e) {
this.applyingOnOriginalDocument = false;
if (isServerError(e)) {
if (e.response.data.hasOwnProperty('errors')) {
const errors = e.response.data.errors;
if (errors.hasOwnProperty('file')) {
this.$toast.add({
severity: 'error',
summary: errors.file[0],
detail: 'There was an error processing your file. Please try again later.',
life: 7000,
});
return;
}
}
if (e.response.data.hasOwnProperty('message')) {
this.$toast.add({
severity: 'error',
summary: e.response.data.message,
detail: 'There was an error processing your file. Please try again later.',
life: 7000,
});
return;
}
}
this.$toast.add({
severity: 'error',
summary: 'Something went wrong.',
detail: 'There was an error processing your file. Please try again later.',
life: 7000,
});
}
}
private canRunSearchers(): boolean {
if (this.fileContent == '' || Object.keys(this.selectedSearchers).length === 0) {
return false;
}
for (let key of Object.keys(this.selectedSearchers)) {
const searcher = this.selectedSearchers[key];
if (!this.isValidParam(searcher.id, searcher.param)) {
return false;
}
}
return true;
}
/**
* Check if a param is valid or not.
*
* @param {string} paramId
* @param {string} paramType
* @returns {boolean}
*/
private isValidParam(paramId: string, paramType: string): boolean {
if (
paramType === 'required' &&
(Object.keys(this.searchersOptions[paramId]).length === 0 || this.searchersOptions[paramId] === undefined)
) {
return false;
}
return true;
}
private onDefineSearcher(): void {
const selection = window.getSelection();
const selectedText = selection ? selection.toString() : '';
if ( ! selectedText) {
this.$toast.add({
severity: 'info',
summary: 'No text selected.',
detail: 'You need to select some text in order to define a new searcher.',
life: 6000,
});
return;
}
this.showDefineSearcher = true;
this.searcherToDefineText = selectedText;
}
private onAddNewSearcher(): void {
this.showDefineSearcher = true;
this.searcherToDefineText = '';
}
private onSearcherDefined(definedSearcher: Object): void {
this.$toast.add({
severity: 'success',
summary: 'Searcher defined.',
detail: 'You can use this newly defined searcher right away.',
life: 6000,
});
this.$emit('newSearcher', definedSearcher);
this.showDefineSearcher = false;
}
/**
* Watch the `showDiffHighlight` property for changes
*
* @param {boolean} newValue
* @param {boolean} oldValue
*/
@Watch('showDiffHighlight')
private onDiffHighlightChanged(newValue: boolean, oldValue: boolean): void {
//
}
@Watch('selectedSearchers', { deep: true, })
private onSelectedSearchersChanged(): void {
const selectedIds = Object.keys(this.selectedSearchers);
const optionsIds = Object.keys(this.searchersOptions);
selectedIds.forEach((selectedId) => {
if (optionsIds.includes(selectedId)) {
return;
}
this.$set(this.searchersOptions, selectedId, {
type: this.selectedSearchers[selectedId].tag ? 'displace' : 'replace',
value: this.selectedSearchers[selectedId].tag ? this.selectedSearchers[selectedId].tag : '',
});
});
optionsIds.forEach((optionId) => {
if (selectedIds.includes(optionId)) {
return;
}
this.$delete(this.selectedSearchers, optionId);
});
}
}