Browse Source

Merge

master
Orzu Ionut 3 years ago
parent
commit
07adbf521c
  1. 26
      app/Http/Controllers/FileController.php
  2. 14
      app/SearchDisplace/Searchers/Mapper.php
  3. 3
      app/SearchDisplace/Searchers/SearchersCollection.php
  4. 7
      app/SearchDisplace/Searchers/SearchersStorage.php
  5. 8
      public/css/app.css
  6. 1418
      public/js/app.js
  7. 7
      resources/js/app.ts
  8. 16
      resources/js/components/Home/Home.ts
  9. 12
      resources/js/components/ProcessFile/ProcessFile.scss
  10. 83
      resources/js/components/ProcessFile/ProcessFile.ts
  11. 15
      resources/js/components/ProcessFile/ProcessFile.vue
  12. 59
      resources/js/components/Regex/Create.vue
  13. 16
      resources/js/components/Searchers/Create.vue
  14. 68
      resources/js/components/Searchers/Index.vue
  15. 25
      resources/js/components/layout/Header.vue
  16. 3
      resources/js/interfaces/Searcher.ts
  17. 31
      resources/js/services/ApiService.ts
  18. 12
      resources/sass/components/regex/create/_index.sass
  19. 5
      routes/api.php

26
app/Http/Controllers/FileController.php

@ -63,10 +63,10 @@ class FileController extends Controller
public function convert(): JsonResponse
{
$mdFileContent = request()->input('content');
$tmpFileId = (string) Str::uuid();
$fileId = request()->input('file_id');
Storage::disk('local')->put('tmp/' . $tmpFileId . '.md', $mdFileContent);
$tmpMdFilePath = Storage::path('tmp/' . $tmpFileId . '.md');
Storage::disk('local')->put('tmp/' . $fileId . '.md', $mdFileContent);
$tmpMdFilePath = Storage::path('tmp/' . $fileId . '.md');
$pandoc = new Process([
'pandoc',
@ -84,10 +84,10 @@ class FileController extends Controller
}
$odtFileContent = $pandoc->getOutput();
Storage::disk('local')->put('tmp/' . $tmpFileId . '.odt', $odtFileContent);
Storage::disk('local')->put('tmp/' . $fileId . '.odt', $odtFileContent);
return response()->json([
'path' => 'tmp/' . $tmpFileId . '.odt'
'path' => 'tmp/' . $fileId . '.odt'
]);
}
@ -100,4 +100,20 @@ class FileController extends Controller
{
return Storage::download($path);
}
/**
* Delete a file currently in progress
*
* @param string $id
* @return JsonResponse
*/
public function delete(string $id): JsonResponse
{
$success = Storage::delete([
"tmp/{$id}.md",
"tmp/{$id}.odt",
"contracts/{$id}.md",
]);
return response()->json(['success' => $success]);
}
}

14
app/SearchDisplace/Searchers/Mapper.php

@ -11,54 +11,67 @@ class Mapper
$this->mapper = [
'amount-of-money' => [
'name' => 'Amount Of Money',
'param' => SearchersCollection::PARAM_REQUIRED
],
'credit-card-number' => [
'name' => 'Credit Card Number',
'param' => SearchersCollection::PARAM_REQUIRED
],
'distance' => [
'name' => 'Distance',
'param' => SearchersCollection::PARAM_REQUIRED
],
'duration' => [
'name' => 'Duration',
'param' => SearchersCollection::PARAM_REQUIRED
],
'email' => [
'name' => 'Email',
'param' => SearchersCollection::PARAM_OPTIONAL
],
'numeral' => [
'name' => 'Numeral',
'param' => SearchersCollection::PARAM_REQUIRED
],
'ordinal' => [
'name' => 'Ordinal',
'param' => SearchersCollection::PARAM_REQUIRED
],
'phone-number' => [
'name' => 'Phone Number',
'param' => SearchersCollection::PARAM_REQUIRED
],
'quantity' => [
'name' => 'Quantity',
'param' => SearchersCollection::PARAM_REQUIRED
],
'temperature' => [
'name' => 'Temperature',
'param' => SearchersCollection::PARAM_REQUIRED
],
'time' => [
'name' => 'Time',
'param' => SearchersCollection::PARAM_REQUIRED
],
'url' => [
'name' => 'URL',
'param' => SearchersCollection::PARAM_REQUIRED
],
'volume' => [
'name' => 'Volume',
'param' => SearchersCollection::PARAM_REQUIRED
],
];
}
@ -76,6 +89,7 @@ class Mapper
$items[$key] = [
'id' => $key,
'name' => $value['name'],
'param' => $value['param']
];
}

3
app/SearchDisplace/Searchers/SearchersCollection.php

@ -4,6 +4,9 @@ namespace App\SearchDisplace\Searchers;
class SearchersCollection
{
const PARAM_REQUIRED = 'required';
const PARAM_OPTIONAL = 'optional';
public function all()
{
$collection = [];

7
app/SearchDisplace/Searchers/SearchersStorage.php

@ -28,6 +28,13 @@ class SearchersStorage
$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]
];
}
}

8
public/css/app.css

@ -10840,7 +10840,7 @@
#regex-create .regex-box {
background-color: #001221;
display: flex;
width: 850px;
width: 90%;
height: 500px;
margin-left: auto;
margin-right: auto;
@ -10849,14 +10849,10 @@
border-radius: 5px;
transition: all 0.25s linear;
}
#regex-create .regex-box:hover {
box-shadow: 0.4rem 1.4rem 1.4rem rgba(0, 0, 0, 0.2);
transform: translateY(-10px);
}
#regex-create .main {
display: flex;
flex-direction: column;
width: 550px;
width: 100%;
}
#regex-create aside {
width: 300px;

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

7
resources/js/app.ts

@ -35,6 +35,7 @@ import Timeline from 'primevue/timeline';
import ScrollPanel from 'primevue/scrollpanel';
import ConfirmationService from 'primevue/confirmationservice';
import ConfirmDialog from 'primevue/confirmdialog';
import Tooltip from 'primevue/tooltip';
// Own components
import AppHeader from './components/layout/Header.vue';
@ -83,15 +84,15 @@ Vue.component('Timeline', Timeline);
Vue.component('ScrollPanel', ScrollPanel);
Vue.component('ConfirmDialog', ConfirmDialog);
Vue.directive('tooltip', Tooltip);
// Layout
Vue.component('app-header', AppHeader);
Vue.component('app-footer', AppFooter);
// Views
Vue.component('home', Home);
Vue.component('regex-create', RegexCreate);
Vue.component('searchers-index', SearchersIndex);
Vue.component('searchers-create', SearchersCreate);
Vue.component('searchers-show', SearchersShow);
@ -102,6 +103,8 @@ Vue.component('process-file', ProcessFile);
Vue.component('app-header', AppHeader);
Vue.component('app-footer', AppFooter);
export const eventBus = new Vue();
new Vue({
el: '#app',
});

16
resources/js/components/Home/Home.ts

@ -1,11 +1,13 @@
import {Vue, Component, Prop} from 'vue-property-decorator';
import FileUploadResponse from '@/interfaces/responses/FileUploadResponse';
import {isServerError} from "@/SearchDisplace/helpers";
import { Searcher } from '@/interfaces/Searcher';
import { eventBus } from '@/app';
@Component
export default class Home extends Vue {
@Prop({default: []})
public readonly searchers!: { [key: string]: string; }
public readonly searchers!: Searcher[];
public uiBlocked = false;
public uploading = false;
@ -16,6 +18,12 @@ export default class Home extends Vue {
};
public error: string = '';
public mounted()
{
eventBus.$on('changeRoute', this.changeRoute);
}
/**
* A method which uploads the files to the server for processing
*
@ -71,4 +79,10 @@ export default class Home extends Vue {
public onError(error: string) {
this.error = error;
}
public changeRoute(url: string) {
if (!this.fileUploaded) {
window.location.href = url;
}
}
}

12
resources/js/components/ProcessFile/ProcessFile.scss

@ -56,3 +56,15 @@ label.switch-label {
font-size: initial;
}
}
@media only screen and (max-width: 1680px) {
.p-card-header .p-toolbar {
flex-flow: column;
align-items: start;
}
}
.p-tooltip {
z-index: 2004 !important;
}

83
resources/js/components/ProcessFile/ProcessFile.ts

@ -2,6 +2,7 @@ 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
export default class ProcessFile extends Vue {
@ -70,7 +71,30 @@ export default class ProcessFile extends Vue {
*
*/
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);
}
/**
@ -94,6 +118,25 @@ export default class ProcessFile extends Vue {
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
*/
@ -136,7 +179,7 @@ export default class ProcessFile extends Vue {
if (typeof newValue !== 'undefined') {
this.uploadDialogVisible = newValue;
} else {
this.uploadDialogVisible = !this.searchersDialogVisible;
this.uploadDialogVisible = !this.uploadDialogVisible;
}
}
@ -146,6 +189,8 @@ export default class ProcessFile extends Vue {
* @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',
@ -154,6 +199,7 @@ export default class ProcessFile extends Vue {
this.fileContent = this.processedFileContent = '';
let file = event.files[0];
this.toggleUploadDialog(false);
this.$api.discardFile(this.file.id);
this.$emit('newFile', file);
},
reject: () => {
@ -248,7 +294,6 @@ export default class ProcessFile extends Vue {
* Create the diff preview for the document
*/
private createDiffPreview() {
console.log('CREATING DIFF PREVIEW: ', this.processedFileContent);
this.processedFileContentPreview = this.processedFileContent;
let indexes: Array<{ start: number; end: number }> = [];
@ -279,11 +324,43 @@ export default class ProcessFile extends Vue {
* Download the document in ODT format
*/
private async downloadOdt() {
let response = await this.$api.convertFile(this.processedFileContent);
let response = await this.$api.convertFile(this.processedFileContent, this.file.id);
window.open(`${window.location.origin}/file/download/` + response.path);
}
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' &&
(this.searchersOptions[paramId] === '' || this.searchersOptions[paramId] === undefined)
) {
return false;
}
return true;
}
/**
* Watch the `showDiffHighlight` property for changes
*

15
resources/js/components/ProcessFile/ProcessFile.vue

@ -69,7 +69,7 @@
label="Run filters"
icon="pi pi-play"
class="p-button-success p-button-outlined p-button-sm"
:disabled="fileContent == '' || Object.keys(selectedSearchers).length === 0"
:disabled="!canRunSearchers()"
@click="runSearchers"/>
</template>
</Toolbar>
@ -119,7 +119,8 @@
<Button
icon="pi pi-list"
class="p-button-info p-button-icon-only sidebar-toggle-button"
@click="toggleSearchersSidebar()"/>
@click="toggleSearchersSidebar()"
:disabled="searchersDialogVisible === true"/>
</div>
<div class="p-grid p-jc-between sidebar-title-container">
@ -209,7 +210,12 @@
:id="`replace_with__${slotProps.data.id}`"
type="text"
class="p-inputtext-sm"
v-model="searchersOptions[slotProps.data.id]"/>
v-model="searchersOptions[slotProps.data.id]"
v-tooltip.top="
(slotProps.data.param === 'required') ?
'This field is required.' : null
"
:class="{'p-invalid': !isValidParam(slotProps.data.id, slotProps.data.param)}"/>
</div>
</div>
@ -221,7 +227,7 @@
label="Run filters"
icon="pi pi-play"
class="p-button-success p-button-sm"
:disabled="fileContent == '' || Object.keys(selectedSearchers).length === 0"
:disabled="!canRunSearchers()"
@click="toggleSearchersSidebar(); runSearchers()"/>
</template>
</DataTable>
@ -234,6 +240,7 @@
:maximizable="true"
:style="{width: '50vw'}"
:contentStyle="{overflow: 'visible'}"
:baseZIndex="2014"
id="upload_dialog"
ref="upload-dialog">

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

@ -1,29 +1,29 @@
<template>
<div id="regex-create">
<div class="regex-header">
<template v-if=" ! regex">
<h1> New regex searcher </h1>
<div class="p-field">
<span class="p-float-label">
<InputText v-model="name"
type="text"
class="p-inputtext-sm"
name="name"
id="name">
</InputText>
</span>
<h1 v-if=" ! regex"> New regex searcher </h1>
<div class="p-d-flex p-jc-center">
<div class="p-formgroup-inline">
<div class="p-field" v-if=" ! regex">
<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>
<Button @click="onSave"
:disabled="( ! name && ! regex) || ! pattern"
class="p-button-sm p-button-raised">
Save
</Button>
</div>
<label for="name">Enter searcher name</label>
</template>
<Button @click="onSave"
:disabled="( ! name && ! regex) || ! pattern"
class="p-button-sm p-button-raised">
Save
</Button>
</div>
</div>
<div class="regex-box">
@ -50,6 +50,7 @@ import TextBox from './TextBox.vue';
import PatternBox from './PatternBox.vue';
import Flags from './Flags.vue';
import SideBar from './SideBar.vue';
import { eventBus } from "@/app";
@Component({
name: 'RegexCreate',
@ -104,10 +105,24 @@ export default class Create extends Vue {
}
}
public async changeRoute(url: string) {
if (this.pattern !== '' && this.pattern !== undefined) {
if (this.name === '' || this.name === undefined) {
this.name = 'Unnamed regex - ' + Date.now();
}
await this.save();
}
window.location.href = url;
}
created() {
if (this.regex) {
this.pattern = this.regex;
}
eventBus.$on('changeRoute', this.changeRoute);
}
};
</script>

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

@ -51,7 +51,8 @@
</template>
<script lang="ts">
import {Component, Prop, Vue} from "vue-property-decorator";
import { eventBus } from "@/app";
import {Component, Prop, Vue} from "vue-property-decorator";
import AddBox from './AddBox.vue';
import SearcherShow from './Show.vue';
@ -162,6 +163,17 @@
}
}
public async changeRoute(url: string) {
if (this.rows.length > 0) {
if (this.name === '' || this.name === undefined) {
this.name = 'Unnamed searcher - ' + Date.now();
}
const searcher = this.id ? await this.update() : await this.create();
}
window.location.href = url;
}
created() {
// Editing.
if (this.searcher.id) {
@ -169,6 +181,8 @@
this.rows = this.searcher.rows;
this.name = this.searcher.name;
}
eventBus.$on('changeRoute', this.changeRoute);
}
};
</script>

68
resources/js/components/Searchers/Index.vue

@ -3,11 +3,7 @@
<h1> Searchers </h1>
<div v-if=" ! allowSelect" style="margin-bottom: 1rem;">
<a href="/searchers/create">
<Button class="fc-button p-button-success">
Add searcher
</Button>
</a>
<Button class="fc-button p-button-success" label="Add searcher" @click="createSearcher()"/>
</div>
<div class="content">
@ -27,9 +23,33 @@
<Column header="Name" sortable>
<template #body="slotProps">
<a @click.stop="onOpen(slotProps.data.id)" class="searcher-link">
{{ slotProps.data.name }}
</a>
{{ slotProps.data.name }}
</template>
</Column>
<Column header="Actions" headerStyle="text-align: right">
<template #body="slotProps">
<div class="action-buttons" style="text-align: right">
<Button
:disabled="slotProps.data.type !== 'custom'"
label="Open"
icon="pi pi-folder-open"
class="p-button-sm"
@click.stop="onOpen(slotProps.data.id)"/>
<Button
:disabled="slotProps.data.type !== 'custom'"
label="Edit"
icon="pi pi-pencil"
class="p-button-sm p-button-success"
@click.stop="onEdit(slotProps.data.id)"/>
<Button
:disabled="slotProps.data.type !== 'custom'"
label="Delete"
icon="pi pi-times"
class="p-button-sm p-button-danger"
@click.stop="onDelete(slotProps.data.id)"/>
</div>
</template>
</Column>
</DataTable>
@ -40,6 +60,7 @@
<script lang="ts">
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
import {Searcher} from "@/interfaces/Searcher";
import { eventBus } from "@/app";
@Component({
})
@ -66,7 +87,27 @@ export default class Index extends Vue {
}
onOpen(id: string) {
window.open(this.getURL(id), '_blank');
window.open(this.getURL(id), '_self');
}
onEdit(id: string) {
window.open(this.getURL(id) + '/edit', '_self');
}
async onDelete(id: string) {
try {
await (window as any).axios.delete(`/searchers/${id}`);
this.$toast.add({
severity: 'success',
summary: 'Searcher deleted.',
life: 3000,
});
this.searchers = this.searchers.filter(x => x.id !== id);
} catch (e) {
console.log(e);
}
}
getURL(id: string) {
@ -78,8 +119,17 @@ export default class Index extends Vue {
this.$emit('selected', value);
}
public async changeRoute(url: string) {
window.location.href = url;
}
createSearcher() {
window.location.href = '/searchers/create';
}
created() {
this.boot();
eventBus.$on('changeRoute', this.changeRoute);
}
};
</script>

25
resources/js/components/layout/Header.vue

@ -20,33 +20,26 @@
label="Searchers" />
</div>
<ConfirmDialog></ConfirmDialog>
<ConfirmDialog :blockScroll="false"></ConfirmDialog>
</div>
</template>
<script lang="ts">
import { eventBus } from '@/app';
import Vue from 'vue';
import Component from 'vue-class-component';
@Component
export default class AppHeader extends Vue {
onHomeButtonClick() {
window.location.href = '/';
}
/**
* Called when we want to change the route
*
* @param {string} url The url to the new route
*/
onRouteChange(url: string) {
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: () => {
console.log("ACCEPT!");
window.location.href = url;
},
reject: () => {
// TODO: Show a message to the user that the action was cancelled.
}
});
eventBus.$emit('changeRoute', url);
}
}
</script>

3
resources/js/interfaces/Searcher.ts

@ -1,5 +1,6 @@
export interface Searcher {
id: string;
name: string,
description: string,
param: string,
type: string;
}

31
resources/js/services/ApiService.ts

@ -10,7 +10,8 @@ export default class ApiService {
private readonly apiRoutes = {
file: this.baseUrl + '/api/file',
fileDownload: this.baseUrl + '/api/file/convert',
searchAndDisplace: this.baseUrl + '/search-and-displace'
fileDiscard: this.baseUrl + '/api/file/',
searchAndDisplace: this.baseUrl + '/search-and-displace',
};
constructor() {
@ -89,11 +90,20 @@ export default class ApiService {
}
public async convertFile(content: string) {
/**
* Convert a file from MD to ODT
*
* @param {string} content
* @param {string} fileId
*
* @returns
*/
public async convertFile(content: string, fileId: string) {
try {
let response = await axios.post(
this.apiRoutes.fileDownload,
{
'file_id': fileId,
'content': content
}
);
@ -103,4 +113,21 @@ export default class ApiService {
throw err;
}
}
/**
* Discard a file in progress
*
* @param {string} fileId
*/
public async discardFile(fileId: string) {
try {
let response = await axios.delete(
this.apiRoutes.fileDiscard + fileId
);
return response.data;
} catch (err) {
throw err;
}
}
}

12
resources/sass/components/regex/create/_index.sass

@ -10,7 +10,8 @@
.regex-box
background-color: #001221
display: flex
width: 850px
// width: 850px
width: 90%
height: 500px
margin-left: auto
margin-right: auto
@ -19,14 +20,15 @@
border-radius: 5px
transition: all 0.25s linear
.regex-box:hover
box-shadow: 0.4rem 1.4rem 1.4rem rgba(0, 0, 0, 0.2)
transform: translateY(-10px)
// .regex-box:hover
// box-shadow: 0.4rem 1.4rem 1.4rem rgba(0, 0, 0, 0.2)
// transform: translateY(-10px)
.main
display: flex
flex-direction: column
width: 550px
// width: 550px
width: 100%
aside
width: 300px

5
routes/api.php

@ -30,6 +30,11 @@ Route::name('file.')->prefix('file')->group(
FileController::class,
'convert'
])->name('convert');
Route::delete('/{id}', [
FileController::class,
'delete'
]);
}
);

Loading…
Cancel
Save