diff --git a/app/static/css/colors.scss b/app/static/css/colors.scss
index 22feeb41e32974ae8d15ab360659d6864b6f5a49..f8f04228d60678e5d20a5e7f97d0a99560a08050 100644
--- a/app/static/css/colors.scss
+++ b/app/static/css/colors.scss
@@ -201,15 +201,15 @@ $color: (
 
 @each $ressource-name, $color-palette in map-get($color, "status") {
   @each $key, $color-code in $color-palette {
-    .#{$ressource-name}-status-color[data-#{$ressource-name}-status="#{$key}"] {
+    .#{$ressource-name}-status-color[data-status="#{$key}"] {
       background-color: $color-code !important;
     }
 
-    .#{$ressource-name}-status-color-border[data-#{$ressource-name}-status="#{$key}"] {
+    .#{$ressource-name}-status-color-border[data-status="#{$key}"] {
       border-color: $color-code !important;
     }
 
-    .#{$ressource-name}-status-color-text[data-#{$ressource-name}-status="#{$key}"] {
+    .#{$ressource-name}-status-color-text[data-status="#{$key}"] {
       color: $color-code !important;
     }
   }
diff --git a/app/static/css/style.css b/app/static/css/style.css
index e0c288b5da551193dd6ea8cae8c2f98b851aa767..ba45e025f3c05fabf3de92fedcd5acbd0c5d732e 100644
--- a/app/static/css/style.css
+++ b/app/static/css/style.css
@@ -27,7 +27,7 @@
   transform: scale(2);
 }
 
-.btn-scale-x2 .nopaque-icons.service-icon {
+.btn-scale-x2 .nopaque-icons.service-icons {
   font-size: 2.5rem;
 }
 
@@ -37,17 +37,20 @@ h1 .nopaque-icons, h2 .nopaque-icons, h3 .nopaque-icons, h4 .nopaque-icons, .tab
 }
 
 
-.corpus-status-text {text-transform: lowercase;}
-.corpus-status-text[data-corpus-status]:empty:before {content: attr(data-corpus-status);}
+.corpus-status-text, .job-status-text {text-transform: lowercase;}
+.corpus-status-text[data-status]:empty::before, .job-status-text[data-status]:empty::before {content: attr(data-status);}
 
-.job-status-text {text-transform: lowercase;}
-.job-status-text[data-job-status]:empty:before {content: attr(data-job-status);}
+.service-scheme[data-service="file-setup-pipeline"] .nopaque-icons.service-icons[data-service="inherit"]:empty::before {content: "E";}
+.service-scheme[data-service="tesseract-ocr-pipeline"] .nopaque-icons.service-icons[data-service="inherit"]:empty::before {content: "F";}
+.service-scheme[data-service="transkribus-htr-pipeline"] .nopaque-icons.service-icons[data-service="inherit"]:empty::before {content: "F";}
+.service-scheme[data-service="spacy-nlp-pipeline"] .nopaque-icons.service-icons[data-service="inherit"]:empty::before {content: "G";}
+.service-scheme[data-service="corpus-analysis"] .nopaque-icons.service-icons[data-service="inherit"]:empty::before {content: "H";}
 
-.nopaque-icons.service-icon[data-service="file-setup-pipeline"]:empty:before {content: "E";}
-.nopaque-icons.service-icon[data-service="tesseract-ocr-pipeline"]:empty:before {content: "F";}
-.nopaque-icons.service-icon[data-service="transkribus-htr-pipeline"]:empty:before {content: "F";}
-.nopaque-icons.service-icon[data-service="spacy-nlp-pipeline"]:empty:before {content: "G";}
-.nopaque-icons.service-icon[data-service="corpus-analysis"]:empty:before {content: "H";}
+.nopaque-icons.service-icons[data-service="file-setup-pipeline"]:empty::before {content: "E";}
+.nopaque-icons.service-icons[data-service="tesseract-ocr-pipeline"]:empty::before {content: "F";}
+.nopaque-icons.service-icons[data-service="transkribus-htr-pipeline"]:empty::before {content: "F";}
+.nopaque-icons.service-icons[data-service="spacy-nlp-pipeline"]:empty::before {content: "G";}
+.nopaque-icons.service-icons[data-service="corpus-analysis"]:empty::before {content: "H";}
 
 .clickable {
   cursor: pointer !important;
diff --git a/app/static/js/RessourceLists/CorpusFileList.js b/app/static/js/RessourceLists/CorpusFileList.js
index a24fcf7e8661a4c2adc00b75e98041b75a381236..af68c9dc19bb7e980689c39024782e992d29095d 100644
--- a/app/static/js/RessourceLists/CorpusFileList.js
+++ b/app/static/js/RessourceLists/CorpusFileList.js
@@ -6,12 +6,12 @@ class CorpusFileList extends RessourceList {
   }
 
   static options = {
-    initialHtmlGenerator: (id) => {
-      return `
+    listContainerInnerHTMLGenerator: (listContainerElement) => {
+      listContainerElement.innerHTML = `
         <div class="input-field">
           <i class="material-icons prefix">search</i>
-          <input id="${id}-search" class="search" type="search"></input>
-          <label for="${id}-search">Search corpus file</label>
+          <input id="${listContainerElement.id}-search" class="search" type="text"></input>
+          <label for="${listContainerElement.id}-search">Search corpus file</label>
         </div>
         <table>
           <thead>
@@ -28,19 +28,6 @@ class CorpusFileList extends RessourceList {
         <ul class="pagination"></ul>
       `.trim();
     },
-    item: `
-      <tr class="clickable hoverable">
-        <td><span class="filename"></span></td>
-        <td><span class="author"></span></td>
-        <td><span class="title"></span></td>
-        <td><span class="publishing-year"></span></td>
-        <td class="right-align">
-          <a class="action-button btn-floating red waves-effect waves-light" data-action="delete"><i class="material-icons">delete</i></a>
-          <a class="action-button btn-floating service-color darken waves-effect waves-light" data-action="download" data-service="corpus-analysis"><i class="material-icons">file_download</i></a>
-          <a class="action-button btn-floating service-color darken waves-effect waves-light" data-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
-        </td>
-      </tr>
-    `.trim(),
     ressourceMapper: (corpusFile) => {
       return {
         'id': corpusFile.id,
@@ -51,20 +38,35 @@ class CorpusFileList extends RessourceList {
         'title': corpusFile.title
       };
     },
-    sortArgs: ['creation-date', {order: 'desc'}],
-    valueNames: [
-      {data: ['id']},
-      {data: ['creation-date']},
-      'author',
-      'filename',
-      'publishing-year',
-      'title'
-    ]
+    sortParams: ['creation-date', {order: 'desc'}],
+    listjs: {
+      item: `
+        <tr class="clickable hoverable">
+          <td><span class="filename"></span></td>
+          <td><span class="author"></span></td>
+          <td><span class="title"></span></td>
+          <td><span class="publishing-year"></span></td>
+          <td class="right-align">
+            <a class="action-button btn-floating red waves-effect waves-light" data-action="delete"><i class="material-icons">delete</i></a>
+            <a class="action-button btn-floating service-color darken waves-effect waves-light" data-action="download" data-service="corpus-analysis"><i class="material-icons">file_download</i></a>
+            <a class="action-button btn-floating service-color darken waves-effect waves-light" data-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
+          </td>
+        </tr>
+      `.trim(),
+      valueNames: [
+        {data: ['id']},
+        {data: ['creation-date']},
+        'author',
+        'filename',
+        'publishing-year',
+        'title'
+      ]
+    }
   };
 
-  constructor(listElement, options = {}) {
-    super(listElement, {...CorpusFileList.options, ...options});
-    this.corpusId = listElement.dataset.corpusId;
+  constructor(listContainerElement, options={}) {
+    super(listContainerElement, _.merge({}, CorpusFileList.options, options));
+    this.corpusId = listContainerElement.dataset.corpusId;
   }
 
   init(user) {
@@ -72,10 +74,12 @@ class CorpusFileList extends RessourceList {
   }
 
   onClick(event) {
-    let actionButtonElement = event.target.closest('.action-button');
-    let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
     let corpusFileElement = event.target.closest('tr');
+    if (corpusFileElement === null) {return;}
     let corpusFileId = corpusFileElement.dataset.id;
+    if (corpusFileId === undefined) {return;}
+    let actionButtonElement = event.target.closest('.action-button');
+    let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
     switch (action) {
       case 'delete': {
         Utils.deleteCorpusFileRequest(this.userId, this.corpusId, corpusFileId);
diff --git a/app/static/js/RessourceLists/CorpusList.js b/app/static/js/RessourceLists/CorpusList.js
index a16b1ce7a0eb4b08bd959df8ce277eb220ad7e8b..fa0fa704b336f2b2a71b2413081b71d389af554a 100644
--- a/app/static/js/RessourceLists/CorpusList.js
+++ b/app/static/js/RessourceLists/CorpusList.js
@@ -1,12 +1,4 @@
 class CorpusList extends RessourceList {
-  static instances = [];
-
-  static getInstance(elem) {
-    return CorpusList.instances.find((instance) => {
-      return instance.listjs.list === elem;
-    });
-  }
-
   static autoInit() {
     for (let corpusListElement of document.querySelectorAll('.corpus-list:not(.no-autoinit)')) {
       new CorpusList(corpusListElement);
@@ -14,12 +6,12 @@ class CorpusList extends RessourceList {
   }
 
   static options = {
-    initialHtmlGenerator: (id) => {
-      return `
+    listContainerInnerHTMLGenerator: (listContainerElement) => {
+      listContainerElement.innerHTML = `
         <div class="input-field">
           <i class="material-icons prefix">search</i>
-          <input id="${id}-search" class="search" type="search"></input>
-          <label for="${id}-search">Search corpus</label>
+          <input id="${listContainerElement.id}-search" class="search" type="text"></input>
+          <label for="${listContainerElement.id}-search">Search corpus</label>
         </div>
         <table>
           <thead>
@@ -35,17 +27,6 @@ class CorpusList extends RessourceList {
         <ul class="pagination"></ul>
       `.trim();
     },
-    item: `
-      <tr class="clickable hoverable">
-        <td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td>
-        <td><b class="title"></b><br><i class="description"></i></td>
-        <td><span class="status badge new corpus-status-color corpus-status-text" data-badge-caption=""></span></td>
-        <td class="right-align">
-          <a class="action-button btn-floating red waves-effect waves-light" data-action="delete-request"><i class="material-icons">delete</i></a>
-          <a class="action-button btn-floating service-color darken waves-effect waves-light" data-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
-        </td>
-      </tr>
-    `.trim(),
     ressourceMapper: (corpus) => {
       return {
         'id': corpus.id,
@@ -55,19 +36,31 @@ class CorpusList extends RessourceList {
         'title': corpus.title
       };
     },
-    sortArgs: ['creation-date', {order: 'desc'}],
-    valueNames: [
-      {data: ['id']},
-      {data: ['creation-date']},
-      {name: 'status', attr: 'data-corpus-status'},
-      'description',
-      'title'
-    ]
+    sortParams: ['creation-date', {order: 'desc'}],
+    listjs: {
+      item: `
+        <tr class="clickable hoverable">
+          <td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td>
+          <td><b class="title"></b><br><i class="description"></i></td>
+          <td><span class="status badge new corpus-status-color corpus-status-text" data-badge-caption=""></span></td>
+          <td class="right-align">
+            <a class="action-button btn-floating red waves-effect waves-light" data-action="delete-request"><i class="material-icons">delete</i></a>
+            <a class="action-button btn-floating service-color darken waves-effect waves-light" data-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
+          </td>
+        </tr>
+      `.trim(),
+      valueNames: [
+        {data: ['id']},
+        {data: ['creation-date']},
+        {name: 'status', attr: 'data-status'},
+        'description',
+        'title'
+      ]
+    }
   };
 
-  constructor(listElement, options = {}) {
-    super(listElement, {...CorpusList.options, ...options});
-    CorpusList.instances.push(this);
+  constructor(listContainerElement, options={}) {
+    super(listContainerElement, _.merge({}, CorpusList.options, options));
   }
 
   init(user) {
@@ -75,10 +68,12 @@ class CorpusList extends RessourceList {
   }
 
   onClick(event) {
-    let actionButtonElement = event.target.closest('.action-button');
-    let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
     let corpusElement = event.target.closest('tr');
+    if (corpusElement === null) {return;}
     let corpusId = corpusElement.dataset.id;
+    if (corpusId === undefined) {return;}
+    let actionButtonElement = event.target.closest('.action-button');
+    let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
     switch (action) {
       case 'delete-request': {
         Utils.deleteCorpusRequest(this.userId, corpusId);
diff --git a/app/static/js/RessourceLists/JobInputList.js b/app/static/js/RessourceLists/JobInputList.js
index 2cd14aa9822bb08834d8e8849ff95ddfbc6c822b..069d470e01bffe7524e4b60f00b659ae3b4a899b 100644
--- a/app/static/js/RessourceLists/JobInputList.js
+++ b/app/static/js/RessourceLists/JobInputList.js
@@ -6,12 +6,12 @@ class JobInputList extends RessourceList {
   }
 
   static options = {
-    initialHtmlGenerator: (id) => {
-      return `
+    listContainerInnerHTMLGenerator: (listContainerElement) => {
+      listContainerElement.innerHTML = `
         <div class="input-field">
           <i class="material-icons prefix">search</i>
-          <input id="${id}-search" class="search" type="search"></input>
-          <label for="${id}-search">Search job input</label>
+          <input id="${listContainerElement.id}-search" class="search" type="text"></input>
+          <label for="${listContainerElement.id}-search">Search job input</label>
         </div>
         <table>
           <thead>
@@ -25,14 +25,6 @@ class JobInputList extends RessourceList {
         <ul class="pagination"></ul>
       `.trim();
     },
-    item: `
-      <tr class="clickable hoverable">
-        <td><span class="filename"></span></td>
-        <td class="right-align">
-          <a class="action-button btn-floating waves-effect waves-light" data-action="download"><i class="material-icons">file_download</i></a>
-        </td>
-      </tr>
-    `.trim(),
     ressourceMapper: (jobInput) => {
       return {
         'id': jobInput.id,
@@ -40,17 +32,27 @@ class JobInputList extends RessourceList {
         'filename': jobInput.filename
       };
     },
-    sortArgs: ['filename', {order: 'asc'}],
-    valueNames: [
-      {data: ['id']},
-      {data: ['creation-date']},
-      'filename'
-    ]
+    sortParams: ['filename', {order: 'asc'}],
+    listjs: {
+      item: `
+        <tr class="clickable hoverable">
+          <td><span class="filename"></span></td>
+          <td class="right-align">
+            <a class="action-button btn-floating waves-effect waves-light" data-action="download"><i class="material-icons">file_download</i></a>
+          </td>
+        </tr>
+      `.trim(),
+      valueNames: [
+        {data: ['id']},
+        {data: ['creation-date']},
+        'filename'
+      ]
+    }
   };
 
-  constructor(listElement, options = {}) {
-    super(listElement, {...JobInputList.options, ...options});
-    this.jobId = listElement.dataset.jobId;
+  constructor(listContainerElement, options={}) {
+    super(listContainerElement, _.merge({}, JobInputList.options, options));
+    this.jobId = listContainerElement.dataset.jobId;
   }
 
   init(user) {
@@ -58,10 +60,12 @@ class JobInputList extends RessourceList {
   }
 
   onClick(event) {
-    let actionButtonElement = event.target.closest('.action-button');
-    let action = actionButtonElement === null ? 'download' : actionButtonElement.dataset.action;
     let jobInputElement = event.target.closest('tr');
+    if (jobInputElement === null) {return;}
     let jobInputId = jobInputElement.dataset.id;
+    if (jobInputId === undefined) {return;}
+    let actionButtonElement = event.target.closest('.action-button');
+    let action = actionButtonElement === null ? 'download' : actionButtonElement.dataset.action;
     switch (action) {
       case 'download': {
         window.location.href = `/jobs/${this.jobId}/inputs/${jobInputId}/download`;
diff --git a/app/static/js/RessourceLists/JobList.js b/app/static/js/RessourceLists/JobList.js
index d6fa789416f9292217cc0d32264f5782d06eedb5..3a8c6d8be5b13f6a4bdae189996d5aebf518189c 100644
--- a/app/static/js/RessourceLists/JobList.js
+++ b/app/static/js/RessourceLists/JobList.js
@@ -6,12 +6,12 @@ class JobList extends RessourceList {
   }
 
   static options = {
-    initialHtmlGenerator: (id) => {
-      return `
+    listContainerInnerHTMLGenerator: (listContainerElement) => {
+      listContainerElement.innerHTML = `
         <div class="input-field">
           <i class="material-icons prefix">search</i>
-          <input id="${id}-search" class="search" type="search"></input>
-          <label for="${id}-search">Search job</label>
+          <input id="${listContainerElement.id}-search" class="search" type="text"></input>
+          <label for="${listContainerElement.id}-search">Search job</label>
         </div>
         <table>
           <thead>
@@ -27,44 +27,43 @@ class JobList extends RessourceList {
         <ul class="pagination"></ul>
       `.trim();
     },
-    item: `
-      <tr class="clickable hoverable service-color lighten">
-        <td><a class="btn-floating disabled"><i class="service-1 nopaque-icons service-color darken service-icon"></i></a></td>
-        <td><b class="title"></b><br><i class="description"></i></td>
-        <td><span class="status badge new job-status-color job-status-text" data-badge-caption=""></span></td>
-        <td class="right-align">
-          <a class="action-button btn-floating red waves-effect waves-light" data-action="delete-request"><i class="material-icons">delete</i></a>
-          <a class="action-button btn-floating service-color darken waves-effect waves-light service-2" data-action="view"><i class="material-icons">send</i></a>
-        </td>
-      </tr>
-    `.trim(),
     ressourceMapper: (job) => {
       return {
         'id': job.id,
         'creation-date': job.creation_date,
         'description': job.description,
         'service': job.service,
-        'service-1': job.service,
-        'service-2': job.service,
         'status': job.status,
         'title': job.title
       };
     },
-    sortArgs: ['creation-date', {order: 'desc'}],
-    valueNames: [
-      {data: ['id']},
-      {data: ['creation-date']},
-      {data: ['service']},
-      {name: 'service-1', attr: 'data-service'},
-      {name: 'service-2', attr: 'data-service'},
-      {name: 'status', attr: 'data-job-status'},
-      'description',
-      'title'
-    ]
+    sortParams: ['creation-date', {order: 'desc'}],
+    listjs: {
+      item: `
+        <tr class="clickable hoverable service-scheme">
+          <td><a class="btn-floating"><i class="nopaque-icons service-icons" data-service="inherit"></i></a></td>
+          <td><b class="title"></b><br><i class="description"></i></td>
+          <td><span class="badge new job-status-color job-status-text status" data-badge-caption=""></span></td>
+          <td class="right-align">
+            <a class="action-button btn-floating red waves-effect waves-light" data-action="delete-request"><i class="material-icons">delete</i></a>
+            <a class="action-button btn-floating darken waves-effect waves-light" data-action="view"><i class="material-icons">send</i></a>
+          </td>
+        </tr>
+      `.trim(),
+      valueNames: [
+        {data: ['id']},
+        {data: ['creation-date']},
+        {data: ['service']},
+        {name: 'status', attr: 'data-status'},
+        'description',
+        'title'
+      ]
+    }
   };
 
-  constructor(listElement, options = {}) {
-    super(listElement, {...JobList.options, ...options});
+  constructor(listContainerElement, options={}) {
+    super(listContainerElement, _.merge({}, JobList.options, options));
+    console.log(this);
   }
 
   init(user) {
@@ -72,10 +71,12 @@ class JobList extends RessourceList {
   }
 
   onClick(event) {
-    let actionButtonElement = event.target.closest('.action-button');
-    let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
     let jobElement = event.target.closest('tr');
+    if (jobElement === null) {return;}
     let jobId = jobElement.dataset.id;
+    if (jobId === undefined) {return;}
+    let actionButtonElement = event.target.closest('.action-button');
+    let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
     switch (action) {
       case 'delete-request': {
         Utils.deleteJobRequest(this.userId, jobId);
diff --git a/app/static/js/RessourceLists/JobResultList.js b/app/static/js/RessourceLists/JobResultList.js
index 3623363a080948aca9e88a34446550eb42c10e77..ea7cfc3ab31b0f4b61086ca7d5b708c87c4524f3 100644
--- a/app/static/js/RessourceLists/JobResultList.js
+++ b/app/static/js/RessourceLists/JobResultList.js
@@ -6,12 +6,12 @@ class JobResultList extends RessourceList {
   }
 
   static options = {
-    initialHtmlGenerator: (id) => {
-      return `
+    listContainerInnerHTMLGenerator: (listContainerElement) => {
+      listContainerElement.innerHTML = `
         <div class="input-field">
           <i class="material-icons prefix">search</i>
-          <input id="${id}-search" class="search" type="search"></input>
-          <label for="${id}-search">Search job result</label>
+          <input id="${listContainerElement.id}-search" class="search" type="text"></input>
+          <label for="${listContainerElement.id}-search">Search job result</label>
         </div>
         <table>
           <thead>
@@ -26,15 +26,6 @@ class JobResultList extends RessourceList {
         <ul class="pagination"></ul>
       `.trim();
     },
-    item: `
-      <tr class="clickable hoverable">
-        <td><span class="description"></span></td>
-        <td><span class="filename"></span></td>
-        <td class="right-align">
-          <a class="action-button btn-floating waves-effect waves-light" data-action="download"><i class="material-icons">file_download</i></a>
-        </td>
-      </tr>
-    `.trim(),
     ressourceMapper: (jobResult) => {
       return {
         'id': jobResult.id,
@@ -43,18 +34,29 @@ class JobResultList extends RessourceList {
         'filename': jobResult.filename
       };
     },
-    sortArgs: ['filename', {order: 'asc'}],
-    valueNames: [
-      {data: ['id']},
-      {data: ['creation-date']},
-      'description',
-      'filename'
-    ]
+    sortParams: ['filename', {order: 'asc'}],
+    listjs: {
+      item: `
+        <tr class="clickable hoverable">
+          <td><span class="description"></span></td>
+          <td><span class="filename"></span></td>
+          <td class="right-align">
+            <a class="action-button btn-floating waves-effect waves-light" data-action="download"><i class="material-icons">file_download</i></a>
+          </td>
+        </tr>
+      `.trim(),
+      valueNames: [
+        {data: ['id']},
+        {data: ['creation-date']},
+        'description',
+        'filename'
+      ]
+    }
   };
 
-  constructor(listElement, options = {}) {
-    super(listElement, {...JobResultList.options, ...options});
-    this.jobId = listElement.dataset.jobId;
+  constructor(listContainerElement, options = {}) {
+    super(listContainerElement, {...JobResultList.options, ...options});
+    this.jobId = listContainerElement.dataset.jobId;
   }
 
   init(user) {
@@ -62,10 +64,12 @@ class JobResultList extends RessourceList {
   }
 
   onClick(event) {
-    let actionButtonElement = event.target.closest('.action-button');
-    let action = actionButtonElement === null ? 'download' : actionButtonElement.dataset.action;
     let jobResultElement = event.target.closest('tr');
+    if (jobResultElement === null) {return;}
     let jobResultId = jobResultElement.dataset.id;
+    if (jobResultId === undefined) {return;}
+    let actionButtonElement = event.target.closest('.action-button');
+    let action = actionButtonElement === null ? 'download' : actionButtonElement.dataset.action;
     switch (action) {
       case 'download': {
         window.location.href = `/jobs/${this.jobId}/results/${jobResultId}/download`;
diff --git a/app/static/js/RessourceLists/PublicUserList.js b/app/static/js/RessourceLists/PublicUserList.js
index f5c93b8b541b2568038a6ebe98d40632bc9fc157..2b1b94c817c676dceef5ceeaf2a560a41c9e441c 100644
--- a/app/static/js/RessourceLists/PublicUserList.js
+++ b/app/static/js/RessourceLists/PublicUserList.js
@@ -6,12 +6,12 @@ class PublicUserList extends RessourceList {
   }
 
   static options = {
-    initialHtmlGenerator: (id) => {
-      return `
+    listContainerInnerHTMLGenerator: (listContainerElement) => {
+      listContainerElement.innerHTML = `
         <div class="input-field">
           <i class="material-icons prefix">search</i>
-          <input id="${id}-search" class="search" type="search"></input>
-          <label for="${id}-search">Search user</label>
+          <input id="${listContainerElement.id}-search" class="search" type="text"></input>
+          <label for="${listContainerElement.id}-search">Search user</label>
         </div>
         <table>
           <thead>
@@ -30,19 +30,6 @@ class PublicUserList extends RessourceList {
         <ul class="pagination"></ul>
       `.trim();
     },
-    item: `
-      <tr class="clickable hoverable">
-        <td><img alt="user-image" class="circle responsive-img avatar" style="width:50%"></td>
-        <td><b><span class="username"></span><b></td>
-        <td><span class="full-name"></span></td>
-        <td><span class="location"></span></td>
-        <td><span class="organization"></span></td>
-        <td><span class="corpora-online"></span></td>
-        <td class="right-align">
-          <a class="action-button btn-floating waves-effect waves-light" data-action="view"><i class="material-icons">send</i></a>
-        </td>
-      </tr>
-    `.trim(),
     ressourceMapper: (user) => {
       return {
         'id': user.id,
@@ -55,21 +42,36 @@ class PublicUserList extends RessourceList {
         'corpora-online': '0'
       };
     },
-    sortArgs: ['member-since', {order: 'desc'}],
-    valueNames: [
-      {data: ['id']},
-      {data: ['member-since']},
-      {name: 'avatar', attr: 'src'},
-      'username',
-      'full-name',
-      'location',
-      'organization',
-      'corpora-online'
-    ]
+    sortParams: ['member-since', {order: 'desc'}],
+    listjs: {
+      item: `
+        <tr class="clickable hoverable">
+          <td><img alt="user-image" class="circle responsive-img avatar" style="width:50%"></td>
+          <td><b><span class="username"></span><b></td>
+          <td><span class="full-name"></span></td>
+          <td><span class="location"></span></td>
+          <td><span class="organization"></span></td>
+          <td><span class="corpora-online"></span></td>
+          <td class="right-align">
+            <a class="action-button btn-floating waves-effect waves-light" data-action="view"><i class="material-icons">send</i></a>
+          </td>
+        </tr>
+      `.trim(),
+      valueNames: [
+        {data: ['id']},
+        {data: ['member-since']},
+        {name: 'avatar', attr: 'src'},
+        'username',
+        'full-name',
+        'location',
+        'organization',
+        'corpora-online'
+      ]
+    }
   };
 
-  constructor(listElement, options = {}) {
-    super(listElement, {...PublicUserList.options, ...options});
+  constructor(listContainerElement, options = {}) {
+    super(listContainerElement, {...PublicUserList.options, ...options});
   }
 
   init(users) {
diff --git a/app/static/js/RessourceLists/RessourceList.js b/app/static/js/RessourceLists/RessourceList.js
index e1f47f2f2243668e3de3cc001d6085705993fec6..8c58ef42ee9fab6c9297124277bc090457a37a09 100644
--- a/app/static/js/RessourceLists/RessourceList.js
+++ b/app/static/js/RessourceLists/RessourceList.js
@@ -16,64 +16,69 @@ class RessourceList {
     UserList.autoInit();
   }
 
-  static options = {page: 5, pagination: {innerWindow: 2, outerWindow: 2}};
+  static options = {
+    listContainerInnerHTMLGenerator: null,
+    ressourceMapper: null,
+    sortParams: null,
+    listjs: {
+      page: 5,
+      pagination: {
+        innerWindow: 2,
+        outerWindow: 2
+      }
+    }
+  };
 
-  constructor(listElement, options = {}) {
-    if (!(listElement.hasAttribute('id'))) {
+  constructor(listContainerElement, options={}) {
+    let mergedOptions = _.merge({}, RessourceList.options, options);
+    this.isInitialized = false;
+    this.listContainerInnerHTMLGenerator = mergedOptions.listContainerInnerHTMLGenerator;
+    this.ressourceMapper = mergedOptions.ressourceMapper;
+    this.sortParams = mergedOptions.sortParams;
+    this.userId = listContainerElement.dataset.userId;
+    // #region Make sure listElement has an id
+    if (!listContainerElement.hasAttribute('id')) {
       let i;
       for (i = 0; true; i++) {
         if (document.querySelector(`#ressource-list-${i}`)) {continue;}
-        listElement.id = `ressource-list-${i}`;
+        listContainerElement.id = `ressource-list-${i}`;
         break;
       }
     }
-    options = {
-      ...RessourceList.options,
-      ...options
-    }
-    if ('ressourceMapper' in options && typeof options.ressourceMapper === 'function') {
-      this.ressourceMapper = options.ressourceMapper;
-      delete options.ressourceMapper;
+    // #endregion
+    if (this.listContainerInnerHTMLGenerator !== null && listContainerElement.textContent.trim() === '') {
+      this.listContainerInnerHTMLGenerator(listContainerElement);
     }
-    if ('initialHtmlGenerator' in options && typeof options.initialHtmlGenerator === 'function') {
-      this.initialHtmlGenerator = options.initialHtmlGenerator;
-      listElement.innerHTML = this.initialHtmlGenerator(listElement.id);
-      delete options.initialHtmlGenerator;
-    }
-    if ('sortArgs' in options) {
-      this.sortArgs = options.sortArgs;
-      delete options.sortArgs;
-    }
-    this.listjs = new List(listElement, {...RessourceList.options, ...options});
+    this.listjs = new List(listContainerElement, mergedOptions.listjs);
+    this.listjs.list.addEventListener('click', (event) => {this.onClick(event)});
     this.listjs.list.innerHTML = `
       <tr>
-        <td class="row" colspan="100%">
-          <div class="col s12">&nbsp;</div>
-          <div class="col s3 m2 xl1">
-            <div class="preloader-wrapper active">
-              <div class="spinner-layer spinner-green-only">
-                <div class="circle-clipper left">
-                  <div class="circle"></div>
-                </div>
-                <div class="gap-patch">
-                  <div class="circle"></div>
-                </div>
-                <div class="circle-clipper right">
-                  <div class="circle"></div>
+        <td colspan="100%">
+          <div class="row">
+            <div class="col s12">&nbsp;</div>
+            <div class="col s3 m2 xl1">
+              <div class="preloader-wrapper active">
+                <div class="spinner-layer spinner-green-only">
+                  <div class="circle-clipper left">
+                    <div class="circle"></div>
+                  </div>
+                  <div class="gap-patch">
+                    <div class="circle"></div>
+                  </div>
+                  <div class="circle-clipper right">
+                    <div class="circle"></div>
+                  </div>
                 </div>
               </div>
             </div>
-          </div>
-          <div class="col s9 m6 xl5">
-            <span class="card-title">Waiting for data...</span>
-            <p>This list is not initialized yet.</p>
+            <div class="col s9 m6 xl5">
+              <span class="card-title">Waiting for data...</span>
+              <p>This list is not initialized yet.</p>
+            </div>
           </div>
         </td>
       </tr>
     `.trim();
-    this.userId = this.listjs.listContainer.dataset.userId;
-    this.listjs.list.addEventListener('click', (event) => {this.onClick(event)});
-    this.isInitialized = false;
     if (this.userId) {
       app.subscribeUser(this.userId)
         .then((response) => {
@@ -113,12 +118,12 @@ class RessourceList {
 
   add(ressources) {
     let values = Array.isArray(ressources) ? ressources : [ressources];
-    if ('ressourceMapper' in this) {
+    if (this.ressourceMapper !== null) {
       values = values.map((value) => {return this.ressourceMapper(value);});
     }
     this.listjs.add(values, () => {
-      if ('sortArgs' in this) {
-        this.listjs.sort(...this.sortArgs);
+      if (this.sortParams !== null) {
+        this.listjs.sort(...this.sortParams);
       }
     });
   }
diff --git a/app/static/js/RessourceLists/SpacyNLPPipelineModelList.js b/app/static/js/RessourceLists/SpacyNLPPipelineModelList.js
index d03c79ebd42f5368c1f93832daba3515bad334ba..e62428cf70ebaaef140d0fc0648880513cc0e54e 100644
--- a/app/static/js/RessourceLists/SpacyNLPPipelineModelList.js
+++ b/app/static/js/RessourceLists/SpacyNLPPipelineModelList.js
@@ -6,12 +6,12 @@ class SpaCyNLPPipelineModelList extends RessourceList {
   }
 
   static options = {
-    initialHtmlGenerator: (id) => {
-      return `
+    listContainerInnerHTMLGenerator: (listContainerElement) => {
+      listContainerElement.innerHTML = `
         <div class="input-field">
           <i class="material-icons prefix">search</i>
-          <input id="${id}-search" class="search" type="search"></input>
-          <label for="${id}-search">Search SpaCy NLP Pipeline Model</label>
+          <input id="${listContainerElement.id}-search" class="search" type="text"></input>
+          <label for="${listContainerElement.id}-search">Search SpaCy NLP Pipeline Model</label>
         </div>
         <table>
           <thead>
@@ -26,26 +26,6 @@ class SpaCyNLPPipelineModelList extends RessourceList {
         <ul class="pagination"></ul>
       `.trim();
     },
-    item: `
-      <tr class="clickable hoverable">
-        <td><b><span class="title"></span> <span class="version"></span></b><br><i><span class="description"></span></i></td>
-        <td><a class="publisher-url"><span class="publisher"></span></a> (<span class="publishing-year"></span>)<br><a class="publishing-url"><span class="publishing-url-2"></span></a></td>
-        <td>
-          <div class="switch action-switch center-align" data-action="share-request">
-            <span class="share"></span>
-            <label>
-              <input type="checkbox" class="is_public">
-              <span class="lever"></span>
-              public
-            </label>
-          </div>
-        </td>
-        <td class="right-align">
-          <a class="action-button btn-floating red waves-effect waves-light" data-action="delete-request"><i class="material-icons">delete</i></a>
-          <a class="action-button btn-floating service-color darken waves-effect waves-light service-2" data-action="view"><i class="material-icons">send</i></a>
-        </td>
-      </tr>
-    `.trim(),
     ressourceMapper: (spaCyNLPPipelineModel) => {
       return {
         'id': spaCyNLPPipelineModel.id,
@@ -62,25 +42,47 @@ class SpaCyNLPPipelineModelList extends RessourceList {
         'is_public': spaCyNLPPipelineModel.is_public ? 'True' : 'False'
       };
     },
-    sortArgs: ['creation-date', {order: 'desc'}],
-    valueNames: [
-      {data: ['id']},
-      {data: ['creation-date']},
-      {name: 'publisher-url', attr: 'href'},
-      {name: 'publishing-url', attr: 'href'},
-      'description',
-      'publisher',
-      'publishing-url-2',
-      'publishing-year',
-      'title',
-      'title-2',
-      'version',
-      {name: 'is_public', attr: 'data-checked'}
-    ]
+    sortParams: ['creation-date', {order: 'desc'}],
+    listjs: {
+      item: `
+        <tr class="clickable hoverable">
+          <td><b><span class="title"></span> <span class="version"></span></b><br><i><span class="description"></span></i></td>
+          <td><a class="publisher-url"><span class="publisher"></span></a> (<span class="publishing-year"></span>)<br><a class="publishing-url"><span class="publishing-url-2"></span></a></td>
+          <td>
+            <div class="switch action-switch center-align" data-action="share-request">
+              <span class="share"></span>
+              <label>
+                <input type="checkbox" class="is_public">
+                <span class="lever"></span>
+                public
+              </label>
+            </div>
+          </td>
+          <td class="right-align">
+            <a class="action-button btn-floating red waves-effect waves-light" data-action="delete-request"><i class="material-icons">delete</i></a>
+            <a class="action-button btn-floating service-color darken waves-effect waves-light service-2" data-action="view"><i class="material-icons">send</i></a>
+          </td>
+        </tr>
+      `.trim(),
+      valueNames: [
+        {data: ['id']},
+        {data: ['creation-date']},
+        {name: 'publisher-url', attr: 'href'},
+        {name: 'publishing-url', attr: 'href'},
+        'description',
+        'publisher',
+        'publishing-url-2',
+        'publishing-year',
+        'title',
+        'title-2',
+        'version',
+        {name: 'is_public', attr: 'data-checked'}
+      ]
+    }
   };
 
-  constructor(listElement, options = {}) {
-    super(listElement, {...SpaCyNLPPipelineModelList.options, ...options});
+  constructor(listContainerElement, options = {}) {
+    super(listContainerElement, {...SpaCyNLPPipelineModelList.options, ...options});
     this.listjs.list.addEventListener('change', (event) => {this.onChange(event)});
   }
 
diff --git a/app/static/js/RessourceLists/TesseractOCRPipelineModelList.js b/app/static/js/RessourceLists/TesseractOCRPipelineModelList.js
index d299af1b41fe352e63e280c185f3b045d6722b67..76532fcc90126a344e89185a42b8d5af8705c3d5 100644
--- a/app/static/js/RessourceLists/TesseractOCRPipelineModelList.js
+++ b/app/static/js/RessourceLists/TesseractOCRPipelineModelList.js
@@ -6,12 +6,12 @@ class TesseractOCRPipelineModelList extends RessourceList {
   }
 
   static options = {
-    initialHtmlGenerator: (id) => {
-      return `
+    listContainerInnerHTMLGenerator: (listContainerElement) => {
+      listContainerElement.innerHTML = `
         <div class="input-field">
           <i class="material-icons prefix">search</i>
-          <input id="${id}-search" class="search" type="search"></input>
-          <label for="${id}-search">Search Tesseract OCR Pipeline Model</label>
+          <input id="${listContainerElement.id}-search" class="search" type="text"></input>
+          <label for="${listContainerElement.id}-search">Search Tesseract OCR Pipeline Model</label>
         </div>
         <table>
           <thead>
@@ -26,26 +26,6 @@ class TesseractOCRPipelineModelList extends RessourceList {
         <ul class="pagination"></ul>
       `.trim();
     },
-    item: `
-      <tr class="clickable hoverable">
-        <td><b><span class="title"></span> <span class="version"></span></b><br><i><span class="description"></span></i></td>
-        <td><a class="publisher-url"><span class="publisher"></span></a> (<span class="publishing-year"></span>)<br><a class="publishing-url"><span class="publishing-url-2"></span></a></td>
-        <td>
-          <div class="switch action-switch center-align" data-action="share-request">
-            <span class="share"></span>
-            <label>
-              <input type="checkbox" class="is_public">
-              <span class="lever"></span>
-              public
-            </label>
-          </div>
-        </td>
-        <td class="right-align">
-          <a class="action-button btn-floating red waves-effect waves-light" data-action="delete-request"><i class="material-icons">delete</i></a>
-          <a class="action-button btn-floating service-color darken waves-effect waves-light service-2" data-action="view"><i class="material-icons">send</i></a>
-        </td>
-      </tr>
-    `.trim(),
     ressourceMapper: (tesseractOCRPipelineModel) => {
       return {
         'id': tesseractOCRPipelineModel.id,
@@ -62,25 +42,47 @@ class TesseractOCRPipelineModelList extends RessourceList {
         'is_public': tesseractOCRPipelineModel.is_public ? 'True' : 'False'
       };
     },
-    sortArgs: ['creation-date', {order: 'desc'}],
-    valueNames: [
-      {data: ['id']},
-      {data: ['creation-date']},
-      {name: 'publisher-url', attr: 'href'},
-      {name: 'publishing-url', attr: 'href'},
-      'description',
-      'publisher',
-      'publishing-url-2',
-      'publishing-year',
-      'title',
-      'title-2',
-      'version',
-      {name: 'is_public', attr: 'data-checked'}
-    ]
+    sortParams: ['creation-date', {order: 'desc'}],
+    listjs: {
+      item: `
+        <tr class="clickable hoverable">
+          <td><b><span class="title"></span> <span class="version"></span></b><br><i><span class="description"></span></i></td>
+          <td><a class="publisher-url"><span class="publisher"></span></a> (<span class="publishing-year"></span>)<br><a class="publishing-url"><span class="publishing-url-2"></span></a></td>
+          <td>
+            <div class="switch action-switch center-align" data-action="share-request">
+              <span class="share"></span>
+              <label>
+                <input type="checkbox" class="is_public">
+                <span class="lever"></span>
+                public
+              </label>
+            </div>
+          </td>
+          <td class="right-align">
+            <a class="action-button btn-floating red waves-effect waves-light" data-action="delete-request"><i class="material-icons">delete</i></a>
+            <a class="action-button btn-floating service-color darken waves-effect waves-light service-2" data-action="view"><i class="material-icons">send</i></a>
+          </td>
+        </tr>
+      `.trim(),
+      valueNames: [
+        {data: ['id']},
+        {data: ['creation-date']},
+        {name: 'publisher-url', attr: 'href'},
+        {name: 'publishing-url', attr: 'href'},
+        'description',
+        'publisher',
+        'publishing-url-2',
+        'publishing-year',
+        'title',
+        'title-2',
+        'version',
+        {name: 'is_public', attr: 'data-checked'}
+      ]
+    }
   };
 
-  constructor(listElement, options = {}) {
-    super(listElement, {...TesseractOCRPipelineModelList.options, ...options});
+  constructor(listContainerElement, options = {}) {
+    super(listContainerElement, {...TesseractOCRPipelineModelList.options, ...options});
     this.listjs.list.addEventListener('change', (event) => {this.onChange(event)});
   }
 
diff --git a/app/static/js/RessourceLists/UserList.js b/app/static/js/RessourceLists/UserList.js
index 986685bafcd9b7a74c9295e498e15ba96833717a..09a25f219b852e69c35cc0227b73b01b70b38556 100644
--- a/app/static/js/RessourceLists/UserList.js
+++ b/app/static/js/RessourceLists/UserList.js
@@ -6,12 +6,12 @@ class UserList extends RessourceList {
   }
 
   static options = {
-    initialHtmlGenerator: (id) => {
-      return `
+    listContainerInnerHTMLGenerator: (listContainerElement) => {
+      listContainerElement.innerHTML = `
         <div class="input-field">
           <i class="material-icons prefix">search</i>
-          <input id="${id}-search" class="search" type="search"></input>
-          <label for="${id}-search">Search user</label>
+          <input id="${listContainerElement.id}-search" class="search" type="text"></input>
+          <label for="${listContainerElement.id}-search">Search user</label>
         </div>
         <table>
           <thead>
@@ -29,20 +29,6 @@ class UserList extends RessourceList {
         <ul class="pagination"></ul>
       `.trim();
     },
-    item: `
-      <tr class="clickable hoverable">
-        <td><span class="id-1"></span></td>
-        <td><span class="username"></span></td>
-        <td><span class="email"></span></td>
-        <td><span class="last-seen"></span></td>
-        <td><span class="role"></span></td>
-        <td class="right-align">
-          <a class="action-button btn-floating red waves-effect waves-light" data-action="delete"><i class="material-icons">delete</i></a>
-          <a class="action-button btn-floating waves-effect waves-light" data-action="edit"><i class="material-icons">edit</i></a>
-          <a class="action-button btn-floating waves-effect waves-light" data-action="view"><i class="material-icons">send</i></a>
-        </td>
-      </tr>
-    `.trim(),
     ressourceMapper: (user) => {
       return {
         'id': user.id,
@@ -54,20 +40,36 @@ class UserList extends RessourceList {
         'role': user.role.name
       };
     },
-    sortArgs: ['member-since', {order: 'desc'}],
-    valueNames: [
-      {data: ['id']},
-      {data: ['member-since']},
-      'email',
-      'id-1',
-      'last-seen',
-      'role',
-      'username'
-    ]
+    sortParams: ['member-since', {order: 'desc'}],
+    listjs: {
+      item: `
+        <tr class="clickable hoverable">
+          <td><span class="id-1"></span></td>
+          <td><span class="username"></span></td>
+          <td><span class="email"></span></td>
+          <td><span class="last-seen"></span></td>
+          <td><span class="role"></span></td>
+          <td class="right-align">
+            <a class="action-button btn-floating red waves-effect waves-light" data-action="delete"><i class="material-icons">delete</i></a>
+            <a class="action-button btn-floating waves-effect waves-light" data-action="edit"><i class="material-icons">edit</i></a>
+            <a class="action-button btn-floating waves-effect waves-light" data-action="view"><i class="material-icons">send</i></a>
+          </td>
+        </tr>
+      `.trim(),
+      valueNames: [
+        {data: ['id']},
+        {data: ['member-since']},
+        'email',
+        'id-1',
+        'last-seen',
+        'role',
+        'username'
+      ]
+    }
   };
 
-  constructor(listElement, options = {}) {
-    super(listElement, {...UserList.options, ...options});
+  constructor(listContainerElement, options = {}) {
+    super(listContainerElement, {...UserList.options, ...options});
   }
 
   init(users) {
@@ -75,10 +77,12 @@ class UserList extends RessourceList {
   }
 
   onClick(event) {
-    let actionButtonElement = event.target.closest('.action-button');
-    let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
     let userElement = event.target.closest('tr');
+    if (userElement === null) {return;}
     let userId = userElement.dataset.id;
+    if (userId === undefined) {return;}
+    let actionButtonElement = event.target.closest('.action-button');
+    let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
     switch (action) {
       case 'delete': {
         Utils.deleteUserRequest(userId);
diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2
index b86d4cdb8b302000f3e405e0aa08cb188b18b4d8..d53dea3402cd5e12e4d7d58b9b678ed0db49246a 100644
--- a/app/templates/_scripts.html.j2
+++ b/app/templates/_scripts.html.j2
@@ -1,5 +1,6 @@
 <script src="https://cdnjs.cloudflare.com/ajax/libs/fast-json-patch/3.1.1/fast-json-patch.min.js" integrity="sha512-5uDdefwnzyq4N+SkmMBmekZLZNmc6dLixvVxCdlHBfqpyz0N3bzLdrJ55OLm7QrZmgZuhLGgHLDtJwU6RZoFCA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js" integrity="sha512-93wYgwrIFL+b+P3RvYxi/WUFRXXUDSLCT2JQk9zhVGXuS2mHl2axj6d+R6pP+gcU5isMHRj1u0oYE/mWyt/RjA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js" integrity="sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.min.js" integrity="sha512-mHO4BJ0ELk7Pb1AzhTi3zvUeRgq3RXVOu9tTRfnA6qOxGK4pG2u57DJYolI4KrEnnLTcH9/J5wNOozRTDaybXg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
 {%- assets
   filters='rjsmin',
@@ -38,17 +39,19 @@
   const currentUserId = {{ current_user.hashid|tojson }};
 
   // Initialize components for current user
-  app.subscribeUser(currentUserId).catch((error) => {throw JSON.stringify(error);});
-  app.getUser(currentUserId, true, true);
+  app.subscribeUser(currentUserId)
+    .catch((error) => {throw JSON.stringify(error);});
+  app.getUser(currentUserId, true, true)
+    .catch((error) => {throw JSON.stringify(error);});
   {%- endif %}
 
   // Disable all option elements with no value
-  for (let optionElementWithoutValue of document.querySelectorAll('option[value=""]')) {
-    optionElementWithoutValue.disabled = true;
+  for (let optionElement of document.querySelectorAll('option[value=""]')) {
+    optionElement.disabled = true;
   }
 
-  // Set the data-length attribute on inputs with the maxlength attribute
-  for (let inputElement of document.querySelectorAll('input[maxlength], textarea[maxlength]')) {
+  // Set the data-length attribute on textareas/inputs with the maxlength attribute
+  for (let inputElement of document.querySelectorAll('textarea[maxlength], input[maxlength]')) {
     inputElement.dataset.length = inputElement.getAttribute('maxlength');
   }
 
@@ -63,7 +66,7 @@
   Form.autoInit();
 
   // Display flashed messages
-  for (let flashedMessage of {{ get_flashed_messages(with_categories=True)|tojson }}) {
-    app.flash(flashedMessage[1], flashedMessage[0]);
+  for (let [category, message] of {{ get_flashed_messages(with_categories=True)|tojson }}) {
+    app.flash(message, message);
   }
 </script>
diff --git a/app/templates/_sidenav.html.j2 b/app/templates/_sidenav.html.j2
index ffdf86a69cf7d62c3d5eee51f24b95b56d13da2f..965b50ccaceaf75af1a93b2ebde5d096b6957c4b 100644
--- a/app/templates/_sidenav.html.j2
+++ b/app/templates/_sidenav.html.j2
@@ -24,13 +24,13 @@
   <li><a href="{{ url_for('contributions.contributions') }}"><i class="material-icons">new_label</i>Contribute</a></li>
   <li><div class="divider"></div></li>
   <li><a class="subheader">Processes & Services</a></li>
-  <li class="service-color service-color-border border-darken" data-service="file-setup-pipeline" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.file_setup_pipeline') }}"><i class="nopaque-icons service-icon" data-service="file-setup-pipeline"></i>File setup</a></li>
-  <li class="service-color service-color-border border-darken" data-service="tesseract-ocr-pipeline" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.tesseract_ocr_pipeline') }}"><i class="nopaque-icons service-icon" data-service="tesseract-ocr-pipeline"></i>OCR</a></li>
+  <li class="service-color service-color-border border-darken" data-service="file-setup-pipeline" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.file_setup_pipeline') }}"><i class="nopaque-icons service-icons" data-service="file-setup-pipeline"></i>File setup</a></li>
+  <li class="service-color service-color-border border-darken" data-service="tesseract-ocr-pipeline" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.tesseract_ocr_pipeline') }}"><i class="nopaque-icons service-icons" data-service="tesseract-ocr-pipeline"></i>OCR</a></li>
   {% if config.NOPAQUE_TRANSKRIBUS_ENABLED %}
-  <li class="service-color service-color-border border-darken" data-service="transkribus-htr-pipeline" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.transkribus_htr_pipeline') }}"><i class="nopaque-icons service-icon" data-service="transkribus-htr-pipeline"></i>HTR</a></li>
+  <li class="service-color service-color-border border-darken" data-service="transkribus-htr-pipeline" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.transkribus_htr_pipeline') }}"><i class="nopaque-icons service-icons" data-service="transkribus-htr-pipeline"></i>HTR</a></li>
   {% endif %}
-  <li class="service-color service-color-border border-darken" data-service="spacy-nlp-pipeline" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.spacy_nlp_pipeline') }}"><i class="nopaque-icons service-icon" data-service="spacy-nlp-pipeline"></i>NLP</a></li>
-  <li class="service-color service-color-border border-darken" data-service="corpus-analysis" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.corpus_analysis') }}"><i class="nopaque-icons service-icon" data-service="corpus-analysis"></i>Corpus analysis</a></li>
+  <li class="service-color service-color-border border-darken" data-service="spacy-nlp-pipeline" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.spacy_nlp_pipeline') }}"><i class="nopaque-icons service-icons" data-service="spacy-nlp-pipeline"></i>NLP</a></li>
+  <li class="service-color service-color-border border-darken" data-service="corpus-analysis" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.corpus_analysis') }}"><i class="nopaque-icons service-icons" data-service="corpus-analysis"></i>Corpus analysis</a></li>
   <li><div class="divider"></div></li>
   <li><a class="subheader">Account</a></li>
   <li><a href="{{ url_for('settings.settings') }}"><i class="material-icons">settings</i>General Settings</a></li>
diff --git a/app/templates/contributions/contributions.html.j2 b/app/templates/contributions/contributions.html.j2
index 4e513b9e8108dcddac06a95854bdf9b9488b1da3..bdbcb10c42addf50fa31f220f682fb104dc12b68 100644
--- a/app/templates/contributions/contributions.html.j2
+++ b/app/templates/contributions/contributions.html.j2
@@ -13,7 +13,7 @@
       <div class="card extension-selector hoverable service-color" data-service="tesseract-ocr-pipeline">
         <a href="{{ url_for('.tesseract_ocr_pipeline_models') }}" style="position: absolute; width: 100%; height: 100%;"></a>
         <div class="card-content">
-          <span class="card-title" data-service="tesseract-ocr-pipeline"><i class="nopaque-icons service-icon" data-service="tesseract-ocr-pipeline"></i>Tesseract OCR Pipeline Models</span>
+          <span class="card-title" data-service="tesseract-ocr-pipeline"><i class="nopaque-icons service-icons" data-service="tesseract-ocr-pipeline"></i>Tesseract OCR Pipeline Models</span>
           <p>Here you can see and edit the models that you have created. You can also create new models.</p>
         </div>
       </div>
@@ -23,7 +23,7 @@
       <div class="card extension-selector hoverable service-color" data-service="spacy-nlp-pipeline">
       <a href="{{ url_for('.spacy_nlp_pipeline_models') }}" style="position: absolute; width: 100%; height: 100%;"></a>
         <div class="card-content">
-          <span class="card-title"><i class="nopaque-icons service-icon" data-service="spacy-nlp-pipeline"></i>SpaCy NLP Pipeline Models</span>
+          <span class="card-title"><i class="nopaque-icons service-icons" data-service="spacy-nlp-pipeline"></i>SpaCy NLP Pipeline Models</span>
           <p>Here you can see and edit the models that you have created. You can also create new models.</p>
         </div>
       </div>
@@ -34,7 +34,7 @@
       <div class="card extension-selector hoverable service-color" data-service="transkribus-htr-pipeline">
       <a href="" style="position: absolute; width: 100%; height: 100%;"></a>
         <div class="card-content">
-          <span class="card-title"><i class="nopaque-icons service-icon" data-service="transkribus-htr-pipeline"></i>Transkribus HTR Pipeline Models</span>
+          <span class="card-title"><i class="nopaque-icons service-icons" data-service="transkribus-htr-pipeline"></i>Transkribus HTR Pipeline Models</span>
           <p>Here you can see and edit the models that you have created. You can also create new models.</p>
         </div>
       </div>
diff --git a/app/templates/contributions/create_spacy_nlp_pipeline_model.html.j2 b/app/templates/contributions/create_spacy_nlp_pipeline_model.html.j2
index c1f452d3ed89b46f3c92e1bc0434a18e209c334b..e17ac9e58cec63afebbb6a307774724f0d72cdbe 100644
--- a/app/templates/contributions/create_spacy_nlp_pipeline_model.html.j2
+++ b/app/templates/contributions/create_spacy_nlp_pipeline_model.html.j2
@@ -16,7 +16,7 @@
         <p class="hide-on-small-only">&nbsp;</p>
         <p class="hide-on-small-only">&nbsp;</p>
         <a class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
-          <i class="nopaque-icons service-color darken service-icon" data-service="spacy-nlp-pipeline"></i>
+          <i class="nopaque-icons service-color darken service-icons" data-service="spacy-nlp-pipeline"></i>
         </a>
       </div>
     </div>
diff --git a/app/templates/contributions/create_tesseract_ocr_pipeline_model.html.j2 b/app/templates/contributions/create_tesseract_ocr_pipeline_model.html.j2
index e4c5a04b4c1c058872b688a98edd9cd85df4f695..ecede20a08d2a8ce5cc1b46e9023808c55191747 100644
--- a/app/templates/contributions/create_tesseract_ocr_pipeline_model.html.j2
+++ b/app/templates/contributions/create_tesseract_ocr_pipeline_model.html.j2
@@ -16,7 +16,7 @@
         <p class="hide-on-small-only">&nbsp;</p>
         <p class="hide-on-small-only">&nbsp;</p>
         <a class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
-          <i class="nopaque-icons service-color darken service-icon" data-service="tesseract-ocr-pipeline"></i>
+          <i class="nopaque-icons service-color darken service-icons" data-service="tesseract-ocr-pipeline"></i>
         </a>
       </div>
     </div>
diff --git a/app/templates/corpora/analyse_corpus.html.j2 b/app/templates/corpora/analyse_corpus.html.j2
index 73ebfd774f0420c7a11e36ad1b0e692399a11dac..69bcfd6e014d38cc4ea8d5c6d927aba8c6c57a14 100644
--- a/app/templates/corpora/analyse_corpus.html.j2
+++ b/app/templates/corpora/analyse_corpus.html.j2
@@ -4,7 +4,7 @@
 
 {% block page_content %}
 <ul class="row tabs no-autoinit" id="corpus-analysis-app-extension-tabs">
-  <li class="tab col s3"><a class="active" href="#corpus-analysis-app-overview"><i class="nopaque-icons service-icon left" data-service="corpus-analysis"></i>Corpus analysis</a></li>
+  <li class="tab col s3"><a class="active" href="#corpus-analysis-app-overview"><i class="nopaque-icons service-icons left" data-service="corpus-analysis"></i>Corpus analysis</a></li>
   <li class="tab col s3"><a href="#concordance-extension-container"><i class="material-icons left">list_alt</i>Concordance</a></li>
   <li class="tab col s3"><a href="#reader-extension-container"><i class="material-icons left">chrome_reader_mode</i>Reader</a></li>
 </ul>
diff --git a/app/templates/jobs/job.html.j2 b/app/templates/jobs/job.html.j2
index 5b78d0555268dede68007a9a563f7bad140ad587..c12221274778c18ccf89e9a3ca56ba126a14b5b1 100644
--- a/app/templates/jobs/job.html.j2
+++ b/app/templates/jobs/job.html.j2
@@ -9,7 +9,7 @@
     <div class="col s12" data-job-id="{{ job.hashid }}" data-user-id="{{ job.user.hashid }}" id="job-display">
       <div class="row">
         <div class="col s8 m9 l10">
-          <h1 id="title"><i style="font-size: inherit;" class="nopaque-icons service-icon" data-service="{{ job.service }}"></i> <span class="job-title"></span></h1>
+          <h1 id="title"><i style="font-size: inherit;" class="nopaque-icons service-icons" data-service="{{ job.service }}"></i> <span class="job-title"></span></h1>
         </div>
         <div class="col s4 m3 l2 right-align">
           <p>&nbsp;</p>
diff --git a/app/templates/main/dashboard.html.j2 b/app/templates/main/dashboard.html.j2
index e296dd853d3b4724728643b8dfba5e691aba5676..d6624f8a64a266e04c2c29e7bc4bb24575f0c5c2 100644
--- a/app/templates/main/dashboard.html.j2
+++ b/app/templates/main/dashboard.html.j2
@@ -82,7 +82,7 @@
           <div class="card-panel center-align hoverable">
             <br>
             <a href="{{ url_for('services.file_setup_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
-              <i class="nopaque-icons service-color darken service-icon" data-service="file-setup-pipeline"></i>
+              <i class="nopaque-icons service-color darken service-icons" data-service="file-setup-pipeline"></i>
             </a>
             <br><br>
             <p class="service-color-text darken" data-service="file-setup-pipeline"><b>File setup</b></p>
@@ -94,7 +94,7 @@
           <div class="card-panel center-align hoverable">
             <br>
             <a href="{{ url_for('services.tesseract_ocr_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
-              <i class="nopaque-icons service-color darken service-icon" data-service="tesseract-ocr-pipeline" style="font-size: 2.5rem;"></i>
+              <i class="nopaque-icons service-color darken service-icons" data-service="tesseract-ocr-pipeline" style="font-size: 2.5rem;"></i>
             </a>
             <br><br>
             <p class="service-color-text darken" data-service="tesseract-ocr-pipeline"><b>Optical Character Recognition</b></p>
@@ -106,7 +106,7 @@
           <div class="card-panel center-align hoverable">
             <br>
             <a href="{{ url_for('services.spacy_nlp_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
-              <i class="nopaque-icons service-color darken service-icon" data-service="spacy-nlp-pipeline" style="font-size: 2.5rem;"></i>
+              <i class="nopaque-icons service-color darken service-icons" data-service="spacy-nlp-pipeline" style="font-size: 2.5rem;"></i>
             </a>
             <br><br>
             <p class="service-color-text darken" data-service="spacy-nlp-pipeline"><b>Natural Language Processing</b></p>
diff --git a/app/templates/main/dashboard2.html.j2 b/app/templates/main/dashboard2.html.j2
index c03834e344b9ae1e61d496d526446c07b11bf4dc..d45aaa041d55a2b123aaeb3dc63f82ab3858c61a 100644
--- a/app/templates/main/dashboard2.html.j2
+++ b/app/templates/main/dashboard2.html.j2
@@ -220,7 +220,7 @@
           <div class="card-panel center-align hoverable">
             <br>
             <a href="{{ url_for('services.file_setup_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
-              <i class="nopaque-icons service-color darken service-icon" data-service="file-setup-pipeline"></i>
+              <i class="nopaque-icons service-color darken service-icons" data-service="file-setup-pipeline"></i>
             </a>
             <br><br>
             <p class="service-color-text darken" data-service="file-setup-pipeline"><b>File setup</b></p>
@@ -232,7 +232,7 @@
           <div class="card-panel center-align hoverable">
             <br>
             <a href="{{ url_for('services.tesseract_ocr_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
-              <i class="nopaque-icons service-color darken service-icon" data-service="tesseract-ocr-pipeline" style="font-size: 2.5rem;"></i>
+              <i class="nopaque-icons service-color darken service-icons" data-service="tesseract-ocr-pipeline" style="font-size: 2.5rem;"></i>
             </a>
             <br><br>
             <p class="service-color-text darken" data-service="tesseract-ocr-pipeline"><b>Optical Character Recognition</b></p>
@@ -244,7 +244,7 @@
           <div class="card-panel center-align hoverable">
             <br>
             <a href="{{ url_for('services.spacy_nlp_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
-              <i class="nopaque-icons service-color darken service-icon" data-service="spacy-nlp-pipeline" style="font-size: 2.5rem;"></i>
+              <i class="nopaque-icons service-color darken service-icons" data-service="spacy-nlp-pipeline" style="font-size: 2.5rem;"></i>
             </a>
             <br><br>
             <p class="service-color-text darken" data-service="spacy-nlp-pipeline"><b>Natural Language Processing</b></p>
diff --git a/app/templates/main/index.html.j2 b/app/templates/main/index.html.j2
index 5b450d61859ae4b380f1ec49c165f635fd783244..74bfa30646af16ad69f1c340c24bd8cce846e0a0 100644
--- a/app/templates/main/index.html.j2
+++ b/app/templates/main/index.html.j2
@@ -77,7 +77,7 @@
             <div class="col s12 m6 l3 center-align">
               <p>&nbsp;</p>
               <a href="{{ url_for('services.file_setup_pipeline') }}" class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
-                <i class="nopaque-icons service-color darken service-icon" data-service="file-setup-pipeline"></i>
+                <i class="nopaque-icons service-color darken service-icons" data-service="file-setup-pipeline"></i>
               </a>
               <br><br>
               <p class="service-color-text text-darken" data-service="file-setup-pipeline"><b>File setup</b></p>
@@ -86,7 +86,7 @@
             <div class="col s12 m6 l3 center-align">
               <p>&nbsp;</p>
               <a href="{{ url_for('services.tesseract_ocr_pipeline') }}" class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
-                <i class="nopaque-icons service-color darken service-icon" data-service="tesseract-ocr-pipeline"></i>
+                <i class="nopaque-icons service-color darken service-icons" data-service="tesseract-ocr-pipeline"></i>
               </a>
               <br><br>
               <p class="service-color-text text-darken" data-service="tesseract-ocr-pipeline"><b>Optical Character Recognition</b></p>
@@ -95,7 +95,7 @@
             <div class="col s12 m6 l3 center-align">
               <p>&nbsp;</p>
               <a href="{{ url_for('services.transkribus_htr_pipeline') }}" class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
-                <i class="nopaque-icons service-color darken service-icon" data-service="transkribus-htr-pipeline"></i>
+                <i class="nopaque-icons service-color darken service-icons" data-service="transkribus-htr-pipeline"></i>
               </a>
               <br><br>
               <p class="service-color-text text-darken" data-service="transkribus-htr-pipeline"><b>Transkribus HTR Pipeline</b></p>
@@ -104,7 +104,7 @@
             <div class="col s12 m6 l3 center-align">
               <p>&nbsp;</p>
               <a href="{{ url_for('services.spacy_nlp_pipeline') }}" class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
-                <i class="nopaque-icons service-color darken service-icon" data-service="spacy-nlp-pipeline"></i>
+                <i class="nopaque-icons service-color darken service-icons" data-service="spacy-nlp-pipeline"></i>
               </a>
               <br><br>
               <p class="service-color-text text-darken" data-service="spacy-nlp-pipeline"><b>Natural Language Processing</b></p>
@@ -113,7 +113,7 @@
             <div class="col s12 m6 l3 center-align">
               <p>&nbsp;</p>
               <a href="{{ url_for('services.corpus_analysis') }}" class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
-                <i class="nopaque-icons service-color darken service-icon" data-service="corpus-analysis"></i>
+                <i class="nopaque-icons service-color darken service-icons" data-service="corpus-analysis"></i>
               </a>
               <br><br>
               <p class="service-color-text text-darken" data-service="corpus-analysis"><b>Corpus analysis</b></p>
diff --git a/app/templates/services/corpus_analysis.html.j2 b/app/templates/services/corpus_analysis.html.j2
index e72d11b88b9d08d9fecd8dff94e3814b8bf628da..a7e3da8e7a04634df25bb02047168a847e06897f 100644
--- a/app/templates/services/corpus_analysis.html.j2
+++ b/app/templates/services/corpus_analysis.html.j2
@@ -13,7 +13,7 @@
     <div class="col s12 m3 push-m9">
       <div class="center-align">
         <a class="btn-floating btn-large btn-scale-x2 waves-effect waves-light" style="transform: scale(2);">
-          <i class="nopaque-icons service-color darken service-icon" data-service="corpus-analysis"></i>
+          <i class="nopaque-icons service-color darken service-icons" data-service="corpus-analysis"></i>
         </a>
       </div>
     </div>
diff --git a/app/templates/services/file_setup_pipeline.html.j2 b/app/templates/services/file_setup_pipeline.html.j2
index eca9b8b7dc4b850ce152b99c209c033c6fd4a6b4..ebc4cfc4cba500de29ff5ced062525244603c9ce 100644
--- a/app/templates/services/file_setup_pipeline.html.j2
+++ b/app/templates/services/file_setup_pipeline.html.j2
@@ -16,7 +16,7 @@
         <p class="hide-on-small-only">&nbsp;</p>
         <p class="hide-on-small-only">&nbsp;</p>
         <a class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
-          <i class="nopaque-icons service-color darken service-icon" data-service="file-setup-pipeline"></i>
+          <i class="nopaque-icons service-color darken service-icons" data-service="file-setup-pipeline"></i>
         </a>
       </div>
     </div>
diff --git a/app/templates/services/spacy_nlp_pipeline.html.j2 b/app/templates/services/spacy_nlp_pipeline.html.j2
index 6443c506ca9915f1fc24aaa9d66287ad37cbc1e9..030ea163456407eb186bced0a079c4dcba6da513 100644
--- a/app/templates/services/spacy_nlp_pipeline.html.j2
+++ b/app/templates/services/spacy_nlp_pipeline.html.j2
@@ -16,7 +16,7 @@
         <p class="hide-on-small-only">&nbsp;</p>
         <p class="hide-on-small-only">&nbsp;</p>
         <a class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
-          <i class="nopaque-icons service-color darken service-icon" data-service="spacy-nlp-pipeline"></i>
+          <i class="nopaque-icons service-color darken service-icons" data-service="spacy-nlp-pipeline"></i>
         </a>
       </div>
     </div>
diff --git a/app/templates/services/tesseract_ocr_pipeline.html.j2 b/app/templates/services/tesseract_ocr_pipeline.html.j2
index c9617d5aa811a156addc37f4eec7a0e54617a1ae..ff4fd38b779260ab86f53ad87a5bfa115f1904a5 100644
--- a/app/templates/services/tesseract_ocr_pipeline.html.j2
+++ b/app/templates/services/tesseract_ocr_pipeline.html.j2
@@ -16,7 +16,7 @@
         <p class="hide-on-small-only">&nbsp;</p>
         <p class="hide-on-small-only">&nbsp;</p>
         <a class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
-          <i class="nopaque-icons service-color darken service-icon" data-service="tesseract-ocr-pipeline"></i>
+          <i class="nopaque-icons service-color darken service-icons" data-service="tesseract-ocr-pipeline"></i>
         </a>
       </div>
     </div>
diff --git a/app/templates/services/transkribus_htr_pipeline.html.j2 b/app/templates/services/transkribus_htr_pipeline.html.j2
index da14d53c6125e4247e2b51c1fdefa2232e090ace..f5468ce97ebc065a95f16981d56542101121da53 100644
--- a/app/templates/services/transkribus_htr_pipeline.html.j2
+++ b/app/templates/services/transkribus_htr_pipeline.html.j2
@@ -16,7 +16,7 @@
         <p class="hide-on-small-only">&nbsp;</p>
         <p class="hide-on-small-only">&nbsp;</p>
         <a class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
-          <i class="nopaque-icons service-color darken service-icon" data-service="transkribus-htr-pipeline"></i>
+          <i class="nopaque-icons service-color darken service-icons" data-service="transkribus-htr-pipeline"></i>
         </a>
       </div>
     </div>