Browse Source

Update and move label components

pull/1222/head
Hironsan 4 years ago
parent
commit
3ebad70cc5
9 changed files with 394 additions and 437 deletions
  1. 76
      frontend/components/containers/labels/LabelActionMenu.vue
  2. 56
      frontend/components/containers/labels/LabelDeletionButton.vue
  3. 147
      frontend/components/containers/labels/LabelList.vue
  4. 42
      frontend/components/label/ActionMenu.vue
  5. 92
      frontend/components/label/FormCreate.vue
  6. 28
      frontend/components/label/FormDelete.vue
  7. 62
      frontend/components/label/FormUpload.vue
  8. 216
      frontend/components/label/LabelList.vue
  9. 112
      frontend/components/organisms/labels/LabelCreationForm.vue

76
frontend/components/containers/labels/LabelActionMenu.vue

@ -1,76 +0,0 @@
<template>
<div>
<action-menu
:items="menuItems"
:text="$t('dataset.actions')"
@create="createDialog=true"
@upload="importDialog=true"
@download="handleDownload"
/>
<v-dialog
v-model="createDialog"
width="800"
>
<label-creation-form
:create-label="createLabel"
:keys="shortkeys"
@close="createDialog=false"
/>
</v-dialog>
<v-dialog
v-model="importDialog"
width="800"
>
<label-import-form
:upload-label="uploadLabel"
@close="importDialog=false"
/>
</v-dialog>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import ActionMenu from '@/components/molecules/ActionMenu'
import LabelCreationForm from '@/components/organisms/labels/LabelCreationForm'
import LabelImportForm from '@/components/organisms/labels/LabelImportForm'
export default {
components: {
ActionMenu,
LabelCreationForm,
LabelImportForm
},
data() {
return {
createDialog: false,
importDialog: false,
menuItems: [
{ title: this.$t('labels.createLabel'), icon: 'mdi-pencil', event: 'create' },
{ title: this.$t('labels.importLabels'), icon: 'mdi-upload', event: 'upload' },
{ title: this.$t('labels.exportLabels'), icon: 'mdi-download', event: 'download' }
]
}
},
computed: {
...mapGetters('labels', ['shortkeys'])
},
created() {
this.setCurrentProject(this.$route.params.id)
},
methods: {
...mapActions('labels', ['createLabel', 'uploadLabel', 'exportLabels']),
...mapActions('projects', ['setCurrentProject']),
handleDownload() {
this.exportLabels({
projectId: this.$route.params.id
})
}
}
}
</script>

56
frontend/components/containers/labels/LabelDeletionButton.vue

@ -1,56 +0,0 @@
<template>
<div>
<v-btn
:disabled="!isLabelSelected"
class="text-capitalize"
outlined
@click="dialog=true"
>
{{ $t('generic.delete') }}
</v-btn>
<v-dialog
v-model="dialog"
width="800"
>
<confirm-form
:items="selected"
title="Delete Label"
:message="$t('labels.deleteMessage')"
item-key="text"
@ok="deleteLabel($route.params.id);dialog=false"
@cancel="dialog=false"
/>
</v-dialog>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex'
import ConfirmForm from '@/components/organisms/utils/ConfirmForm'
export default {
components: {
ConfirmForm
},
data() {
return {
dialog: false
}
},
computed: {
...mapState('labels', ['selected']),
...mapGetters('labels', ['isLabelSelected'])
},
methods: {
...mapActions('labels', ['deleteLabel']),
handleDeleteLabel() {
const projectId = this.$route.params.id
this.deleteLabel(projectId)
}
}
}
</script>

147
frontend/components/containers/labels/LabelList.vue

@ -1,147 +0,0 @@
<template>
<v-data-table
:value="selected"
:headers="headers"
:items="items"
:search="search"
:loading="loading"
:loading-text="$t('generic.loading')"
:no-data-text="$t('vuetify.noDataAvailable')"
:footer-props="{
'showFirstLastPage': true,
'items-per-page-options': [5, 10, 15, $t('generic.all')],
'items-per-page-text': $t('vuetify.itemsPerPageText'),
'page-text': $t('dataset.pageText')
}"
item-key="id"
show-select
@input="updateSelected"
>
<template v-slot:top>
<v-text-field
v-model="search"
prepend-inner-icon="search"
:label="$t('generic.search')"
single-line
hide-details
filled
/>
</template>
<template v-slot:item.text="{ item }">
<v-edit-dialog>
{{ item.text }}
<template v-slot:input>
<v-text-field
:value="item.text"
:rules="labelNameRules($t('rules.labelNameRules'))"
:label="$t('generic.edit')"
single-line
@change="handleUpdateLabel({ id: item.id, text: $event })"
/>
</template>
</v-edit-dialog>
</template>
<template v-slot:item.suffix_key="{ item }">
<v-edit-dialog>
<div>{{ item.suffix_key }}</div>
<template v-slot:input>
<v-select
:value="item.suffix_key"
:items="availableShortkeys(item.suffix_key)"
:label="$t('annotation.key')"
@change="handleUpdateLabel({ id: item.id, suffix_key: $event })"
/>
</template>
</v-edit-dialog>
</template>
<template v-slot:item.background_color="{ item }">
<v-edit-dialog>
<v-chip
:color="item.background_color"
:text-color="textColor(item.background_color)"
dark
>
{{ item.background_color }}
</v-chip>
<template v-slot:input>
<v-color-picker
:value="item.background_color"
:rules="colorRules($t('rules.colorRules'))"
show-swatches
hide-mode-switch
width="800"
mode="hexa"
class="ma-2"
@update:color="handleUpdateLabel({ id:item.id, background_color: $event.hex })"
/>
</template>
</v-edit-dialog>
</template>
</v-data-table>
</template>
<script>
import { mapGetters, mapState, mapActions, mapMutations } from 'vuex'
import { colorRules, labelNameRules } from '@/rules/index'
import { idealColor } from '~/plugins/utils'
export default {
data() {
return {
search: '',
headers: [
{
text: this.$t('generic.name'),
align: 'left',
value: 'text'
},
{
text: this.$t('labels.shortkey'),
value: 'suffix_key'
},
{
text: this.$t('labels.color'),
sortable: false,
value: 'background_color'
}
],
colorRules,
labelNameRules
}
},
computed: {
...mapState('labels', ['items', 'selected', 'loading']),
...mapGetters('labels', ['shortkeys'])
},
created() {
this.getLabelList({
projectId: this.$route.params.id
})
},
methods: {
...mapActions('labels', ['getLabelList', 'updateLabel']),
...mapMutations('labels', ['updateSelected']),
handleUpdateLabel(payload) {
const data = {
projectId: this.$route.params.id,
...payload
}
this.updateLabel(data)
},
availableShortkeys(suffixKey) {
const usedKeys = this.items.map(item => item.suffix_key)
const unusedKeys = this.shortkeys.filter(item => item === suffixKey || !usedKeys.includes(item))
return unusedKeys
},
textColor(backgroundColor) {
return idealColor(backgroundColor)
}
}
}
</script>

42
frontend/components/label/ActionMenu.vue

@ -0,0 +1,42 @@
<template>
<action-menu
:items="items"
:text="$t('dataset.actions')"
@create="$emit('create')"
@upload="$emit('upload')"
@download="$emit('download')"
/>
</template>
<script lang="ts">
import Vue from 'vue'
import ActionMenu from '@/components/molecules/ActionMenu.vue'
export default Vue.extend({
components: {
ActionMenu
},
computed: {
items() {
return [
{
title: this.$t('labels.createLabel'),
icon: 'mdi-pencil',
event: 'create'
},
{
title: this.$t('labels.importLabels'),
icon: 'mdi-upload',
event: 'upload'
},
{
title: this.$t('labels.exportLabels'),
icon: 'mdi-download',
event: 'download'
}
]
}
}
})
</script>

92
frontend/components/label/FormCreate.vue

@ -0,0 +1,92 @@
<template>
<base-card
:disabled="!valid"
:title="$t('labels.createLabel')"
:agree-text="$t('generic.create')"
:cancel-text="$t('generic.cancel')"
@agree="$emit('save')"
@cancel="$emit('cancel')"
>
<template #content>
<v-form
ref="form"
v-model="valid"
>
<v-text-field
v-model="item.text"
:label="$t('labels.labelName')"
:rules="labelNameRules($t('rules.labelNameRules'))"
prepend-icon="label"
single-line
counter
autofocus
/>
<v-select
v-model="item.suffix_key"
:items="shortkeys"
:label="$t('labels.key')"
prepend-icon="mdi-keyboard"
/>
<v-color-picker
v-model="item.background_color"
:rules="colorRules($t('rules.colorRules'))"
show-swatches
hide-mode-switch
width="800"
/>
</v-form>
</template>
</base-card>
</template>
<script lang="ts">
import Vue from 'vue'
import BaseCard from '@/components/molecules/BaseCard.vue'
import { colorRules, labelNameRules } from '@/rules/index'
export default Vue.extend({
components: {
BaseCard
},
props: {
value: {
type: Object,
default: () => {},
required: true
}
},
data() {
return {
valid: false,
labelNameRules,
colorRules
}
},
computed: {
shortkeys() {
return '0123456789abcdefghijklmnopqrstuvwxyz'.split('')
},
item: {
get() {
// Property '$emit' does not exist on type '() => any'
// @ts-ignore
return this.value
},
set(val) {
// Property '$emit' does not exist on type '() => any'
// @ts-ignore
this.$emit('input', val)
}
}
},
methods: {
hoge() {
this.$emit('inpupt')
}
}
})
</script>

28
frontend/components/label/FormDelete.vue

@ -0,0 +1,28 @@
<template>
<confirm-form
:items="selected"
title="Delete Label"
:message="$t('labels.deleteMessage')"
item-key="text"
@ok="$emit('remove')"
@cancel="$emit('cancel')"
/>
</template>
<script lang="ts">
import Vue from 'vue'
import ConfirmForm from '@/components/organisms/utils/ConfirmForm.vue'
export default Vue.extend({
components: {
ConfirmForm
},
props: {
selected: {
type: Array,
default: () => []
}
}
})
</script>

frontend/components/organisms/labels/LabelImportForm.vue → frontend/components/label/FormUpload.vue

@ -4,17 +4,13 @@
:title="$t('labels.importTitle')"
:agree-text="$t('generic.upload')"
:cancel-text="$t('generic.cancel')"
@agree="create"
@cancel="cancel"
@agree="$emit('upload', file)"
@cancel="$emit('cancel')"
>
<template #content>
<v-form
ref="form"
v-model="valid"
>
<v-form v-model="valid">
<v-alert
v-show="showError"
v-model="showError"
v-show="errorMessage"
type="error"
dismissible
>
@ -41,27 +37,28 @@
</base-card>
</template>
<script>
import BaseCard from '@/components/molecules/BaseCard'
<script lang="ts">
import Vue from 'vue'
import BaseCard from '@/components/molecules/BaseCard.vue'
import { uploadSingleFileRules } from '@/rules/index'
export default {
export default Vue.extend({
components: {
BaseCard
},
props: {
uploadLabel: {
type: Function,
default: () => {},
required: true
errorMessage: {
type: String,
default: ''
}
},
data() {
return {
valid: false,
file: null,
valid: false,
uploadSingleFileRules,
showError: false
}
},
@ -83,33 +80,6 @@ export default {
]
return JSON.stringify(data, null, 4)
}
},
methods: {
cancel() {
this.$emit('close')
},
validate() {
return this.$refs.form.validate()
},
reset() {
this.$refs.form.reset()
},
create() {
if (this.validate()) {
this.uploadLabel({
projectId: this.$route.params.id,
file: this.file
})
.then((response) => {
this.reset()
this.cancel()
})
.catch(() => {
this.showError = true
})
}
}
}
}
}
})
</script>

216
frontend/components/label/LabelList.vue

@ -0,0 +1,216 @@
<template>
<v-data-table
v-model="selected"
:headers="headers"
:items="items"
:search="search"
:loading="isLoading"
:loading-text="$t('generic.loading')"
:no-data-text="$t('vuetify.noDataAvailable')"
:footer-props="{
'showFirstLastPage': true,
'items-per-page-options': [5, 10, 15, $t('generic.all')],
'items-per-page-text': $t('vuetify.itemsPerPageText'),
'page-text': $t('dataset.pageText')
}"
item-key="id"
show-select
>
<template v-slot:top>
<v-toolbar flat>
<action-menu
@create="dialogCreate=true"
@upload="dialogUpload=true"
@download="download"
/>
<v-btn
class="text-capitalize ms-2"
:disabled="!canDelete"
outlined
@click.stop="dialogDelete=true"
>
{{ $t('generic.delete') }}
</v-btn>
<v-dialog v-model="dialogCreate">
<form-create
v-model="editedItem"
@cancel="close"
@save="save"
/>
</v-dialog>
<v-dialog v-model="dialogUpload">
<form-upload
@cancel="dialogUpload=false"
@upload="upload"
/>
</v-dialog>
<v-dialog v-model="dialogDelete">
<form-delete
:selected="selected"
@cancel="dialogDelete=false"
@remove="remove"
/>
</v-dialog>
</v-toolbar>
<v-text-field
v-model="search"
prepend-inner-icon="search"
:label="$t('generic.search')"
single-line
hide-details
filled
/>
</template>
<template v-slot:[`item.background_color`]="props">
<v-chip
:color="props.item.background_color"
:text-color="textColor(props.item.background_color)"
>
{{ props.item.background_color }}
</v-chip>
</template>
<template v-slot:[`item.actions`]="{ item }">
<v-icon
small
@click="editItem(item)"
>
mdi-pencil
</v-icon>
</template>
</v-data-table>
</template>
<script lang="ts">
import Vue from 'vue'
import { LabelDTO } from '@/services/application/label.service'
import ActionMenu from './ActionMenu.vue'
import FormCreate from './FormCreate.vue'
import FormDelete from './FormDelete.vue'
import FormUpload from './FormUpload.vue'
import { idealColor } from '~/plugins/utils'
export default Vue.extend({
components: {
ActionMenu,
FormCreate,
FormDelete,
FormUpload
},
data() {
return {
dialogCreate: false,
dialogDelete: false,
dialogUpload: false,
headers: [
{ text: this.$t('generic.name'), value: 'text' },
{ text: this.$t('labels.shortkey'), value: 'suffix_key' },
{ text: this.$t('labels.color'), value: 'background_color' },
{ text: 'Actions', value: 'actions', sortable: false },
],
editedIndex: -1,
editedItem: {
text: '',
prefix_key: null,
suffix_key: null,
background_color: '#2196F3',
text_color: '#ffffff'
} as LabelDTO,
defaultItem: {
text: '',
prefix_key: null,
suffix_key: null,
background_color: '#2196F3',
text_color: '#ffffff'
} as LabelDTO,
isLoading: false,
items: [] as LabelDTO[],
selected: [] as LabelDTO[],
search: ''
}
},
computed: {
canDelete(): boolean {
return this.selected.length > 0
},
projectId(): string {
return this.$route.params.id
}
},
created() {
this.list()
},
methods: {
async list() {
this.isLoading = true
this.items = await this.$services.label.list(this.projectId)
this.isLoading = false
},
async create(item: LabelDTO) {
await this.$services.label.create(this.projectId, item)
this.list()
this.dialogCreate = false
},
async update(item: LabelDTO) {
await this.$services.label.update(this.projectId, item)
this.list()
this.dialogCreate = false
},
async remove() {
await this.$services.label.bulkDelete(this.projectId, this.selected)
this.list()
this.dialogDelete = false
this.selected = []
},
async download() {
await this.$services.label.export(this.projectId)
},
async upload(file: File) {
await this.$services.label.upload(this.projectId, file)
this.list()
this.dialogUpload = false
},
editItem(item: LabelDTO) {
this.editedIndex = this.items.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialogCreate = true
},
close() {
this.dialogCreate = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
save() {
if (this.editedIndex > -1) {
this.update(this.editedItem)
} else {
this.create(this.editedItem)
}
this.close()
},
textColor(backgroundColor: string) {
return idealColor(backgroundColor)
}
}
})
</script>
<style scoped>
::v-deep .v-dialog {
width: 800px;
}
</style>

112
frontend/components/organisms/labels/LabelCreationForm.vue

@ -1,112 +0,0 @@
<template>
<base-card
:disabled="!valid"
:title="$t('labels.createLabel')"
:agree-text="$t('generic.create')"
:cancel-text="$t('generic.cancel')"
@agree="create"
@cancel="reset"
>
<template #content>
<v-form
ref="form"
v-model="valid"
>
<v-alert
v-show="showError"
v-model="showError"
type="error"
dismissible
>
{{ $t('errors.labelCannotCreate') }}
</v-alert>
<v-text-field
v-model="labelName"
:rules="labelNameRules($t('rules.labelNameRules'))"
:label="$t('labels.labelName')"
prepend-icon="label"
/>
<v-select
v-model="suffixKey"
:items="keys"
:label="$t('labels.key')"
prepend-icon="mdi-keyboard"
/>
<v-color-picker
v-model="color"
:rules="colorRules($t('rules.colorRules'))"
show-swatches
hide-mode-switch
width="800"
mode="hexa"
class="ma-2"
/>
</v-form>
</template>
</base-card>
</template>
<script>
import BaseCard from '@/components/molecules/BaseCard'
import { colorRules, labelNameRules } from '@/rules/index'
export default {
components: {
BaseCard
},
props: {
createLabel: {
type: Function,
default: () => {},
required: true
},
keys: {
type: Array,
default: () => [],
required: true
}
},
data() {
return {
valid: false,
labelName: '',
suffixKey: '',
color: '',
labelNameRules,
colorRules,
showError: false
}
},
methods: {
cancel() {
this.$emit('close')
},
validate() {
return this.$refs.form.validate()
},
reset() {
this.$refs.form.reset()
this.cancel('close')
},
create() {
if (this.validate()) {
this.createLabel({
projectId: this.$route.params.id,
text: this.labelName,
prefix_key: null,
suffix_key: this.suffixKey ? this.suffixKey : null,
background_color: this.color.slice(0, 7), // #12345678 -> #123456
text_color: '#ffffff'
})
.then(() => {
this.reset()
})
.catch(() => {
this.showError = true
})
}
}
}
}
</script>
Loading…
Cancel
Save