Compare commits

...

16 Commits

Author SHA1 Message Date
Luka Trovic
386131774d fix: update comment input ui
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-03-25 14:20:09 +01:00
Luka Trovic
97b7c1a2f2 Merge branch 'master' of next.github.com:nextcloud/deck into feature/update-card-modal-ui 2022-03-23 13:22:06 +01:00
Luka Trovic
10a1c68917 feat: show title and tab buttons at the top while scrolling
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-03-23 13:18:40 +01:00
Luka Trovic
83b739d117 style: update modal UI styles
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-03-21 09:12:43 +01:00
Luka Trovic
eef33ac220 Merge branch 'master' of next.github.com:nextcloud/deck into feature/update-card-modal-ui 2022-03-21 09:08:40 +01:00
Luka Trovic
69896d5cca feat: improve tabs behavior & style
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-02-16 09:14:59 +01:00
Luka Trovic
021d55226b fix: feedback
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-02-14 10:38:55 +01:00
Luka Trovic
6c59f52c31 style: fix some UI issues
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-02-10 12:36:47 +01:00
Luka Trovic
7d414891f9 feat: add attachments tab 2022-02-09 22:00:44 +01:00
Luka Trovic
3cd94f330f feat: project and comments tabs
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-02-08 10:42:54 +01:00
Luka Trovic
00a3c8e20f Merge branch 'master' of next.github.com:nextcloud/deck into feature/update-card-modal-ui 2022-02-06 10:56:31 -08:00
Luka Trovic
309fbc735c Merge branch 'master' of next.github.com:nextcloud/deck into feature/update-card-modal-ui 2022-02-04 15:28:52 +01:00
Luka Trovic
bcaa74b33f feat: add due date tab
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-02-04 15:24:03 +01:00
Luka Trovic
427f960c80 add tags tab
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-01-31 15:51:47 +01:00
Luka Trovic
3ad4fbd96c feat: add members tab
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-01-27 13:05:43 +01:00
Luka Trovic
49dccb6199 feat: add a new CardModal component
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-01-25 15:54:36 +01:00
15 changed files with 1162 additions and 23 deletions

3
img/flash-black.svg Normal file
View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"><g><g><polygon points="426.667,213.333 288.36,213.333 333.706,0 148.817,0 85.333,298.667 227.556,298.667 227.556,512 " /></g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g></svg>

After

Width:  |  Height:  |  Size: 594 B

1
img/plus.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="#26e07f" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px"><path fill-rule="evenodd" d="M 11 2 L 11 11 L 2 11 L 2 13 L 11 13 L 11 22 L 13 22 L 13 13 L 22 13 L 22 11 L 13 11 L 13 2 Z"/></svg>

After

Width:  |  Height:  |  Size: 235 B

View File

@@ -157,10 +157,9 @@ export default {
.modal__card {
min-width: 320px;
width: 50vw;
max-width: 800px;
width: 100%;
min-height: 200px;
height: 80vh;
height: auto;
}
</style>
@@ -170,4 +169,10 @@ export default {
width: 100%;
}
.modal-wrapper--normal .modal-container{
width: 900px !important;
height: 800px !important;
overflow-y: hidden !important;
}
</style>

View File

@@ -269,7 +269,7 @@ export default {
padding-left: 44px;
background-position: 16px center;
flex-grow: 1;
height: 44px;
height: auto;
margin-bottom: 12px;
text-align: left;
}

View File

@@ -0,0 +1,39 @@
<template>
<div v-if="activeTabs.includes('attachment')" class="section-details">
<AttachmentList
:card-id="card.id"
:removable="true"
@delete-attachment="deleteAttachment"
@restore-attachment="restoreAttachment" />
</div>
</template>
<script>
import AttachmentList from './AttachmentList'
export default {
components: { AttachmentList },
props: {
card: {
type: Object,
default: null,
},
activeTabs: {
type: Array,
default: () => [],
},
},
data() {
return {
}
},
methods: {
deleteAttachment(attachment) {
this.$store.dispatch('deleteAttachment', attachment)
},
restoreAttachment(attachment) {
this.$store.dispatch('restoreAttachment', attachment)
},
},
}
</script>

View File

@@ -0,0 +1,460 @@
<!--
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<div v-if="currentCard" ref="modalContainer" class="container">
<div :class="{defaultTop: scrollPosition < 100, fixedTop: scrollPosition > 100}">
<div class="top">
<h1 class="top-title">
{{ currentCard.title }}
</h1>
<p class="top-modified">
{{ t('deck', 'Modified') }}: {{ currentCard.lastModified | fromNow }}. {{ t('deck', 'Created') }} {{ currentCard.createdAt | fromNow }}
</p>
</div>
<div class="tabs">
<div class="tab members" :class="{active: activeTabs.includes('members')}" @click="changeActiveTab('members')">
<i class="icon-user icon" />
{{ t('deck', 'Members') }}
</div>
<div class="tab tags" :class="{active: activeTabs.includes('tags')}" @click="changeActiveTab('tags')">
<i class="icon icon-tag" />
{{ t('deck', 'Tags') }}
</div>
<div class="tab due-date" :class="{active: activeTabs.includes('duedate')}" @click="changeActiveTab('duedate')">
<i class="icon icon-calendar-dark" />
{{ t('deck', 'Due date') }}
</div>
<div class="tab project" :class="{active: activeTabs.includes('project')}" @click="changeActiveTab('project')">
<i class="icon icon-deck" />
{{ t('deck', 'Project') }}
</div>
<div class="tab attachments" :class="{active: activeTabs.includes('attachment')}" @click="changeActiveTab('attachment')">
<i class="icon-attach icon icon-attach-dark" />
{{ t('deck', 'Attachments') }}
</div>
</div>
</div>
<div class="content">
<div :class="[currentCard.labels.length > 3 ? 'content-two-tabs' : 'content-three-tabs']">
<MembersTab
:card="currentCard"
:active-tabs="activeTabs"
:current-tab="currentTab"
@click="activeTabs.push('members')"
@active-tab="changeActiveTab"
@remove-active-tab="removeActiveTab" />
<TagsTab
:active-tabs="activeTabs"
:card="currentCard"
:current-tab="currentTab"
@click="activeTabs.push('tags')"
@active-tab="changeActiveTab"
@remove-active-tab="removeActiveTab" />
<DueDateTab
:active-tabs="activeTabs"
:card="currentCard"
:current-tab="currentTab"
@click="activeTabs.push('duedate')"
@active-tab="changeActiveTab"
@remove-active-tab="removeActiveTab" />
<ProjectTab
:active-tabs="activeTabs"
:card="currentCard"
:current-tab="currentTab"
@click="activeTabs.push('project')"
@active-tab="changeActiveTab" />
<AttachmentsTab
:active-tabs="activeTabs"
:card="currentCard"
:current-tab="currentTab"
@click="activeTabs.push('attachment')"
@active-tab="changeActiveTab" />
</div>
<Description :key="currentCard.id" :card="currentCard" @change="descriptionChanged" />
</div>
<div class="activities">
<div class="activities-header">
<div class="activities-title">
<i class="icon-activity" /> {{ t('deck', 'Activity') }}
</div>
<div class="show-details-btn" @click="showDetails = !showDetails">
{{ showDetails ? t('deck', 'Hide details') : t('deck', 'Show details') }}
</div>
</div>
<template v-if="showDetails">
<CardSidebarTabComments :card="currentCard" :tab-query="tabQuery" />
<ActivityList v-if="hasActivity"
filter="deck"
:object-id="currentBoard.id"
object-type="deck"
type="deck" />
</template>
</div>
</div>
</template>
<script>
import { generateUrl } from '@nextcloud/router'
import { mapState, mapGetters } from 'vuex'
import relativeDate from '../../mixins/relativeDate'
import { showError } from '@nextcloud/dialogs'
import { getCurrentUser } from '@nextcloud/auth'
import MembersTab from './MembersTab.vue'
import TagsTab from './TagsTab.vue'
import DueDateTab from './DueDateTab.vue'
import Description from './Description.vue'
import ProjectTab from './ProjectTab.vue'
import AttachmentsTab from './AttachmentsTab.vue'
import CardSidebarTabComments from './CardSidebarTabComments'
import moment from '@nextcloud/moment'
import ActivityList from '../ActivityList'
const capabilities = window.OC.getCapabilities()
export default {
name: 'CardModal',
components: {
MembersTab,
Description,
TagsTab,
DueDateTab,
ProjectTab,
AttachmentsTab,
CardSidebarTabComments,
ActivityList,
},
filters: {
fromNow(value) {
return moment.unix(value).fromNow()
},
},
mixins: [relativeDate],
props: {
id: {
type: Number,
required: true,
},
tabId: {
type: String,
required: false,
default: null,
},
tabQuery: {
type: String,
required: false,
default: null,
},
},
data() {
return {
newComment: '',
titleEditable: false,
titleEditing: '',
hasActivity: capabilities && capabilities.activity,
currentUser: getCurrentUser(),
comment: '',
currentTab: null,
activeTabs: [],
showDetails: false,
scrollPosition: null,
}
},
computed: {
...mapGetters([
'getCommentsForCard',
'hasMoreComments',
]),
...mapState({
currentBoard: state => state.currentBoard,
replyTo: state => state.comment.replyTo,
}),
...mapGetters(['canEdit', 'assignables', 'cardActions', 'stackById']),
title() {
return this.titleEditable ? this.titleEditing : this.currentCard.title
},
currentCard() {
return this.$store.getters.cardById(this.id)
},
subtitle() {
return t('deck', 'Modified') + ': ' + this.relativeDate(this.currentCard.lastModified * 1000) + ' ' + t('deck', 'Created') + ': ' + this.relativeDate(this.currentCard.createdAt * 1000)
},
cardRichObject() {
return {
id: '' + this.currentCard.id,
name: this.currentCard.title,
boardname: this.currentBoard.title,
stackname: this.stackById(this.currentCard.stackId)?.title,
link: window.location.protocol + '//' + window.location.host + generateUrl('/apps/deck/') + `#/board/${this.currentBoard.id}/card/${this.currentCard.id}`,
}
},
cardDetailsInModal: {
get() {
return this.$store.getters.config('cardDetailsInModal')
},
set(newValue) {
this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
},
},
},
mounted() {
this.$nextTick(() => {
this.$refs.modalContainer.addEventListener('scroll', this.onScroll)
})
this.loadComments()
},
methods: {
cancelReply() {
this.$store.dispatch('setReplyTo', null)
},
async createComment(content) {
const commentObj = {
cardId: this.currentCard.id,
comment: content,
}
await this.$store.dispatch('createComment', commentObj)
this.$store.dispatch('setReplyTo', null)
this.newComment = ''
await this.loadComments()
},
async loadComments() {
this.$store.dispatch('setReplyTo', null)
this.error = null
this.isLoading = true
try {
await this.$store.dispatch('fetchComments', { cardId: this.currentCard.id })
this.isLoading = false
if (this.currentCard.commentsUnread > 0) {
await this.$store.dispatch('markCommentsAsRead', this.currentCard.id)
}
} catch (e) {
this.isLoading = false
console.error('Failed to fetch more comments during infinite loading', e)
this.error = t('deck', 'Failed to load comments')
}
},
descriptionChanged(newDesc) {
this.copiedCard.description = newDesc
},
handleUpdateTitleEditable(value) {
this.titleEditable = value
if (value) {
this.titleEditing = this.currentCard.title
}
},
handleUpdateTitle(value) {
this.titleEditing = value
},
handleSubmitTitle(value) {
if (value.trim === '') {
showError(t('deck', 'The title cannot be empty.'))
return
}
this.titleEditable = false
this.$store.dispatch('updateCardTitle', { ...this.currentCard, title: this.titleEditing })
},
closeSidebar() {
this.$router.push({ name: 'board' })
},
showModal() {
this.$store.dispatch('setConfig', { cardDetailsInModal: true })
},
closeModal() {
this.$store.dispatch('setConfig', { cardDetailsInModal: false })
},
changeActiveTab(tab) {
this.currentTab = tab
this.activeTabs = this.activeTabs.filter((item) => !['project', 'attachment'].includes(item))
if (!this.activeTabs.includes(tab)) {
this.activeTabs.push(tab)
}
},
removeActiveTab(tab) {
const index = this.activeTabs.indexOf(tab)
if (index > -1) {
this.activeTabs = this.activeTabs.splice(index, 1)
}
},
onScroll() {
this.scrollPosition = this.$refs.modalContainer.scrollTop
},
},
}
</script>
<style lang="scss" scoped>
.show-details-btn {
cursor: pointer;
text-decoration: underline;
color: var(--color-text-maxcontrast);
}
.activities-header{
display: flex;
justify-content: space-between;
}
.content-two-tabs, .content-three-tabs {
display: grid;
align-items: flex-start;
}
.content-two-tabs {
grid-template-columns: 1fr 2fr;
}
.content-three-tabs {
grid-template-columns: 1fr 2fr 1fr;
}
.icon-activity {
background-image: url(../../../img/flash-black.svg);
width: 15px;
height: 15px;
margin-right: 5px;
}
.icon {
margin-right: 5px;
}
.icon-plus {
background-image: url(../../../img/plus.svg);
width: 15px;
height: 15px;
margin-right: 5px;
}
.log-item {
display: flex;
justify-content: flex-start;
line-height: 45px;
align-items: center;
}
.activities {
&-title{
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 15px;
font-weight: bold;
}
margin-top: 100px;
padding-left: 20px !important;
padding-right: 20px !important;
}
.content{
padding-left: 20px;
padding-right: 20px;
}
.comments {
display: flex;
justify-content: space-between;
align-items: center;
&-input{
width: 100%;
margin-left: 10px;
}
.comment-form{
width: 95%;
}
}
.container {
overflow-y: scroll;
height: 800px;
}
.top {
padding: 20px 20px 0px 20px;
&-title {
color: black;
font-size: 20px;
font-weight: bold;
}
&-modified {
color: #767676;
line-height: 40px;
}
}
.tabs {
padding-left: 20px;
padding-right: 20px;
margin-top: 20px;
margin-bottom: 5px;
display: flex;
}
.tab {
cursor: pointer;
font-weight: bold;
background-color: #ededed;
color: rgb(0, 0, 0);
flex-grow: 0;
flex-shrink: 1;
display: flex;
flex-direction: row;
overflow: hidden;
padding: 10px 20px;
border-radius: 10px;
font-size: 85%;
margin-bottom: 3px;
margin-right: 15px;
}
.action-btn {
list-style: none;
}
.edit-btns {
display: flex;
align-items: center;
}
.description {
display: flex;
justify-content: space-between;
margin-top: 30px;
}
.active {
color: #409eff;
background-color: #ecf5ff;
}
.fixedTop {
position: sticky;
top: 0;
background-color: #ffffff;
z-index: 1000;
margin-top: 0px;
padding-bottom: 5px;
}
</style>

View File

@@ -1,19 +1,16 @@
<template>
<div>
<div class="comment--header">
<Avatar :user="currentUser.uid" />
<span class="has-tooltip username">
{{ currentUser.displayName }}
</span>
<div class="comment-wrapper">
<div class="comment--header">
<Avatar :user="currentUser.uid" />
</div>
<CommentItem v-if="replyTo"
:comment="replyTo"
:reply="true"
:preview="true"
@cancel="cancelReply" />
<CommentForm v-model="newComment" @submit="createComment" />
</div>
<CommentItem v-if="replyTo"
:comment="replyTo"
:reply="true"
:preview="true"
@cancel="cancelReply" />
<CommentForm v-model="newComment" @submit="createComment" />
<ul v-if="getCommentsForCard(card.id).length > 0" id="commentsFeed">
<CommentItem v-for="comment in getCommentsForCard(card.id)"
:key="comment.id"
@@ -26,8 +23,7 @@
</InfiniteLoading>
</ul>
<div v-else-if="isLoading" class="icon icon-loading" />
<div v-else class="emptycontent">
<div :class="{ 'icon-comment': !error, 'icon-error': error }" />
<div v-else>
<p>{{ error || t('deck', 'No comments yet. Begin the discussion!') }}</p>
</div>
</div>

View File

@@ -280,7 +280,9 @@ h5 {
border-bottom: 1px solid var(--color-border);
margin-top: 20px;
margin-bottom: 5px;
color: var(--color-text-maxcontrast);
color: var(--color-main-text);
font-weight: bold;
font-size: 20px;
.icon-info {
display: inline-block;

View File

@@ -0,0 +1,187 @@
<template>
<div v-if="activeTabs.includes('duedate') || (copiedCard && copiedCard.duedate)"
v-show="!['project', 'attachment'].includes(currentTab)"
class="section-details">
<div @click="$emit('active-tab', 'duedate')">
<DatetimePicker v-model="duedate"
:placeholder="t('deck', 'Set a due date')"
type="datetime"
:minute-step="5"
:show-second="false"
:lang="lang"
:disabled="saving || !canEdit"
:shortcuts="shortcuts"
confirm />
</div>
<Actions v-if="canEdit">
<ActionButton v-if="copiedCard.duedate" icon="icon-delete" @click="removeDue()">
{{ t('deck', 'Remove due date') }}
</ActionButton>
</Actions>
</div>
</template>
<script>
import { DatetimePicker, Actions, ActionButton } from '@nextcloud/vue'
import { mapState, mapGetters } from 'vuex'
import Color from '../../mixins/color'
import labelStyle from '../../mixins/labelStyle'
import {
getDayNamesMin,
getFirstDay,
getMonthNamesShort,
} from '@nextcloud/l10n'
import moment from '@nextcloud/moment'
export default {
components: { DatetimePicker, Actions, ActionButton },
mixins: [Color, labelStyle],
props: {
card: {
type: Object,
default: null,
},
activeTabs: {
type: Array,
default: () => [],
},
currentTab: {
type: String,
default: '',
},
},
data() {
return {
saving: false,
copiedCard: null,
lang: {
days: getDayNamesMin(),
months: getMonthNamesShort(),
formatLocale: {
firstDayOfWeek: getFirstDay() === 0 ? 7 : getFirstDay(),
},
placeholder: {
date: t('deck', 'Select Date'),
},
},
format: {
stringify: this.stringify,
parse: this.parse,
},
shortcuts: [
{
text: t('deck', 'Today'),
onClick() {
const date = new Date()
date.setDate(date.getDate())
date.setHours(23)
date.setMinutes(59)
return date
},
},
{
text: t('deck', 'Tomorrow'),
onClick() {
const date = new Date()
date.setDate(date.getDate() + 1)
date.setHours(23)
date.setMinutes(59)
return date
},
},
{
text: t('deck', 'Next week'),
onClick() {
const date = new Date()
date.setDate(date.getDate() + 7)
date.setHours(23)
date.setMinutes(59)
return date
},
},
{
text: t('deck', 'Next month'),
onClick() {
const date = new Date()
date.setDate(date.getDate() + 30)
date.setHours(23)
date.setMinutes(59)
return date
},
},
],
}
},
computed: {
...mapState({
currentBoard: state => state.currentBoard,
}),
...mapGetters(['canEdit']),
labelsSorted() {
return [...this.currentBoard.labels].sort((a, b) => (a.title < b.title) ? -1 : 1)
},
duedate: {
get() {
return this.card.duedate ? new Date(this.card.duedate) : null
},
async set(val) {
this.saving = true
await this.$store.dispatch('updateCardDue', {
...this.copiedCard,
duedate: val ? moment(val).format('YYYY-MM-DD H:mm:ss') : null,
})
this.saving = false
},
},
},
watch: {
card() {
this.initialize()
if (this.copiedCard.duedate) {
this.$emit('active-tab', 'duedate')
} else {
this.$emit('remove-active-tab', 'duedate')
}
},
},
mounted() {
this.initialize()
if (this.copiedCard.duedate) {
this.$emit('active-tab', 'duedate')
} else {
this.$emit('remove-active-tab', 'duedate')
}
},
methods: {
async initialize() {
if (!this.card) {
return
}
this.copiedCard = JSON.parse(JSON.stringify(this.card))
},
removeDue() {
this.copiedCard.duedate = null
this.$store.dispatch('updateCardDue', this.copiedCard)
},
},
}
</script>
<style lang="scss" scoped>
.section-details{
margin-right: 5px;
display: flex;
align-items: flex-start;
margin-top: 10px;
}
</style>
<style>
.section-details .mx-input{
height: 36px !important;
margin: 0;
}
.section-details .action-item {
height: 30px !important;
}
</style>

View File

@@ -0,0 +1,208 @@
<template>
<div v-if="activeTabs.includes('members') || (assignedUsers && assignedUsers.length > 0)"
v-show="!['project', 'attachment'].includes(currentTab)"
class="section-details">
<div v-if="showSelelectMembers" @mouseleave="showSelelectMembers = false">
<Multiselect v-if="canEdit"
v-model="assignedUsers"
:multiple="true"
:options="formatedAssignables"
:user-select="true"
:auto-limit="false"
:placeholder="t('deck', 'Assign a user to this card…')"
label="displayname"
track-by="multiselectKey"
@select="assignUserToCard"
@remove="removeUserFromCard">
<template #tag="scope">
<div class="avatarlist--inline">
<Avatar :user="scope.option.uid"
:display-name="scope.option.displayname"
:size="24"
:is-no-user="scope.option.isNoUser"
:disable-menu="true" />
</div>
</template>
</Multiselect>
<div v-else class="avatar-list--readonly">
<Avatar v-for="option in assignedUsers"
:key="option.primaryKey"
:user="option.uid"
:display-name="option.displayname"
:is-no-user="option.isNoUser"
:size="32" />
</div>
</div>
<template v-else>
<div class="members">
<Avatar v-for="option in assignedUsers"
:key="option.primaryKey"
:user="option.uid"
:display-name="option.displayname"
:is-no-user="option.isNoUser"
:size="32" />
<div class="button new select-member-btn" @click="selectMembers">
<span class="icon icon-add" />
<span class="hidden-visually" />
</div>
</div>
</template>
</div>
</template>
<script>
import { Multiselect, Avatar } from '@nextcloud/vue'
import { mapState, mapGetters } from 'vuex'
export default {
name: 'MembersTab',
components: {
Multiselect,
Avatar,
},
props: {
card: {
type: Object,
default: null,
},
activeTabs: {
type: Array,
default: () => [],
},
currentTab: {
type: String,
default: '',
},
},
data() {
return {
assignedUsers: null,
copiedCard: null,
showSelelectMembers: false,
}
},
computed: {
...mapState({
currentBoard: state => state.currentBoard,
}),
...mapGetters(['canEdit', 'assignables']),
formatedAssignables() {
return this.assignables.map(item => {
const assignable = {
...item,
user: item.primaryKey,
displayName: item.displayname,
icon: 'icon-user',
isNoUser: false,
multiselectKey: item.type + ':' + item.uid,
}
if (item.type === 1) {
assignable.icon = 'icon-group'
assignable.isNoUser = true
}
if (item.type === 7) {
assignable.icon = 'icon-circles'
assignable.isNoUser = true
}
return assignable
})
},
},
watch: {
card() {
this.initialize()
},
assignedUsers(value) {
if (value.length > 0) {
this.$emit('active-tab', 'members')
} else {
this.$emit('remove-active-tab', 'members')
}
},
},
mounted() {
this.initialize()
},
methods: {
selectMembers() {
this.showSelelectMembers = true
this.$emit('active-tab', 'members')
},
removeUserFromCard(user) {
this.$store.dispatch('removeUserFromCard', {
card: this.copiedCard,
assignee: {
userId: user.uid,
type: user.type,
},
})
},
addLabelToCard(newLabel) {
this.copiedCard.labels.push(newLabel)
const data = {
card: this.copiedCard,
labelId: newLabel.id,
}
this.$store.dispatch('addLabel', data)
},
assignUserToCard(user) {
this.$store.dispatch('assignCardToUser', {
card: this.copiedCard,
assignee: {
userId: user.uid,
type: user.type,
},
})
},
async initialize() {
if (!this.card) {
return
}
this.copiedCard = JSON.parse(JSON.stringify(this.card))
this.assignedLabels = [...this.card.labels].sort((a, b) => (a.title < b.title) ? -1 : 1)
if (this.card.assignedUsers && this.card.assignedUsers.length > 0) {
this.assignedUsers = this.card.assignedUsers.map((item) => ({
...item.participant,
isNoUser: item.participant.type !== 0,
multiselectKey: item.participant.type + ':' + item.participant.primaryKey,
}))
} else {
this.assignedUsers = []
}
},
},
}
</script>
<style lang="scss" scoped>
.select-member-btn {
box-sizing: border-box;
display: flex;
height: 32px;
width: 34px;
padding: 5px 9px;
align-items: center;
justify-content: center;
margin-left: 5px;
}
.section-details {
width: 100%;
margin-top: 10px;
}
.members {
display: flex;
align-items: center;
}
</style>
<style>
.members .multiselect__tags{
height: 34px !important;
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<div v-if="activeTabs.includes('project')" class="section-details">
<div class="section-wrapper project-tab">
<CollectionList v-if="card.id"
:id="`${card.id}`"
:name="card.title"
type="deck-card" />
</div>
</div>
</template>
<script>
import { CollectionList } from 'nextcloud-vue-collections'
export default {
components: { CollectionList },
props: {
card: {
type: Object,
default: null,
},
activeTabs: {
type: Array,
default: () => [],
},
},
}
</script>
<style scoped>
.section-details{
margin-top: 10px;
min-width: 500px;
}
</style>
<style lang="scss">
#collection-select-container p {
display: none;
}
#collection-list li {
align-items: flex-end;
}
.project-tab .collection-list-item {
display: flex !important;
align-items: center !important;
}
.project-tab .linked-icons {
img {
height: 20px !important;
}
}
</style>

View File

@@ -0,0 +1,171 @@
<template>
<div v-if="activeTabs.includes('tags') || card.labels.length > 0"
v-show="!['project', 'attachment'].includes(currentTab)"
class="section-details">
<div v-if="showSelelectTags || card.labels.length <= 0" @mouseleave="showSelelectTags = false">
<Multiselect v-model="assignedLabels"
:multiple="true"
:disabled="!canEdit"
:options="labelsSorted"
:placeholder="t('deck', 'Assign a tag to this card')"
:taggable="true"
label="title"
track-by="id"
@select="addLabelToCard"
@remove="removeLabelFromCard">
<template #option="scope">
<div :style="{ backgroundColor: '#' + scope.option.color, color: textColor(scope.option.color)}" class="tag">
{{ scope.option.title }}
</div>
</template>
<template #tag="scope">
<div :style="{ backgroundColor: '#' + scope.option.color, color: textColor(scope.option.color)}" class="tag">
{{ scope.option.title }}
</div>
</template>
</Multiselect>
</div>
<div v-else-if="card.labels.length > 0" class="labels">
<div v-for="label in card.labels"
:key="label.id"
:style="labelStyle(label)"
class="labels-item">
<span @click.stop="applyLabelFilter(label)">{{ label.title }}</span>
</div>
<div class="button new select-tag" @click="add">
<span class="icon icon-add" />
<span class="hidden-visually" />
</div>
</div>
</div>
</template>
<script>
import { Multiselect } from '@nextcloud/vue'
import { mapState, mapGetters } from 'vuex'
import Color from '../../mixins/color'
import labelStyle from '../../mixins/labelStyle'
export default {
components: { Multiselect },
mixins: [Color, labelStyle],
props: {
card: {
type: Object,
default: null,
},
activeTabs: {
type: Array,
default: () => [],
},
currentTab: {
type: String,
default: '',
},
},
data() {
return {
assignedLabels: null,
showSelelectTags: false,
copiedCard: null,
}
},
computed: {
...mapState({
currentBoard: state => state.currentBoard,
}),
...mapGetters(['canEdit']),
labelsSorted() {
return [...this.currentBoard.labels].sort((a, b) => (a.title < b.title) ? -1 : 1)
},
},
watch: {
card(value) {
if (value.labels.length > 0) {
this.$emit('active-tab', 'tags')
} else {
this.$emit('remove-active-tab', 'tags')
}
},
},
mounted() {
this.initialize()
if (this.card.labels.length > 0) {
this.$emit('active-tab', 'tags')
}
},
methods: {
add() {
this.showSelelectTags = true
this.$emit('active-tab', 'tags')
},
async initialize() {
if (!this.card) {
return
}
this.copiedCard = JSON.parse(JSON.stringify(this.card))
this.assignedLabels = [...this.card.labels].sort((a, b) => (a.title < b.title) ? -1 : 1)
},
openCard() {
const boardId = this.card && this.card.boardId ? this.card.boardId : this.$route.params.id
this.$router.push({ name: 'card', params: { id: boardId, cardId: this.card.id } }).catch(() => {})
},
addLabelToCard(newLabel) {
this.copiedCard.labels.push(newLabel)
const data = {
card: this.copiedCard,
labelId: newLabel.id,
}
this.$store.dispatch('addLabel', data)
},
removeLabelFromCard(removedLabel) {
const removeIndex = this.copiedCard.labels.findIndex((label) => {
return label.id === removedLabel.id
})
if (removeIndex !== -1) {
this.copiedCard.labels.splice(removeIndex, 1)
}
const data = {
card: this.copiedCard,
labelId: removedLabel.id,
}
this.$store.dispatch('removeLabel', data)
},
},
}
</script>
<style lang="scss" scoped>
.labels {
display: flex;
justify-content: flex-start;
align-items: center;
&-item {
border-radius: 15px;
margin-right: 5px;
min-width: 110px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
}
}
.select-tag {
height: 32px;
width: 34px;
padding: 5px 8px;
}
.tag{
padding: 0px 5px;
border-radius: 15px;
}
.section-details{
margin-top: 10px;
}
</style>

View File

@@ -135,7 +135,7 @@ export default {
},
activeBoards() {
return this.$store.getters.boards.filter((item) => item.deletedAt === 0 && item.archived === false)
}
},
},
methods: {
openCard() {

View File

@@ -34,6 +34,7 @@
display: flex;
align-items: center;
color: var(--color-text-light);
margin-right: 10px;
.username {
padding: 10px;
@@ -50,3 +51,16 @@
margin-left: 44px;
word-break: break-word;
}
.comment-wrapper {
display: flex;
align-items: center;
}
.comment-form {
width: 100%;
}
.comment-form .comment-form__contenteditable {
border-radius: 4px;
}

View File

@@ -28,7 +28,7 @@ import Boards from './components/boards/Boards'
import Board from './components/board/Board'
import Sidebar from './components/Sidebar'
import BoardSidebar from './components/board/BoardSidebar'
import CardSidebar from './components/card/CardSidebar'
import CardModal from './components/card/CardModal'
import Overview from './components/overview/Overview'
Vue.use(Router)
@@ -119,7 +119,7 @@ export default new Router({
path: 'card/:cardId/:tabId?/:tabQuery?',
name: 'card',
components: {
sidebar: CardSidebar,
sidebar: CardModal,
},
props: {
default: (route) => {