From f23c738d669f51efed32ee9d1717e7e208196c78 Mon Sep 17 00:00:00 2001
From: ml bonhomme <bonhomme@teklia.com>
Date: Wed, 23 Aug 2023 11:00:42 +0000
Subject: [PATCH] Piniaize the model store

---
 package-lock.json                             | 128 +++++------
 package.json                                  |   2 +-
 src/api/model.ts                              |  12 +-
 src/components/Model/ModelPicker.vue          |  44 ++--
 src/components/Model/Selection.vue            |  10 +-
 src/components/Model/Versions/DeleteModal.vue |  25 ++-
 src/components/Model/Versions/List.vue        |  31 +--
 .../Model/Versions/ModelVersionPicker.vue     |  46 ++--
 src/components/Model/Versions/Row.vue         |  17 +-
 src/helpers/index.ts                          |   2 +-
 src/store/index.js                            |   9 +-
 src/store/model.js                            |  85 -------
 src/stores/index.ts                           |   1 +
 src/stores/model.ts                           |  72 ++++++
 src/types/index.ts                            |  15 +-
 src/types/model.ts                            |  14 ++
 src/views/Model/Create.vue                    |  52 +++--
 src/views/Model/List.vue                      |  39 ++--
 src/views/Model/Model.vue                     |  17 +-
 src/views/Model/Version.vue                   |  18 +-
 tests/unit/store/auth.spec.js                 |   5 -
 tests/unit/store/elements.spec.js             |   3 +-
 tests/unit/store/index.spec.js                |   2 -
 tests/unit/store/model.spec.js                | 212 ------------------
 tests/unit/stores/model.spec.js               | 156 +++++++++++++
 25 files changed, 497 insertions(+), 520 deletions(-)
 delete mode 100644 src/store/model.js
 create mode 100644 src/stores/model.ts
 delete mode 100644 tests/unit/store/model.spec.js
 create mode 100644 tests/unit/stores/model.spec.js

diff --git a/package-lock.json b/package-lock.json
index 2eb80eead..8c6385249 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,18 +1,18 @@
 {
   "name": "arkindex",
-  "version": "1.5.1-alpha1",
+  "version": "1.5.1-beta1",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "arkindex",
-      "version": "1.5.1-alpha1",
+      "version": "1.5.1-beta1",
       "license": "MIT",
       "dependencies": {
         "@sentry/integrations": "^7.16.0",
         "@sentry/vue": "^7.16.0",
         "ansi-to-html": "^0.7.2",
-        "axios": "^1.1.3",
+        "axios": "^1.4.0",
         "bulma": "^0.9.3",
         "bulma-switch": "^2.0.0",
         "bulma-tooltip": "^3.0.2",
@@ -3602,6 +3602,26 @@
         }
       }
     },
+    "node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15": {
+      "name": "vue-loader",
+      "version": "15.10.1",
+      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.1.tgz",
+      "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==",
+      "dev": true,
+      "dependencies": {
+        "@vue/component-compiler-utils": "^3.1.0",
+        "hash-sum": "^1.0.2",
+        "loader-utils": "^1.1.0",
+        "vue-hot-reload-api": "^2.3.0",
+        "vue-style-loader": "^4.1.0"
+      }
+    },
+    "node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/node_modules/hash-sum": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz",
+      "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
+      "dev": true
+    },
     "node_modules/@vue/cli-shared-utils": {
       "version": "5.0.8",
       "resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-5.0.8.tgz",
@@ -4012,38 +4032,6 @@
       "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.37.tgz",
       "integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw=="
     },
-    "node_modules/@vue/vue-loader-v15": {
-      "name": "vue-loader",
-      "version": "15.10.1",
-      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.1.tgz",
-      "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==",
-      "dev": true,
-      "dependencies": {
-        "@vue/component-compiler-utils": "^3.1.0",
-        "hash-sum": "^1.0.2",
-        "loader-utils": "^1.1.0",
-        "vue-hot-reload-api": "^2.3.0",
-        "vue-style-loader": "^4.1.0"
-      },
-      "peerDependencies": {
-        "css-loader": "*",
-        "webpack": "^3.0.0 || ^4.1.0 || ^5.0.0-0"
-      },
-      "peerDependenciesMeta": {
-        "cache-loader": {
-          "optional": true
-        },
-        "vue-template-compiler": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@vue/vue-loader-v15/node_modules/hash-sum": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz",
-      "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
-      "dev": true
-    },
     "node_modules/@vue/web-component-wrapper": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz",
@@ -4518,6 +4506,7 @@
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-3.3.2.tgz",
       "integrity": "sha512-L5TiS8E2Hn/Yz7SSnWIVbZw0ZfEIXZCa5VUiVxD9P53JvSrf4aStvsFDlGWPvpIdCR+aly2CfoB79B9/JjKFqg==",
+      "deprecated": "The `apollo-datasource` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.",
       "dev": true,
       "dependencies": {
         "@apollo/utils.keyvaluecache": "^1.0.1",
@@ -4531,6 +4520,7 @@
       "version": "3.3.3",
       "resolved": "https://registry.npmjs.org/apollo-reporting-protobuf/-/apollo-reporting-protobuf-3.3.3.tgz",
       "integrity": "sha512-L3+DdClhLMaRZWVmMbBcwl4Ic77CnEBPXLW53F7hkYhkaZD88ivbCVB1w/x5gunO6ZHrdzhjq0FHmTsBvPo7aQ==",
+      "deprecated": "The `apollo-reporting-protobuf` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). This package's functionality is now found in the `@apollo/usage-reporting-protobuf` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.",
       "dev": true,
       "dependencies": {
         "@apollo/protobufjs": "1.2.6"
@@ -4540,6 +4530,7 @@
       "version": "3.11.1",
       "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-3.11.1.tgz",
       "integrity": "sha512-t/eCKrRFK1lYZlc5pHD99iG7Np7CEm3SmbDiONA7fckR3EaB/pdsEdIkIwQ5QBBpT5JLp/nwvrZRVwhaWmaRvw==",
+      "deprecated": "The `apollo-server-core` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.",
       "dev": true,
       "dependencies": {
         "@apollo/utils.keyvaluecache": "^1.0.1",
@@ -4586,6 +4577,7 @@
       "version": "4.2.1",
       "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-4.2.1.tgz",
       "integrity": "sha512-vm/7c7ld+zFMxibzqZ7SSa5tBENc4B0uye9LTfjJwGoQFY5xsUPH5FpO5j0bMUDZ8YYNbrF9SNtzc5Cngcr90g==",
+      "deprecated": "The `apollo-server-env` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). This package's functionality is now found in the `@apollo/utils.fetcher` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.",
       "dev": true,
       "dependencies": {
         "node-fetch": "^2.6.7"
@@ -4598,6 +4590,7 @@
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-3.3.1.tgz",
       "integrity": "sha512-xnZJ5QWs6FixHICXHxUfm+ZWqqxrNuPlQ+kj5m6RtEgIpekOPssH/SD9gf2B4HuWV0QozorrygwZnux8POvyPA==",
+      "deprecated": "The `apollo-server-errors` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.",
       "dev": true,
       "engines": {
         "node": ">=12.0"
@@ -4610,6 +4603,7 @@
       "version": "3.10.0",
       "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-3.10.0.tgz",
       "integrity": "sha512-ww3tZq9I/x3Oxtux8xlHAZcSB0NNQ17lRlY6yCLk1F+jCzdcjuj0x8XNg0GdTrMowt5v43o786bU9VYKD5OVnA==",
+      "deprecated": "The `apollo-server-express` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.",
       "dev": true,
       "dependencies": {
         "@types/accepts": "^1.3.5",
@@ -4636,6 +4630,7 @@
       "version": "3.7.1",
       "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-3.7.1.tgz",
       "integrity": "sha512-g3vJStmQtQvjGI289UkLMfThmOEOddpVgHLHT2bNj0sCD/bbisj4xKbBHETqaURokteqSWyyd4RDTUe0wAUDNQ==",
+      "deprecated": "The `apollo-server-plugin-base` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.",
       "dev": true,
       "dependencies": {
         "apollo-server-types": "^3.7.1"
@@ -4651,6 +4646,7 @@
       "version": "3.7.1",
       "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-3.7.1.tgz",
       "integrity": "sha512-aE9RDVplmkaOj/OduNmGa+0a1B5RIWI0o3zC1zLvBTVWMKTpo0ifVf11TyMkLCY+T7cnZqVqwyShziOyC3FyUw==",
+      "deprecated": "The `apollo-server-types` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.",
       "dev": true,
       "dependencies": {
         "@apollo/utils.keyvaluecache": "^1.0.1",
@@ -4913,9 +4909,9 @@
       }
     },
     "node_modules/axios": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
-      "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
+      "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
       "dependencies": {
         "follow-redirects": "^1.15.0",
         "form-data": "^4.0.0",
@@ -6262,6 +6258,7 @@
       "version": "0.15.1",
       "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz",
       "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==",
+      "deprecated": "Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog",
       "dev": true,
       "dependencies": {
         "bluebird": "^3.1.1"
@@ -16201,6 +16198,7 @@
       "version": "2.2.16",
       "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz",
       "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==",
+      "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
       "dev": true,
       "dependencies": {
         "nanoid": "^2.1.0"
@@ -16531,7 +16529,8 @@
     "node_modules/sourcemap-codec": {
       "version": "1.4.8",
       "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
-      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
+      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+      "deprecated": "Please use @jridgewell/sourcemap-codec instead"
     },
     "node_modules/spdx-correct": {
       "version": "3.1.1",
@@ -18370,6 +18369,7 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
       "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
+      "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.",
       "dev": true,
       "dependencies": {
         "browser-process-hrtime": "^1.0.0"
@@ -22131,6 +22131,29 @@
         "webpack-merge": "^5.7.3",
         "webpack-virtual-modules": "^0.4.2",
         "whatwg-fetch": "^3.6.2"
+      },
+      "dependencies": {
+        "@vue/vue-loader-v15": {
+          "version": "npm:vue-loader@15.10.1",
+          "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.1.tgz",
+          "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==",
+          "dev": true,
+          "requires": {
+            "@vue/component-compiler-utils": "^3.1.0",
+            "hash-sum": "^1.0.2",
+            "loader-utils": "^1.1.0",
+            "vue-hot-reload-api": "^2.3.0",
+            "vue-style-loader": "^4.1.0"
+          },
+          "dependencies": {
+            "hash-sum": {
+              "version": "1.0.2",
+              "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz",
+              "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
+              "dev": true
+            }
+          }
+        }
       }
     },
     "@vue/cli-shared-utils": {
@@ -22456,27 +22479,6 @@
       "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.37.tgz",
       "integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw=="
     },
-    "@vue/vue-loader-v15": {
-      "version": "npm:vue-loader@15.10.1",
-      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.1.tgz",
-      "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==",
-      "dev": true,
-      "requires": {
-        "@vue/component-compiler-utils": "^3.1.0",
-        "hash-sum": "^1.0.2",
-        "loader-utils": "^1.1.0",
-        "vue-hot-reload-api": "^2.3.0",
-        "vue-style-loader": "^4.1.0"
-      },
-      "dependencies": {
-        "hash-sum": {
-          "version": "1.0.2",
-          "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz",
-          "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
-          "dev": true
-        }
-      }
-    },
     "@vue/web-component-wrapper": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz",
@@ -23141,9 +23143,9 @@
       }
     },
     "axios": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
-      "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
+      "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
       "requires": {
         "follow-redirects": "^1.15.0",
         "form-data": "^4.0.0",
diff --git a/package.json b/package.json
index 402e57505..38b1e9630 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
     "@sentry/integrations": "^7.16.0",
     "@sentry/vue": "^7.16.0",
     "ansi-to-html": "^0.7.2",
-    "axios": "^1.1.3",
+    "axios": "^1.4.0",
     "bulma": "^0.9.3",
     "bulma-switch": "^2.0.0",
     "bulma-tooltip": "^3.0.2",
diff --git a/src/api/model.ts b/src/api/model.ts
index 4f422878e..3d4fb3282 100644
--- a/src/api/model.ts
+++ b/src/api/model.ts
@@ -1,13 +1,13 @@
 import axios from 'axios'
-import { ModelVersion, PageNumberPagination, UUID } from '@/types'
-import { Model } from '@/types/model'
+import { PageNumberPagination, UUID } from '@/types'
+import { Model, ModelVersion } from '@/types/model'
 import { PageNumberPaginationParameters, unique } from '.'
 
-type CreateModelPayload = Pick<Model, 'name' | 'description'>
+export type CreateModelPayload = Pick<Model, 'name'> & Partial<Pick<Model, 'description'>>
 
 export const createModel = unique(async (params: CreateModelPayload): Promise<Model> => (await axios.post('/models/', params)).data)
 
-interface ModelListParameters extends PageNumberPaginationParameters {
+export interface ModelListParameters extends PageNumberPaginationParameters {
   /**
    * Restrict to models declared as compatible with a worker ID
    */
@@ -19,11 +19,11 @@ interface ModelListParameters extends PageNumberPaginationParameters {
   name?: string
 }
 
-export const listModels = unique(async (params: ModelListParameters): Promise<PageNumberPagination<Model[]>> => (await axios.get('/models/', { params })).data)
+export const listModels = unique(async (params: ModelListParameters): Promise<PageNumberPagination<Model>> => (await axios.get('/models/', { params })).data)
 
 export const retrieveModel = unique(async (id: UUID): Promise<Model> => (await axios.get(`/model/${id}/`)).data)
 
-export const listModelVersions = unique(async (modelId: UUID, params: PageNumberPaginationParameters): Promise<ModelVersion[]> => (await axios.get(`/model/${modelId}/versions/`, { params })).data)
+export const listModelVersions = unique(async (modelId: UUID, params: PageNumberPaginationParameters): Promise<PageNumberPagination<ModelVersion>> => (await axios.get(`/model/${modelId}/versions/`, { params })).data)
 
 export const deleteModelVersion = unique(async (id: UUID) => (await axios.delete(`/modelversion/${id}/`)))
 
diff --git a/src/components/Model/ModelPicker.vue b/src/components/Model/ModelPicker.vue
index 36e8b22cf..a0da34592 100644
--- a/src/components/Model/ModelPicker.vue
+++ b/src/components/Model/ModelPicker.vue
@@ -106,18 +106,22 @@
   </div>
 </template>
 
-<script>
-import { mapActions, mapMutations } from 'vuex'
-import Paginator from '@/components/Paginator.vue'
+<script lang="ts">
+import { defineComponent, PropType } from 'vue'
+import { mapActions } from 'pinia'
+
+import { ModelListParameters } from '@/api'
 import { errorParser } from '@/helpers'
-import Modal from '@/components/Modal'
-import CreateModel from '@/views/Model/Create.vue'
 import { truncateMixin } from '@/mixins'
+import { useModelStore, useNotificationStore } from '@/stores'
+import { Model } from '@/types/model'
+import { PageNumberPagination, UUID } from '@/types'
+
+import Paginator from '@/components/Paginator.vue'
+import Modal from '@/components/Modal.vue'
+import CreateModel from '@/views/Model/Create.vue'
 
-/*
- * A component allowing to pick a specific model
- */
-export default {
+export default defineComponent({
   mixins: [
     truncateMixin
   ],
@@ -126,10 +130,14 @@ export default {
     Modal,
     CreateModel
   },
-  emits: ['update:modelValue'],
+  emits: {
+    'update:modelValue' (value: Model | null) {
+      return value === null || value.id !== undefined
+    }
+  },
   props: {
     modelValue: {
-      type: Object,
+      type: Object as PropType<Model | null>,
       default: null
     },
     placeholder: {
@@ -137,7 +145,7 @@ export default {
       default: 'Pick a model…'
     },
     compatibleWorkerId: {
-      type: String,
+      type: String as PropType<UUID>,
       default: ''
     }
   },
@@ -145,18 +153,18 @@ export default {
     opened: false,
     loading: false,
     page: 1,
-    modelsPage: null,
+    modelsPage: null as PageNumberPagination<Model> | null,
     nameFilter: '',
     creationModal: false,
     allModels: false
   }),
   methods: {
-    ...mapMutations('notifications', ['notify']),
-    ...mapActions('model', ['listModels']),
+    ...mapActions(useNotificationStore, ['notify']),
+    ...mapActions(useModelStore, ['listModels']),
     async updateModelsPage () {
       this.loading = true
       try {
-        const payload = { page: this.page }
+        const payload: ModelListParameters = { page: this.page }
         if (this.nameFilter) payload.name = this.nameFilter
         if (this.compatibleWorkerId && !this.allModels) payload.compatible_worker = this.compatibleWorkerId
         this.modelsPage = await this.listModels(payload)
@@ -171,7 +179,7 @@ export default {
       if (this.page === 1) return this.updateModelsPage()
       this.page = 1
     },
-    selectModel (model) {
+    selectModel (model: Model) {
       this.$emit('update:modelValue', model)
       // Automatically close modals upon selection or creation
       this.opened = false
@@ -192,7 +200,7 @@ export default {
       handler: 'updateModelsPage'
     }
   }
-}
+})
 </script>
 
 <style scoped lang="scss">
diff --git a/src/components/Model/Selection.vue b/src/components/Model/Selection.vue
index 43a210428..dc7401dd8 100644
--- a/src/components/Model/Selection.vue
+++ b/src/components/Model/Selection.vue
@@ -33,9 +33,11 @@
 </template>
 
 <script>
-import { mapState, mapActions } from 'vuex'
+import { mapState as mapVuexState } from 'vuex'
+import { mapState, mapActions } from 'pinia'
 import Modal from '@/components/Modal.vue'
 import ModelList from '@/views/Model/List'
+import { useModelStore } from '@/stores'
 
 export default {
   components: {
@@ -68,8 +70,8 @@ export default {
     openModal: false
   }),
   computed: {
-    ...mapState('process', ['processWorkerRuns']),
-    ...mapState('model', ['models', 'modelVersions']),
+    ...mapVuexState('process', ['processWorkerRuns']),
+    ...mapState(useModelStore, ['models', 'modelVersions']),
     needModel () {
       return this.modelNeeded && !this.selectedModelVersionId
     },
@@ -99,7 +101,7 @@ export default {
     }
   },
   methods: {
-    ...mapActions('model', ['getModelVersion', 'retrieveModel'])
+    ...mapActions(useModelStore, ['getModelVersion', 'retrieveModel'])
   },
   watch: {
     selectedModelVersionId: {
diff --git a/src/components/Model/Versions/DeleteModal.vue b/src/components/Model/Versions/DeleteModal.vue
index 029bbf999..7f09e6ad6 100644
--- a/src/components/Model/Versions/DeleteModal.vue
+++ b/src/components/Model/Versions/DeleteModal.vue
@@ -2,7 +2,7 @@
   <span>
     <button
       class="button is-danger is-small"
-      :disabled="!canDelete || null"
+      :disabled="!canDelete || undefined"
       v-on:click.prevent="open"
       :title="canDelete ? 'Delete this model version' : 'You are not allowed to delete this version.'"
     >
@@ -23,7 +23,7 @@
         <button
           class="button is-danger"
           :class="{ 'is-loading': loading }"
-          :disabled="loading || !canDelete || null"
+          :disabled="loading || !canDelete || undefined"
           v-on:click.prevent="performDelete"
         >
           Delete
@@ -33,18 +33,21 @@
   </span>
 </template>
 
-<script>
+<script lang="ts">
+import { PropType, defineComponent } from 'vue'
 import Modal from '@/components/Modal.vue'
-import { mapState, mapActions, mapMutations } from 'vuex'
+import { mapState, mapActions } from 'pinia'
+import { useModelStore, useNotificationStore } from '@/stores'
+import { ModelVersion } from '@/types/model'
 
-export default {
+export default defineComponent({
   components: {
     Modal
   },
   props: {
     // The model version to delete.
     version: {
-      type: Object,
+      type: Object as PropType<ModelVersion>,
       required: true
     }
   },
@@ -53,7 +56,7 @@ export default {
     loading: false
   }),
   computed: {
-    ...mapState('model', ['models']),
+    ...mapState(useModelStore, ['models']),
     modelName () {
       return this.models[this.version.model_id].name
     },
@@ -62,8 +65,8 @@ export default {
     }
   },
   methods: {
-    ...mapActions('model', ['deleteModelVersion']),
-    ...mapMutations('notifications', ['notify']),
+    ...mapActions(useModelStore, ['deleteModelVersion']),
+    ...mapActions(useNotificationStore, ['notify']),
     open () {
       this.opened = this.canDelete
     },
@@ -71,12 +74,12 @@ export default {
       if (!this.canDelete || this.loading) return
       this.loading = true
       try {
-        await this.deleteModelVersion({ versionId: this.version.id })
+        await this.deleteModelVersion(this.version.id)
         this.opened = false
       } finally {
         this.loading = false
       }
     }
   }
-}
+})
 </script>
diff --git a/src/components/Model/Versions/List.vue b/src/components/Model/Versions/List.vue
index 1d32515a3..046ae1bce 100644
--- a/src/components/Model/Versions/List.vue
+++ b/src/components/Model/Versions/List.vue
@@ -34,13 +34,18 @@
   </div>
 </template>
 
-<script>
-import { mapActions, mapMutations, mapState } from 'vuex'
+<script lang="ts">
+import { defineComponent } from 'vue'
+import { mapActions, mapState } from 'pinia'
 import { ensureArray, errorParser } from '@/helpers'
 import Paginator from '@/components/Paginator.vue'
-import Row from './Row'
+import Row from './Row.vue'
+import { useModelStore } from '@/stores/model'
+import { useNotificationStore } from '@/stores'
+import { ModelVersion } from '@/types/model'
+import { PageNumberPagination } from '@/types'
 
-export default {
+export default defineComponent({
   components: {
     Paginator,
     Row
@@ -61,30 +66,26 @@ export default {
     }
   },
   data: () => ({
-    versionsError: null,
-    versionsPage: null,
+    versionsError: null as string | null,
+    versionsPage: null as PageNumberPagination<ModelVersion> | null,
     loading: false,
     page: 1
   }),
   computed: {
-    ...mapState('model', ['modelVersions']),
+    ...mapState(useModelStore, ['modelVersions']),
     modelVersionCount () {
       return ensureArray(this.modelVersions).filter(version => version.model_id === this.modelId).length
     }
   },
   methods: {
-    ...mapMutations('notifications', ['notify']),
-    ...mapActions('model', ['listModelVersions']),
+    ...mapActions(useNotificationStore, ['notify']),
+    ...mapActions(useModelStore, ['listModelVersions']),
     async fetchVersions () {
       this.versionsPage = null
       this.versionsError = null
       this.loading = true
-      const payload = {
-        modelId: this.modelId,
-        page: this.page
-      }
       try {
-        this.versionsPage = await this.listModelVersions(payload)
+        this.versionsPage = await this.listModelVersions(this.modelId, this.page)
       } catch (err) {
         this.versionsError = errorParser(err)
         this.notify({ type: 'error', text: `An error occurred listing model versions: ${this.versionsError}` })
@@ -104,5 +105,5 @@ export default {
       }
     }
   }
-}
+})
 </script>
diff --git a/src/components/Model/Versions/ModelVersionPicker.vue b/src/components/Model/Versions/ModelVersionPicker.vue
index c6a4c6555..f72f46243 100644
--- a/src/components/Model/Versions/ModelVersionPicker.vue
+++ b/src/components/Model/Versions/ModelVersionPicker.vue
@@ -45,31 +45,37 @@
   </div>
 </template>
 
-<script>
-import { mapActions, mapMutations } from 'vuex'
-import Row from './Row'
-import Paginator from '@/components/Paginator.vue'
+<script lang="ts">
+import { defineComponent, PropType } from 'vue'
+import { mapActions } from 'pinia'
+
 import { errorParser } from '@/helpers'
-import Modal from '@/components/Modal'
+import { useModelStore, useNotificationStore } from '@/stores'
+import { PageNumberPagination, UUID } from '@/types'
+import { ModelVersion } from '@/types/model'
+
+import Paginator from '@/components/Paginator.vue'
+import Modal from '@/components/Modal.vue'
+import Row from './Row.vue'
 
-/*
- * A component allowing to pick a specific model
- */
-export default {
+export default defineComponent({
   components: {
     Row,
     Paginator,
     Modal
   },
-  emits: ['update:modelValue'],
+  emits: {
+    'update:modelValue' (value: ModelVersion | null) {
+      return value === null || value.id !== undefined
+    }
+  },
   props: {
     modelId: {
       type: String,
       required: true
     },
-    // The worker version object
     modelValue: {
-      type: Object,
+      type: Object as PropType<ModelVersion | null>,
       default: null
     },
     placeholder: {
@@ -81,9 +87,7 @@ export default {
     opened: false,
     loading: false,
     page: 1,
-    versionsPage: null,
-    nameFilter: '',
-    selectedVersion: null
+    versionsPage: null as PageNumberPagination<ModelVersion> | null
   }),
   computed: {
     shortId () {
@@ -91,26 +95,26 @@ export default {
     }
   },
   methods: {
-    ...mapMutations('notifications', ['notify']),
-    ...mapActions('model', ['listModelVersions']),
+    ...mapActions(useNotificationStore, ['notify']),
+    ...mapActions(useModelStore, ['listModelVersions']),
     async updateVersionsPage () {
       this.loading = true
       try {
-        this.versionsPage = await this.listModelVersions({ modelId: this.modelId, page: this.page })
+        this.versionsPage = await this.listModelVersions(this.modelId, this.page)
       } catch (err) {
         this.notify({ type: 'error', text: `An error occurred listing model versions: ${errorParser(err)}` })
       } finally {
         this.loading = false
       }
     },
-    selectVersion (version) {
+    selectVersion (version: ModelVersion) {
       this.$emit('update:modelValue', version)
     }
   },
   watch: {
     modelId: {
       immediate: true,
-      handler (newValue) {
+      handler (newValue: UUID) {
         // The ModelVersion gets deselected if the new model ID does not match it
         if (this.modelValue?.model_id !== newValue) this.$emit('update:modelValue', null)
         this.updateVersionsPage()
@@ -120,5 +124,5 @@ export default {
       handler: 'updateVersionsPage'
     }
   }
-}
+})
 </script>
diff --git a/src/components/Model/Versions/Row.vue b/src/components/Model/Versions/Row.vue
index 10af370f5..10853dc91 100644
--- a/src/components/Model/Versions/Row.vue
+++ b/src/components/Model/Versions/Row.vue
@@ -50,9 +50,16 @@
 </template>
 
 <script>
-import { mapState, mapActions, mapMutations } from 'vuex'
+import {
+  mapState as mapVuexState,
+  mapActions as mapVuexActions
+} from 'vuex'
+import { mapState, mapActions } from 'pinia'
+
 import { MODEL_VERSION_STATE_COLORS } from '@/config'
 import { ago } from '@/helpers/text'
+import { useModelStore, useNotificationStore } from '@/stores'
+
 import ItemId from '@/components/ItemId.vue'
 import DeleteModal from './DeleteModal'
 
@@ -96,8 +103,8 @@ export default {
     loading: false
   }),
   computed: {
-    ...mapState('model', ['models']),
-    ...mapState('process', ['processWorkerRuns']),
+    ...mapState(useModelStore, ['models']),
+    ...mapVuexState('process', ['processWorkerRuns']),
     createdDate () {
       if (!this.version) return
       return new Date(this.version.created)
@@ -128,8 +135,8 @@ export default {
     }
   },
   methods: {
-    ...mapMutations('notifications', ['notify']),
-    ...mapActions('process', ['updateWorkerRun']),
+    ...mapActions(useNotificationStore, ['notify']),
+    ...mapVuexActions('process', ['updateWorkerRun']),
     async copyId () {
       if (!this.version.id) return
       try {
diff --git a/src/helpers/index.ts b/src/helpers/index.ts
index 36e521d18..db75a7483 100644
--- a/src/helpers/index.ts
+++ b/src/helpers/index.ts
@@ -65,7 +65,7 @@ export function removeEmptyStrings<T extends string> (obj: Record<string, T> = {
  * Falsy values (false, NaN, undefined, null, '', 0, …) will return [].
  * Objects become an array of their values, and other primitives become [value].
  */
-export function ensureArray<T> (value: T | ArrayLike<T>): T[] {
+export function ensureArray<T> (value: T | ArrayLike<T> | { [key: string | number | symbol]: T }): T[] {
   if (!value) return []
   if (Array.isArray(value)) return value
   if (typeof value === 'object') return Object.values(value)
diff --git a/src/store/index.js b/src/store/index.js
index ad64f75a3..023bea41e 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -5,7 +5,8 @@ import {
   useImageStore,
   useIngestStore,
   useJobsStore,
-  useSearchStore
+  useSearchStore,
+  useModelStore
 } from '@/stores'
 
 /**
@@ -20,7 +21,6 @@ const moduleNames = [
   'elements',
   'entity',
   'files',
-  'model',
   'navigation',
   'notifications',
   'oauth',
@@ -41,7 +41,10 @@ export const piniaStores = [
   useImageStore,
   useIngestStore,
   useJobsStore,
-  useSearchStore
+  useSearchStore,
+  useFolderPickerStore,
+  useJobsStore,
+  useModelStore
 ]
 
 export const actions = {
diff --git a/src/store/model.js b/src/store/model.js
deleted file mode 100644
index 0ed90d91d..000000000
--- a/src/store/model.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { assign } from 'lodash'
-import * as api from '@/api'
-import { errorParser } from '@/helpers'
-
-export const initialState = () => ({
-  /*
-   * Stores ML models and their versions.
-   * { [modelId]: model }
-   */
-  models: {},
-  // { [modelVersionId]: modelVersion }
-  modelVersions: {}
-})
-
-export const mutations = {
-  setModels (state, models) {
-    state.models = {
-      ...state.models,
-      ...Object.fromEntries(models.map(model => [model.id, model]))
-    }
-  },
-
-  setModelVersions (state, versions) {
-    state.modelVersions = {
-      ...state.modelVersions,
-      ...Object.fromEntries(versions.map(v => [v.id, v]))
-    }
-  },
-
-  removeModelVersion (state, versionId) {
-    const newModelVersions = { ...state.modelVersions }
-    delete newModelVersions[versionId]
-    state.modelVersions = newModelVersions
-  },
-
-  reset (state) {
-    assign(state, initialState())
-  }
-}
-
-export const actions = {
-  async createModel ({ commit }, params) {
-    const resp = await api.createModel(params)
-    commit('setModels', [resp])
-    return resp
-  },
-
-  async listModels ({ commit }, params) {
-    const resp = await api.listModels(params)
-    commit('setModels', resp.results)
-    return resp
-  },
-
-  async listModelVersions ({ commit }, { modelId, page = 1 }) {
-    const resp = await api.listModelVersions(modelId, { page })
-    commit('setModelVersions', resp.results)
-    return resp
-  },
-
-  async retrieveModel ({ commit }, modelId) {
-    const resp = await api.retrieveModel(modelId)
-    commit('setModels', [resp])
-  },
-
-  async getModelVersion ({ commit }, modelVersionId) {
-    const resp = await api.retrieveModelVersion(modelVersionId)
-    commit('setModelVersions', [resp])
-  },
-
-  async deleteModelVersion ({ commit }, { versionId }) {
-    try {
-      await api.deleteModelVersion(versionId)
-      commit('removeModelVersion', versionId)
-    } catch (err) {
-      commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true })
-    }
-  }
-}
-
-export default {
-  namespaced: true,
-  state: initialState(),
-  mutations,
-  actions
-}
diff --git a/src/stores/index.ts b/src/stores/index.ts
index e666647a0..4680fcb24 100644
--- a/src/stores/index.ts
+++ b/src/stores/index.ts
@@ -3,5 +3,6 @@ export { useFolderPickerStore } from './folderpicker'
 export { useImageStore } from './image'
 export { useIngestStore } from './ingest'
 export { useJobsStore } from './jobs'
+export { useModelStore } from './model'
 export { useNotificationStore } from './notification'
 export { useSearchStore } from './search'
diff --git a/src/stores/model.ts b/src/stores/model.ts
new file mode 100644
index 000000000..e38c9ba63
--- /dev/null
+++ b/src/stores/model.ts
@@ -0,0 +1,72 @@
+import { defineStore } from 'pinia'
+import { errorParser } from '@/helpers'
+import { UUID } from '@/types'
+import { Model, ModelVersion } from '@/types/model'
+import {
+  CreateModelPayload,
+  ModelListParameters,
+  createModel,
+  listModels,
+  listModelVersions,
+  retrieveModel,
+  retrieveModelVersion,
+  deleteModelVersion
+} from '@/api'
+import { useNotificationStore } from '.'
+
+interface State {
+  models: { [id: UUID]: Model }
+  modelVersions: { [id: UUID]: ModelVersion }
+}
+
+export const useModelStore = defineStore('model', {
+  state: (): State => ({
+    models: {},
+    modelVersions: {}
+  }),
+  actions: {
+    async createModel (params: CreateModelPayload) {
+      const model = await createModel(params)
+      this.models[model.id] = model
+      return model
+    },
+
+    async listModels (params: ModelListParameters) {
+      const resp = await listModels(params)
+      this.models = {
+        ...this.models,
+        ...Object.fromEntries(resp.results.map(model => [model.id, model]))
+      }
+      return resp
+    },
+
+    async listModelVersions (modelId: UUID, page = 1) {
+      const resp = await listModelVersions(modelId, { page })
+      this.modelVersions = {
+        ...this.modelVersions,
+        ...Object.fromEntries(resp.results.map(modelVersion => [modelVersion.id, modelVersion]))
+      }
+      return resp
+    },
+
+    async retrieveModel (modelId: UUID) {
+      const model = await retrieveModel(modelId)
+      this.models[model.id] = model
+    },
+
+    async getModelVersion (modelVersionId: UUID) {
+      const modelVersion = await retrieveModelVersion(modelVersionId)
+      this.modelVersions[modelVersion.id] = modelVersion
+    },
+
+    async deleteModelVersion (versionId: UUID) {
+      try {
+        await deleteModelVersion(versionId)
+        delete this.modelVersions[versionId]
+      } catch (err) {
+        const notificationStore = useNotificationStore()
+        notificationStore.notify({ type: 'error', text: errorParser(err) })
+      }
+    }
+  }
+})
\ No newline at end of file
diff --git a/src/types/index.ts b/src/types/index.ts
index 7873b2a5c..495204901 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,5 +1,6 @@
 import { METADATA_TYPES, PROCESS_STATES, PROCESS_MODES, DATASET_STATES, CLASSIFICATION_STATES } from '@/config'
 import { S3FileStatus, GitRefType, WorkerVersionGPUUsage, WorkerVersionState, ProcessActivityState, ModelVersionState } from '@/enums'
+import { ModelVersionLight } from './model'
 
 /**
  * A universally unique identifier, using the format
@@ -339,25 +340,13 @@ export interface WorkerConfiguration {
   archived: boolean
 }
 
-export interface ModelVersion {
-  id: UUID
-  model: {
-    id: UUID
-    name: string
-  }
-  tag: string | null
-  state: ModelVersionState
-  size: number
-  configuration: object
-}
-
 export interface WorkerRun {
   id: UUID
   parents: UUID[]
   worker_version: WorkerVersion
   process: TrainingProcess
   configuration: WorkerConfiguration | null
-  model_version: ModelVersion | null
+  model_version: ModelVersionLight | null
 }
 
 export interface Dataset {
diff --git a/src/types/model.ts b/src/types/model.ts
index 54e011461..2939c7874 100644
--- a/src/types/model.ts
+++ b/src/types/model.ts
@@ -1,6 +1,20 @@
 import { ModelVersionState } from '@/enums'
 import { Right, UUID } from '.'
 
+export interface ModelLight {
+  id: UUID
+  name: string
+}
+
+export interface ModelVersionLight {
+  id: UUID
+  model: ModelLight
+  tag: string | null
+  state: ModelVersionState
+  size: number
+  configuration: object
+}
+
 export interface Model {
   id: UUID
   name: string
diff --git a/src/views/Model/Create.vue b/src/views/Model/Create.vue
index 7c70e60d1..f921f6463 100644
--- a/src/views/Model/Create.vue
+++ b/src/views/Model/Create.vue
@@ -13,9 +13,9 @@
         <div class="control">
           <input
             class="input is-fullwidth"
-            :disabled="loading || null"
+            :disabled="loading || undefined"
             type="text"
-            v-model="fields.name"
+            v-model.trim="fields.name"
           />
         </div>
         <p v-if="errors.name" class="help is-danger">{{ errors.name }}</p>
@@ -26,9 +26,9 @@
         <div class="control">
           <input
             class="input is-fullwidth"
-            :disabled="loading || null"
+            :disabled="loading || undefined"
             type="text"
-            v-model="fields.description"
+            v-model.trim="fields.description"
           />
         </div>
         <p v-if="errors.description" class="help is-danger">{{ errors.description }}</p>
@@ -40,7 +40,7 @@
             type="submit"
             class="button is-primary is-fullwidth"
             v-on:click="create"
-            :disabled="!allowCreate || null"
+            :disabled="!allowCreate || undefined"
             :title="allowCreate ? 'Create a new model' : 'A name is required to create a new model'"
           >
             <i v-if="loading" class="loader"></i>
@@ -52,15 +52,23 @@
   </main>
 </template>
 
-<script>
-import { mapMutations } from 'vuex'
-import { corporaMixin } from '@/mixins.js'
+<script lang="ts">
+import { isAxiosError } from 'axios'
+import { defineComponent } from 'vue'
+import { mapActions } from 'pinia'
+
+import { CreateModelPayload } from '@/api'
+import { corporaMixin } from '@/mixins'
 import { errorParser } from '@/helpers'
+import { useModelStore, useNotificationStore } from '@/stores'
+import { Model } from '@/types/model'
 
-export default {
-  emits: [
-    'create-model'
-  ],
+export default defineComponent({
+  emits: {
+    'create-model' (value: Model) {
+      return value.id !== undefined
+    }
+  },
   mixins: [
     corporaMixin
   ],
@@ -69,8 +77,8 @@ export default {
     fields: {
       name: '',
       description: ''
-    },
-    errors: {}
+    } as CreateModelPayload,
+    errors: {} as Record<string, unknown>
   }),
   computed: {
     mainView () {
@@ -78,29 +86,29 @@ export default {
       return this.$route.name === 'model-create'
     },
     allowCreate () {
-      return this.fields.name && !this.loading
+      return Boolean(this.fields.name) && !this.loading
     }
   },
   methods: {
-    ...mapMutations('notifications', ['notify']),
+    ...mapActions(useNotificationStore, ['notify']),
     async create () {
       if (!this.allowCreate) return
       this.loading = true
       this.errors = {}
       try {
-        const payload = { name: this.fields.name }
-        if (this.fields.description.trim()) payload.description = this.fields.description
-        const resp = await this.$store.dispatch('model/createModel', payload)
+        const payload: CreateModelPayload = { name: this.fields.name }
+        if (this.fields.description?.trim()) payload.description = this.fields.description
+        const model = await useModelStore().createModel(payload)
         this.notify({ type: 'success', text: `Model "${this.fields.name}" created successfully.` })
-        this.$emit('create-model', resp)
+        this.$emit('create-model', model)
         this.fields.name = this.fields.description = ''
       } catch (err) {
-        if (err?.response?.data) this.errors = err.response.data
+        if (isAxiosError(err) && err.response?.data) this.errors = err.response.data
         this.notify({ type: 'error', text: errorParser(err) })
       } finally {
         this.loading = false
       }
     }
   }
-}
+})
 </script>
diff --git a/src/views/Model/List.vue b/src/views/Model/List.vue
index 9fab55b06..80e8f4ead 100644
--- a/src/views/Model/List.vue
+++ b/src/views/Model/List.vue
@@ -91,28 +91,35 @@
   </main>
 </template>
 
-<script>
-import { mapActions, mapMutations } from 'vuex'
-import Model from '@/components/Model'
-import Paginator from '@/components/Paginator.vue'
+<script lang="ts">
+import { defineComponent } from 'vue'
+import { mapActions } from 'pinia'
+
+import { ModelListParameters } from '@/api'
 import { errorParser } from '@/helpers'
 import { truncateMixin } from '@/mixins'
+import { useModelStore, useNotificationStore } from '@/stores'
+import { Model } from '@/types/model'
+
+import Paginator from '@/components/Paginator.vue'
+import ModelComponent from '@/components/Model/Model.vue'
+import { PageNumberPagination } from '@/types'
 
-export default {
+export default defineComponent({
   mixins: [
     truncateMixin
   ],
   components: {
     Paginator,
-    Model
+    Model: ModelComponent
   },
   props: {
+    /**
+     * If a process ID is defined, this component allows to select a model
+     * and add one of its versions to a WorkerRun.
+     * Otherwise, list models with their versions and members.
+     */
     processId: {
-      /**
-       * If a process ID is defined, this component allows to select a model
-       * and add one of its versions to a WorkerRun.
-       * Otherwise, list models with their versions and members.
-       */
       type: String,
       default: ''
     },
@@ -141,7 +148,7 @@ export default {
   },
   data: () => ({
     loading: false,
-    modelsPage: null,
+    modelsPage: null as PageNumberPagination<Model> | null,
     // ID of the selected model
     selectedModel: null,
     page: 1,
@@ -149,13 +156,13 @@ export default {
     allModels: false
   }),
   methods: {
-    ...mapMutations('notifications', ['notify']),
-    ...mapActions('model', ['listModels']),
+    ...mapActions(useNotificationStore, ['notify']),
+    ...mapActions(useModelStore, ['listModels']),
     async updateModelsPage () {
       this.loading = true
       try {
         this.selectedModel = null
-        const payload = { page: this.page }
+        const payload: ModelListParameters = { page: this.page }
         if (this.nameFilter) payload.name = this.nameFilter
         if (this.compatibleWorkerId && !this.allModels) payload.compatible_worker = this.compatibleWorkerId
         this.modelsPage = await this.listModels(payload)
@@ -181,7 +188,7 @@ export default {
       handler: 'updateModelsPage'
     }
   }
-}
+})
 </script>
 
 <style lang="scss" scoped>
diff --git a/src/views/Model/Model.vue b/src/views/Model/Model.vue
index cb05fc7f8..d5ed328d4 100644
--- a/src/views/Model/Model.vue
+++ b/src/views/Model/Model.vue
@@ -46,11 +46,12 @@
 
 <script lang="ts">
 import { defineComponent } from 'vue'
-import { mapState, mapMutations, mapActions } from 'vuex'
+import { mapState, mapActions } from 'pinia'
 import { UUID_REGEX } from '@/config'
 import { ago, errorParser } from '@/helpers'
 import { UUID } from '@/types'
 import Model from '@/components/Model'
+import { useModelStore, useNotificationStore } from '@/stores'
 
 export default defineComponent({
   props: {
@@ -68,22 +69,22 @@ export default defineComponent({
     error: null as string | null
   }),
   computed: {
-    ...mapState('model', ['models']),
+    ...mapState(useModelStore, ['models']),
     model () {
       return this.models[this.modelId]
     },
-    creationDate () {
-      if (!this.model) return
+    creationDate (): string | null {
+      if (!this.model) return null
       return ago(new Date(this.model.created))
     },
-    updateDate () {
-      if (!this.model) return
+    updateDate (): string | null {
+      if (!this.model) return null
       return ago(new Date(this.model.updated))
     }
   },
   methods: {
-    ...mapMutations('notifications', ['notify']),
-    ...mapActions('model', ['retrieveModel']),
+    ...mapActions(useNotificationStore, ['notify']),
+    ...mapActions(useModelStore, ['retrieveModel']),
     async loadModel (modelId: UUID) {
       this.loading = true
       try {
diff --git a/src/views/Model/Version.vue b/src/views/Model/Version.vue
index a46b5a071..d07cf9498 100644
--- a/src/views/Model/Version.vue
+++ b/src/views/Model/Version.vue
@@ -48,14 +48,14 @@
       <div class="field">
         <label class="label">Size</label>
         <div class="control">
-          <p :title="version.size">{{ size }}</p>
+          <p :title="version.size.toString()">{{ size }}</p>
         </div>
       </div>
 
       <div class="field">
         <label class="label">Configuration</label>
         <div class="control">
-          <pre v-if="model.configuration">{{ model.configuration }}</pre>
+          <pre v-if="version.configuration">{{ version.configuration }}</pre>
           <template v-else>—</template>
         </div>
       </div>
@@ -67,11 +67,14 @@
 
 <script lang="ts">
 import { defineComponent } from 'vue'
-import { mapActions, mapState } from 'vuex'
+import { mapActions, mapState } from 'pinia'
 import { MODEL_VERSION_STATE_COLORS, UUID_REGEX } from '@/config'
 import { errorParser, formatBytes } from '@/helpers'
 import { truncateMixin } from '@/mixins'
 import ItemId from '@/components/ItemId.vue'
+import { useModelStore } from '@/stores'
+import { UUID } from '@/types'
+import { ModelVersion } from '@/types/model'
 
 export default defineComponent({
   mixins: [
@@ -91,7 +94,7 @@ export default defineComponent({
     error: null as string | null
   }),
   computed: {
-    ...mapState('model', ['models', 'modelVersions']),
+    ...mapState(useModelStore, ['models', 'modelVersions']),
     model () {
       if (!this.version) return null
       return this.models[this.version.model_id]
@@ -105,17 +108,16 @@ export default defineComponent({
     },
     stateClass () {
       if (!this.version) return null
-      // @ts-expect-error Requires migrating the models store to Pinia to get a ModelVersion type
       return MODEL_VERSION_STATE_COLORS[this.version.state]
     }
   },
   methods: {
-    ...mapActions('model', ['getModelVersion', 'retrieveModel'])
+    ...mapActions(useModelStore, ['getModelVersion', 'retrieveModel'])
   },
   watch: {
     versionId: {
       immediate: true,
-      async handler (newValue) {
+      async handler (newValue: UUID) {
         try {
           await this.getModelVersion(newValue)
         } catch (err) {
@@ -125,7 +127,7 @@ export default defineComponent({
     },
     version: {
       immediate: true,
-      async handler (newValue) {
+      async handler (newValue: ModelVersion) {
         if (!newValue?.model_id) return
         try {
           await this.retrieveModel(newValue.model_id)
diff --git a/tests/unit/store/auth.spec.js b/tests/unit/store/auth.spec.js
index 1b1eca3d3..2399196e8 100644
--- a/tests/unit/store/auth.spec.js
+++ b/tests/unit/store/auth.spec.js
@@ -118,7 +118,6 @@ describe('auth', () => {
           { mutation: 'elements/reset' },
           { mutation: 'entity/reset' },
           { mutation: 'files/reset' },
-          { mutation: 'model/reset' },
           { mutation: 'navigation/reset' },
           { mutation: 'notifications/reset' },
           { mutation: 'oauth/reset' },
@@ -154,7 +153,6 @@ describe('auth', () => {
           { mutation: 'elements/reset' },
           { mutation: 'entity/reset' },
           { mutation: 'files/reset' },
-          { mutation: 'model/reset' },
           { mutation: 'navigation/reset' },
           { mutation: 'notifications/reset' },
           { mutation: 'oauth/reset' },
@@ -188,7 +186,6 @@ describe('auth', () => {
           { mutation: 'elements/reset' },
           { mutation: 'entity/reset' },
           { mutation: 'files/reset' },
-          { mutation: 'model/reset' },
           { mutation: 'navigation/reset' },
           { mutation: 'notifications/reset' },
           { mutation: 'oauth/reset' },
@@ -224,7 +221,6 @@ describe('auth', () => {
           { mutation: 'elements/reset' },
           { mutation: 'entity/reset' },
           { mutation: 'files/reset' },
-          { mutation: 'model/reset' },
           { mutation: 'navigation/reset' },
           { mutation: 'notifications/reset' },
           { mutation: 'oauth/reset' },
@@ -256,7 +252,6 @@ describe('auth', () => {
         { mutation: 'elements/reset' },
         { mutation: 'entity/reset' },
         { mutation: 'files/reset' },
-        { mutation: 'model/reset' },
         { mutation: 'navigation/reset' },
         { mutation: 'notifications/reset' },
         { mutation: 'oauth/reset' },
diff --git a/tests/unit/store/elements.spec.js b/tests/unit/store/elements.spec.js
index 0520bcedd..dcd5cef89 100644
--- a/tests/unit/store/elements.spec.js
+++ b/tests/unit/store/elements.spec.js
@@ -830,7 +830,8 @@ describe('elements', () => {
 
           await store.dispatch('elements/nextChildren', { id: 'element1', max: Infinity })
           assert.strictEqual(mock.history.get.length, 1)
-          assert.deepStrictEqual(mock.history.get[0].headers, {
+          assert.deepStrictEqual({ ...mock.history.get[0].headers }, {
+            Accept: 'application/json, text/plain, */*',
             'Cache-Control': 'no-cache',
             'If-Modified-Since': 'Wed, 21 Oct 2015 07:28:00 GMT'
           })
diff --git a/tests/unit/store/index.spec.js b/tests/unit/store/index.spec.js
index 539bec1c4..32a0dbba3 100644
--- a/tests/unit/store/index.spec.js
+++ b/tests/unit/store/index.spec.js
@@ -44,7 +44,6 @@ describe('store', () => {
         { mutation: 'elements/reset' },
         { mutation: 'entity/reset' },
         { mutation: 'files/reset' },
-        { mutation: 'model/reset' },
         { mutation: 'navigation/reset' },
         { mutation: 'notifications/reset' },
         { mutation: 'oauth/reset' },
@@ -65,7 +64,6 @@ describe('store', () => {
   require('./elements.spec.js')
   require('./entity.spec.js')
   require('./files.spec.js')
-  require('./model.spec.js')
   require('./navigation.spec.js')
   require('./oauth.spec.js')
   require('./ponos.spec.js')
diff --git a/tests/unit/store/model.spec.js b/tests/unit/store/model.spec.js
deleted file mode 100644
index e5140b20c..000000000
--- a/tests/unit/store/model.spec.js
+++ /dev/null
@@ -1,212 +0,0 @@
-import { assert } from 'chai'
-import axios from 'axios'
-import { pick } from 'lodash'
-import { mutations } from '@/store/model.js'
-import { modelsSample, modelVersionsSample } from '../samples.js'
-import store from './index.spec.js'
-import { FakeAxios } from '../testhelpers.js'
-
-describe('model', () => {
-  describe('mutations', () => {
-    describe('setModels', () => {
-      it('sets a Model', () => {
-        const state = {
-          models: { model_0: { id: 'model_0' } }
-        }
-        mutations.setModels(state, modelsSample.results)
-        assert.deepStrictEqual(state, {
-          models: {
-            model_0: { id: 'model_0' },
-            ...Object.fromEntries(modelsSample.results.map(model => [model.id, model]))
-          }
-        })
-      })
-    })
-
-    describe('setModelVersions', () => {
-      it('sets a Model Version', () => {
-        const state = {
-          modelVersions: { model_version_0: { id: 'model_version_0' } }
-        }
-        mutations.setModelVersions(state, modelVersionsSample.results)
-        assert.deepStrictEqual(state, {
-          modelVersions: {
-            model_version_0: { id: 'model_version_0' },
-            ...Object.fromEntries(modelVersionsSample.results.map(v => [v.id, v]))
-          }
-        })
-      })
-    })
-
-    describe('removeModelVersion', () => {
-      it('removes a Model Version', () => {
-        const state = {
-          modelVersions: { model_version_0: { id: 'model_version_0' } }
-        }
-        mutations.removeModelVersion(state, 'model_version_0')
-        assert.deepStrictEqual(state, {
-          modelVersions: {}
-        })
-      })
-    })
-  })
-
-  describe('actions', () => {
-    let mock
-
-    before('Setting up Axios mock', () => {
-      mock = new FakeAxios(axios)
-    })
-
-    afterEach(() => {
-      // Remove any handlers, but leave mocking in place
-      mock.reset()
-      store.reset()
-    })
-
-    after('Removing Axios mock', () => {
-      // Remove mocking entirely
-      mock.restore()
-    })
-
-    describe('createModel', () => {
-      it('creates a model', async () => {
-        store.state.model.models = { model_0: { id: 'model_0' } }
-        const attrs = { name: 'test', description: 'A' }
-        mock.onPost('/models/').reply(201, { id: 'model_1', ...attrs })
-
-        await store.dispatch('model/createModel', attrs)
-
-        assert.deepStrictEqual(store.history, [
-          { action: 'model/createModel', payload: attrs },
-          { mutation: 'model/setModels', payload: [{ id: 'model_1', ...attrs }] }
-        ])
-        assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url', 'data'])), [
-          {
-            method: 'post',
-            url: '/models/',
-            data: attrs
-          }
-        ])
-        assert.deepStrictEqual(store.state.model.models, {
-          model_0: { id: 'model_0' },
-          model_1: { id: 'model_1', ...attrs }
-        })
-      })
-    })
-
-    describe('listModels', () => {
-      it('lists stored models with filters', async () => {
-        const reply = {
-          count: 1,
-          results: [
-            { id: 'model1' }
-          ]
-        }
-        mock.onGet('/models/').reply(200, reply)
-        await store.dispatch('model/listModels', { name: '1' })
-        assert.deepStrictEqual(store.history, [
-          { action: 'model/listModels', payload: { name: '1' } },
-          { mutation: 'model/setModels', payload: reply.results }
-        ])
-        assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url', 'params'])), [
-          {
-            method: 'get',
-            url: '/models/',
-            params: { name: '1' }
-          }
-        ])
-        assert.deepStrictEqual(store.state.model.models, { model1: { id: 'model1' } })
-      })
-    })
-
-    describe('retrieveModel', () => {
-      it('retrieves a model', async () => {
-        const model = { id: 'model1' }
-        mock.onGet('/model/model1/').reply(200, model)
-
-        await store.dispatch('model/retrieveModel', 'model1')
-
-        assert.deepStrictEqual(store.history, [
-          {
-            action: 'model/retrieveModel',
-            payload: 'model1'
-          },
-          {
-            mutation: 'model/setModels',
-            payload: [model]
-          }
-        ])
-        assert.deepStrictEqual(store.state.model.models, {
-          model1: model
-        })
-      })
-    })
-
-    describe('listModelVersions', () => {
-      it('lists stored model versions', async () => {
-        const reply = {
-          count: 1,
-          results: [
-            { id: 'model_version1' }
-          ]
-        }
-        mock.onGet('/model/model1/versions/').reply(200, reply)
-        await store.dispatch('model/listModelVersions', { modelId: 'model1', page: '1' })
-        assert.deepStrictEqual(store.history, [
-          { action: 'model/listModelVersions', payload: { modelId: 'model1', page: '1' } },
-          { mutation: 'model/setModelVersions', payload: reply.results }
-        ])
-        assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url', 'params'])), [
-          {
-            method: 'get',
-            url: '/model/model1/versions/',
-            params: { page: '1' }
-          }])
-        assert.deepStrictEqual(store.state.model.modelVersions, { model_version1: { id: 'model_version1' } })
-      })
-    })
-
-    describe('getModelVersion', () => {
-      it('retrieves a model version', async () => {
-        const version = { id: 'model_version1' }
-        mock.onGet('/modelversion/model_version1/').reply(200, version)
-
-        await store.dispatch('model/getModelVersion', 'model_version1')
-
-        assert.deepStrictEqual(store.history, [
-          {
-            action: 'model/getModelVersion',
-            payload: 'model_version1'
-          },
-          {
-            mutation: 'model/setModelVersions',
-            payload: [version]
-          }
-        ])
-        assert.deepStrictEqual(store.state.model.modelVersions, {
-          model_version1: version
-        })
-      })
-    })
-
-    describe('deleteModelVersions', () => {
-      it('deletes a model version', async () => {
-        store.state.model.modelVersions = { model_version_0: { id: 'model_version_0' } }
-
-        mock.onDelete('/modelversion/model_version_0/').reply(204)
-        await store.dispatch('model/deleteModelVersion', { versionId: 'model_version_0' })
-        assert.deepStrictEqual(store.history, [
-          { action: 'model/deleteModelVersion', payload: { versionId: 'model_version_0' } },
-          { mutation: 'model/removeModelVersion', payload: 'model_version_0' }
-        ])
-        assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url'])), [
-          {
-            method: 'delete',
-            url: '/modelversion/model_version_0/'
-          }])
-        assert.deepStrictEqual(store.state.model.modelVersions, {})
-      })
-    })
-  })
-})
diff --git a/tests/unit/stores/model.spec.js b/tests/unit/stores/model.spec.js
new file mode 100644
index 000000000..052fffcc6
--- /dev/null
+++ b/tests/unit/stores/model.spec.js
@@ -0,0 +1,156 @@
+import axios from 'axios'
+import { assert } from 'chai'
+import { pick } from 'lodash'
+import { createPinia, setActivePinia } from 'pinia'
+import { useModelStore, useNotificationStore } from '@/stores'
+import { FakeAxios } from '../testhelpers'
+
+describe('model', () => {
+  describe('actions', () => {
+    let mock, store, notificationStore
+
+    before('Setting up Axios mock', () => {
+      mock = new FakeAxios(axios)
+      setActivePinia(createPinia())
+      store = useModelStore()
+      notificationStore = useNotificationStore()
+    })
+
+    afterEach(() => {
+      // Remove any handlers, but leave mocking in place
+      mock.reset()
+      store.$reset()
+      notificationStore.$reset()
+    })
+
+    after('Removing Axios mock', () => {
+      // Remove mocking entirely
+      mock.restore()
+    })
+
+    describe('createModel', () => {
+      it('creates a model', async () => {
+        store.models = { model_0: { id: 'model_0' } }
+        const attrs = { name: 'test', description: 'A' }
+        mock.onPost('/models/').reply(201, { id: 'model_1', ...attrs })
+
+        await store.createModel(attrs)
+
+        assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url', 'data'])), [
+          {
+            method: 'post',
+            url: '/models/',
+            data: attrs
+          }
+        ])
+        assert.deepStrictEqual(store.models, {
+          model_0: { id: 'model_0' },
+          model_1: { id: 'model_1', ...attrs }
+        })
+      })
+    })
+
+    describe('listModels', () => {
+      it('lists stored models with filters', async () => {
+        const reply = {
+          count: 1,
+          results: [
+            { id: 'model1' }
+          ]
+        }
+        mock.onGet('/models/').reply(200, reply)
+        await store.listModels({ name: '1' })
+        assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url', 'params'])), [
+          {
+            method: 'get',
+            url: '/models/',
+            params: { name: '1' }
+          }
+        ])
+        assert.deepStrictEqual(store.models, { model1: { id: 'model1' } })
+      })
+    })
+
+    describe('retrieveModel', () => {
+      it('retrieves a model', async () => {
+        const model = { id: 'model1' }
+        mock.onGet('/model/model1/').reply(200, model)
+
+        await store.retrieveModel('model1')
+
+        assert.deepStrictEqual(store.models, {
+          model1: model
+        })
+      })
+    })
+
+    describe('listModelVersions', () => {
+      it('lists stored model versions', async () => {
+        const reply = {
+          count: 1,
+          results: [
+            { id: 'model_version1' }
+          ]
+        }
+        mock.onGet('/model/model1/versions/').reply(200, reply)
+        await store.listModelVersions('model1', 1)
+        assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url', 'params'])), [
+          {
+            method: 'get',
+            url: '/model/model1/versions/',
+            params: { page: 1 }
+          }])
+        assert.deepStrictEqual(store.modelVersions, { model_version1: { id: 'model_version1' } })
+      })
+    })
+
+    describe('getModelVersion', () => {
+      it('retrieves a model version', async () => {
+        const version = { id: 'model_version1' }
+        mock.onGet('/modelversion/model_version1/').reply(200, version)
+
+        await store.getModelVersion('model_version1')
+
+        assert.deepStrictEqual(store.modelVersions, {
+          model_version1: version
+        })
+      })
+    })
+
+    describe('deleteModelVersions', () => {
+      it('deletes a model version', async () => {
+        store.modelVersions = { model_version_0: { id: 'model_version_0' } }
+
+        mock.onDelete('/modelversion/model_version_0/').reply(204)
+        await store.deleteModelVersion('model_version_0')
+        assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url'])), [
+          {
+            method: 'delete',
+            url: '/modelversion/model_version_0/'
+          }])
+        assert.deepStrictEqual(store.modelVersions, {})
+      })
+
+      it('handles errors', async () => {
+        store.modelVersions = { model_version_0: { id: 'model_version_0' } }
+        mock.onDelete('/modelversion/model_version_0/').reply(500)
+
+        await store.deleteModelVersion('model_version_0')
+
+        assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url'])), [
+          {
+            method: 'delete',
+            url: '/modelversion/model_version_0/'
+          }])
+        assert.deepStrictEqual(store.modelVersions, { model_version_0: { id: 'model_version_0' } })
+        assert.deepStrictEqual(notificationStore.notifications, [
+          {
+            id: 0,
+            type: 'error',
+            text: 'Request failed with status code 500'
+          }
+        ])
+      })
+    })
+  })
+})
-- 
GitLab