commit 00466dc7b5153a0e4848e022e71b002de9e4cedc
Author: Ondřej Váně <onry.vane@gmail.com>
Date:   Fri Apr 3 23:34:56 2020 +0200

    Re #7726: Implementace architektury FE
    
    - implementace stránky pro nahrávání obrázků
    - přidána metoda do BE pro přijímání obrázku

diff --git a/be/fulltextsearch/src/main/java/cz/zcu/kiv/aswi/fulltextsearch/IndexController.java b/be/fulltextsearch/src/main/java/cz/zcu/kiv/aswi/fulltextsearch/IndexController.java
index 4fa2617..e068b48 100644
--- a/be/fulltextsearch/src/main/java/cz/zcu/kiv/aswi/fulltextsearch/IndexController.java
+++ b/be/fulltextsearch/src/main/java/cz/zcu/kiv/aswi/fulltextsearch/IndexController.java
@@ -2,11 +2,16 @@ package cz.zcu.kiv.aswi.fulltextsearch;
 
 import cz.zcu.kiv.aswi.fulltextsearch.model.Query;
 import cz.zcu.kiv.aswi.fulltextsearch.model.QueryResponse;
+import cz.zcu.kiv.aswi.fulltextsearch.model.ResponseMessage;
 import org.apache.solr.client.solrj.SolrServerException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.xml.bind.JAXBException;
 import java.io.IOException;
+import java.util.List;
 
 @CrossOrigin(origins = "*", allowedHeaders = "*")
 @RestController
@@ -31,6 +36,21 @@ public class IndexController {
         return new QueryResponse(response);
     }
 
+
+    @PostMapping("/upload")
+    public ResponseEntity<ResponseMessage> uploadFile(@RequestParam("file") List<MultipartFile> files) {
+        String message = "";
+        try {
+            // TODO: Převést MultipartFile na normální file a pak ho dále zpracovat
+
+            message = "Successfully uploaded " + files.size() + " files";
+            return ResponseEntity.status(HttpStatus.OK).body(new ResponseMessage(message));
+        } catch (Exception e) {
+            message = "Could not upload the files! " + e.toString();
+            return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(new ResponseMessage(message));
+        }
+    }
+
     @GetMapping("/add")
     public String add() {
         String response;
diff --git a/be/fulltextsearch/src/main/java/cz/zcu/kiv/aswi/fulltextsearch/model/ResponseMessage.java b/be/fulltextsearch/src/main/java/cz/zcu/kiv/aswi/fulltextsearch/model/ResponseMessage.java
new file mode 100644
index 0000000..ae2f294
--- /dev/null
+++ b/be/fulltextsearch/src/main/java/cz/zcu/kiv/aswi/fulltextsearch/model/ResponseMessage.java
@@ -0,0 +1,17 @@
+package cz.zcu.kiv.aswi.fulltextsearch.model;
+
+public class ResponseMessage {
+    private String message;
+
+    public ResponseMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+}
diff --git a/fe/fulltextsearch/src/app/app.module.ts b/fe/fulltextsearch/src/app/app.module.ts
index 04bb572..0c1460b 100644
--- a/fe/fulltextsearch/src/app/app.module.ts
+++ b/fe/fulltextsearch/src/app/app.module.ts
@@ -17,6 +17,7 @@ import { UploadComponent } from './components/pages/upload/upload.component';
 import { HelpComponent } from './components/pages/help/help.component';
 import { MatSelectModule } from '@angular/material/select';
 import { SearchBoxComponent } from './components/shared-components/search-box/search-box.component';
+import { MatProgressBarModule } from '@angular/material/progress-bar';
 
 @NgModule({
   declarations: [
@@ -38,7 +39,8 @@ import { SearchBoxComponent } from './components/shared-components/search-box/se
     MatToolbarModule,
     MatButtonModule,
     MatIconModule,
-    MatSelectModule
+    MatSelectModule,
+    MatProgressBarModule
   ],
   providers: [],
   bootstrap: [AppComponent]
diff --git a/fe/fulltextsearch/src/app/components/pages/upload/upload.component.css b/fe/fulltextsearch/src/app/components/pages/upload/upload.component.css
index e69de29..e709bf9 100644
--- a/fe/fulltextsearch/src/app/components/pages/upload/upload.component.css
+++ b/fe/fulltextsearch/src/app/components/pages/upload/upload.component.css
@@ -0,0 +1,36 @@
+h1 {
+  font-size: xx-large;
+}
+
+.text-box {
+  margin: auto;
+  width: 80%;
+  padding: 10px;
+}
+
+input {
+  display: none;
+}
+
+button {
+  font-weight: bold;
+  font-size: large;
+  background-color: var(--primary-color);
+  color: var(--search-text-color);
+  border-style: none;
+  min-width: 80pt;
+  margin: 10px;
+}
+
+button:hover {
+  background-color: var(--primary-color-dark);
+}
+
+.center {
+  margin: auto;
+  width: 35%;
+}
+
+.center h2 {
+
+}
diff --git a/fe/fulltextsearch/src/app/components/pages/upload/upload.component.html b/fe/fulltextsearch/src/app/components/pages/upload/upload.component.html
index 804edf2..79060a9 100644
--- a/fe/fulltextsearch/src/app/components/pages/upload/upload.component.html
+++ b/fe/fulltextsearch/src/app/components/pages/upload/upload.component.html
@@ -1 +1,24 @@
-<p>upload works!</p>
+<div class="text-box">
+  <div class="card">
+    <h1 class="card-header">Upload files</h1>
+    <div class="card-body">
+      <div class="row justify-content-center align-items-center">
+        <input type="file" (change)="onFilesSelected($event)" multiple #fileInput>
+        <button class="btn btn-outline-secondary" type="button" (click)="fileInput.click()">Pick files</button>
+        <button class="btn btn-outline-secondary" type="button" (click)="onUpload()">Upload</button>
+      </div>
+      <mat-progress-bar mode="determinate" [value]="progress" color="warn"></mat-progress-bar>
+      <div class="row justify-content-center align-items-center">
+        <h2>{{progress + '%'}}</h2>
+      </div>
+      <ul class="list-group">
+        <li class="list-group-item" *ngFor="let file of selectedFiles">
+          {{ file.name }}
+        </li>
+      </ul>
+      <div class="row justify-content-center align-items-center">
+        <h2>{{resultMessage}}</h2>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/fe/fulltextsearch/src/app/components/pages/upload/upload.component.ts b/fe/fulltextsearch/src/app/components/pages/upload/upload.component.ts
index 9c50623..77c73ff 100644
--- a/fe/fulltextsearch/src/app/components/pages/upload/upload.component.ts
+++ b/fe/fulltextsearch/src/app/components/pages/upload/upload.component.ts
@@ -1,4 +1,6 @@
 import { Component, OnInit } from '@angular/core';
+import {QueryService} from '../../../services/query.service';
+import {HttpEventType} from '@angular/common/http';
 
 @Component({
   selector: 'app-upload',
@@ -6,10 +8,34 @@ import { Component, OnInit } from '@angular/core';
   styleUrls: ['./upload.component.css']
 })
 export class UploadComponent implements OnInit {
+  selectedFiles: File[] = null;
+  resultMessage: string = null;
+  progress = 0;
 
-  constructor() { }
+  constructor( private queryService: QueryService ) { }
 
   ngOnInit(): void {
   }
 
+  onFilesSelected(event) {
+    this.selectedFiles = event.target.files;
+    this.progress = 0;
+    this.resultMessage = null;
+  }
+
+  onUpload() {
+    console.log(this.selectedFiles);
+    if (this.selectedFiles === null) {
+      this.resultMessage = 'Select files first';
+      return;
+    }
+    this.queryService.uploadFile(this.selectedFiles).subscribe( event => {
+      if (event.type === HttpEventType.UploadProgress) {
+        this.progress = Math.round(event.loaded / event.total * 100);
+      } else if (event.type === HttpEventType.Response ) {
+        this.resultMessage = 'Successfully uploaded ' + this.selectedFiles.length + ' files';
+        this.selectedFiles = null;
+      }
+    });
+  }
 }
diff --git a/fe/fulltextsearch/src/app/components/shared-components/search-box/search-box.component.css b/fe/fulltextsearch/src/app/components/shared-components/search-box/search-box.component.css
index 4c284e0..8790a77 100644
--- a/fe/fulltextsearch/src/app/components/shared-components/search-box/search-box.component.css
+++ b/fe/fulltextsearch/src/app/components/shared-components/search-box/search-box.component.css
@@ -27,3 +27,7 @@ button:hover {
 input:focus {
   box-shadow: none;
 }
+
+.clickable {
+  cursor: pointer;
+}
diff --git a/fe/fulltextsearch/src/app/components/shared-components/search-box/search-box.component.html b/fe/fulltextsearch/src/app/components/shared-components/search-box/search-box.component.html
index a4a4fdd..83ba80b 100644
--- a/fe/fulltextsearch/src/app/components/shared-components/search-box/search-box.component.html
+++ b/fe/fulltextsearch/src/app/components/shared-components/search-box/search-box.component.html
@@ -7,7 +7,7 @@
     </div>
     <input type="text" class="form-control" [(ngModel)]="expression" placeholder="Enter text to search..." aria-label="Search" aria-describedby="basic-addon2">
     <div class="input-group-append">
-      <span class="input-group-text">
+      <span class="input-group-text clickable">
         <mat-icon *ngIf="expression" matSuffix aria-label="Clear" (click)="expression=''">clear</mat-icon>
       </span>
       <button class="btn btn-outline-secondary" type="button" (click)="onSubmit()">Search</button>
diff --git a/fe/fulltextsearch/src/app/model/DocumentRequest.ts b/fe/fulltextsearch/src/app/model/DocumentRequest.ts
deleted file mode 100644
index 2a99d07..0000000
--- a/fe/fulltextsearch/src/app/model/DocumentRequest.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export class DocumentRequest {
-  public xmlFile: string;
-  public imgFile: string;
-}
diff --git a/fe/fulltextsearch/src/app/services/query.service.ts b/fe/fulltextsearch/src/app/services/query.service.ts
index 775e7ce..7a99b23 100644
--- a/fe/fulltextsearch/src/app/services/query.service.ts
+++ b/fe/fulltextsearch/src/app/services/query.service.ts
@@ -1,8 +1,8 @@
-import { Injectable } from '@angular/core';
-import { HttpClient, HttpHeaders } from '@angular/common/http';
-import { Observable } from 'rxjs';
-import { QueryResponse } from '../model/QueryResponse';
-import { Query } from '../model/Query';
+import {Injectable} from '@angular/core';
+import {HttpClient, HttpEventType, HttpHeaders} from '@angular/common/http';
+import {Observable} from 'rxjs';
+import {QueryResponse} from '../model/QueryResponse';
+import {Query} from '../model/Query';
 
 const httpOptions = {
   headers: new HttpHeaders( {
@@ -15,11 +15,24 @@ const httpOptions = {
 })
 export class QueryService {
 
-  backendUrl: string = 'http://localhost:8080/';
+  backendUrl = 'http://localhost:8080/';
+  uploadFileUrl = 'upload';
 
   constructor(private httpClient: HttpClient) {}
 
   sendQuery(query: Query): Observable<QueryResponse> {
     return this.httpClient.post<QueryResponse>(this.backendUrl, query, httpOptions);
   }
+
+
+  uploadFile(files: File[]): Observable<any> {
+    const formDate = new FormData();
+    for (const file of files) {
+      formDate.append('file', file);
+    }
+    return this.httpClient.post(this.backendUrl + this.uploadFileUrl, formDate, {
+      reportProgress: true,
+      observe: 'events'
+    });
+  }
 }
