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 kind: pipeline
name: frontend name: frontend
steps: steps:
- name: install
image: node:11-alpine
commands:
- npm install
- name: eslint - name: eslint
image: node:11-alpine image: node:11-alpine
commands: commands:
@@ -232,7 +236,6 @@ steps:
- name: jsbuild - name: jsbuild
image: node:11-alpine image: node:11-alpine
commands: commands:
- npm install
- npm run build - npm run build
trigger: trigger:
branch: branch:

View File

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

View File

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

View File

@@ -21,145 +21,58 @@
--> -->
<template> <template>
<div class="sidebar"> <app-sidebar
<div class="sidebar-header"> :actions="[]"
<h3>{{ board.title }}</h3> :title="board.title"
</div> @close="closeSidebar">
<ul class="tab-headers"> <AppSidebarTab name="Sharing" icon="icon-shared">
<li v-for="tab in tabs" :class="{ 'selected': tab.isSelected }" :key="tab.name"> <SharingTabSidebard :board="board" />
<a @click="setSelectedHeader(tab.name)">{{ tab.name }}</a> </AppSidebarTab>
</li>
</ul>
<div class="tabsContainer"> <AppSidebarTab name="Tags" icon="icon-tag">
<div class="tab"> <TagsTabSidebard :board="board" />
<div v-if="activeTab === 'Sharing'"> </AppSidebarTab>
<multiselect v-model="value" :options="board.sharees" /> <AppSidebarTab name="Deleted items" icon="icon-delete">
<DeletedTabSidebard :board="board" />
</AppSidebarTab>
<ul <AppSidebarTab name="Timeline" icon="icon-activity">
id="shareWithList" <TimelineTabSidebard :board="board" />
class="shareWithList" </AppSidebarTab>
>
<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>
<div </app-sidebar>
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>
</template> </template>
<script> <script>
import { Avatar, Multiselect } from 'nextcloud-vue'
import { mapState } from 'vuex' 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 { export default {
name: 'BoardSidebar', name: 'BoardSidebar',
components: { components: {
Avatar, AppSidebar,
Multiselect AppSidebarTab,
}, SharingTabSidebard,
props: { TagsTabSidebard,
}, DeletedTabSidebard,
data() { TimelineTabSidebard
return {
activeTab: 'Sharing',
tabs: [
{
name: 'Sharing',
isSelected: true
},
{
name: 'Tags',
isSelected: false
},
{
name: 'Deleted items',
isSelected: false
},
{
name: 'Timeline',
isSelected: false
}
]
}
}, },
computed: { computed: {
...mapState({ ...mapState({
board: state => state.currentBoard board: state => state.currentBoard,
labels: state => state.labels
}) })
}, },
methods: { methods: {
closeSidebar() { closeSidebar() {
this.$store.dispatch('toggleSidebar') this.$router.push({ name: 'board' })
},
setSelectedHeader(tabName) {
this.activeTab = tabName
this.tabs.forEach(tab => {
tab.isSelected = (tab.name === tabName)
})
} }
} }
} }
</script> </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' 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 {boolean} archived
* @property {number} order * @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, sidebarShown: false,
currentBoard: null, currentBoard: null,
boards: [], boards: [],
sharees: [],
boardFilter: BOARD_FILTERS.ALL boardFilter: BOARD_FILTERS.ALL
}, },
getters: { getters: {
boards: state => { boards: state => {
return state.boards return state.boards
}, },
sharees: state => {
return state.sharees
},
noneArchivedBoards: state => { noneArchivedBoards: state => {
return state.boards.filter(board => { return state.boards.filter(board => {
return board.archived === false && !board.deletedAt return board.archived === false && !board.deletedAt
@@ -80,6 +84,9 @@ export default new Vuex.Store({
|| (state.boardFilter === BOARD_FILTERS.SHARED && board.shared === 1) || (state.boardFilter === BOARD_FILTERS.SHARED && board.shared === 1)
}) })
return boards.map(boardToMenuItem) return boards.map(boardToMenuItem)
},
currentBoardLabels: state => {
return state.currentBoard.labels
} }
}, },
mutations: { mutations: {
@@ -132,6 +139,30 @@ export default new Vuex.Store({
}, },
setCurrentBoard(state, board) { setCurrentBoard(state, board) {
state.currentBoard = 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: { actions: {
@@ -185,14 +216,15 @@ export default new Vuex.Store({
const boards = await apiClient.loadBoards() const boards = await apiClient.loadBoards()
commit('setBoards', boards) commit('setBoards', boards)
}, },
async loadSharees({ commit }) { loadSharees({ commit }) {
const params = { const params = new URLSearchParams()
format: 'json', params.append('format', 'json')
perPage: 4, params.append('perPage', 4)
itemType: [0, 1] params.append('itemType', 0)
} params.append('itemType', 1)
const { data } = await axios.get(OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees', { params }) axios.get(OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees', { params }).then((response) => {
commit('setSharees', data.users) commit('setSharees', response.data.ocs.data.users)
})
}, },
setBoardFilter({ commmit }, filter) { setBoardFilter({ commmit }, filter) {
commmit('setBoardFilter', filter) commmit('setBoardFilter', filter)
@@ -208,6 +240,27 @@ export default new Vuex.Store({
}, },
setCurrentBoard({ commit }, board) { setCurrentBoard({ commit }, board) {
commit('setCurrentBoard', 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)
})
} }
} }
}) })