Browse Source

Displace action which tags the found results

master
Orzu Ionut 3 years ago
parent
commit
592d46fdec
  1. 7
      app/Http/Controllers/RegexController.php
  2. 14
      app/Http/Controllers/SearcherController.php
  3. 4
      app/SearchDisplace/Regex/RegexFactory.php
  4. 14
      app/SearchDisplace/SearchAndDisplace.php
  5. 13
      app/SearchDisplace/Searchers/SearcherCreator.php
  6. 4
      app/SearchDisplace/Searchers/SearcherFactory.php
  7. 31
      app/SearchDisplace/Searchers/SearchersStorage.php
  8. 5
      public/css/app.css
  9. 967
      public/js/app.js
  10. 12
      resources/js/components/Home/Home.ts
  11. 3
      resources/js/components/Home/Home.vue
  12. 91
      resources/js/components/ProcessFile/ProcessFile.ts
  13. 74
      resources/js/components/ProcessFile/ProcessFile.vue
  14. 18
      resources/js/components/Regex/Create.vue
  15. 41
      resources/js/components/Searchers/Create.vue
  16. 56
      resources/js/components/Searchers/DefineSearcher.vue
  17. 2
      resources/js/services/ApiService.ts
  18. 1
      resources/sass/components/_index.sass
  19. 3
      resources/sass/components/searchers/_create.sass
  20. 1
      resources/sass/components/searchers/_index.sass

7
app/Http/Controllers/RegexController.php

@ -15,11 +15,16 @@ class RegexController extends Controller
{
request()->validate([
'name' => 'required',
'tag' => 'nullable',
'expression' => 'required',
]);
try {
$factory = new RegexFactory(request()->get('name'), request()->get('expression'));
$factory = new RegexFactory(
request()->get('name'),
request()->get('expression'),
request()->get('tag')
);
$searcher = $factory->create();

14
app/Http/Controllers/SearcherController.php

@ -37,12 +37,17 @@ class SearcherController extends Controller
{
request()->validate([
'name' => 'required',
'tag' => 'nullable',
'rows' => 'required|array',
'rows.*' => 'array',
]);
try {
$factory = new SearcherFactory(request()->get('name'), request()->get('rows'));
$factory = new SearcherFactory(
request()->get('name'),
request()->get('rows'),
request()->get('tag')
);
$searcher = $factory->create();
@ -107,12 +112,17 @@ class SearcherController extends Controller
{
request()->validate([
'name' => 'required',
'tag' => 'nullable',
'rows' => 'required|array',
'rows.*' => 'array',
]);
try {
$factory = new SearcherFactory(request()->get('name'), request()->get('rows'));
$factory = new SearcherFactory(
request()->get('name'),
request()->get('rows'),
request()->get('tag')
);
$searcher = $factory->update($id);

4
app/SearchDisplace/Regex/RegexFactory.php

@ -8,9 +8,9 @@ class RegexFactory extends SearcherCreator
{
protected $expression;
public function __construct($name, $expression)
public function __construct($name, $expression, $tag = '')
{
parent::__construct($name, '');
parent::__construct($name, $tag, '');
$this->expression = $expression;
}

14
app/SearchDisplace/SearchAndDisplace.php

@ -40,7 +40,7 @@ class SearchAndDisplace
$replacements = [];
foreach ($this->info['searchers'] as $searcher) {
$replacements[$searcher['key']] = $searcher['replace_with'];
$replacements[$searcher['key']] = $searcher;
}
$searchers = [];
@ -67,7 +67,11 @@ class SearchAndDisplace
$start = mb_strlen($updatedDocumentContent);
$updatedDocumentContent = $updatedDocumentContent . $replacements[$searcherItem['searcher']];
$replacementValue = $replacements[$searcherItem['searcher']]['type'] === 'replace'
? $replacements[$searcherItem['searcher']]['value']
: $this->getDisplaceValue($replacements[$searcherItem['searcher']]['value'], $searcherItem['content']);
$updatedDocumentContent = $updatedDocumentContent . $replacementValue;
$end = mb_strlen($updatedDocumentContent) - 1;
@ -90,4 +94,10 @@ class SearchAndDisplace
'indexes' => $replacementIndexes,
];
}
protected function getDisplaceValue($displaceValue, $content)
{
// return "<$displaceValue>$content</$displaceValue>";
return "{{$displaceValue}}{$content}{/{$displaceValue}}";
}
}

13
app/SearchDisplace/Searchers/SearcherCreator.php

@ -9,13 +9,15 @@ abstract class SearcherCreator
protected $name;
protected $id;
protected $description;
protected $tag;
protected $rows = [];
protected $storage;
protected $searchersCollection;
public function __construct($name, $description)
public function __construct($name, $tag, $description)
{
$this->name = $name;
$this->tag = $tag;
$this->description = $description;
$this->id = $this->generateID();
$this->storage = Storage::disk('local');
@ -28,6 +30,10 @@ abstract class SearcherCreator
{
$id = "{$this->id}_{$this->name}";
if ($this->tag) {
$id = "{$id}_{$this->tag}";
}
return $this->save($id);
}
@ -38,6 +44,10 @@ abstract class SearcherCreator
$newId = $uniqueId . '_' . $this->name;
if ($this->tag) {
$newId = "{$newId}_{$this->tag}";
}
if ($id !== $newId) {
$this->storage->move("searchers/$id.json", "searchers/$newId.json");
}
@ -52,6 +62,7 @@ abstract class SearcherCreator
$contents = [
'id' => $id,
'name' => $this->name,
'tag' => $this->tag,
'description' => $this->description,
'rows' => $this->rows,
];

4
app/SearchDisplace/Searchers/SearcherFactory.php

@ -4,9 +4,9 @@ namespace App\SearchDisplace\Searchers;
class SearcherFactory extends SearcherCreator
{
public function __construct($name, $rows)
public function __construct($name, $rows, $tag = '')
{
parent::__construct($name, '');
parent::__construct($name, $tag, '');
$this->rows = $rows;
}

31
app/SearchDisplace/Searchers/SearchersStorage.php

@ -24,19 +24,26 @@ class SearchersStorage
$fileName = pathinfo($file, PATHINFO_FILENAME);
$result = explode('_', $fileName);
if (count($result) === 2) {
$searchers[$fileName] = [
'id' => $fileName,
'name' => $result[1],
'param' => SearchersCollection::PARAM_REQUIRED
];
} else if (count($result) === 3) {
$searchers[$fileName] = [
'id' => $fileName,
'name' => $result[1],
'param' => $result[2]
];
if (count($result) < 2) {
continue;
}
$searchers[$fileName] = [
'id' => $fileName,
'name' => $result[1],
'tag' => '',
'param' => SearchersCollection::PARAM_REQUIRED,
];
if (count($result) === 3) {
$searchers[$fileName]['tag'] = $result[2];
// $searchers[$fileName]['param'] = SearchersCollection::PARAM_REQUIRED;
}
// if (count($result) === 3) {
// $searchers[$fileName]['param'] = $result[2];
// }
}
}

5
public/css/app.css

@ -10873,6 +10873,11 @@
box-shadow: inset 0 0 6px rgba(54, 52, 52, 0.863) !important;
}
#searchers-create.is-defining {
max-height: 100%;
overflow-y: auto;
}
body {
height: 100%;
margin: 0;

967
public/js/app.js
File diff suppressed because it is too large
View File

12
resources/js/components/Home/Home.ts

@ -6,9 +6,7 @@ import { eventBus } from '@/app';
@Component
export default class Home extends Vue {
@Prop({default: []})
public readonly searchers!: Searcher[];
private availableSearchers: Array<Searcher> = [];
public uiBlocked = false;
public uploading = false;
public fileUploaded: boolean = false;
@ -18,10 +16,14 @@ export default class Home extends Vue {
};
public error: string = '';
@Prop({default: []})
public searchers!: Searcher[];
public mounted()
{
eventBus.$on('changeRoute', this.changeRoute);
this.availableSearchers = this.searchers;
}
/**
@ -76,6 +78,10 @@ export default class Home extends Vue {
}
}
private onNewSearcher(searcher: Searcher): void {
this.availableSearchers.unshift(searcher);
}
public onError(error: string) {
this.error = error;
}

3
resources/js/components/Home/Home.vue

@ -32,8 +32,9 @@
<template v-else>
<process-file :file="uploadResult"
:searchers="searchers"
:searchers="availableSearchers"
@newFile="uploadNewFile"
@newSearcher="onNewSearcher"
@error="onError">
</process-file>
</template>

91
resources/js/components/ProcessFile/ProcessFile.ts

@ -1,10 +1,16 @@
import marked from 'marked';
import {Vue, Component, Prop, Watch} from 'vue-property-decorator';
import {FileData} from '@/interfaces/FileData';
import { isServerError, getServerErrorMessage } from '@/SearchDisplace/helpers';
import { eventBus } from '@/app';
@Component
import DefineSearcher from '../Searchers/DefineSearcher.vue';
import RadioButton from "primevue/radiobutton";
@Component({
components: {
RadioButton,
DefineSearcher,
},
})
export default class ProcessFile extends Vue {
/**
@ -60,13 +66,17 @@ export default class ProcessFile extends Vue {
private expandedRows: Array<any> = [];
// The list of options applied to the searchers (for the moment, only replace_with)
private searchersOptions: { [key: string]: string } = {};
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 = '';
/**
*
*/
@ -175,7 +185,6 @@ export default class ProcessFile extends Vue {
* @param {boolean} newValue
*/
toggleUploadDialog(newValue?: boolean) {
if (typeof newValue !== 'undefined') {
this.uploadDialogVisible = newValue;
} else {
@ -264,12 +273,13 @@ export default class ProcessFile extends Vue {
this.processing = true;
this.processedFileContent = '';
let searchers: Array<{ key: string; replace_with: string; }> = [];
let searchers: Array<{ key: string; type: string; value: string; }> = [];
Object.values(this.selectedSearchers).forEach((searcher: any) => {
searchers.push({
'key': searcher.id,
'replace_with': this.searchersOptions[searcher.id] || ''
key: searcher.id,
type: this.searchersOptions[searcher.id].type,
value: this.searchersOptions[searcher.id].value || '',
});
});
@ -336,6 +346,7 @@ export default class ProcessFile extends Vue {
for (let key of Object.keys(this.selectedSearchers)) {
const searcher = this.selectedSearchers[key];
if (!this.isValidParam(searcher.id, searcher.param)) {
return false;
}
@ -354,13 +365,50 @@ export default class ProcessFile extends Vue {
private isValidParam(paramId: string, paramType: string): boolean {
if (
paramType === 'required' &&
(this.searchersOptions[paramId] === '' || this.searchersOptions[paramId] === undefined)
(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
*
@ -371,4 +419,29 @@ export default class ProcessFile extends Vue {
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);
});
}
}

74
resources/js/components/ProcessFile/ProcessFile.vue

@ -8,6 +8,17 @@
</template>
<template #right>
<Button @click="onAddNewSearcher"
type="button"
label="Add new searcher"
class="p-button p-button-outlined p-button-secondary p-button-sm">
</Button>
<Button @click="onDefineSearcher"
type="button"
label="Define searcher"
class="p-button p-button-outlined p-button-primary p-button-sm">
</Button>
<Button
label="Try another document"
@ -66,7 +77,7 @@
@click="downloadOdt"/>
<Button
label="Run filters"
label="Run S&D"
icon="pi pi-play"
class="p-button-success p-button-outlined p-button-sm"
:disabled="!canRunSearchers()"
@ -199,26 +210,52 @@
<template #expansion="slotProps">
<div class="options-subtable">
<div class="p-fluid">
<div class="p-field">
<label :for="`replace_with__${slotProps.data.id}`">
Replace values with:
</label>
<InputText
:id="`replace_with__${slotProps.data.id}`"
type="text"
class="p-inputtext-sm"
v-model="searchersOptions[slotProps.data.id]"
v-tooltip.top="
<div class="p-field-radiobutton">
<RadioButton name="action_type"
id="action_type_replace"
value="replace"
v-model="searchersOptions[slotProps.data.id].type">
</RadioButton>
<label for="action_type_replace">Replace</label>
</div>
<div class="p-field-radiobutton">
<RadioButton name="action_type"
id="action_type_displace"
value="displace"
v-model="searchersOptions[slotProps.data.id].type">
</RadioButton>
<label for="action_type_displace">Displace</label>
</div>
</div>
</div>
<div class="p-field">
<label :for="`displace_with__${slotProps.data.id}`">
<span>
{{ searchersOptions[slotProps.data.id].type === 'replace' ? 'Replace' : 'Displace (tag)' }}
</span>
<span>
values with:
</span>
</label>
<InputText
:id="`displace_with__${slotProps.data.id}`"
type="text"
class="p-inputtext-sm"
v-model="searchersOptions[slotProps.data.id].value"
v-tooltip.top="
(slotProps.data.param === 'required') ?
'This field is required.' : null
"
:class="{'p-invalid': !isValidParam(slotProps.data.id, slotProps.data.param)}"/>
</div>
:class="{'p-invalid': !isValidParam(slotProps.data.id, slotProps.data.param)}"/>
</div>
</div>
</template>
@ -233,7 +270,6 @@
</DataTable>
</Sidebar>
<!-- File upload dialog -->
<Dialog header="Upload a new file"
:visible.sync="uploadDialogVisible"
@ -256,6 +292,12 @@
</FileUpload>
</Dialog>
<define-searcher v-if="showDefineSearcher"
:text="searcherToDefineText"
@done="onSearcherDefined"
@close="showDefineSearcher = false">
</define-searcher>
</div>
</template>

18
resources/js/components/Regex/Create.vue

@ -9,17 +9,29 @@
<span class="p-float-label">
<InputText v-model="name"
type="text"
class="p-inputtext-sm"
name="name"
id="name">
</InputText>
<label for="name">Enter searcher name</label>
</span>
</div>
<div class="p-field">
<span class="p-float-label">
<InputText v-model="tag"
type="text"
name="tag"
id="tag">
</InputText>
<label for="tag">Enter searcher tag (optional)</label>
</span>
</div>
<Button @click="onSave"
:disabled="( ! name && ! regex) || ! pattern"
class="p-button-sm p-button-raised">
class="p-button-raised">
Save
</Button>
</div>
@ -65,6 +77,7 @@ import { eventBus } from "@/app";
export default class Create extends Vue {
private name: string = '';
private tag: string = '';
private pattern: string = '';
private flags: Array<string> = ['g', 'i'];
@ -87,6 +100,7 @@ export default class Create extends Vue {
try {
const { data } = await (window as any).axios.post('/regex', {
name: this.name,
tag: this.tag,
expression: this.pattern,
});

41
resources/js/components/Searchers/Create.vue

@ -1,5 +1,5 @@
<template>
<div id="searchers-create">
<div id="searchers-create" :class="{'is-defining': isDefining,}">
<div>
<h1> {{ searcher.id ? 'Edit' : 'New' }} searcher </h1>
@ -14,6 +14,16 @@
</Button>
</div>
<div>
<h3>Default tag (optional)</h3>
<InputText v-model="tag"
type="text"
placeholder="Enter the default tag"
class="input">
</InputText>
</div>
<div v-if="standalone">
<p>
A searcher may contain multiple compounded searchers on multiple rows and columns.
@ -67,6 +77,7 @@ import {Component, Prop, Vue} from "vue-property-decorator";
export default class Create extends Vue {
private id: String = '';
private name: String = '';
private tag: String = '';
private rows: Array<Array<any>> = [];
@Prop({
@ -74,6 +85,7 @@ import {Component, Prop, Vue} from "vue-property-decorator";
return {
id: '',
name: '',
tag: '',
rows: [],
};
}
@ -83,6 +95,12 @@ import {Component, Prop, Vue} from "vue-property-decorator";
@Prop({default: true})
public readonly standalone!: boolean;
@Prop({default: true})
public readonly isDefining!: boolean;
@Prop({default: ''})
public readonly definedSearcher!: string;
onNewRowSearcherAdded(searcher: Object) {
const length = this.rows.push([]);
@ -108,6 +126,7 @@ import {Component, Prop, Vue} from "vue-property-decorator";
const updatedSearcher = Object.assign(this.searcher, {
name: this.name,
tag: this.tag,
rows: this.rows,
});
@ -118,6 +137,12 @@ import {Component, Prop, Vue} from "vue-property-decorator";
try {
const searcher = this.id ? await this.update() : await this.create();
if (this.isDefining) {
this.$emit('defined', searcher);
return;
}
window.location.href = `/searchers/${searcher.id}`;
} catch (e) {
console.log(e);
@ -128,6 +153,7 @@ import {Component, Prop, Vue} from "vue-property-decorator";
async update() {
const { data } = await (window as any).axios.put(`/searchers/${this.id}`, {
name: this.name,
tag: this.tag,
rows: this.rows,
});
@ -137,6 +163,7 @@ import {Component, Prop, Vue} from "vue-property-decorator";
async create() {
const { data } = await (window as any).axios.post('/searchers', {
name: this.name,
tag: this.tag,
rows: this.rows,
});
@ -168,6 +195,7 @@ import {Component, Prop, Vue} from "vue-property-decorator";
if (this.name === '' || this.name === undefined) {
this.name = 'Unnamed searcher - ' + Date.now();
}
const searcher = this.id ? await this.update() : await this.create();
}
@ -180,6 +208,17 @@ import {Component, Prop, Vue} from "vue-property-decorator";
this.id = this.searcher.id;
this.rows = this.searcher.rows;
this.name = this.searcher.name;
this.tag = this.searcher.tag;
}
if (this.isDefining && this.definedSearcher) {
this.name = this.definedSearcher;
this.rows.push([
{
expression: this.definedSearcher,
},
]);
}
eventBus.$on('changeRoute', this.changeRoute);

56
resources/js/components/Searchers/DefineSearcher.vue

@ -0,0 +1,56 @@
<template>
<Dialog header="Define searcher"
:visible.sync="showDialog"
:maximizable="true"
:modal="true"
:block-scroll="true"
:style="{width: '95vw', height: '95vh',}"
:contentStyle="{overflow: 'visible'}"
:baseZIndex="2014"
id="define-searcher"
ref="define-searcher">
<create-searcher :is-defining="true"
:defined-searcher="text"
@defined="onDefined">
</create-searcher>
</Dialog>
</template>
<script lang="ts">
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
import CreateSearcher from './Create';
@Component({
components: {
CreateSearcher,
},
})
export default class DefineSearcher extends Vue {
private showDialog: boolean = false;
@Prop({default: ''})
public readonly text!: string;
private onDefined(searcher: Object) {
this.$emit('done', searcher);
}
@Watch('showDialog')
showDialogChanged() {
if ( ! this.showDialog) {
this.$emit('close');
}
}
mounted() {
this.showDialog = true;
}
};
</script>
<style lang="sass">
#define-searcher
.p-dialog-content
min-height: 90%
</style>

2
resources/js/services/ApiService.ts

@ -73,7 +73,7 @@ export default class ApiService {
* @param {string} content The content of the document
* @param {Array} searchers The list of searchers and their replace values
*/
public async filterDocument(content: string, searchers: Array<{ key: string; replace_with: string; }>) {
public async filterDocument(content: string, searchers: Array<{ key: string; type: string; value: string; }>) {
try {
let response = await axios.post(
this.apiRoutes.searchAndDisplace,

1
resources/sass/components/_index.sass

@ -1 +1,2 @@
@import "regex/index"
@import "searchers/index"

3
resources/sass/components/searchers/_create.sass

@ -0,0 +1,3 @@
#searchers-create.is-defining
max-height: 100%
overflow-y: auto

1
resources/sass/components/searchers/_index.sass

@ -0,0 +1 @@
@import "create"
Loading…
Cancel
Save