Browse Source

Allow member to create label types

pull/2207/head
Hironsan 2 years ago
parent
commit
85c5aa8887
21 changed files with 159 additions and 63 deletions
  1. 35
      frontend/components/label/ActionMenu.vue
  2. 17
      frontend/components/label/LabelList.vue
  3. 8
      frontend/components/layout/TheSideBar.vue
  4. 30
      frontend/components/project/FormUpdate.vue
  5. 37
      frontend/domain/models/project/project.ts
  6. 3
      frontend/layouts/project.vue
  7. 4
      frontend/middleware/isProjectAdmin.ts
  8. 3
      frontend/pages/projects/_id/comments/index.vue
  9. 2
      frontend/pages/projects/_id/dataset/_example_id/edit.vue
  10. 2
      frontend/pages/projects/_id/dataset/export.vue
  11. 2
      frontend/pages/projects/_id/dataset/import.vue
  12. 2
      frontend/pages/projects/_id/guideline/index.vue
  13. 2
      frontend/pages/projects/_id/labels/_label_id/edit.vue
  14. 2
      frontend/pages/projects/_id/labels/import.vue
  15. 35
      frontend/pages/projects/_id/labels/index.vue
  16. 3
      frontend/pages/projects/_id/members/index.vue
  17. 2
      frontend/pages/projects/_id/metrics/index.vue
  18. 3
      frontend/pages/projects/_id/settings/index.vue
  19. 19
      frontend/pages/projects/create.vue
  20. 2
      frontend/repositories/project/apiProjectRepository.ts
  21. 9
      frontend/store/projects.js

35
frontend/components/label/ActionMenu.vue

@ -18,25 +18,38 @@ export default Vue.extend({
ActionMenu ActionMenu
}, },
props: {
addOnly: {
type: Boolean,
default: false
}
},
computed: { computed: {
items() { items() {
return [
const items = [
{ {
title: this.$t('labels.createLabel'), title: this.$t('labels.createLabel'),
icon: mdiPencil, icon: mdiPencil,
event: 'create' event: 'create'
},
{
title: this.$t('labels.importLabels'),
icon: mdiUpload,
event: 'upload'
},
{
title: this.$t('labels.exportLabels'),
icon: mdiDownload,
event: 'download'
} }
] ]
if (this.addOnly) {
return items
} else {
return items.concat([
{
title: this.$t('labels.importLabels'),
icon: mdiUpload,
event: 'upload'
},
{
title: this.$t('labels.exportLabels'),
icon: mdiDownload,
event: 'download'
}
])
}
} }
} }
}) })

17
frontend/components/label/LabelList.vue

@ -64,6 +64,10 @@ export default Vue.extend({
type: Array as PropType<LabelDTO[]>, type: Array as PropType<LabelDTO[]>,
default: () => [], default: () => [],
required: true required: true
},
disableEdit: {
type: Boolean,
default: false
} }
}, },
@ -77,12 +81,15 @@ export default Vue.extend({
computed: { computed: {
headers() { headers() {
return [
{ text: this.$t('generic.name'), value: 'text' },
{ text: this.$t('labels.shortkey'), value: 'suffixKey' },
{ text: this.$t('labels.color'), value: 'backgroundColor' },
{ text: 'Actions', value: 'actions', sortable: false }
const headers = [
{ text: this.$t('generic.name'), value: 'text', sortable: true },
{ text: this.$t('labels.shortkey'), value: 'suffixKey', sortable: true },
{ text: this.$t('labels.color'), value: 'backgroundColor', sortable: true }
] ]
if (!this.disableEdit) {
headers.push({ text: 'Actions', value: 'actions', sortable: false })
}
return headers
} }
} }
}) })

8
frontend/components/layout/TheSideBar.vue

@ -81,13 +81,17 @@ export default {
icon: mdiLabel, icon: mdiLabel,
text: this.$t('labels.labels'), text: this.$t('labels.labels'),
link: 'labels', link: 'labels',
isVisible: this.isProjectAdmin && this.project.canDefineLabel
isVisible:
(this.isProjectAdmin || this.project.allowMemberToCreateLabelType) &&
this.project.canDefineLabel
}, },
{ {
icon: mdiLabel, icon: mdiLabel,
text: 'Relations', text: 'Relations',
link: 'links', link: 'links',
isVisible: this.isProjectAdmin && this.project.canDefineRelation
isVisible:
(this.isProjectAdmin || this.project.allowMemberToCreateLabelType) &&
this.project.canDefineRelation
}, },
{ {
icon: mdiAccount, icon: mdiAccount,

30
frontend/components/project/FormUpdate.vue

@ -9,33 +9,31 @@
<tag-list v-model="tags" /> <tag-list v-model="tags" />
<random-order-field v-model="project.enableRandomOrder" /> <random-order-field v-model="project.enableRandomOrder" />
<sharing-mode-field v-model="project.enableSharingMode" /> <sharing-mode-field v-model="project.enableSharingMode" />
<v-checkbox
v-if="project.canDefineLabel"
v-model="project.allowMemberToCreateLabelType"
label="Allow project members to create label types"
/>
</v-col> </v-col>
</v-row> </v-row>
</v-form> </v-form>
</v-card-text> </v-card-text>
<v-card-actions class="ps-4 pt-0"> <v-card-actions class="ps-4 pt-0">
<v-btn
v-if="!isEditing"
color="primary"
class="text-capitalize"
@click="isEditing = true"
v-text="`Edit`"
/>
<v-btn v-if="!isEditing" color="primary" class="text-capitalize" @click="isEditing = true">
Edit
</v-btn>
<v-btn <v-btn
v-show="isEditing" v-show="isEditing"
color="primary" color="primary"
:disabled="!valid || isUpdating" :disabled="!valid || isUpdating"
class="mr-4 text-capitalize" class="mr-4 text-capitalize"
@click="save" @click="save"
v-text="$t('generic.save')"
/>
<v-btn
v-show="isEditing"
:disabled="isUpdating"
class="text-capitalize"
@click="cancel"
v-text="$t('generic.cancel')"
/>
>
{{ $t('generic.save') }}
</v-btn>
<v-btn v-show="isEditing" :disabled="isUpdating" class="text-capitalize" @click="cancel">
{{ $t('generic.cancel') }}
</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</template> </template>

37
frontend/domain/models/project/project.ts

@ -33,6 +33,24 @@ export const validateNameMaxLength = (name: string): boolean => {
return name.trim().length <= MAX_PROJECT_NAME_LENGTH return name.trim().length <= MAX_PROJECT_NAME_LENGTH
} }
export const canDefineCategory = (projectType: ProjectType): boolean => {
return [
DocumentClassification,
IntentDetectionAndSlotFilling,
ImageClassification,
BoundingBox,
Segmentation
].includes(projectType)
}
export const canDefineSpan = (projectType: ProjectType): boolean => {
return [SequenceLabeling, IntentDetectionAndSlotFilling].includes(projectType)
}
export const canDefineLabel = (projectType: ProjectType): boolean => {
return canDefineCategory(projectType) || canDefineSpan(projectType)
}
export class Project { export class Project {
name: string name: string
description: string description: string
@ -50,6 +68,7 @@ export class Project {
readonly enableGraphemeMode: boolean, readonly enableGraphemeMode: boolean,
readonly useRelation: boolean, readonly useRelation: boolean,
readonly tags: TagItem[], readonly tags: TagItem[],
readonly allowMemberToCreateLabelType: boolean = false,
readonly users: number[] = [], readonly users: number[] = [],
readonly createdAt: string = '', readonly createdAt: string = '',
readonly updatedAt: string = '', readonly updatedAt: string = '',
@ -85,7 +104,8 @@ export class Project {
allowOverlappingSpans: boolean, allowOverlappingSpans: boolean,
enableGraphemeMode: boolean, enableGraphemeMode: boolean,
useRelation: boolean, useRelation: boolean,
tags: TagItem[]
tags: TagItem[],
allowMemberToCreateLabelType: boolean
) { ) {
return new Project( return new Project(
id, id,
@ -99,26 +119,21 @@ export class Project {
allowOverlappingSpans, allowOverlappingSpans,
enableGraphemeMode, enableGraphemeMode,
useRelation, useRelation,
tags
tags,
allowMemberToCreateLabelType
) )
} }
get canDefineLabel(): boolean { get canDefineLabel(): boolean {
return this.canDefineCategory || this.canDefineSpan
return canDefineLabel(this.projectType)
} }
get canDefineCategory(): boolean { get canDefineCategory(): boolean {
return [
DocumentClassification,
IntentDetectionAndSlotFilling,
ImageClassification,
BoundingBox,
Segmentation
].includes(this.projectType)
return canDefineCategory(this.projectType)
} }
get canDefineSpan(): boolean { get canDefineSpan(): boolean {
return [SequenceLabeling, IntentDetectionAndSlotFilling].includes(this.projectType)
return canDefineSpan(this.projectType)
} }
get canDefineRelation(): boolean { get canDefineRelation(): boolean {

3
frontend/layouts/project.vue

@ -32,7 +32,8 @@ export default {
TheSideBar, TheSideBar,
TheHeader TheHeader
}, },
middleware: ['check-auth', 'auth', 'check-admin'],
middleware: ['check-auth', 'auth', 'setCurrentProject'],
data() { data() {
return { return {

4
frontend/middleware/isProjectAdmin.ts

@ -3,10 +3,8 @@ import _ from 'lodash'
export default _.debounce(async ({ app, route, redirect }: NuxtAppOptions) => { export default _.debounce(async ({ app, route, redirect }: NuxtAppOptions) => {
const member = await app.$repositories.member.fetchMyRole(route.params.id) const member = await app.$repositories.member.fetchMyRole(route.params.id)
const projectRoot = app.localePath('/projects/' + route.params.id)
const path = route.fullPath.replace(/\/$/g, '')
if (!member.isProjectAdmin && path !== projectRoot) {
if (!member.isProjectAdmin) {
return redirect(app.localePath('/projects/' + route.params.id)) return redirect(app.localePath('/projects/' + route.params.id))
} }
}, 1000) }, 1000)

3
frontend/pages/projects/_id/comments/index.vue

@ -39,8 +39,11 @@ export default Vue.extend({
CommentList, CommentList,
FormDelete FormDelete
}, },
layout: 'project', layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params }) { validate({ params }) {
return /^\d+$/.test(params.id) return /^\d+$/.test(params.id)
}, },

2
frontend/pages/projects/_id/dataset/_example_id/edit.vue

@ -27,6 +27,8 @@ import { ExampleDTO } from '~/services/application/example/exampleData'
export default Vue.extend({ export default Vue.extend({
layout: 'project', layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params, app }) { validate({ params, app }) {
if (/^\d+$/.test(params.id) && /^\d+$/.test(params.example_id)) { if (/^\d+$/.test(params.id) && /^\d+$/.test(params.example_id)) {
return app.$services.project.findById(params.id).then((res: Project) => { return app.$services.project.findById(params.id).then((res: Project) => {

2
frontend/pages/projects/_id/dataset/export.vue

@ -44,6 +44,8 @@ import { Format } from '~/domain/models/download/format'
export default Vue.extend({ export default Vue.extend({
layout: 'project', layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params }) { validate({ params }) {
return /^\d+$/.test(params.id) return /^\d+$/.test(params.id)
}, },

2
frontend/pages/projects/_id/dataset/import.vue

@ -105,6 +105,8 @@ export default {
layout: 'project', layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params }) { validate({ params }) {
return /^\d+$/.test(params.id) return /^\d+$/.test(params.id)
}, },

2
frontend/pages/projects/_id/guideline/index.vue

@ -24,6 +24,8 @@ export default {
layout: 'project', layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params }) { validate({ params }) {
return /^\d+$/.test(params.id) return /^\d+$/.test(params.id)
}, },

2
frontend/pages/projects/_id/labels/_label_id/edit.vue

@ -19,6 +19,8 @@ export default Vue.extend({
layout: 'project', layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params, query, app }) { validate({ params, query, app }) {
if (!['category', 'span', 'relation'].includes(query.type as string)) { if (!['category', 'span', 'relation'].includes(query.type as string)) {
return false return false

2
frontend/pages/projects/_id/labels/import.vue

@ -14,6 +14,8 @@ export default Vue.extend({
layout: 'project', layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params, query, app }) { validate({ params, query, app }) {
if (!['category', 'span', 'relation'].includes(query.type as string)) { if (!['category', 'span', 'relation'].includes(query.type as string)) {
return false return false

35
frontend/pages/projects/_id/labels/index.vue

@ -12,11 +12,13 @@
</v-tabs> </v-tabs>
<v-card-title> <v-card-title>
<action-menu <action-menu
:add-only="canOnlyAdd"
@create="$router.push('labels/add?type=' + labelType)" @create="$router.push('labels/add?type=' + labelType)"
@upload="$router.push('labels/import?type=' + labelType)" @upload="$router.push('labels/import?type=' + labelType)"
@download="download" @download="download"
/> />
<v-btn <v-btn
v-if="!canOnlyAdd"
class="text-capitalize ms-2" class="text-capitalize ms-2"
:disabled="!canDelete" :disabled="!canDelete"
outlined outlined
@ -28,7 +30,13 @@
<form-delete :selected="selected" @cancel="dialogDelete = false" @remove="remove" /> <form-delete :selected="selected" @cancel="dialogDelete = false" @remove="remove" />
</v-dialog> </v-dialog>
</v-card-title> </v-card-title>
<label-list v-model="selected" :items="items" :is-loading="isLoading" @edit="editItem" />
<label-list
v-model="selected"
:items="items"
:is-loading="isLoading"
:disable-edit="canOnlyAdd"
@edit="editItem"
/>
</v-card> </v-card>
</template> </template>
@ -39,6 +47,7 @@ import FormDelete from '@/components/label/FormDelete.vue'
import LabelList from '@/components/label/LabelList.vue' import LabelList from '@/components/label/LabelList.vue'
import { Project } from '~/domain/models/project/project' import { Project } from '~/domain/models/project/project'
import { LabelDTO } from '~/services/application/label/labelData' import { LabelDTO } from '~/services/application/label/labelData'
import { MemberItem } from '~/domain/models/member/member'
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -46,12 +55,21 @@ export default Vue.extend({
FormDelete, FormDelete,
LabelList LabelList
}, },
layout: 'project', layout: 'project',
validate({ params, app }) { validate({ params, app }) {
if (/^\d+$/.test(params.id)) { if (/^\d+$/.test(params.id)) {
return app.$services.project.findById(params.id).then((res: Project) => {
return res.canDefineLabel
return app.$services.project.findById(params.id).then((project: Project) => {
if (!project.canDefineLabel) {
return false
}
return app.$repositories.member.fetchMyRole(params.id).then((member: MemberItem) => {
if (member.isProjectAdmin) {
return true
}
return project.allowMemberToCreateLabelType
})
}) })
} }
return false return false
@ -64,11 +82,19 @@ export default Vue.extend({
selected: [] as LabelDTO[], selected: [] as LabelDTO[],
isLoading: false, isLoading: false,
tab: 0, tab: 0,
project: {} as Project
project: {} as Project,
member: {} as MemberItem
} }
}, },
computed: { computed: {
canOnlyAdd(): boolean {
if (this.member.isProjectAdmin) {
return false
}
return this.project.allowMemberToCreateLabelType
},
canDelete(): boolean { canDelete(): boolean {
return this.selected.length > 0 return this.selected.length > 0
}, },
@ -129,6 +155,7 @@ export default Vue.extend({
async created() { async created() {
this.project = await this.$services.project.findById(this.projectId) this.project = await this.$services.project.findById(this.projectId)
this.member = await this.$repositories.member.fetchMyRole(this.projectId)
await this.list() await this.list()
}, },

3
frontend/pages/projects/_id/members/index.vue

@ -41,8 +41,11 @@ export default Vue.extend({
FormCreate, FormCreate,
FormDelete FormDelete
}, },
layout: 'project', layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params }) { validate({ params }) {
return /^\d+$/.test(params.id) return /^\d+$/.test(params.id)
}, },

2
frontend/pages/projects/_id/metrics/index.vue

@ -39,6 +39,8 @@ export default {
layout: 'project', layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params }) { validate({ params }) {
return /^\d+$/.test(params.id) return /^\d+$/.test(params.id)
}, },

3
frontend/pages/projects/_id/settings/index.vue

@ -28,8 +28,11 @@ export default Vue.extend({
ConfigList, ConfigList,
FormUpdate FormUpdate
}, },
layout: 'project', layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params }) { validate({ params }) {
return /^\d+$/.test(params.id) return /^\d+$/.test(params.id)
}, },

19
frontend/pages/projects/create.vue

@ -12,6 +12,11 @@
v-model="editedItem.exclusiveCategories" v-model="editedItem.exclusiveCategories"
:label="$t('overview.allowSingleLabel')" :label="$t('overview.allowSingleLabel')"
/> />
<v-checkbox
v-if="_canDefineLabel"
v-model="editedItem.allowMemberToCreateLabelType"
label="Allow project members to create label types"
/>
<template v-if="isSequenceLabelingProject"> <template v-if="isSequenceLabelingProject">
<v-checkbox v-model="editedItem.allowOverlappingSpans" label="Allow overlapping spans" /> <v-checkbox v-model="editedItem.allowOverlappingSpans" label="Allow overlapping spans" />
<v-img <v-img
@ -54,8 +59,9 @@
style="text-transform: none" style="text-transform: none"
outlined outlined
@click="create" @click="create"
v-text="$t('generic.create')"
/>
>
{{ $t('generic.create') }}
</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</template> </template>
@ -71,7 +77,8 @@ import TagList from '~/components/project/TagList.vue'
import { import {
DocumentClassification, DocumentClassification,
ImageClassification, ImageClassification,
SequenceLabeling
SequenceLabeling,
canDefineLabel
} from '~/domain/models/project/project' } from '~/domain/models/project/project'
const initializeProject = () => { const initializeProject = () => {
@ -86,7 +93,8 @@ const initializeProject = () => {
enableGraphemeMode: false, enableGraphemeMode: false,
useRelation: false, useRelation: false,
tags: [] as string[], tags: [] as string[],
guideline: ''
guideline: '',
allowMemberToCreateLabelType: false
} }
} }
@ -117,6 +125,9 @@ export default Vue.extend({
}, },
isSequenceLabelingProject(): boolean { isSequenceLabelingProject(): boolean {
return this.editedItem.projectType === SequenceLabeling return this.editedItem.projectType === SequenceLabeling
},
_canDefineLabel(): boolean {
return canDefineLabel(this.editedItem.projectType as any)
} }
}, },

2
frontend/repositories/project/apiProjectRepository.ts

@ -38,6 +38,7 @@ function toModel(item: { [key: string]: any }): Project {
item.grapheme_mode, item.grapheme_mode,
item.use_relation, item.use_relation,
item.tags.map((tag: { [key: string]: any }) => new TagItem(tag.id, tag.text, tag.project)), item.tags.map((tag: { [key: string]: any }) => new TagItem(tag.id, tag.text, tag.project)),
item.allow_member_to_create_label_type,
item.users, item.users,
item.created_at, item.created_at,
item.updated_at, item.updated_at,
@ -60,6 +61,7 @@ function toPayload(item: Project): { [key: string]: any } {
grapheme_mode: item.enableGraphemeMode, grapheme_mode: item.enableGraphemeMode,
use_relation: item.useRelation, use_relation: item.useRelation,
tags: item.tags, tags: item.tags,
allow_member_to_create_label_type: item.allowMemberToCreateLabelType,
resourcetype: item.resourceType resourcetype: item.resourceType
} }
} }

9
frontend/store/projects.js

@ -6,12 +6,9 @@ export const getters = {
currentProject(state) { currentProject(state) {
return state.current return state.current
}, },
getCurrentUserRole(state) {
return state.current.current_users_role || {}
},
canViewApproveButton(state) {
const role = state.current.current_users_role
return role && !role.is_annotator
project(state) {
return state.current
} }
} }

Loading…
Cancel
Save