Browse Source

Merge

master
Orzu Ionut 3 years ago
parent
commit
84d7cb093d
  1. 31
      .vscode/launch.json
  2. 89
      app/Http/Controllers/FileController.php
  3. 21
      app/Http/Controllers/FilterController.php
  4. 10
      app/Http/Controllers/HomeController.php
  5. 24
      app/Http/Controllers/PagesController.php
  6. 4
      app/Http/Controllers/SearchAndDisplaceController.php
  7. 84
      package-lock.json
  8. 2
      package.json
  9. BIN
      public/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-500.woff
  10. BIN
      public/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-500.woff2
  11. BIN
      public/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-700.woff
  12. BIN
      public/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-700.woff2
  13. BIN
      public/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-regular.woff
  14. BIN
      public/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-regular.woff2
  15. 26610
      public/js/app.js
  16. 32
      resources/js/app.ts
  17. 100
      resources/js/components/Home.vue
  18. 56
      resources/js/components/Home/Home.ts
  19. 35
      resources/js/components/Home/Home.vue
  20. 82
      resources/js/components/ProcessFile.vue
  21. 54
      resources/js/components/ProcessFile/ProcessFile.scss
  22. 160
      resources/js/components/ProcessFile/ProcessFile.ts
  23. 188
      resources/js/components/ProcessFile/ProcessFile.vue
  24. 69
      resources/js/components/helpers/Filter.vue
  25. 2
      resources/js/components/layout/Header.vue
  26. 7
      resources/js/interfaces/FilterInterface.ts
  27. 4
      resources/js/interfaces/FilterOptions.ts
  28. 4
      resources/js/interfaces/responses/ErrorResponse.ts
  29. 5
      resources/js/interfaces/responses/FileStatusResponse.ts
  30. 6
      resources/js/interfaces/responses/FileUploadResponse.ts
  31. 18
      resources/js/plugins/ApiPlugin.ts
  32. 7
      resources/js/plugins/plugins.d.ts
  33. 110
      resources/js/services/ApiService.ts
  34. 10
      resources/views/pages/home.blade.php
  35. 10
      routes/api.php
  36. 15
      routes/web.php
  37. 7
      webpack.mix.js
  38. 10261
      yarn.lock

31
.vscode/launch.json

@ -0,0 +1,31 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003,
"stopOnEntry": false,
"log": true,
"pathMappings": {
"/home/vagrant/sandd-core": "/mnt/Multimedia/Projects/Web/sandd/searchanddisplace-core",
}
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 9003,
"pathMappings": {
"/home/vagrant/sandd-core": "/mnt/Multimedia/Projects/Web/sandd/searchanddisplace-core",
}
}
]
}

89
app/Http/Controllers/FileController.php

@ -5,6 +5,11 @@ namespace App\Http\Controllers;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\UploadedFile;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\BadResponseException;
use Illuminate\Support\Str;
use Symfony\Component\Process\Process;
class FileController extends Controller
{
@ -17,29 +22,85 @@ class FileController extends Controller
*/
public function create(): JsonResponse
{
$file = request()->file('file');
$filename = time() . $file->getClientOriginalName();
try {
/** @var UploadedFile $file */
$file = request()->file('file');
$time = time();
$fileName = $time . '-' . $file->getClientOriginalName();
$fileId = $time . pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
$path = Storage::putFileAs('files/', $file, $filename);
$path = Storage::putFileAs('files/', $file, $fileName);
$guzzle = new Client();
/** @var \Psr\Http\Message\ResponseInterface $response */
$response = $guzzle->request('POST', 'http://ingest.sandd/ingest', [
'multipart' => [
[
'name' => 'id',
'contents' => $fileId,
'headers' => [ 'Content-Type' => 'application/json' ]
],
[
'name' => 'document',
'contents' => fopen($file->getPathname(), 'r')
]
]
]);
return response()->json([
'file' => $file->getClientOriginalName(),
'id' => $fileId,
'path' => $path,
]);
} catch (BadResponseException $e) {
return response()->json([
'message' => $e->getMessage(),
'response' => $e->getResponse()
], 400);
}
}
/**
*
*/
public function convert(): JsonResponse
{
$mdFileContent = request()->input('content');
$tmpFileId = (string) Str::uuid();
Storage::disk('local')->put('tmp/' . $tmpFileId . '.md', $mdFileContent);
$tmpMdFilePath = Storage::path('tmp/' . $tmpFileId . '.md');
$pandoc = new Process([
'pandoc',
'-f',
'markdown',
'-t',
'odt',
$tmpMdFilePath
]);
$pandoc->start();
while ($pandoc->isRunning()) {
//
}
$odtFileContent = $pandoc->getOutput();
Storage::disk('local')->put('tmp/' . $tmpFileId . '.odt', $odtFileContent);
return response()->json([
'file' => $file->getClientOriginalName(),
'id' => $filename,
'path' => $path
'path' => 'tmp/' . $tmpFileId . '.odt'
]);
}
/**
*
* @param string $id
*
* @return JsonResponse
*
* @throws BindingResolutionException
* @param string $path
* @return mixed
*/
public function get(string $id): JsonResponse
public function download(string $path)
{
return response()->json(['success']);
return Storage::download($path);
}
}

21
app/Http/Controllers/FilterController.php

@ -11,19 +11,28 @@ class FilterController extends Controller
'phone_number' => [
'display_name' => 'Phone number',
'options' => [
'replace_with' => [
'#', '*', 'x',
[
'name' => 'Replace with',
'type' => 'select',
'values' => [
'#', '*', 'x'
]
]
]
],
'email' => [
'display_name' => 'Email',
'options' => [
'replace_with' => [
'#', '*', 'x',
[
'name' => 'Replace with',
'type' => 'select',
'values' => [
'#', '*', 'x'
]
],
'include_domain' => [
true, false
[
'name' => 'Include domain',
'type' => 'switch'
]
]
]

10
app/Http/Controllers/HomeController.php

@ -3,10 +3,18 @@
namespace App\Http\Controllers;
use App\SearchDisplace\Searchers\Mapper;
use Illuminate\View\View;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\Container\BindingResolutionException;
class HomeController extends Controller
{
public function index()
/**
*
* @return View|Factory
* @throws BindingResolutionException
*/
public function index(): View
{
return view('pages.home', [
'searchers' => (new Mapper())->get(),

24
app/Http/Controllers/PagesController.php

@ -1,24 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\View\View;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\Container\BindingResolutionException;
class PagesController extends Controller
{
/**
* Return the home page view
*
* @return View|Factory
*
* @throws BindingResolutionException
*/
public function home(): View
{
$filters = FilterController::$filters;
return view('pages.home')->with('filters', $filters);
}
}

4
app/Http/Controllers/SearchAndDisplaceController.php

@ -32,8 +32,8 @@ class SearchAndDisplaceController extends Controller
'searchers' => 'required|array', // Check if matches all rules, must have 'key' and 'replace_with'.
]);
$searchAndDisplace = new SearchAndDisplace(request()->get('content'), [
'searchers' => request()->get('searchers'),
$searchAndDisplace = new SearchAndDisplace(request()->input('content'), [
'searchers' => request()->input('searchers'),
]);
try {

84
package-lock.json

@ -1529,6 +1529,12 @@
"@types/node": "*"
}
},
"@types/highlight.js": {
"version": "9.12.4",
"resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.4.tgz",
"integrity": "sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==",
"dev": true
},
"@types/http-proxy": {
"version": "1.17.5",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.5.tgz",
@ -1595,6 +1601,29 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
},
"@types/linkify-it": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.1.tgz",
"integrity": "sha512-pQv3Sygwxxh6jYQzXaiyWDAHevJqWtqDUv6t11Sa9CPGiXny66II7Pl6PR8QO5OVysD6HYOkHMeBgIjLnk9SkQ==",
"dev": true
},
"@types/markdown-it": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.0.1.tgz",
"integrity": "sha512-mHfT8j/XkPb1uLEfs0/C3se6nd+webC2kcqcy8tgcVr0GDEONv/xaQzAN+aQvkxQXk/jC0Q6mPS+0xhFwRF35g==",
"dev": true,
"requires": {
"@types/highlight.js": "^9.7.0",
"@types/linkify-it": "*",
"@types/mdurl": "*"
}
},
"@types/mdurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==",
"dev": true
},
"@types/micromatch": {
"version": "2.3.30",
"resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-2.3.30.tgz",
@ -9400,6 +9429,14 @@
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
"dev": true
},
"linkify-it": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz",
"integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==",
"requires": {
"uc.micro": "^1.0.1"
}
},
"loader-runner": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
@ -9599,6 +9636,30 @@
"object-visit": "^1.0.0"
}
},
"markdown-it": {
"version": "12.0.6",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.6.tgz",
"integrity": "sha512-qv3sVLl4lMT96LLtR7xeRJX11OUFjsaD5oVat2/SNBIb21bJXwal2+SklcRbTwGwqWpWH/HRtYavOoJE+seL8w==",
"requires": {
"argparse": "^2.0.1",
"entities": "~2.1.0",
"linkify-it": "^3.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
},
"dependencies": {
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"entities": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
}
}
},
"md5": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
@ -9627,6 +9688,11 @@
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
"dev": true
},
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -11920,8 +11986,7 @@
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"q": {
"version": "1.5.1",
@ -14149,6 +14214,11 @@
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
"dev": true
},
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"uglify-js": {
"version": "3.4.10",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz",
@ -14525,6 +14595,16 @@
"vue-style-loader": "^4.1.0"
}
},
"vue-markdown-render": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/vue-markdown-render/-/vue-markdown-render-1.1.1.tgz",
"integrity": "sha512-NkkGOC4ip6C3Cp0dyFBCEPZ9dDF2yI2wrePSwoU2xamyhvcs0eJk0t9VZcUiKCz1bm5SvVeTelIU0qzVgi6jVQ==",
"requires": {
"markdown-it": "^12.0.4",
"punycode": "^2.1.1",
"vue": "^2.6.12"
}
},
"vue-property-decorator": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-8.5.1.tgz",

2
package.json

@ -10,6 +10,7 @@
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"@types/markdown-it": "^12.0.1",
"@vue/cli-plugin-babel": "^3.8.0",
"@vue/cli-plugin-typescript": "^3.8.0",
"@vue/cli-service": "^3.8.0",
@ -33,6 +34,7 @@
"tsconfig-paths-webpack-plugin": "^3.5.1",
"vue": "^2.6.12",
"vue-class-component": "^7.2.6",
"vue-markdown-render": "^1.1.1",
"vue-property-decorator": "^8.1.0"
}
}

BIN
public/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-500.woff

BIN
public/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-500.woff2

BIN
public/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-700.woff

BIN
public/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-700.woff2

BIN
public/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-regular.woff

BIN
public/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-regular.woff2

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

32
resources/js/app.ts

@ -21,23 +21,35 @@ import ToastService from 'primevue/toastservice';
import Toast from 'primevue/toast';
import Fieldset from 'primevue/fieldset';
import SelectButton from 'primevue/selectbutton';
import Dropdown from 'primevue/dropdown';
import InputSwitch from 'primevue/inputswitch';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import OverlayPanel from 'primevue/overlaypanel';
import Checkbox from 'primevue/checkbox';
import ProgressSpinner from 'primevue/progressspinner';
import InputText from 'primevue/inputtext';
import Dialog from 'primevue/dialog';
import Message from 'primevue/message';
// Own components
import AppHeader from './components/layout/Header.vue';
import AppFooter from './components/layout/Footer.vue';
import ProcessFile from './components/ProcessFile.vue';
import Filter from './components/helpers/Filter.vue';
import Home from './components/Home.vue';
import RegexCreate from './components/Regex/Create.vue';
import SearchersCreate from './components/Searchers/Create.vue';
import SearchersShow from './components/Searchers/Show.vue';
import ApiPlugin from './plugins/ApiPlugin';
import Home from './components/Home/Home.vue';
import ProcessFile from './components/ProcessFile/ProcessFile.vue';
Vue.use(PrimeVue, {
ripple: true,
});
Vue.use(ToastService);
Vue.use(ApiPlugin);
// Vue.component('vue-markdown', VueMarkdown);
Vue.component('Button', Button);
Vue.component('Panel', Panel);
Vue.component('Card', Card);
@ -51,8 +63,17 @@ Vue.component('Listbox', Listbox);
Vue.component('Toolbar', Toolbar);
Vue.component('Skeleton', Skeleton);
Vue.component('Toast', Toast);
Vue.component('SelectButton', SelectButton);
Vue.component('Dropdown', Dropdown);
Vue.component('Fieldset', Fieldset);
Vue.component('InputSwitch', InputSwitch);
Vue.component('DataTable', DataTable);
Vue.component('Column', Column);
Vue.component('OverlayPanel', OverlayPanel);
Vue.component('Checkbox', Checkbox);
Vue.component('ProgressSpinner', ProgressSpinner);
Vue.component('InputText', InputText);
Vue.component('Dialog', Dialog);
Vue.component('Message', Message);
// Layout
Vue.component('app-header', AppHeader);
@ -68,7 +89,8 @@ Vue.component('searchers-show', SearchersShow);
// Includes
Vue.component('process-file', ProcessFile);
Vue.component('filter-view', Filter);
Vue.component('app-header', AppHeader);
Vue.component('app-footer', AppFooter);
new Vue({
el: '#app',

100
resources/js/components/Home.vue

@ -1,100 +0,0 @@
<template>
<div class="wrap" v-if="!fileUploaded && !uploading">
<Toast position="top-right" />
<Panel header="Please upload a file">
<FileUpload
name="demo[]"
:customUpload="true"
:auto="true"
@uploader="uploadFile"
>
<template #empty>
<p>Drag and drop files to here to upload.</p>
</template>
</FileUpload>
</Panel>
<BlockUI :blocked="uiBlocked" :fullScreen="true"></BlockUI>
</div>
<div class="wrap" v-else-if="!fileUploaded && uploading">
<Skeleton />
<Skeleton />
<Skeleton />
<Skeleton />
<Skeleton />
<Skeleton />
<Skeleton />
</div>
<div class="wrap" v-else>
<process-file :file="uploadResult" :filters="filters"></process-file>
</div>
</template>
<script lang="ts">
import axios from 'axios';
// import Vue from 'vue';
// import Component from 'vue-class-component';
import { Vue, Component, Prop } from 'vue-property-decorator';
@Component
export default class Home extends Vue {
@Prop({ default: [] })
public readonly filters!: Array<object>
public uiBlocked = false;
public uploading = false;
public fileUploaded: boolean = false;
public uploadResult = null;
/**
*
*/
public created()
{
console.log(this.filters);
}
/**
* A method which uploads the files to the server for processing
*
* @param event The event containing the uploaded files information
*/
public uploadFile(event: any): void {
this.uploading = true;
this.fileUploaded = false;
this['$toast'].add({severity:'success', summary: 'Success Message', detail:'Order submitted', life: 3000});
let file = event.files[0];
let formData = new FormData();
formData.append('file', file);
setTimeout(
() => {
axios.post(
'http://core.sandd/api/file',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
).then(
(response) => {
this.fileUploaded = true;
this.uploadResult = response.data;
// console.log('Success: ', response);
}
).catch(
(err) => {
console.log('Error: ', err);
}
);
}, 500
)
}
}
</script>

56
resources/js/components/Home/Home.ts

@ -0,0 +1,56 @@
import { Vue, Component, Prop } from 'vue-property-decorator';
import FileUploadResponse from '@interfaces/responses/FileUploadResponse';
@Component
export default class Home extends Vue {
@Prop({ default: [] })
public readonly searchers!: { [key: string]: string; }
public uiBlocked = false;
public uploading = false;
public fileUploaded: boolean = false;
public uploadResult: FileUploadResponse = {
id: '',
file: '',
path: ''
};
/**
*
*/
public created()
{}
/**
* 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> {
this.uploading = true;
this.fileUploaded = false;
this.$toast.add({severity:'info', summary: 'Uploading...', detail:'Uploading your file...', life: 3000});
try {
let file = event.files[0];
let response = await this.$api.uploadFile(file);
this.fileUploaded = true;
this.uploadResult = response;
} catch (err) {
console.log('Error uploading file: ', err);
this.$toast.add({
severity: 'error',
summary: 'Error!',
detail: 'There was an error uploading your file. Please try again later',
life: 3000
});
this.uploading = false;
this.fileUploaded = false;
}
}
}

35
resources/js/components/Home/Home.vue

@ -0,0 +1,35 @@
<template>
<div class="wrap" v-if="!fileUploaded && !uploading">
<Toast />
<Panel header="Please upload a file">
<FileUpload
name="demo[]"
:customUpload="true"
:auto="true"
@uploader="uploadFile"
>
<template #empty>
<p>Drag and drop files to here to upload.</p>
</template>
</FileUpload>
</Panel>
<BlockUI :blocked="uiBlocked" :fullScreen="true"></BlockUI>
</div>
<div class="wrap" v-else-if="!fileUploaded && uploading">
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
</div>
<div class="wrap" v-else>
<process-file :file="uploadResult" :searchers="searchers"></process-file>
</div>
</template>
<script lang="ts" src="./Home.ts">
</script>

82
resources/js/components/ProcessFile.vue

@ -1,82 +0,0 @@
<template>
<div class="p-d-flex p-flex-row p-jc-between p-ai-stretch">
<Panel
class="p-mr-2 p-as-stretch file-card"
>
<template #header>
File preview
</template>
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
</Panel>
<Card class="p-mr-2 p-as-stretch filters-card">
<template #header>
<Toolbar>
<template #left>
<h3>Available filters</h3>
</template>
<template #right>
<Button
icon="pi pi-plus"
class="p-button-success"
/>
</template>
</Toolbar>
</template>
<template #content>
<filter-view
v-for="(filter, id, index) in filters"
:key="index"
:id="id"
:display-name="filter.display_name"
:options="filter.options"
></filter-view>
</template>
</Card>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator';
import { FileData } from '../interfaces/FileData';
import { FilterInterface } from '../interfaces/FilterInterface';
@Component
export default class ProcessFile extends Vue {
@Prop({ default: null })
public readonly file!: FileData|null;
@Prop({ default: [] })
public readonly filters!: { [keys:string]: FilterInterface }
private selectedFile: File|null = null;
private selectedFilters = [];
/**
*
*/
created() {
console.log('FILE: ', this.file);
console.log('FILTERS: ', this.filters);
}
}
</script>
<style lang="scss">
.file-card {
flex: 0 1 66%;
}
.filters-card {
flex: 0 1 32%
}
</style>

54
resources/js/components/ProcessFile/ProcessFile.scss

@ -0,0 +1,54 @@
.file-card {
flex: 0 1 49%;
}
// .filters-card {
// flex: 0 1 24%
// }
.p-overlaypanel {
// width: 450px;
min-width: 450px;
}
.p-toolbar-group-right {
button {
margin-left: 10px;
}
}
.p-sidebar-content {
.p-toolbar {
margin-top: 32px;
}
}
.p-card-header {
.p-toolbar {
border: unset;
border-radius: unset;
border-bottom: 1px solid #dee2e6;
padding: .5rem;
}
}
button.add-searchers {
height: 100%;
}
.p-col.sidebar-title {
display: flex;
align-content: flex-start;
justify-content: flex-start;
}
.p-grid.sidebar-title-container {
padding-top: 50px;
}
.md-viewer {
text-align: start;
h1, h2, h3, h4, h5 {
font-size: initial;
}
}

160
resources/js/components/ProcessFile/ProcessFile.ts

@ -0,0 +1,160 @@
import axios from 'axios';
// import OverlayPanel from 'primevue/overlaypanel/OverlayPanel';
import { Vue, Component, Prop, Watch } from 'vue-property-decorator';
import { FileData } from '@/interfaces/FileData';
@Component
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!: { [keys: string]: string; }
/**
* 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 = '';
// 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;
// The list of filters/searchers in a format usable by the datatable
private searchersData: Array<{ id: string; name: string; }> = [];
// The list of filters applied to the selected searchers
private searchersFilters: any = [];
// The list of selected filters/searchers
private selectedSearchers: Array<{ id: string; name: string; }> = [];
//The list of expanded rows in the selected filters/searchers table
private expandedRows: Array<{id: string; name: string; }> = [];
// The list of options applied to the searchers (for the moment, only replace_with)
private searchersOptions: { [key: string]: string } = {};
/**
*
*/
created() {
for(let index in this.searchers) {
let searcherData = {
id: index,
name: this.searchers[index]
};
this.searchersData.push(searcherData);
}
this.intervalId = setInterval(this.waitForFile, 3000);
}
/**
* 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;
}
}
/**
* Wait for the file to be processed in ingest
*/
private async waitForFile() {
const response = await this.$api.getFileData(this.file.id);
if (response.content !== null) {
if (response.ingest_status === 'fail') {
this.$toast.add({
severity: 'error',
summary: 'File error',
detail: 'THere was an error processing the file in ingest',
life: 3000
});
} else {
this.fileContent = response.content;
this.$toast.add({
severity:'success',
summary: 'File loaded',
detail: 'The file has been processed by ingest.',
life: 3000
});
clearInterval(this.intervalId);
}
}
}
private onSelectedSearchersReorder($event: any)
{
this.selectedSearchers = $event.value;
}
/**
* Run the searchers
*/
private async runSearchers() {
this.processing = true;
this.processedFileContent = '';
let searchers: Array<{ key: string; replace_with: string; }> = [];
this.selectedSearchers.forEach( (searcher) => {
searchers.push({
'key': searcher.id,
'replace_with': this.searchersOptions[searcher.id] || ''
});
});
const response = await this.$api.filterDocument(this.fileContent, searchers);
console.log(response);
this.processedFileContent = response.content;
this.processing = false;
}
private async downloadOdt()
{
let response = await this.$api.convertFile(this.processedFileContent);
window.open('http://core.sandd/file/download/' + response.path);
}
}

188
resources/js/components/ProcessFile/ProcessFile.vue

@ -0,0 +1,188 @@
<template>
<div class="p-d-flex p-flex-row p-jc-between p-ai-stretch">
<!-- <Toast /> -->
<Card
class="p-mr-2 p-as-stretch file-card"
>
<template #header>
<Toolbar>
<template #left>
<h3>Original document content</h3>
</template>
</Toolbar>
</template>
<template #content>
<div class="md-viewer">
<template v-if="fileContent === ''">
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
</template>
<template v-else>
<vue-markdown :source="fileContent" />
</template>
</div>
</template>
</Card>
<Card
class="p-mr-2 p-as-stretch file-card"
>
<template #header>
<Toolbar>
<template #left>
<h3>Processed document content</h3>
</template>
<template #right>
<Button
label="Download document"
icon="pi pi-download"
class="p-button-secondary p-button-outlined p-button-sm"
@click="downloadOdt()"/>
<Button
label="Run filters"
icon="pi pi-play"
class="p-button-success p-button-outlined p-button-sm"
:disabled="fileContent == ''"
@click="runSearchers()"/>
<Button
label="Toggle filters list"
icon="pi pi-list"
class="p-button-info p-button-outlined p-button-sm"
@click="toggleSearchersSidebar()"/>
</template>
</Toolbar>
</template>
<template #content>
<div class="md-viewer">
<template v-if="processing === true">
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
<Skeleton /><br />
</template>
<template v-else-if="processedFileContent !== ''">
<vue-markdown :source="processedFileContent" />
</template>
<template v-else>
<Message severity="info" :closable="false">
Not processed yet. Please select and run some filters to see the result.
</Message>
</template>
</div>
</template>
</Card>
<Sidebar
:visible.sync="searchersSidebarVisible"
:showCloseIcon="true"
class="p-sidebar-md"
position="right">
<div class="p-grid p-jc-between sidebar-title-container">
<div class="p-col sidebar-title">
<h3>Document searchers</h3>
</div>
<div class="p-col-2">
<Button
icon="pi pi-plus"
class="p-button-success p-button-sm p-button-text add-searchers"
@click="toggleSearchersDialog(true)"
aria:haspopup="true"
aria-controls="searcers_dialog" />
</div>
</div>
<Dialog
header="Available document searchers"
:visible.sync="searchersDialogVisible"
:maximizable="true"
:style="{width: '50vw'}"
:contentStyle="{overflow: 'visible'}"
id="searcers_dialog"
ref="searcers-dialog">
<DataTable
:value.sync="searchersData"
:selection.sync="selectedSearchers"
dataKey="id"
selectionMode="multiple"
class="p-datatable-sm"
:metaKeySelection="false"
:paginator="true" :rows="10"
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords}"
:rowsPerPageOptions="[10,20,50]">
<Column selectionMode="multiple" headerStyle="width: 3em"></Column>
<Column field="name" header="Name" sortable></Column>
</DataTable>
<template #footer>
<Button
label="Done"
icon="pi pi-check"
class="p-button-info p-button-outlined p-button-sm"
@click="toggleSearchersDialog(false)"/>
</template>
</Dialog>
<DataTable
:value.sync="selectedSearchers"
dataKey="id"
class="p-datatable-sm"
:expandedRows.sync="expandedRows"
@row-reorder="onSelectedSearchersReorder">
<Column :rowReorder="true" headerStyle="width: 3rem" />
<Column field="name" header="Name">
</Column>
<Column :expander="true" headerStyle="width: 3rem" />
<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]"/>
</div>
</div>
</div>
</template>
</DataTable>
</Sidebar>
</div>
</template>
<script lang="ts" src="./ProcessFile.ts"></script>
<style lang="scss" src="./ProcessFile.scss"></style>

69
resources/js/components/helpers/Filter.vue

@ -1,69 +0,0 @@
<template>
<Fieldset :legend="displayName" :toggleable="true" class="filter-container">
<div v-for="option of optionsList" :key="option.name" class="filter-option">
<h5>{{ option.name }}</h5>
<SelectButton
:key="option.name"
v-model="selectedOption"
:options="option.list"
optionLabel="name" />
</div>
</Fieldset>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator';
import { FilterOptions } from '../../interfaces/FilterOptions';
@Component
export default class Filter extends Vue {
@Prop(String)
public readonly id!: string;
@Prop(String)
public readonly displayName!: string;
@Prop({ default: [] })
public readonly options!: FilterOptions;
private optionsList = new Array;
public selectedOption = null;
public created()
{
for (let index in this.options) {
let words = index.split('_');
for (let i = 0; i < words.length; i++) {
words[i] = words[i].charAt(0).toUpperCase() + words[i].substr(1);
}
let option = {
name: words.join(' '),
list: new Array
};
this.options[index].forEach( opt => {
option.list.push({
'name': opt,
'value': opt
});
});
this.optionsList.push(option);
}
}
}
</script>
<style lang="scss">
.filter-container,
.filter-option {
margin-bottom: 10px;
}
</style>

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

@ -2,7 +2,7 @@
<div class="header">
<!-- Left side of header -->
<div class="left">
<Button class="p-button-text" label="Search and Displace" />
<Button class="p-button-primary" label="Search and Displace" />
</div>
<!-- Right side of header -->

7
resources/js/interfaces/FilterInterface.ts

@ -1,7 +0,0 @@
import { FilterOptions } from "./FilterOptions";
export interface FilterInterface
{
display_name: string;
options: FilterOptions;
}

4
resources/js/interfaces/FilterOptions.ts

@ -1,4 +0,0 @@
export interface FilterOptions
{
[keys: string]: string[]
}

4
resources/js/interfaces/responses/ErrorResponse.ts

@ -0,0 +1,4 @@
export default interface ErrorResponse
{
message: string;
}

5
resources/js/interfaces/responses/FileStatusResponse.ts

@ -0,0 +1,5 @@
export default interface FileStatusResponse
{
content: string;
ingest_status: string;
}

6
resources/js/interfaces/responses/FileUploadResponse.ts

@ -0,0 +1,6 @@
export default interface FileUploadResponse
{
id: string;
file: string;
path: string;
}

18
resources/js/plugins/ApiPlugin.ts

@ -0,0 +1,18 @@
import _Vue, { PluginFunction } from 'vue';
import ApiService from '@/services/ApiService';
const ApiPlugin = {
install: (Vue: typeof _Vue, options?: any) => {
let apiService = new ApiService();
Vue.mixin({
created() {
Vue.prototype.$api = apiService;
}
})
}
}
export default ApiPlugin;

7
resources/js/plugins/plugins.d.ts

@ -0,0 +1,7 @@
import ApiService from '@/services/ApiService';
declare module 'vue/types/vue' {
interface Vue {
$api: ApiService;
}
}

110
resources/js/services/ApiService.ts

@ -0,0 +1,110 @@
import axios from 'axios';
import FileUploadResponse from '@interfaces/responses/FileUploadResponse';
import FileStatusResponse from '@interfaces/responses/FileStatusResponse';
export default class ApiService {
/** @type {string} */
private readonly baseUrl: string = 'http://core.sandd';
/** @type { [key:string] : string; } */
private readonly apiRoutes = {
file: this.baseUrl + '/api/file',
fileDownload: this.baseUrl + '/api/file/convert',
searchAndDisplace: this.baseUrl + '/search-and-displace'
};
constructor() {}
/**
* Upload a file to the server and return its response.
* Throws an error if the response wasn't successful
*
* TODO: Annotate the return type correctly
*
* @param {File} file The file we want to upload
*
* @returns {Promise<FileUploadResponse>} The response from the server
*/
public async uploadFile(file: File): Promise<FileUploadResponse>
{
let formData = new FormData();
formData.append('file', file);
try {
let response = await axios.post<FileUploadResponse>(
this.apiRoutes.file,
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
)
return response.data;
} catch (err) {
throw err;
}
}
/**
* Get data for a file from the server.
* Throws an error if the response wasn't successful
*
* @param {string} fileId The id of the file we want to query
*
* @returns {Promise<FileStatusResponse>} The response from the server
*
* @throws
*/
public async getFileData(fileId: string): Promise<FileStatusResponse>
{
try {
let response = await axios.get<FileStatusResponse>(this.apiRoutes.searchAndDisplace + `/${fileId}`);
return response.data;
} catch (err) {
throw err;
}
}
/**
* Perform a search and displace operation on a document
*
* @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; }>)
{
try {
let response = await axios.post(
this.apiRoutes.searchAndDisplace,
{
'content': content,
'searchers': searchers
}
);
return response.data;
} catch (err) {
throw err;
}
}
public async convertFile(content:string)
{
try {
let response = await axios.post(
this.apiRoutes.fileDownload,
{
'content': content
}
);
return response.data;
} catch (err) {
throw err;
}
}
}

10
resources/views/pages/home.blade.php

@ -1,5 +1,13 @@
@extends('app')
@section('content')
<home :filters="{{ json_encode($filters) }}"></home>
<div class="page-wrapper">
<app-header></app-header>
<div class="content">
<home :searchers="{{ json_encode($searchers) }}"></home>
</div>
</div>
<app-footer></app-footer>
@endsection

10
routes/api.php

@ -26,16 +26,14 @@ Route::name('file.')->prefix('file')->group(
'create'
])->name('create');
# GET "/file/{id}" (to retrieve data about a file)
Route::get('/{id}', [
Route::post('/convert', [
FileController::class,
'get'
])->name('get');
'convert'
])->name('convert');
}
);
# File routes
# Filter routes
// Route::name('filter.')->prefix('filter')->middleware('auth:api')->group(
Route::name('filter.')->prefix('filter')->group(

15
routes/web.php

@ -1,6 +1,6 @@
<?php
use App\Http\Controllers\PagesController;
use App\Http\Controllers\FileController;
use Illuminate\Support\Facades\Route;
/*
@ -14,10 +14,15 @@ use Illuminate\Support\Facades\Route;
|
*/
Route::get('/', [
PagesController::class,
'home'
]);
Route::get('/', 'HomeController@index');
Route::get('/search-and-displace/{id}', 'SearchAndDisplaceController@show');
Route::post('/search-and-displace', 'SearchAndDisplaceController@store');
Route::get('/file/download/{path}', [
FileController::class,
'download'
])->where('path', '.*')->name('file.download');
Route::get('/search-and-displace/{id}', 'SearchAndDisplaceController@show');
Route::post('/search-and-displace', 'SearchAndDisplaceController@store');

7
webpack.mix.js

@ -15,8 +15,11 @@ const path = require('path');
mix.webpackConfig({
resolve: {
alias: {
"@/*": path.resolve(__dirname, "resources/js/"),
"@components/*": path.resolve(__dirname, "resources/js/components/"),
"@": path.resolve(__dirname, "resources/js/"),
"@components": path.resolve(__dirname, "resources/js/components/"),
"@interfaces": path.resolve(__dirname, "resources/js/interfaces/"),
"@services": path.resolve(__dirname, "resources/js/services/"),
"@plugins": path.resolve(__dirname, "resources/js/plugins/"),
}
}
});

10261
yarn.lock
File diff suppressed because it is too large
View File

Loading…
Cancel
Save