diff --git a/frontend/components/label/ActionMenu.vue b/frontend/components/label/ActionMenu.vue index 229ce73f..d7239d09 100644 --- a/frontend/components/label/ActionMenu.vue +++ b/frontend/components/label/ActionMenu.vue @@ -18,25 +18,38 @@ export default Vue.extend({ ActionMenu }, + props: { + addOnly: { + type: Boolean, + default: false + } + }, + computed: { items() { - return [ + const items = [ { title: this.$t('labels.createLabel'), icon: mdiPencil, 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' + } + ]) + } } } }) diff --git a/frontend/components/label/LabelList.vue b/frontend/components/label/LabelList.vue index a6acf418..ece9c14c 100644 --- a/frontend/components/label/LabelList.vue +++ b/frontend/components/label/LabelList.vue @@ -64,6 +64,10 @@ export default Vue.extend({ type: Array as PropType, default: () => [], required: true + }, + disableEdit: { + type: Boolean, + default: false } }, @@ -77,12 +81,15 @@ export default Vue.extend({ computed: { 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 } } }) diff --git a/frontend/components/layout/TheSideBar.vue b/frontend/components/layout/TheSideBar.vue index 6d3fb8fd..801ab859 100644 --- a/frontend/components/layout/TheSideBar.vue +++ b/frontend/components/layout/TheSideBar.vue @@ -81,13 +81,17 @@ export default { icon: mdiLabel, text: this.$t('labels.labels'), link: 'labels', - isVisible: this.isProjectAdmin && this.project.canDefineLabel + isVisible: + (this.isProjectAdmin || this.project.allowMemberToCreateLabelType) && + this.project.canDefineLabel }, { icon: mdiLabel, text: 'Relations', link: 'links', - isVisible: this.isProjectAdmin && this.project.canDefineRelation + isVisible: + (this.isProjectAdmin || this.project.allowMemberToCreateLabelType) && + this.project.canDefineRelation }, { icon: mdiAccount, diff --git a/frontend/components/project/FormUpdate.vue b/frontend/components/project/FormUpdate.vue index 69ae4386..693cf9c0 100644 --- a/frontend/components/project/FormUpdate.vue +++ b/frontend/components/project/FormUpdate.vue @@ -9,33 +9,31 @@ + - + + Edit + - + > + {{ $t('generic.save') }} + + + {{ $t('generic.cancel') }} + diff --git a/frontend/domain/models/project/project.ts b/frontend/domain/models/project/project.ts index ee2bb5a4..f7b0e98a 100644 --- a/frontend/domain/models/project/project.ts +++ b/frontend/domain/models/project/project.ts @@ -33,6 +33,24 @@ export const validateNameMaxLength = (name: string): boolean => { 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 { name: string description: string @@ -50,6 +68,7 @@ export class Project { readonly enableGraphemeMode: boolean, readonly useRelation: boolean, readonly tags: TagItem[], + readonly allowMemberToCreateLabelType: boolean = false, readonly users: number[] = [], readonly createdAt: string = '', readonly updatedAt: string = '', @@ -85,7 +104,8 @@ export class Project { allowOverlappingSpans: boolean, enableGraphemeMode: boolean, useRelation: boolean, - tags: TagItem[] + tags: TagItem[], + allowMemberToCreateLabelType: boolean ) { return new Project( id, @@ -99,26 +119,21 @@ export class Project { allowOverlappingSpans, enableGraphemeMode, useRelation, - tags + tags, + allowMemberToCreateLabelType ) } get canDefineLabel(): boolean { - return this.canDefineCategory || this.canDefineSpan + return canDefineLabel(this.projectType) } get canDefineCategory(): boolean { - return [ - DocumentClassification, - IntentDetectionAndSlotFilling, - ImageClassification, - BoundingBox, - Segmentation - ].includes(this.projectType) + return canDefineCategory(this.projectType) } get canDefineSpan(): boolean { - return [SequenceLabeling, IntentDetectionAndSlotFilling].includes(this.projectType) + return canDefineSpan(this.projectType) } get canDefineRelation(): boolean { diff --git a/frontend/layouts/project.vue b/frontend/layouts/project.vue index 84e95fd1..2642eb12 100644 --- a/frontend/layouts/project.vue +++ b/frontend/layouts/project.vue @@ -32,7 +32,8 @@ export default { TheSideBar, TheHeader }, - middleware: ['check-auth', 'auth', 'check-admin'], + + middleware: ['check-auth', 'auth', 'setCurrentProject'], data() { return { diff --git a/frontend/middleware/isProjectAdmin.ts b/frontend/middleware/isProjectAdmin.ts index 612f423c..76e8e189 100644 --- a/frontend/middleware/isProjectAdmin.ts +++ b/frontend/middleware/isProjectAdmin.ts @@ -3,10 +3,8 @@ import _ from 'lodash' export default _.debounce(async ({ app, route, redirect }: NuxtAppOptions) => { 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)) } }, 1000) diff --git a/frontend/pages/projects/_id/comments/index.vue b/frontend/pages/projects/_id/comments/index.vue index 7e669a17..9fe2434a 100644 --- a/frontend/pages/projects/_id/comments/index.vue +++ b/frontend/pages/projects/_id/comments/index.vue @@ -39,8 +39,11 @@ export default Vue.extend({ CommentList, FormDelete }, + layout: 'project', + middleware: ['isProjectAdmin'], + validate({ params }) { return /^\d+$/.test(params.id) }, diff --git a/frontend/pages/projects/_id/dataset/_example_id/edit.vue b/frontend/pages/projects/_id/dataset/_example_id/edit.vue index a115ef8c..79094ed8 100644 --- a/frontend/pages/projects/_id/dataset/_example_id/edit.vue +++ b/frontend/pages/projects/_id/dataset/_example_id/edit.vue @@ -27,6 +27,8 @@ import { ExampleDTO } from '~/services/application/example/exampleData' export default Vue.extend({ layout: 'project', + middleware: ['isProjectAdmin'], + validate({ params, app }) { if (/^\d+$/.test(params.id) && /^\d+$/.test(params.example_id)) { return app.$services.project.findById(params.id).then((res: Project) => { diff --git a/frontend/pages/projects/_id/dataset/export.vue b/frontend/pages/projects/_id/dataset/export.vue index 40edc075..2c113292 100644 --- a/frontend/pages/projects/_id/dataset/export.vue +++ b/frontend/pages/projects/_id/dataset/export.vue @@ -44,6 +44,8 @@ import { Format } from '~/domain/models/download/format' export default Vue.extend({ layout: 'project', + middleware: ['isProjectAdmin'], + validate({ params }) { return /^\d+$/.test(params.id) }, diff --git a/frontend/pages/projects/_id/dataset/import.vue b/frontend/pages/projects/_id/dataset/import.vue index aeb8b131..3e39d452 100644 --- a/frontend/pages/projects/_id/dataset/import.vue +++ b/frontend/pages/projects/_id/dataset/import.vue @@ -105,6 +105,8 @@ export default { layout: 'project', + middleware: ['isProjectAdmin'], + validate({ params }) { return /^\d+$/.test(params.id) }, diff --git a/frontend/pages/projects/_id/guideline/index.vue b/frontend/pages/projects/_id/guideline/index.vue index 47fbcebf..23fcfba8 100644 --- a/frontend/pages/projects/_id/guideline/index.vue +++ b/frontend/pages/projects/_id/guideline/index.vue @@ -24,6 +24,8 @@ export default { layout: 'project', + middleware: ['isProjectAdmin'], + validate({ params }) { return /^\d+$/.test(params.id) }, diff --git a/frontend/pages/projects/_id/labels/_label_id/edit.vue b/frontend/pages/projects/_id/labels/_label_id/edit.vue index f8c34f4e..f833cf5d 100644 --- a/frontend/pages/projects/_id/labels/_label_id/edit.vue +++ b/frontend/pages/projects/_id/labels/_label_id/edit.vue @@ -19,6 +19,8 @@ export default Vue.extend({ layout: 'project', + middleware: ['isProjectAdmin'], + validate({ params, query, app }) { if (!['category', 'span', 'relation'].includes(query.type as string)) { return false diff --git a/frontend/pages/projects/_id/labels/import.vue b/frontend/pages/projects/_id/labels/import.vue index 0c8f98f6..22cd8df9 100644 --- a/frontend/pages/projects/_id/labels/import.vue +++ b/frontend/pages/projects/_id/labels/import.vue @@ -14,6 +14,8 @@ export default Vue.extend({ layout: 'project', + middleware: ['isProjectAdmin'], + validate({ params, query, app }) { if (!['category', 'span', 'relation'].includes(query.type as string)) { return false diff --git a/frontend/pages/projects/_id/labels/index.vue b/frontend/pages/projects/_id/labels/index.vue index 6bb0825e..2cb6a538 100644 --- a/frontend/pages/projects/_id/labels/index.vue +++ b/frontend/pages/projects/_id/labels/index.vue @@ -12,11 +12,13 @@ - + @@ -39,6 +47,7 @@ import FormDelete from '@/components/label/FormDelete.vue' import LabelList from '@/components/label/LabelList.vue' import { Project } from '~/domain/models/project/project' import { LabelDTO } from '~/services/application/label/labelData' +import { MemberItem } from '~/domain/models/member/member' export default Vue.extend({ components: { @@ -46,12 +55,21 @@ export default Vue.extend({ FormDelete, LabelList }, + layout: 'project', validate({ params, app }) { 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 @@ -64,11 +82,19 @@ export default Vue.extend({ selected: [] as LabelDTO[], isLoading: false, tab: 0, - project: {} as Project + project: {} as Project, + member: {} as MemberItem } }, computed: { + canOnlyAdd(): boolean { + if (this.member.isProjectAdmin) { + return false + } + return this.project.allowMemberToCreateLabelType + }, + canDelete(): boolean { return this.selected.length > 0 }, @@ -129,6 +155,7 @@ export default Vue.extend({ async created() { this.project = await this.$services.project.findById(this.projectId) + this.member = await this.$repositories.member.fetchMyRole(this.projectId) await this.list() }, diff --git a/frontend/pages/projects/_id/members/index.vue b/frontend/pages/projects/_id/members/index.vue index 97b59723..a957781b 100644 --- a/frontend/pages/projects/_id/members/index.vue +++ b/frontend/pages/projects/_id/members/index.vue @@ -41,8 +41,11 @@ export default Vue.extend({ FormCreate, FormDelete }, + layout: 'project', + middleware: ['isProjectAdmin'], + validate({ params }) { return /^\d+$/.test(params.id) }, diff --git a/frontend/pages/projects/_id/metrics/index.vue b/frontend/pages/projects/_id/metrics/index.vue index df8e57d7..98c45aa8 100644 --- a/frontend/pages/projects/_id/metrics/index.vue +++ b/frontend/pages/projects/_id/metrics/index.vue @@ -39,6 +39,8 @@ export default { layout: 'project', + middleware: ['isProjectAdmin'], + validate({ params }) { return /^\d+$/.test(params.id) }, diff --git a/frontend/pages/projects/_id/settings/index.vue b/frontend/pages/projects/_id/settings/index.vue index b0bc3527..017d5bb8 100644 --- a/frontend/pages/projects/_id/settings/index.vue +++ b/frontend/pages/projects/_id/settings/index.vue @@ -28,8 +28,11 @@ export default Vue.extend({ ConfigList, FormUpdate }, + layout: 'project', + middleware: ['isProjectAdmin'], + validate({ params }) { return /^\d+$/.test(params.id) }, diff --git a/frontend/pages/projects/create.vue b/frontend/pages/projects/create.vue index c05aa8cb..5b3c073b 100644 --- a/frontend/pages/projects/create.vue +++ b/frontend/pages/projects/create.vue @@ -12,6 +12,11 @@ v-model="editedItem.exclusiveCategories" :label="$t('overview.allowSingleLabel')" /> + @@ -71,7 +77,8 @@ import TagList from '~/components/project/TagList.vue' import { DocumentClassification, ImageClassification, - SequenceLabeling + SequenceLabeling, + canDefineLabel } from '~/domain/models/project/project' const initializeProject = () => { @@ -86,7 +93,8 @@ const initializeProject = () => { enableGraphemeMode: false, useRelation: false, tags: [] as string[], - guideline: '' + guideline: '', + allowMemberToCreateLabelType: false } } @@ -117,6 +125,9 @@ export default Vue.extend({ }, isSequenceLabelingProject(): boolean { return this.editedItem.projectType === SequenceLabeling + }, + _canDefineLabel(): boolean { + return canDefineLabel(this.editedItem.projectType as any) } }, diff --git a/frontend/repositories/project/apiProjectRepository.ts b/frontend/repositories/project/apiProjectRepository.ts index 5489dc43..17081612 100644 --- a/frontend/repositories/project/apiProjectRepository.ts +++ b/frontend/repositories/project/apiProjectRepository.ts @@ -38,6 +38,7 @@ function toModel(item: { [key: string]: any }): Project { item.grapheme_mode, item.use_relation, 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.created_at, item.updated_at, @@ -60,6 +61,7 @@ function toPayload(item: Project): { [key: string]: any } { grapheme_mode: item.enableGraphemeMode, use_relation: item.useRelation, tags: item.tags, + allow_member_to_create_label_type: item.allowMemberToCreateLabelType, resourcetype: item.resourceType } } diff --git a/frontend/store/projects.js b/frontend/store/projects.js index 124a8097..f3b4a558 100644 --- a/frontend/store/projects.js +++ b/frontend/store/projects.js @@ -6,12 +6,9 @@ export const getters = { currentProject(state) { 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 } }