mirror of https://github.com/doccano/doccano.git
9 changed files with 394 additions and 437 deletions
Split View
Diff Options
-
76frontend/components/containers/labels/LabelActionMenu.vue
-
56frontend/components/containers/labels/LabelDeletionButton.vue
-
147frontend/components/containers/labels/LabelList.vue
-
42frontend/components/label/ActionMenu.vue
-
92frontend/components/label/FormCreate.vue
-
28frontend/components/label/FormDelete.vue
-
62frontend/components/label/FormUpload.vue
-
216frontend/components/label/LabelList.vue
-
112frontend/components/organisms/labels/LabelCreationForm.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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
Write
Preview
Loading…
Cancel
Save