Skip to content
Snippets Groups Projects
Commit d9bcadf7 authored by ml bonhomme's avatar ml bonhomme :bee: Committed by Bastien Abadie
Browse files

Re-use the model edition modal as a model creation modal

parent f7e71d0a
No related branches found
No related tags found
1 merge request!1674Re-use the model edition modal as a model creation modal
<template>
<slot name="action">
<button
class="button"
v-on:click="openModal = allowEdition"
v-bind="$attrs"
:disabled="!allowEdition || null"
:title="allowTitle"
>
<i class="icon-edit"></i>
</button>
</slot>
<button
v-if="mode === 'edit'"
class="button"
v-on:click="openModal = allowOpen"
v-bind="$attrs"
:disabled="!allowOpen || undefined"
:title="openButtonTitle"
>
<i class="icon-edit"></i>
</button>
<button
v-else-if="mode === 'create'"
class="button is-primary"
v-on:click="openModal = allowOpen"
v-bind="$attrs"
:disabled="!allowOpen || undefined"
:title="openButtonTitle"
>
Create
</button>
<Modal v-model="openModal" :title="modalTitle" is-large>
<form class="form" v-on:submit.prevent="update">
<div class="field">
<label class="label">Name</label>
<div class="control">
<input
class="input"
:class="{ 'is-danger': fieldErrors.name }"
type="text"
maxlength="100"
v-model.trim="payload.name"
:disabled="loading || null"
/>
<template v-if="fieldErrors.name">
<p class="help is-danger" v-for="err in fieldErrors.name" :key="err">{{ err }}</p>
</template>
<Teleport to="body">
<Modal v-model="openModal" :title="modalTitle" is-large>
<form class="form" v-on:submit.prevent="saveAction">
<div class="field">
<label class="label">Name</label>
<div class="control">
<input
class="input"
:class="{ 'is-danger': fieldErrors.name }"
type="text"
maxlength="100"
v-model.trim="payload.name"
:disabled="loading || undefined"
/>
<template v-if="fieldErrors.name">
<p class="help is-danger" v-for="err in fieldErrors.name" :key="err">{{ err }}</p>
</template>
</div>
</div>
</div>
<div class="field">
<label class="label">Description</label>
<div class="control">
<textarea
class="textarea"
:class="{ 'is-danger': fieldErrors.description }"
type="text"
v-model.trim="payload.description"
:disabled="loading || null"
></textarea>
<template v-if="fieldErrors.description">
<p class="help is-danger" v-for="err in fieldErrors.description" :key="err">{{ err }}</p>
</template>
<div class="field">
<label class="label">Description</label>
<div class="control">
<textarea
class="textarea"
:class="{ 'is-danger': fieldErrors.description }"
type="text"
v-model.trim="payload.description"
:disabled="loading || undefined"
></textarea>
<template v-if="fieldErrors.description">
<p class="help is-danger" v-for="err in fieldErrors.description" :key="err">{{ err }}</p>
</template>
</div>
</div>
</div>
</form>
</form>
<template v-slot:footer="{ close }">
<button class="button" v-on:click="close">Cancel</button>
<button
type="submit"
v-on:click="update"
class="button is-primary ml-auto"
:class="{ 'is-loading': loading }"
:disabled="loading || null"
>
Update
</button>
</template>
</Modal>
<template v-slot:footer="{ close }">
<button class="button" v-on:click="close">Cancel</button>
<button
type="submit"
v-on:click="saveAction"
class="button is-primary ml-auto"
:class="{ 'is-loading': loading }"
:disabled="loading || !payload.name.length || undefined"
>
{{ saveButtonName }}
</button>
</template>
</Modal>
</Teleport>
</template>
<script lang="ts">
......@@ -67,7 +78,7 @@ import { mapActions } from 'pinia'
import { errorParser, ensureArray } from '@/helpers'
import Modal from '@/components/Modal.vue'
import { useNotificationStore, useModelStore } from '@/stores'
import { UpdateModelPayload } from '@/api'
import { CreateModelPayload } from '@/api'
import { PropType, defineComponent } from 'vue'
import { isAxiosError } from 'axios'
import { Model } from '@/types/model'
......@@ -76,10 +87,17 @@ export default defineComponent({
components: {
Modal
},
emits: {
reload: (value: boolean) => typeof value === 'boolean'
},
props: {
model: {
type: Object as PropType<Model>,
mode: {
type: String,
required: true
},
modelInstance: {
type: Object as PropType<Model>,
default: null
}
},
data: () => ({
......@@ -87,33 +105,48 @@ export default defineComponent({
payload: {
name: '',
description: ''
} as UpdateModelPayload,
} as CreateModelPayload,
loading: false,
fieldErrors: {} as Partial<Record<keyof UpdateModelPayload, string[]>>
fieldErrors: {} as Partial<Record<keyof CreateModelPayload, string[]>>
}),
computed: {
allowEdition () {
return this.model.rights.includes('write')
allowOpen () {
if (this.mode === 'edit' && this.modelInstance) return this.modelInstance.rights.includes('write')
return true
},
allowTitle () {
return this.allowEdition
? 'Edit model'
: 'A contributor access level is required to edit a model'
openButtonTitle () {
if (this.mode === 'edit') {
return this.allowOpen
? 'Edit model'
: 'A contributor access level is required to edit a model'
}
return 'Create a new model'
},
modalTitle () {
return `Edit model "${this.model.name}"`
if (this.mode === 'edit' && this.modelInstance) return `Edit model "${this.modelInstance.name}"`
return 'Create a new model'
},
saveButtonName () {
if (this.mode === 'edit') return 'Update'
return 'Create'
}
},
methods: {
...mapActions(useModelStore, ['updateModel']),
...mapActions(useModelStore, ['updateModel', 'createModel', 'listModels']),
...mapActions(useNotificationStore, ['notify']),
async update () {
if (this.loading) return
async saveAction () {
if (this.loading || (this.mode === 'edit' && !this.modelInstance) || !this.payload.name.length) return
this.loading = true
this.fieldErrors = {}
try {
await this.updateModel(this.model.id, this.payload)
this.notify({ type: 'info', text: 'Model updated successfully.' })
if (this.mode === 'edit' && this.modelInstance) {
await this.updateModel(this.modelInstance.id, this.payload)
this.notify({ type: 'info', text: 'Model updated successfully.' })
} else {
await this.createModel(this.payload)
this.$emit('reload', true)
this.notify({ type: 'success', text: 'Model successfully created.' })
}
this.openModal = false
} catch (err) {
this.notify({ type: 'error', text: errorParser(err) })
......@@ -128,11 +161,12 @@ export default defineComponent({
}
},
watch: {
model: {
modelInstance: {
immediate: true,
handler () {
this.payload.name = this.model.name
this.payload.description = this.model.description
if (!this.modelInstance) return
this.payload.name = this.modelInstance.name
this.payload.description = this.modelInstance.description
}
}
}
......
......@@ -29,7 +29,6 @@ const ImportFromBucket = () => import(/* webpackChunkName: "users" */ '@/views/I
const ProcessList = () => import(/* webpackChunkName: "users" */ '@/views/Process/List')
const ModelsList = () => import(/* webpackChunkName: "users" */ '@/views/Model/List')
const Model = () => import(/* webpackChunkName: "users" */ '@/views/Model')
const ModelCreate = () => import(/* webpackChunkName: "users" */ '@/views/Model/Create')
const ModelVersion = () => import(/* webpackChunkName: "users" */ '@/views/Model/Version')
const ProcessDetails = () => import(/* webpackChunkName: "users" */ '@/views/Process/Details')
const ProcessDatasets = () => import(/* webpackChunkName: "users" */ '@/views/Process/Datasets')
......@@ -308,13 +307,6 @@ const routes = [
meta: { requiresLogin: true, requiresVerified: true },
props: true
},
{
path: '/models/create',
name: 'model-create',
component: ModelCreate,
meta: { requiresLogin: true, requiresVerified: true },
props: false
},
{
path: `/model/:modelId(${UUID})`,
name: 'model',
......
<template>
<main class="container is-fluid">
<router-link v-if="mainView" class="button is-pulled-right" :to="{ name: 'models-list' }">
<i class="icon-arrow-left"></i>
Models
</router-link>
<h1 class="title">New model</h1>
<h2 class="subtitle">Add a Machine-Learning model</h2>
<form v-on:submit.prevent="create">
<div class="field">
<label class="label">Model name *</label>
<div class="control">
<input
class="input is-fullwidth"
:disabled="loading || undefined"
type="text"
v-model.trim="fields.name"
/>
</div>
<p v-if="errors.name" class="help is-danger">{{ errors.name }}</p>
</div>
<div class="field">
<label class="label">Model description</label>
<div class="control">
<input
class="input is-fullwidth"
:disabled="loading || undefined"
type="text"
v-model.trim="fields.description"
/>
</div>
<p v-if="errors.description" class="help is-danger">{{ errors.description }}</p>
</div>
<div class="columns is-pulled-right mt-2">
<div class="column is-narrow">
<button
type="submit"
class="button is-primary is-fullwidth"
v-on:click="create"
: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>
<span v-else class="icon-plus">Create model</span>
</button>
</div>
</div>
</form>
</main>
</template>
<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 defineComponent({
emits: {
'create-model' (value: Model) {
return value.id !== undefined
}
},
mixins: [
corporaMixin
],
data: () => ({
loading: false,
fields: {
name: '',
description: ''
} as CreateModelPayload,
errors: {} as Record<string, unknown>
}),
computed: {
mainView () {
// The component can be mounted as a single view
return this.$route.name === 'model-create'
},
allowCreate () {
return Boolean(this.fields.name) && !this.loading
}
},
methods: {
...mapActions(useNotificationStore, ['notify']),
async create () {
if (!this.allowCreate) return
this.loading = true
this.errors = {}
try {
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', model)
this.fields.name = this.fields.description = ''
} catch (err) {
if (isAxiosError(err) && err.response?.data) this.errors = err.response.data
this.notify({ type: 'error', text: errorParser(err) })
} finally {
this.loading = false
}
}
}
})
</script>
<template>
<main class="container is-fluid">
<div class="columns">
<div class="column is-one-third">
<div class="has-text-right">
<router-link
class="button mb-2"
:to="{ name: 'model-create' }"
>
Create a model
</router-link>
<div class="field column is-one-third">
<div class="has-text-right mb-2">
<CreateForm
mode="create"
v-on:reload="updateModelsPage"
/>
</div>
<div v-if="compatibleWorkerId" class="field has-text-right">
......@@ -93,7 +91,7 @@
<div v-if="!selectedModel" class="notification is-info">
Please select a <strong>model</strong> on the left panel.
</div>
<Model
<ModelComponent
v-else
:model-id="selectedModel"
:process-id="processId"
......@@ -118,6 +116,7 @@ import { Model } from '@/types/model'
import Paginator from '@/components/Paginator.vue'
import ModelComponent from '@/components/Model/Model.vue'
import CreateForm from '@/components/Model/EditForm.vue'
import { PageNumberPagination } from '@/types'
export default defineComponent({
......@@ -126,7 +125,8 @@ export default defineComponent({
],
components: {
Paginator,
Model: ModelComponent
ModelComponent,
CreateForm
},
props: {
/**
......
......@@ -7,7 +7,7 @@
</router-link>
<div class="title">
<span v-if="model.archived" class="tag">Archived</span> {{ model.name }}
<EditForm class="ml-2 is-primary" :model="model" />
<EditForm class="ml-2 is-primary" :model-instance="model" mode="edit" />
</div>
<div class="subtitle is-5">
<p>Model <ItemId :item-id="model.id" /></p>
......@@ -51,7 +51,7 @@
</div>
<hr />
<Model :model-id="modelId" />
<ModelDetails :model-id="modelId" />
</template>
<div v-else-if="error" class="notification is-danger">{{ error }}</div>
<div v-else class="loader is-size-2 mx-auto"></div>
......@@ -72,7 +72,8 @@ 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 { Model } from '@/types/model'
import ModelDetails from '@/components/Model'
import EditForm from '@/components/Model/EditForm.vue'
import { useModelStore, useNotificationStore } from '@/stores'
import ItemId from '@/components/ItemId.vue'
......@@ -87,7 +88,7 @@ export default defineComponent({
}
},
components: {
Model,
ModelDetails,
EditForm,
ItemId,
ArchivalModal
......@@ -100,7 +101,7 @@ export default defineComponent({
}),
computed: {
...mapState(useModelStore, ['models']),
model () {
model (): Model {
return this.models[this.modelId]
},
creationDate (): string | null {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment