mirror of https://github.com/doccano/doccano.git
Browse Source
Merge pull request #1680 from doccano/enhancement/labelCreationProcess
Merge pull request #1680 from doccano/enhancement/labelCreationProcess
[Enhancement] Label creation processpull/1681/head
Hiroki Nakayama
2 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 390 additions and 128 deletions
Unified View
Diff Options
-
234frontend/components/label/FormCreate.vue
-
2frontend/domain/models/label/labelRepository.ts
-
88frontend/pages/projects/_id/labels/_label_id/edit.vue
-
105frontend/pages/projects/_id/labels/add.vue
-
78frontend/pages/projects/_id/labels/index.vue
-
6frontend/repositories/label/apiLabelRepository.ts
-
5frontend/services/application/label/labelApplicationService.ts
@ -1,108 +1,204 @@ |
|||||
<template> |
<template> |
||||
<base-card |
|
||||
:disabled="!valid" |
|
||||
:title="$t('labels.createLabel')" |
|
||||
:agree-text="$t('generic.save')" |
|
||||
:cancel-text="$t('generic.cancel')" |
|
||||
@agree="$emit('save')" |
|
||||
@cancel="$emit('cancel')" |
|
||||
> |
|
||||
<template #content> |
|
||||
<v-form v-model="valid"> |
|
||||
<v-text-field |
|
||||
:value="text" |
|
||||
:label="$t('labels.labelName')" |
|
||||
:rules="[rules.required, rules.counter, rules.nameDuplicated]" |
|
||||
:prepend-icon="mdiLabel" |
|
||||
single-line |
|
||||
counter |
|
||||
autofocus |
|
||||
@input="updateValue('text', $event)" |
|
||||
/> |
|
||||
<v-select |
|
||||
:value="suffixKey" |
|
||||
:items="shortkeys" |
|
||||
:label="$t('labels.key')" |
|
||||
:rules="[rules.keyDuplicated]" |
|
||||
:prepend-icon="mdiKeyboard" |
|
||||
@input="updateValue('suffixKey', $event)" |
|
||||
/> |
|
||||
<v-color-picker |
|
||||
:value="backgroundColor" |
|
||||
:rules="[rules.required]" |
|
||||
show-swatches |
|
||||
hide-mode-switch |
|
||||
width="800" |
|
||||
@input="updateValue('backgroundColor', $event)" |
|
||||
/> |
|
||||
|
<v-card> |
||||
|
<v-card-title>Create a Label Type</v-card-title> |
||||
|
<v-card-text> |
||||
|
<v-form |
||||
|
ref="form" |
||||
|
v-model="valid" |
||||
|
> |
||||
|
<v-row> |
||||
|
<v-col cols="12" sm="6"> |
||||
|
<v-text-field |
||||
|
:value="text" |
||||
|
:counter="100" |
||||
|
:label="$t('labels.labelName')" |
||||
|
:rules="[rules.required, rules.counter, rules.nameDuplicated]" |
||||
|
outlined |
||||
|
required |
||||
|
@input="$emit('update:text', $event)" |
||||
|
/> |
||||
|
</v-col> |
||||
|
<v-col cols="12" sm="6"> |
||||
|
<v-select |
||||
|
:value="suffixKey" |
||||
|
:items="availableSuffixKeys" |
||||
|
:label="$t('labels.key')" |
||||
|
:rules="[rules.keyDuplicated]" |
||||
|
outlined |
||||
|
@input="$emit('update:suffixKey', $event)" |
||||
|
/> |
||||
|
</v-col> |
||||
|
</v-row> |
||||
|
|
||||
|
<v-row> |
||||
|
<v-col cols="12" sm="12"> |
||||
|
<v-text-field |
||||
|
:value="backgroundColor" |
||||
|
:rules="[rules.validColor]" |
||||
|
label="Color" |
||||
|
hide-details="auto" |
||||
|
outlined |
||||
|
required |
||||
|
@input="$emit('update:backgroundColor', $event)" |
||||
|
/> |
||||
|
<v-chip-group |
||||
|
v-model="selectedColorIndex" |
||||
|
column |
||||
|
mandatory |
||||
|
> |
||||
|
<v-chip |
||||
|
v-for="color in predefinedColors" |
||||
|
:key="color" |
||||
|
:color="color" |
||||
|
filter |
||||
|
label |
||||
|
style="height: 32px; width: 32px;" |
||||
|
/> |
||||
|
<v-tooltip bottom> |
||||
|
<template #activator="{ on, attrs }"> |
||||
|
<v-chip |
||||
|
label |
||||
|
v-bind="attrs" |
||||
|
v-on="on" |
||||
|
@click="setRandomColor" |
||||
|
> |
||||
|
<v-icon>{{ mdiReload }}</v-icon> |
||||
|
</v-chip> |
||||
|
</template> |
||||
|
<span>Random color</span> |
||||
|
</v-tooltip> |
||||
|
</v-chip-group> |
||||
|
</v-col> |
||||
|
</v-row> |
||||
|
|
||||
|
<v-row> |
||||
|
<v-col> |
||||
|
<div class="title black--text mb-2">Preview</div> |
||||
|
<v-chip |
||||
|
:color="backgroundColor" |
||||
|
:text-color="textColor" |
||||
|
> |
||||
|
{{ text }} |
||||
|
<v-avatar |
||||
|
v-if="suffixKey" |
||||
|
right |
||||
|
color="white" |
||||
|
class="black--text font-weight-bold" |
||||
|
> |
||||
|
{{ suffixKey }} |
||||
|
</v-avatar> |
||||
|
</v-chip> |
||||
|
</v-col> |
||||
|
</v-row> |
||||
|
|
||||
|
<v-row> |
||||
|
<v-col cols="12" sm="12"> |
||||
|
<slot :valid="valid" /> |
||||
|
</v-col> |
||||
|
</v-row> |
||||
</v-form> |
</v-form> |
||||
</template> |
|
||||
</base-card> |
|
||||
|
</v-card-text> |
||||
|
</v-card> |
||||
</template> |
</template> |
||||
|
|
||||
<script lang="ts"> |
<script lang="ts"> |
||||
import Vue from 'vue' |
|
||||
import { mdiLabel, mdiKeyboard } from '@mdi/js' |
|
||||
import BaseCard from '@/components/utils/BaseCard.vue' |
|
||||
|
import Vue, { PropType } from 'vue' |
||||
|
import { mdiReload } from '@mdi/js'; |
||||
|
import { LabelDTO } from '~/services/application/label/labelData' |
||||
|
|
||||
export default Vue.extend({ |
export default Vue.extend({ |
||||
components: { |
|
||||
BaseCard |
|
||||
}, |
|
||||
|
|
||||
props: { |
props: { |
||||
text: { |
|
||||
type: String, |
|
||||
default: '', |
|
||||
|
items: { |
||||
|
type: Array as PropType<LabelDTO[]>, |
||||
|
default: () => [], |
||||
required: true |
required: true |
||||
}, |
}, |
||||
suffixKey: { |
|
||||
|
id: { |
||||
|
type: Number as () => number | undefined, |
||||
|
default: undefined |
||||
|
}, |
||||
|
text: { |
||||
type: String, |
type: String, |
||||
default: null, |
|
||||
|
required: true, |
||||
}, |
}, |
||||
backgroundColor: { |
backgroundColor: { |
||||
type: String, |
type: String, |
||||
default: '#ffffff', |
|
||||
required: true |
|
||||
}, |
|
||||
usedNames: { |
|
||||
type: Array, |
|
||||
default: () => [], |
|
||||
required: true |
|
||||
|
required: true, |
||||
}, |
}, |
||||
usedKeys: { |
|
||||
type: Array, |
|
||||
default: () => [], |
|
||||
required: true |
|
||||
|
suffixKey: { |
||||
|
type: String as () => string | null, |
||||
|
default: null |
||||
} |
} |
||||
}, |
}, |
||||
|
|
||||
data() { |
data() { |
||||
return { |
return { |
||||
|
selectedColorIndex: 0, |
||||
valid: false, |
valid: false, |
||||
rules: { |
rules: { |
||||
required: (v: string) => !!v || 'Required', |
required: (v: string) => !!v || 'Required', |
||||
// @ts-ignore |
// @ts-ignore |
||||
counter: (v: string) => (v && v.length <= 100) || this.$t('rules.labelNameRules').labelLessThan100Chars, |
counter: (v: string) => (v && v.length <= 100) || this.$t('rules.labelNameRules').labelLessThan100Chars, |
||||
// @ts-ignore |
// @ts-ignore |
||||
nameDuplicated: (v: string) => (!this.usedNames.includes(v)) || this.$t('rules.labelNameRules').duplicated, |
|
||||
|
nameDuplicated: (v: string) => (!this.isUsedName(v)) || this.$t('rules.labelNameRules').duplicated, |
||||
// @ts-ignore |
// @ts-ignore |
||||
keyDuplicated: (v: string) => !this.usedKeys.includes(v) || this.$t('rules.keyNameRules').duplicated, |
|
||||
|
keyDuplicated: (v: string) => (!this.isUsedSuffixKey(v)) || this.$t('rules.keyNameRules').duplicated, |
||||
|
validColor: (v: string) => (/^#[0-9A-F]{6}$/i.test(v)) || 'This string is NOT a valid hex color.' |
||||
}, |
}, |
||||
mdiLabel, |
|
||||
mdiKeyboard |
|
||||
|
mdiReload |
||||
} |
} |
||||
}, |
}, |
||||
|
|
||||
computed: { |
computed: { |
||||
shortkeys() { |
|
||||
return '0123456789abcdefghijklmnopqrstuvwxyz'.split('') |
|
||||
|
availableSuffixKeys(): string[] { |
||||
|
const usedSuffixKeys = this.items.map(item => item.suffixKey).filter(item => item !== this.suffixKey) |
||||
|
const allSuffixKeys = '0123456789abcdefghijklmnopqrstuvwxyz'.split('') |
||||
|
return allSuffixKeys.filter(item => !usedSuffixKeys.includes(item)) |
||||
|
}, |
||||
|
|
||||
|
predefinedColors(): string[] { |
||||
|
return [ |
||||
|
'#73D8FF', '#009CE0', '#0062B1', |
||||
|
'#AEA1FF', '#7B64FF', '#653294', |
||||
|
'#FDA1FF', '#FA28FF', '#AB149E', |
||||
|
'#68CCCA', '#16A5A5', '#0C797D', |
||||
|
'#A4DD00', '#68BC00', '#194D33', |
||||
|
'#FCDC00', '#FCC400', '#FB9E00', |
||||
|
'#F44E3B', '#D33115', '#9F0500' |
||||
|
] |
||||
|
}, |
||||
|
|
||||
|
textColor(): string { |
||||
|
return this.$contrastColor(this.backgroundColor) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
watch: { |
||||
|
selectedColorIndex(value) { |
||||
|
if (value < this.predefinedColors.length) { |
||||
|
this.$emit('update:backgroundColor', this.predefinedColors[this.selectedColorIndex]) |
||||
|
} |
||||
} |
} |
||||
}, |
}, |
||||
|
|
||||
methods: { |
methods: { |
||||
updateValue(key: string, value: string) { |
|
||||
this.$emit(`update:${key}`, value); |
|
||||
|
isUsedName(text: string): boolean { |
||||
|
return this.items.filter(item => item.id !== this.id && item.text === text).length > 0 |
||||
|
}, |
||||
|
|
||||
|
isUsedSuffixKey(key: string) { |
||||
|
if (key === null) { |
||||
|
return false |
||||
|
} |
||||
|
return this.items.filter(item => item.id !== this.id && item.suffixKey === key).length > 0 |
||||
|
}, |
||||
|
|
||||
|
setRandomColor() { |
||||
|
const maxVal = 0xFFFFFF |
||||
|
const randomNumber = Math.floor(Math.random() * maxVal) |
||||
|
const randomString = randomNumber.toString(16) |
||||
|
const randColor = randomString.padStart(6, '0') |
||||
|
this.$emit('update:backgroundColor', `#${randColor.toUpperCase()}`) |
||||
} |
} |
||||
} |
} |
||||
}) |
}) |
||||
|
@ -0,0 +1,88 @@ |
|||||
|
<template> |
||||
|
<form-create |
||||
|
v-slot="slotProps" |
||||
|
v-bind.sync="editedItem" |
||||
|
:items="items" |
||||
|
> |
||||
|
<v-btn |
||||
|
:disabled="!slotProps.valid" |
||||
|
color="primary" |
||||
|
class="text-capitalize" |
||||
|
@click="save" |
||||
|
> |
||||
|
Save |
||||
|
</v-btn> |
||||
|
</form-create> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import Vue from 'vue' |
||||
|
import { LabelDTO } from '~/services/application/label/labelData' |
||||
|
import { ProjectDTO } from '~/services/application/project/projectData' |
||||
|
import FormCreate from '~/components/label/FormCreate.vue' |
||||
|
|
||||
|
export default Vue.extend({ |
||||
|
components: { |
||||
|
FormCreate, |
||||
|
}, |
||||
|
|
||||
|
layout: 'project', |
||||
|
|
||||
|
validate({ params, query, app }) { |
||||
|
if (!['category', 'span'].includes((query.type as string))) { |
||||
|
return false |
||||
|
} |
||||
|
if (/^\d+$/.test(params.id)) { |
||||
|
return app.$services.project.findById(params.id) |
||||
|
.then((res:ProjectDTO) => { |
||||
|
return res.canDefineLabel |
||||
|
}) |
||||
|
} |
||||
|
return false |
||||
|
}, |
||||
|
|
||||
|
data() { |
||||
|
return { |
||||
|
editedItem: { |
||||
|
text: '', |
||||
|
prefixKey: null, |
||||
|
suffixKey: null, |
||||
|
backgroundColor: '#73D8FF', |
||||
|
textColor: '#ffffff' |
||||
|
} as LabelDTO, |
||||
|
items: [] as LabelDTO[] |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
computed: { |
||||
|
projectId(): string { |
||||
|
return this.$route.params.id |
||||
|
}, |
||||
|
|
||||
|
labelId(): string { |
||||
|
return this.$route.params.label_id |
||||
|
}, |
||||
|
|
||||
|
service(): any { |
||||
|
const type = this.$route.query.type |
||||
|
if (type === 'category') { |
||||
|
return this.$services.categoryType |
||||
|
} else { |
||||
|
return this.$services.spanType |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
|
||||
|
async created() { |
||||
|
this.items = await this.service.list(this.projectId) |
||||
|
this.editedItem = await this.service.findById(this.projectId, this.labelId) |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
async save() { |
||||
|
await this.service.update(this.projectId, this.editedItem) |
||||
|
this.$router.push(`/projects/${this.projectId}/labels`) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
</script> |
@ -0,0 +1,105 @@ |
|||||
|
<template> |
||||
|
<form-create |
||||
|
v-slot="slotProps" |
||||
|
v-bind.sync="editedItem" |
||||
|
:items="items" |
||||
|
> |
||||
|
<v-btn |
||||
|
:disabled="!slotProps.valid" |
||||
|
color="primary" |
||||
|
class="text-capitalize" |
||||
|
@click="save" |
||||
|
> |
||||
|
Save |
||||
|
</v-btn> |
||||
|
|
||||
|
<v-btn |
||||
|
:disabled="!slotProps.valid" |
||||
|
color="primary" |
||||
|
style="text-transform: none" |
||||
|
outlined |
||||
|
@click="saveAndAnother" |
||||
|
> |
||||
|
Save and add another |
||||
|
</v-btn> |
||||
|
</form-create> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import Vue from 'vue' |
||||
|
import { LabelDTO } from '~/services/application/label/labelData' |
||||
|
import { ProjectDTO } from '~/services/application/project/projectData' |
||||
|
import FormCreate from '~/components/label/FormCreate.vue' |
||||
|
|
||||
|
export default Vue.extend({ |
||||
|
components: { |
||||
|
FormCreate, |
||||
|
}, |
||||
|
|
||||
|
layout: 'project', |
||||
|
|
||||
|
validate({ params, query, app }) { |
||||
|
if (!['category', 'span'].includes((query.type as string))) { |
||||
|
return false |
||||
|
} |
||||
|
if (/^\d+$/.test(params.id)) { |
||||
|
return app.$services.project.findById(params.id) |
||||
|
.then((res:ProjectDTO) => { |
||||
|
return res.canDefineLabel |
||||
|
}) |
||||
|
} |
||||
|
return false |
||||
|
}, |
||||
|
|
||||
|
data() { |
||||
|
return { |
||||
|
editedItem: { |
||||
|
text: '', |
||||
|
prefixKey: null, |
||||
|
suffixKey: null, |
||||
|
backgroundColor: '#73D8FF', |
||||
|
textColor: '#ffffff' |
||||
|
} as LabelDTO, |
||||
|
defaultItem: { |
||||
|
text: '', |
||||
|
prefixKey: null, |
||||
|
suffixKey: null, |
||||
|
backgroundColor: '#73D8FF', |
||||
|
textColor: '#ffffff' |
||||
|
} as LabelDTO, |
||||
|
items: [] as LabelDTO[] |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
computed: { |
||||
|
projectId(): string { |
||||
|
return this.$route.params.id |
||||
|
}, |
||||
|
|
||||
|
service(): any { |
||||
|
const type = this.$route.query.type |
||||
|
if (type === 'category') { |
||||
|
return this.$services.categoryType |
||||
|
} else { |
||||
|
return this.$services.spanType |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
|
||||
|
async created() { |
||||
|
this.items = await this.service.list(this.projectId) |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
async save() { |
||||
|
await this.service.create(this.projectId, this.editedItem) |
||||
|
this.$router.push(`/projects/${this.projectId}/labels`) |
||||
|
}, |
||||
|
|
||||
|
async saveAndAnother() { |
||||
|
await this.service.create(this.projectId, this.editedItem) |
||||
|
this.editedItem = Object.assign({}, this.defaultItem) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
</script> |
Write
Preview
Loading…
Cancel
Save