commit ef5792a8244b676ecb2488488896a6102b73791b
Author: Lukáš Vlček <vlcek@net-inout.cz>
Date:   Mon May 16 22:24:19 2022 +0200

    Annotators removing from documents

diff --git a/webapp/api/api.ts b/webapp/api/api.ts
index e812b10..2c1bce9 100644
--- a/webapp/api/api.ts
+++ b/webapp/api/api.ts
@@ -571,6 +571,19 @@ export interface CreateUserRequest {
      */
     'role'?: ERole;
 }
+/**
+ * 
+ * @export
+ * @interface DeleteDocumentsRequest
+ */
+export interface DeleteDocumentsRequest {
+    /**
+     * 
+     * @type {Array<string>}
+     * @memberof DeleteDocumentsRequest
+     */
+    'documentIds'?: Array<string> | null;
+}
 /**
  * 
  * @export
@@ -781,24 +794,6 @@ export interface DocumentListInfo {
  * @interface DocumentListResponse
  */
 export interface DocumentListResponse {
-    /**
-     * 
-     * @type {number}
-     * @memberof DocumentListResponse
-     */
-    'totalCount'?: number;
-    /**
-     * 
-     * @type {number}
-     * @memberof DocumentListResponse
-     */
-    'pageCount'?: number;
-    /**
-     * 
-     * @type {number}
-     * @memberof DocumentListResponse
-     */
-    'pageIndex'?: number;
     /**
      * 
      * @type {Array<DocumentListInfo>}
@@ -957,6 +952,19 @@ export const ETagType = {
 export type ETagType = typeof ETagType[keyof typeof ETagType];
 
 
+/**
+ * 
+ * @export
+ * @interface ExportRequest
+ */
+export interface ExportRequest {
+    /**
+     * 
+     * @type {Array<string>}
+     * @memberof ExportRequest
+     */
+    'documentIds'?: Array<string> | null;
+}
 /**
  * 
  * @export
@@ -2917,12 +2925,11 @@ export const DocumentApiAxiosParamCreator = function (configuration?: Configurat
         },
         /**
          * 
-         * @param {number} [pageIndex] 
-         * @param {number} [pageSize] 
+         * @param {DeleteDocumentsRequest} [deleteDocumentsRequest] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        documentsGet: async (pageIndex?: number, pageSize?: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        documentsDelete: async (deleteDocumentsRequest?: DeleteDocumentsRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             const localVarPath = `/documents`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -2931,18 +2938,112 @@ export const DocumentApiAxiosParamCreator = function (configuration?: Configurat
                 baseOptions = configuration.baseOptions;
             }
 
-            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
             const localVarHeaderParameter = {} as any;
             const localVarQueryParameter = {} as any;
 
-            if (pageIndex !== undefined) {
-                localVarQueryParameter['pageIndex'] = pageIndex;
+
+    
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            localVarRequestOptions.data = serializeDataIfNeeded(deleteDocumentsRequest, localVarRequestOptions, configuration)
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @param {string} documentId 
+         * @param {string} annotatorId 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        documentsDocumentIdAnnotatorsAnnotatorIdDelete: async (documentId: string, annotatorId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'documentId' is not null or undefined
+            assertParamExists('documentsDocumentIdAnnotatorsAnnotatorIdDelete', 'documentId', documentId)
+            // verify required parameter 'annotatorId' is not null or undefined
+            assertParamExists('documentsDocumentIdAnnotatorsAnnotatorIdDelete', 'annotatorId', annotatorId)
+            const localVarPath = `/documents/{documentId}/annotators/{annotatorId}`
+                .replace(`{${"documentId"}}`, encodeURIComponent(String(documentId)))
+                .replace(`{${"annotatorId"}}`, encodeURIComponent(String(annotatorId)));
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
             }
 
-            if (pageSize !== undefined) {
-                localVarQueryParameter['pageSize'] = pageSize;
+            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+
+    
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @param {ExportRequest} [exportRequest] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        documentsExportPost: async (exportRequest?: ExportRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/documents/export`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
             }
 
+            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+
+    
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            localVarRequestOptions.data = serializeDataIfNeeded(exportRequest, localVarRequestOptions, configuration)
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        documentsGet: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/documents`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
 
     
             setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -3114,13 +3215,42 @@ export const DocumentApiFp = function(configuration?: Configuration) {
         },
         /**
          * 
-         * @param {number} [pageIndex] 
-         * @param {number} [pageSize] 
+         * @param {DeleteDocumentsRequest} [deleteDocumentsRequest] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async documentsDelete(deleteDocumentsRequest?: DeleteDocumentsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.documentsDelete(deleteDocumentsRequest, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
+        /**
+         * 
+         * @param {string} documentId 
+         * @param {string} annotatorId 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async documentsGet(pageIndex?: number, pageSize?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DocumentListResponse>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.documentsGet(pageIndex, pageSize, options);
+        async documentsDocumentIdAnnotatorsAnnotatorIdDelete(documentId: string, annotatorId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.documentsDocumentIdAnnotatorsAnnotatorIdDelete(documentId, annotatorId, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
+        /**
+         * 
+         * @param {ExportRequest} [exportRequest] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async documentsExportPost(exportRequest?: ExportRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.documentsExportPost(exportRequest, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async documentsGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DocumentListResponse>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.documentsGet(options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
         /**
@@ -3192,13 +3322,39 @@ export const DocumentApiFactory = function (configuration?: Configuration, baseP
         },
         /**
          * 
-         * @param {number} [pageIndex] 
-         * @param {number} [pageSize] 
+         * @param {DeleteDocumentsRequest} [deleteDocumentsRequest] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        documentsDelete(deleteDocumentsRequest?: DeleteDocumentsRequest, options?: any): AxiosPromise<void> {
+            return localVarFp.documentsDelete(deleteDocumentsRequest, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @param {string} documentId 
+         * @param {string} annotatorId 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        documentsGet(pageIndex?: number, pageSize?: number, options?: any): AxiosPromise<DocumentListResponse> {
-            return localVarFp.documentsGet(pageIndex, pageSize, options).then((request) => request(axios, basePath));
+        documentsDocumentIdAnnotatorsAnnotatorIdDelete(documentId: string, annotatorId: string, options?: any): AxiosPromise<void> {
+            return localVarFp.documentsDocumentIdAnnotatorsAnnotatorIdDelete(documentId, annotatorId, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @param {ExportRequest} [exportRequest] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        documentsExportPost(exportRequest?: ExportRequest, options?: any): AxiosPromise<void> {
+            return localVarFp.documentsExportPost(exportRequest, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        documentsGet(options?: any): AxiosPromise<DocumentListResponse> {
+            return localVarFp.documentsGet(options).then((request) => request(axios, basePath));
         },
         /**
          * 
@@ -3269,14 +3425,46 @@ export class DocumentApi extends BaseAPI {
 
     /**
      * 
-     * @param {number} [pageIndex] 
-     * @param {number} [pageSize] 
+     * @param {DeleteDocumentsRequest} [deleteDocumentsRequest] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof DocumentApi
+     */
+    public documentsDelete(deleteDocumentsRequest?: DeleteDocumentsRequest, options?: AxiosRequestConfig) {
+        return DocumentApiFp(this.configuration).documentsDelete(deleteDocumentsRequest, options).then((request) => request(this.axios, this.basePath));
+    }
+
+    /**
+     * 
+     * @param {string} documentId 
+     * @param {string} annotatorId 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof DocumentApi
+     */
+    public documentsDocumentIdAnnotatorsAnnotatorIdDelete(documentId: string, annotatorId: string, options?: AxiosRequestConfig) {
+        return DocumentApiFp(this.configuration).documentsDocumentIdAnnotatorsAnnotatorIdDelete(documentId, annotatorId, options).then((request) => request(this.axios, this.basePath));
+    }
+
+    /**
+     * 
+     * @param {ExportRequest} [exportRequest] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof DocumentApi
+     */
+    public documentsExportPost(exportRequest?: ExportRequest, options?: AxiosRequestConfig) {
+        return DocumentApiFp(this.configuration).documentsExportPost(exportRequest, options).then((request) => request(this.axios, this.basePath));
+    }
+
+    /**
+     * 
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof DocumentApi
      */
-    public documentsGet(pageIndex?: number, pageSize?: number, options?: AxiosRequestConfig) {
-        return DocumentApiFp(this.configuration).documentsGet(pageIndex, pageSize, options).then((request) => request(this.axios, this.basePath));
+    public documentsGet(options?: AxiosRequestConfig) {
+        return DocumentApiFp(this.configuration).documentsGet(options).then((request) => request(this.axios, this.basePath));
     }
 
     /**
diff --git a/webapp/pages/_app.tsx b/webapp/pages/_app.tsx
index d45f973..895e66f 100644
--- a/webapp/pages/_app.tsx
+++ b/webapp/pages/_app.tsx
@@ -1,5 +1,5 @@
 // @ts-nocheck
-import '../styles/globals.css';
+import '../styles/globals.scss';
 import 'bootstrap/dist/css/bootstrap.css';
 import '@fortawesome/fontawesome-svg-core/styles.css';
 
diff --git a/webapp/pages/documents/admin/index.tsx b/webapp/pages/documents/admin/index.tsx
index 3540bf6..fd267d7 100644
--- a/webapp/pages/documents/admin/index.tsx
+++ b/webapp/pages/documents/admin/index.tsx
@@ -38,6 +38,10 @@ import {
 } from '@ant-design/icons';
 import { SweetAlertIcon } from 'sweetalert2';
 import { Stack } from 'react-bootstrap';
+import { AxiosRequestConfig } from 'axios';
+import { constants } from 'http2';
+import HTTP_STATUS_ACCEPTED = module;
+import HTTP_STATUS_OK = module;
 
 function AdminDocumentPage() {
     const redirecting = useUnauthRedirect('/login');
@@ -58,7 +62,7 @@ function AdminDocumentPage() {
     const [annotationCount, setAnnotationCount] = useState<number>();
 
     async function fetchData() {
-        const docs = (await documentController.documentsGet(0, 1000)).data.documents;
+        const docs = (await documentController.documentsGet()).data.documents;
         // @ts-ignore
         const tableDocs: TableDocInfo[] = docs?.map((doc, index) => {
             return { key: index, ...doc };
@@ -161,6 +165,19 @@ function AdminDocumentPage() {
         setFinalizing(false);
     };
 
+    async function removeUserFromDocument(documentID: string, annotatorID: string) {
+        const res =
+            await documentController.documentsDocumentIdAnnotatorsAnnotatorIdDelete(
+                documentID,
+                annotatorID
+            );
+
+        if (res.status === 200) {
+            ShowToast('Uživatel byl úspěšně odebrán z dokumentu');
+        }
+        await fetchData();
+    }
+
     const getFinalizationStateIcon = (state: EState) => {
         const color = getAnnotationStateColor(state);
         const label = getAnnotationStateString(state);
@@ -226,6 +243,55 @@ function AdminDocumentPage() {
         setVisiblePreview(false);
     };
 
+    function getUserTag(user: DocumentUserInfo, record: DocumentListInfo) {
+        return (
+            <span
+                className={'userTagWrapper'}
+                title={getUserInfoAlt(user) + '\nStav: ' + user.state}
+                onClick={() => {
+                    ShowConfirm(
+                        async () => {
+                            if (!record.id || !user?.id) {
+                                return;
+                            }
+                            await removeUserFromDocument(record.id, user.id);
+                        },
+                        'odebrat uživatele ' +
+                            user.name +
+                            ' ' +
+                            user.surname +
+                            ' (' +
+                            user.username +
+                            ') z tohoto dokumentu',
+                        'Dosavadní postup tohoto anotátora na daném dokumentu bude nenávratně smazán'
+                    );
+                }}
+            >
+                <span
+                    key={user.username + '.' + record.id}
+                    style={{
+                        color: getAnnotationStateColor(user.state),
+                    }}
+                    className={'me-3 userTag'}
+                >
+                    <FontAwesomeIcon
+                        icon={faUser}
+                        title={getUserInfoAlt(user)}
+                        className={'me-2'}
+                    />
+                    {record.finalAnnotations?.some(
+                        (annot) => annot.userId === user.id
+                    ) ? (
+                        <u>{getNameTruncated(user)}</u>
+                    ) : (
+                        getNameTruncated(user)
+                    )}
+                </span>
+                <span className={'remove'}>Odebrat</span>
+            </span>
+        );
+    }
+
     const columns = [
         {
             title: 'Název dokumentu',
@@ -305,34 +371,7 @@ function AdminDocumentPage() {
                 record: DocumentListInfo,
                 index: number
             ) => {
-                return (
-                    <div>
-                        {columnData.map((e) => (
-                            <span
-                                key={e.username + '.' + record.id}
-                                title={getUserInfoAlt(e) + '\nStav: ' + e.state}
-                                style={{
-                                    color: getAnnotationStateColor(e.state),
-                                    padding: 3,
-                                }}
-                                className={'me-3'}
-                            >
-                                <FontAwesomeIcon
-                                    icon={faUser}
-                                    title={getUserInfoAlt(e)}
-                                    className={'me-2'}
-                                />
-                                {record.finalAnnotations?.some(
-                                    (annot) => annot.userId === e.id
-                                ) ? (
-                                    <u>{getNameTruncated(e)}</u>
-                                ) : (
-                                    getNameTruncated(e)
-                                )}
-                            </span>
-                        ))}
-                    </div>
-                );
+                return <div>{columnData.map((e) => getUserTag(e, record))}</div>;
             },
             filters: userFilters,
             filterSearch: true,
diff --git a/webapp/styles/globals.css b/webapp/styles/globals.css
deleted file mode 100644
index 2dc21d3..0000000
--- a/webapp/styles/globals.css
+++ /dev/null
@@ -1,26 +0,0 @@
-html,
-body {
-  padding: 0;
-  margin: 0;
-  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
-    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
-}
-
-a {
-  color: inherit;
-  text-decoration: none;
-}
-
-* {
-  box-sizing: border-box;
-}
-
-.navbar-wrapper{
-  width: 100vw;
-  margin: 0;
-  display: flex;
-  justify-content: space-around;
-  padding-left: 10px;
-  padding-right: 10px;
-  background: #001529;
-}
\ No newline at end of file
diff --git a/webapp/styles/globals.scss b/webapp/styles/globals.scss
new file mode 100644
index 0000000..4e76c18
--- /dev/null
+++ b/webapp/styles/globals.scss
@@ -0,0 +1,57 @@
+html,
+body {
+  padding: 0;
+  margin: 0;
+  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+}
+
+a {
+  color: inherit;
+  text-decoration: none;
+}
+
+* {
+  box-sizing: border-box;
+}
+
+.navbar-wrapper{
+  width: 100vw;
+  margin: 0;
+  display: flex;
+  justify-content: space-around;
+  padding-left: 10px;
+  padding-right: 10px;
+  background: #001529;
+}
+
+.userTagWrapper{
+  padding: 2px;
+  position: relative;
+  cursor: pointer;
+
+
+  .userTag{
+    opacity: 1;
+
+  }
+  .remove {
+    transition: .5s ease;
+    opacity: 0;
+    position: absolute;
+    top: 80%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    -ms-transform: translate(-50%, -50%);
+    text-align: center;
+  }
+}
+
+.userTagWrapper:hover .remove{
+  opacity: 1;
+}
+
+.userTagWrapper:hover .userTag{
+  opacity: 0.3
+}
+
