Label editing in board sidebar (#935)

Label editing in board sidebar
This commit is contained in:
Julius Härtl
2019-04-26 16:27:17 +02:00
committed by GitHub
12 changed files with 442 additions and 130 deletions

View File

@@ -225,6 +225,10 @@ trigger:
kind: pipeline
name: frontend
steps:
- name: install
image: node:11-alpine
commands:
- npm install
- name: eslint
image: node:11-alpine
commands:
@@ -232,7 +236,6 @@ steps:
- name: jsbuild
image: node:11-alpine
commands:
- npm install
- npm run build
trigger:
branch:

View File

@@ -21,9 +21,7 @@
-->
<template>
<div id="app-sidebar">
<router-view name="sidebar" />
</div>
<router-view name="sidebar" />
</template>
<script>

View File

@@ -103,6 +103,8 @@ export default {
this.$store.dispatch('setCurrentBoard', board)
this.$store.dispatch('loadStacks', board)
this.loading = false
console.log(board)
this.$store.state.labels = board.labels
})
},
onDropStack({ removedIndex, addedIndex }) {

View File

@@ -21,145 +21,58 @@
-->
<template>
<div class="sidebar">
<div class="sidebar-header">
<h3>{{ board.title }}</h3>
</div>
<app-sidebar
:actions="[]"
:title="board.title"
@close="closeSidebar">
<ul class="tab-headers">
<li v-for="tab in tabs" :class="{ 'selected': tab.isSelected }" :key="tab.name">
<a @click="setSelectedHeader(tab.name)">{{ tab.name }}</a>
</li>
</ul>
<AppSidebarTab name="Sharing" icon="icon-shared">
<SharingTabSidebard :board="board" />
</AppSidebarTab>
<div class="tabsContainer">
<div class="tab">
<div v-if="activeTab === 'Sharing'">
<AppSidebarTab name="Tags" icon="icon-tag">
<TagsTabSidebard :board="board" />
</AppSidebarTab>
<multiselect v-model="value" :options="board.sharees" />
<AppSidebarTab name="Deleted items" icon="icon-delete">
<DeletedTabSidebard :board="board" />
</AppSidebarTab>
<ul
id="shareWithList"
class="shareWithList"
>
<li>
<avatar :user="board.owner.uid" />
<span class="has-tooltip username">
{{ board.owner.displayname }}
</span>
</li>
<li v-for="acl in board.acl" :key="acl.participant.uid">
<avatar :user="acl.participant.uid" />
<span class="has-tooltip username">
{{ acl.participant.displayname }}
</span>
</li>
</ul>
</div>
<AppSidebarTab name="Timeline" icon="icon-activity">
<TimelineTabSidebard :board="board" />
</AppSidebarTab>
<div
v-if="activeTab === 'Tags'"
id="board-detail-labels"
>
<ul class="labels">
<li v-for="label in board.labels" :key="label.id">
<span v-if="!label.edit" :style="{ backgroundColor: `#${label.color}`, color: `#${label.color || '000'}` }" class="label-title">
<span v-if="label.title">{{ label.title }}</span><i v-if="!label.title"><br></i>
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</app-sidebar>
</template>
<script>
import { Avatar, Multiselect } from 'nextcloud-vue'
import { mapState } from 'vuex'
import SharingTabSidebard from './SharingTabSidebard'
import TagsTabSidebard from './TagsTabSidebard'
import DeletedTabSidebard from './DeletedTabSidebard'
import TimelineTabSidebard from './TimelineTabSidebard'
import { AppSidebar, AppSidebarTab } from 'nextcloud-vue'
export default {
name: 'BoardSidebar',
components: {
Avatar,
Multiselect
},
props: {
},
data() {
return {
activeTab: 'Sharing',
tabs: [
{
name: 'Sharing',
isSelected: true
},
{
name: 'Tags',
isSelected: false
},
{
name: 'Deleted items',
isSelected: false
},
{
name: 'Timeline',
isSelected: false
}
]
}
AppSidebar,
AppSidebarTab,
SharingTabSidebard,
TagsTabSidebard,
DeletedTabSidebard,
TimelineTabSidebard
},
computed: {
...mapState({
board: state => state.currentBoard
board: state => state.currentBoard,
labels: state => state.labels
})
},
methods: {
closeSidebar() {
this.$store.dispatch('toggleSidebar')
},
setSelectedHeader(tabName) {
this.activeTab = tabName
this.tabs.forEach(tab => {
tab.isSelected = (tab.name === tabName)
})
this.$router.push({ name: 'board' })
}
}
}
</script>
<style lang="scss" scoped>
.sidebar-header {
h3 {
font-size: 14pt;
padding: 15px 15px 3px;
margin: 0;
overflow: hidden;
}
}
.icon-close {
position: absolute;
top: 0px;
right: 0px;
padding: 14px;
height: 24px;
width: 24px;
}
ul.tab-headers {
margin: 15px 15px 0 15px;
li {
display: inline-block;
padding: 12px;
&.selected {
color: #000;
border-bottom: 1px solid #4d4d4d;
font-weight: 600;
}
}
}
.tabsContainer {
.tab {
padding: 0 15px 15px;
}
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<div>
deleted
</div>
</template>
<script>
export default {
name: 'DeletedTabSidebard',
components: {
},
props: {
board: {
type: Object,
default: undefined
}
},
data() {
return {
}
}
}
</script>

View File

@@ -0,0 +1,65 @@
<template>
<div>
<multiselect :options="sharees" label="label" @search-change="asyncFind">
<template #option="scope">
{{ scope.option.label }}
</template>
</multiselect>
<ul
id="shareWithList"
class="shareWithList"
>
<li>
<avatar :user="board.owner.uid" />
<span class="has-tooltip username">
{{ board.owner.displayname }}
</span>
</li>
<li v-for="acl in board.acl" :key="acl.participant.uid">
<avatar :user="acl.participant.uid" />
<span class="has-tooltip username">
{{ acl.participant.displayname }}
</span>
</li>
</ul>
</div>
</template>
<script>
import { Avatar, Multiselect } from 'nextcloud-vue'
import { mapGetters } from 'vuex'
export default {
name: 'SharingTabSidebard',
components: {
Avatar,
Multiselect
},
props: {
board: {
type: Object,
default: undefined
}
},
data() {
return {
isLoading: false
}
},
computed: {
...mapGetters({
sharees: 'sharees'
})
},
methods: {
asyncFind(query) {
this.isLoading = true
this.$store.dispatch('loadSharees').then(response => {
this.isLoading = false
})
}
}
}
</script>

View File

@@ -0,0 +1,164 @@
<template>
<div>
<ul class="labels">
<li v-for="label in labels" :key="label.id" :class="{editing: (editingLabelId === label.id)}">
<template v-if="editingLabelId === label.id">
<form class="label-form">
<input v-model="editingLabel.title" type="text">
<input v-tooltip="{content: missingDataLabel, show: !editLabelObjValidated, trigger: 'manual' }" :disabled="!editLabelObjValidated" type="submit"
value="" class="icon-confirm"
@click="updateLabel(label)">
<input v-tooltip="t('deck', 'Cancel')" type="submit" value=""
class="icon-close" @click="editingLabelId = null">
</form>
<ColorPicker :value="'#' + editingLabel.color" @input="updateColor" />
</template>
<template v-else>
<div :style="{ backgroundColor: `#${label.color}`, color:textColor(label.color) }" class="label-title">
<span>{{ label.title }}</span>
</div>
<button v-tooltip="t('deck', 'Edit')" class="icon-rename" @click="clickEdit(label)" />
<button v-tooltip="t('deck', 'Delete')" class="icon-delete" @click="deleteLabel(label.id)" />
</template>
</li>
<li v-if="addLabel" class="editing">
<template>
<form class="label-form">
<input v-model="addLabelObj.title" type="text">
<input v-tooltip="{content: missingDataLabel, show: !addLabelObjValidated, trigger: 'manual' }" :disabled="!addLabelObjValidated" type="submit"
value="" class="icon-confirm"
@click="clickAddLabel()">
<input v-tooltip="t('deck', 'Cancel')" type="submit" value=""
class="icon-close" @click="addLabel=false">
</form>
<ColorPicker :value="'#' + addLabelObj.color" @input="updateColor" />
</template>
</li>
<button @click="clickShowAddLabel()">
<span class="icon-add" />{{ t('deck', 'Add a new label') }}</button>
</ul>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Color from '../../mixins/color'
import { Compact } from 'vue-color'
import ColorPicker from '../ColorPicker'
export default {
name: 'TagsTabSidebard',
components: {
ColorPicker,
'compact-picker': Compact
},
mixins: [Color],
data() {
return {
editingLabelId: null,
editingLabel: null,
addLabelObj: null,
addLabel: false,
missingDataLabel: t('deck', 'title and color value must be provided'),
defaultColors: ['#31CC7C', '#317CCC', '#FF7A66', '#F1DB50', '#7C31CC', '#CC317C', '#3A3B3D', '#CACBCD']
}
},
computed: {
...mapGetters({
labels: 'currentBoardLabels'
}),
addLabelObjValidated() {
if (this.addLabelObj.title === '') {
return false
}
if (this.colorIsValid(this.addLabelObj.color) === false) {
return false
}
return true
},
editLabelObjValidated() {
if (this.editingLabel.title === '') {
return false
}
if (this.colorIsValid(this.editingLabel.color) === false) {
return false
}
return true
}
},
methods: {
updateColor(c) {
if (this.editingLabel === null) {
this.addLabelObj.color = c.hex.substring(1, 7)
} else {
this.editingLabel.color = c.hex.substring(1, 7)
}
},
clickEdit(label) {
this.editingLabelId = label.id
this.editingLabel = Object.assign({}, label)
},
deleteLabel(id) {
this.$store.dispatch('removeLabelFromCurrentBoard', id)
},
updateLabel(label) {
this.$store.dispatch('updateLabelFromCurrentBoard', this.editingLabel)
this.editingLabelId = null
},
clickShowAddLabel() {
this.addLabelObj = { cardId: null, color: '000000', title: '' }
this.addLabel = true
},
clickAddLabel() {
this.$store.dispatch('addLabelToCurrentBoard', this.addLabelObj)
this.addLabel = false
this.addLabelObj = null
}
}
}
</script>
<style scoped lang="scss">
.labels li {
margin-bottom: 3px;
.label-title {
flex-grow: 1;
border-radius: 3px;
padding: 4px;
}
&:not(.editing) button {
width: 40px;
height: 34px;
margin: 0;
margin-left: -3px;
}
}
.labels li {
display: flex;
&.editing {
display: block;
}
form {
display: flex;
input[type=text] {
flex-grow: 1;
}
}
button,
input:not([type='text']):last-child {
border-bottom-right-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
border-bottom-left-radius: 0;
border-top-left-radius: 0;
margin-left: -1px;
width: 35px;
}
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<div>
timeline
</div>
</template>
<script>
export default {
name: 'TimelineTabSidebard',
components: {
},
props: {
board: {
type: Object,
default: undefined
}
},
data() {
return {
}
}
}
</script>

View File

@@ -81,6 +81,15 @@ export default {
return '#000000'
}
},
colorIsValid(hex) {
var re = new RegExp('[A-Fa-f0-9]{6}')
if (re.test(hex)) {
return true
}
return false
}
}

View File

@@ -46,3 +46,10 @@
* @property {boolean} archived
* @property {number} order
*/
/**
* Label model
*
* @typedef {Object} Label
* @property {String} title
* @property {String} color
*/

View File

@@ -135,4 +135,50 @@ export class BoardApi {
})
}
// Label API Calls
deleteLabel(id) {
return axios.delete(this.url(`/labels/${id}`))
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
updateLabel(label) {
return axios.put(this.url(`/labels/${label.id}`), label)
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
createLabel(labelData) {
return axios.post(this.url('/labels'), labelData)
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
}

View File

@@ -51,12 +51,16 @@ export default new Vuex.Store({
sidebarShown: false,
currentBoard: null,
boards: [],
sharees: [],
boardFilter: BOARD_FILTERS.ALL
},
getters: {
boards: state => {
return state.boards
},
sharees: state => {
return state.sharees
},
noneArchivedBoards: state => {
return state.boards.filter(board => {
return board.archived === false && !board.deletedAt
@@ -80,6 +84,9 @@ export default new Vuex.Store({
|| (state.boardFilter === BOARD_FILTERS.SHARED && board.shared === 1)
})
return boards.map(boardToMenuItem)
},
currentBoardLabels: state => {
return state.currentBoard.labels
}
},
mutations: {
@@ -132,6 +139,30 @@ export default new Vuex.Store({
},
setCurrentBoard(state, board) {
state.currentBoard = board
},
// label mutators
removeLabelFromCurrentBoard(state, labelId) {
const removeIndex = state.currentBoard.labels.findIndex((l) => {
return labelId === l.id
})
if (removeIndex > -1) {
state.currentBoard.labels.splice(removeIndex, 1)
}
},
updateLabelFromCurrentBoard(state, newLabel) {
let labelToUpdate = state.currentBoard.labels.find((l) => {
return newLabel.id === l.id
})
labelToUpdate.title = newLabel.title
labelToUpdate.color = newLabel.color
},
addLabelToCurrentBoard(state, newLabel) {
state.currentBoard.labels.push(newLabel)
}
},
actions: {
@@ -185,14 +216,15 @@ export default new Vuex.Store({
const boards = await apiClient.loadBoards()
commit('setBoards', boards)
},
async loadSharees({ commit }) {
const params = {
format: 'json',
perPage: 4,
itemType: [0, 1]
}
const { data } = await axios.get(OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees', { params })
commit('setSharees', data.users)
loadSharees({ commit }) {
const params = new URLSearchParams()
params.append('format', 'json')
params.append('perPage', 4)
params.append('itemType', 0)
params.append('itemType', 1)
axios.get(OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees', { params }).then((response) => {
commit('setSharees', response.data.ocs.data.users)
})
},
setBoardFilter({ commmit }, filter) {
commmit('setBoardFilter', filter)
@@ -208,6 +240,27 @@ export default new Vuex.Store({
},
setCurrentBoard({ commit }, board) {
commit('setCurrentBoard', board)
},
// label actions
removeLabelFromCurrentBoard({ commit }, label) {
apiClient.deleteLabel(label)
.then((label) => {
commit('removeLabelFromCurrentBoard', label.id)
})
},
updateLabelFromCurrentBoard({ commit }, newLabel) {
apiClient.updateLabel(newLabel)
.then((newLabel) => {
commit('updateLabelFromCurrentBoard', newLabel)
})
},
addLabelToCurrentBoard({ commit }, newLabel) {
newLabel.boardId = this.state.currentBoard.id
apiClient.createLabel(newLabel)
.then((newLabel) => {
commit('addLabelToCurrentBoard', newLabel)
})
}
}
})