feat: create new card from smart picker

Signed-off-by: Luka Trovic <luka@nextcloud.com>
This commit is contained in:
Luka Trovic
2023-08-04 07:25:45 +02:00
committed by Julius Härtl
parent ba56687982
commit 12217afe65
16 changed files with 927 additions and 469 deletions

View File

@@ -0,0 +1,354 @@
<template>
<div class="modal-scroller">
<div v-if="!creating && !created" id="modal-inner" :class="{ 'icon-loading': loading }">
<h2>{{ t('deck', 'Create a new card') }}</h2>
<input ref="cardTitleInput"
v-model="card.title"
v-focus
type="text"
class="card-title"
:placeholder="t('deck', 'Card title')"
:disabled="loading">
<div class="row">
<div class="col selector-wrapper">
<div class="selector-wrapper--icon">
<DeckIcon :size="20" />
</div>
<NcMultiselect v-model="selectedBoard"
:placeholder="t('deck', 'Select a board')"
:options="boards"
:disabled="loading"
label="title"
class="selector-wrapper--selector multiselect-board"
@select="fetchBoardDetails">
<template slot="singleLabel" slot-scope="props">
<span>
<span :style="{ 'backgroundColor': '#' + props.option.color }" class="board-bullet" />
<span>{{ props.option.title }}</span>
</span>
</template>
<template slot="option" slot-scope="props">
<span>
<span :style="{ 'backgroundColor': '#' + props.option.color }" class="board-bullet" />
<span>{{ props.option.title }}</span>
</span>
</template>
</NcMultiselect>
</div>
<div class="col selector-wrapper">
<div class="selector-wrapper--icon">
<FormatColumnsIcon :size="20" />
</div>
<NcMultiselect v-model="selectedStack"
:placeholder="t('deck', 'Select a list')"
:options="stacksFromBoard"
:max-height="100"
:disabled="loading || !selectedBoard"
class="selector-wrapper--selector multiselect-list"
label="title" />
</div>
</div>
<TagSelector :card="card"
:labels="labels"
:disabled="loading || !selectedBoard"
@select="onSelectLabel"
@remove="onRemoveLabel"
@newtag="addLabelToBoardAndCard" />
<AssignmentSelector :card="card"
:assignables="assignables"
@select="onSelectUser"
@remove="onRemoveUser" />
<DueDateSelector :card="card" :can-edit="!loading && !!selectedBoard" @change="updateCardDue" />
<Description :key="card.id"
:card="card"
@change="descriptionChanged" />
<div class="modal-buttons">
<NcButton @click="close">
{{ t('deck', 'Cancel') }}
</NcButton>
<NcButton :disabled="loading || !isBoardAndStackChoosen"
type="primary"
@click="createCard">
{{ t('deck', 'Create card') }}
</NcButton>
</div>
</div>
<div v-else id="modal-inner">
<NcEmptyContent v-if="creating">
<template #icon>
<NcLoadingIcon />
</template>
<template #title>
{{ t('deck', 'Creating the new card …') }}
</template>
</NcEmptyContent>
<NcEmptyContent v-else-if="created && showCreatedNotice">
<template #icon>
<CardPlusOutline />
</template>
<template #title>
{{ t('deck', 'Card "{card}" was added to "{board}"', { card: card.title, board: selectedBoard.title }) }}
</template>
<template #action>
<button class="primary" @click="openNewCard">
{{ t('deck', 'Open card') }}
</button>
<button @click="close">
{{ t('deck', 'Close') }}
</button>
</template>
</NcEmptyContent>
</div>
</div>
</template>
<script>
import { generateUrl } from '@nextcloud/router'
import {
NcButton,
NcMultiselect,
NcEmptyContent,
NcLoadingIcon,
} from '@nextcloud/vue'
import axios from '@nextcloud/axios'
import { CardApi } from '../services/CardApi.js'
import Color from '../mixins/color.js'
import AssignmentSelector from '../components/card/AssignmentSelector.vue'
import TagSelector from '../components/card/TagSelector.vue'
import { BoardApi } from '../services/BoardApi.js'
import DueDateSelector from '../components/card/DueDateSelector.vue'
import Description from '../components/card/Description.vue'
import CardPlusOutline from 'vue-material-design-icons/CardPlusOutline.vue'
import FormatColumnsIcon from 'vue-material-design-icons/FormatColumns.vue'
import DeckIcon from '../components/icons/DeckIcon.vue'
const cardApi = new CardApi()
const apiClient = new BoardApi()
export default {
name: 'CreateNewCardCustomPicker',
components: {
DeckIcon,
FormatColumnsIcon,
CardPlusOutline,
Description,
DueDateSelector,
TagSelector,
AssignmentSelector,
NcButton,
NcMultiselect,
NcEmptyContent,
NcLoadingIcon,
},
mixins: [Color],
props: {
showCreatedNotice: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '',
},
description: {
type: String,
default: '',
},
action: {
type: String,
default: t('deck', 'Create card'),
},
},
data() {
return {
card: {
title: '',
description: '',
labels: [],
assignedUsers: [],
duedate: null,
},
boards: [],
stacksFromBoard: [],
labels: [],
selectedUsers: [],
loading: true,
selectedStack: '',
selectedBoard: '',
selectedLabels: [],
boardUsers: [],
boardAcl: [],
creating: false,
created: false,
newCard: {},
}
},
computed: {
isBoardAndStackChoosen() {
return !(this.selectedBoard === '' || this.selectedStack === '')
},
assignables() {
return [
...this.boardUsers.map((user) => ({ ...user, type: 0 })),
...this.boardAcl.filter((acl) => acl.type === 1 && typeof acl.participant === 'object').map((group) => ({ ...group.participant, type: 1 })),
...this.boardAcl.filter((acl) => acl.type === 7 && typeof acl.participant === 'object').map((circle) => ({ ...circle.participant, type: 7 })),
]
},
},
beforeMount() {
this.$set(this.card, 'title', this.title)
this.$set(this.card, 'description', this.description)
this.fetchBoards()
},
mounted() {
this.$nextTick(() => {
this.$refs.cardTitleInput.focus()
})
},
methods: {
fetchBoards() {
axios.get(generateUrl('/apps/deck/boards')).then((response) => {
this.boards = response.data.filter((board) => {
return board?.permissions?.PERMISSION_EDIT
})
this.loading = false
})
},
async fetchBoardDetails(board) {
try {
const url = generateUrl('/apps/deck/boards/' + board.id)
const response = await axios.get(url)
this.stacksFromBoard = response.data.stacks
this.labels = response.data.labels
this.boardUsers = response.data.users
this.boardAcl = response.data.acl
} catch (err) {
return err
}
},
close() {
this.$emit('cancel')
},
async createCard() {
this.creating = true
const response = await cardApi.addCard({
boardId: this.selectedBoard.id,
stackId: this.selectedStack.id,
title: this.card.title,
description: this.card.description,
duedate: this.card.duedate,
labels: this.card.labels.map(label => label.id),
users: this.card.assignedUsers.map(user => { return { id: user.uid, type: user.type } }),
})
this.newCard = response
this.creating = false
this.created = true
this.$emit('submit', window.location.protocol + '//' + window.location.host + generateUrl('/apps/deck') + `/card/${this.newCard.id}`)
},
onSelectLabel(label) {
this.card.labels.push(label)
},
onRemoveLabel(removedLabel) {
this.card.labels = this.card.label.filter(label => label.id !== removedLabel.id)
},
onSelectUser(user) {
this.card.assignedUsers.push(user)
},
onRemoveUser(removedUser) {
this.card.assignedUsers = this.card.assignedUsers.filter(user => user.uid !== removedUser.uid)
},
async addLabelToBoardAndCard(name) {
const label = await apiClient.createLabel({
title: name,
color: this.randomColor(),
boardId: this.selectedBoard.id,
})
this.card.labels.push(label)
this.labels.push(label)
},
updateCardDue(newValue) {
this.card.duedate = newValue
},
descriptionChanged(newValue) {
this.card.description = newValue
},
openNewCard() {
window.location = generateUrl('/apps/deck') + `#/board/${this.selectedBoard.id}/card/${this.newCard.id}`
},
},
}
</script>
<style lang="scss" scoped>
@import '../css/selector';
.modal-scroller {
overflow: scroll;
max-height: calc(80vh - 40px);
width: calc(100% - 24px);
padding: 12px;
}
#modal-inner {
width: auto;
margin-left: 10px;
margin-right: 10px;
}
h2 {
text-align: center;
}
.card-title {
width: 100%;
}
.board-bullet {
display: inline-block;
width: 12px;
height: 12px;
border: none;
border-radius: 50%;
cursor: pointer;
}
.modal-buttons {
display: flex;
justify-content: flex-end;
position: sticky;
bottom: 0;
z-index: 10100;
gap: 12px;
}
.multiselect {
min-width: auto;
}
.empty-content {
margin-top: 5vh !important;
&:deep(h2) {
margin-bottom: 5vh;
}
}
.row {
display: flex;
gap: 12px;
.col {
display: flex;
width: 50%;
}
}
</style>