<template> <v-dialog v-model="dialog" persistent max-width="1200" overlay-color="white" content-class="rounded-xl" > <template v-slot:activator="{ on, attrs }"> <v-btn v-model="fab" fab style="margin-top: -60px; margin-bottom: -40px" color="primary" class="elevation-3" v-bind="attrs" v-on="on" :disabled="finished" > <v-icon large>mdi-plus</v-icon> </v-btn> </template> <v-card> <v-card-title class="accent white--text pt-10 pb-4 text-h5 pl-9"> Define Labels </v-card-title> <v-card-text> <v-container fluid> <v-form ref="form" v-model="form.valid" lazy-validation> <v-row no-gutters class="text--primary body-2 my-5"> <v-col cols="12"> Please select predefined labels (e.g. Design, Business, Tech,...) that are relevant for your next prototyping cycle or add custom labels: </v-col> </v-row> <v-row> <v-col cols="12"> <div class="text-h5 text--primary mb-3">Select or Create Labels</div> <v-combobox v-model="form.selectedLabels" :items="form.labelOptions" :search-input.sync="form.search" hint="For already existing labels, backlog tasks will be appended" persistent-hint label="Select a label or create a custom label..." return-object multiple outlined chips deletable-chips maxlength="60" :rules="[ value => value && form.selectedLabels.length > 0 || 'At least one label is required', value => !!value || 'Label is required.' ]" > <template v-slot:no-data> <v-list-item> <v-list-item-content> <v-list-item-title> No results matching "<strong>{{ form.search }}</strong>". Press <kbd>enter</kbd> to create a new one </v-list-item-title> </v-list-item-content> </v-list-item> </template> </v-combobox> </v-col> </v-row> </v-form> </v-container> </v-card-text> <v-card-actions class="pa-10"> <v-btn text @click="onClose" > Cancel </v-btn> <v-spacer></v-spacer> <v-btn color="primary" min-width="150" depressed @click="onSubmit" :loading="loading" > Continue </v-btn> </v-card-actions> </v-card> </v-dialog> </template> <script> import {mapGetters} from "vuex"; export default { name: "AddLabels", props: ['labels', 'finished', 'cycle'], data: () => ({ cycleId: null, backlogLabels: [], nextLabelId: 4, newLabelName: 'label 1', showAlert: false, dialog: false, loading: false, fab: false, form: { search: null, valid: true, selectedLabels: [], labelOptions: [ { header: 'Select labels from default stack' }, { text: 'User & Marketing (Default)', name: 'User & Marketing', }, { text: 'Tech & Design (Default)', name: 'Tech & Design', }, { text: 'Business (Default)', name: 'Business', }, ], }, }), methods: { onSubmit() { if (this.$refs.form.validate()) { this.loading = true this.mergeSelectedLabels.forEach(label => { if (!label.name) { // label comes from user defined input i.e. label is a single string input label = {name: label} } this.$http.post('/board/label', { cycle_id: this.cycleId, id: label.id, // backlog tasks already have an ID text: label.text, name: label.name, finish: label.finish, }).then(response => { this.$store.dispatch('site/addLabel', { id: response.data.label.id, name: label.name, tasks: response.data.label.tasks, // list of tasks if backlog o.w. it is an empty list finish: !!response.data.label.finish, purpose: response.data.label.purpose, rating: response.data.label.rating, }).then(() => { // or delete entire selectedLabels and labelOptions in the end? this.form.selectedLabels = this.form.selectedLabels.filter(m => m.name !== label.name) this.$refs.form.reset() // this.form.labelOptions = this.form.labelOptions.filter(m => m.name !== label.name) if (response.data.message) { this.$snackbar.showMessage({ content: response.data.message, color: 'warning' }) } else { this.$snackbar.showMessage({ content: 'Labels added and saved!' }) } }) }).catch(() => { this.$snackbar.showMessage({ content: 'Something went wrong, please try again!', color: 'warning' }) }) }) this.onClose(); } this.loading = false }, onClose() { this.dialog = false this.$refs.form.reset() this.form.selectedLabels = [] } }, computed: { ...mapGetters('site', ['getBoard']), mergeSelectedLabels() { // if label with name 'abc' has backlog tasks and one adds label 'abc' from default stack // the label is sent twice to the backend on submit and thus appears twice in database. // Solving this by removing duplicates in the frontend: sort by backlog > default and // merge by name preserving backlog tasks. let sorted = [...this.form.selectedLabels].sort((a,b) => (b.id ? 1 : -1)) return sorted .map(e => e['name']) .map((e, i, final) => final.indexOf(e) === i && i) .filter(obj=> sorted[obj]) .map(e => sorted[e]); } }, watch: { dialog(visible) { if (visible) { // dialog was opened -> load backlog this.cycleId = this.getBoard.id this.$http.get(`/board/backlog?cycle-id=${this.cycleId}`).then(res => { // Requirements let requirements = [] if (['Requirements Selection', 'Function Exploration', 'Embodiment Design'].includes(this.cycle.phase)) requirements = Object.values(res.data.requirements) requirements.forEach(requirement => { this.form.labelOptions.unshift({ id: requirement.id, name: requirement.name, text: `${requirement.name} (${requirement.purpose.length} ${requirement.purpose.length > 1 ? 'purposes' : 'purpose'} in requirement)`, tasks: [], finish: requirement.finish }) }) if (requirements.length > 0) { this.form.labelOptions.unshift({ header: 'Select labels from requirement list', }) } // Backlog let backlog = Object.values(res.data.backlog) backlog.forEach(label => { this.form.labelOptions.unshift({ id: label.id, cycleId: label.cycle_id, name: label.name, // description: label.description, text: `${label.name} (${label.tasks.length} ${label.tasks.length > 1 ? 'tasks' : 'task'} in backlog)`, tasks: label.tasks, }) }) if (backlog.length > 0) { // add header this.form.labelOptions.unshift({ header: 'Select labels with backlog tasks', }) } }) } else { // dialog was closed -> do nothing } } }, mounted() { ////////////////////////// Apply if condition like in Kanban.vue and move to computed! // this.form.labelOptions.push( // { // header: 'Select labels from default stack' // }) // let labelCandidates = [ // { // text: 'User & Marketing (Default)', // name: 'User & Marketing', // color: 'teal lighten-4', // source: 'default', // }, // { // text: 'Tech & Design (Default)', // name: 'Tech & Design', // color: 'orange lighten-4', // source: 'default', // }, // { // text: 'Business (Default)', // name: 'Business', // color: 'lime lighten-4', // source: 'default', // }, // ] // console.log(this.labels) // if (this.labels.length === 0) { // console.log('a') // // labels are empty, add all candidates // this.form.labelOptions.push(...labelCandidates) // } else { // console.log('b') // // at least one label is in list // labelCandidates.forEach(candidate => { // // check for duplicates // if (this.labels.length && this.checkForDuplicates(candidate.name)) { // // label name not in list of available labels thus add to dropdown // this.form.labelOptions.push(candidate) // } // }) // } }, } </script> <style lang="scss" scoped> @import '~vuetify/src/styles/settings/_variables'; .add-label { @media #{map-get($display-breakpoints, 'lg-and-up')} { margin-right: 300px } } </style>