mirror of https://github.com/nocodb/nocodb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
431 lines
11 KiB
431 lines
11 KiB
<template> |
|
<v-container fluid class="h-100 d-100" style="overflow:auto "> |
|
<kanban-board |
|
:stages="kanban.groupingColumnItems" |
|
:blocks="kanban.blocks" |
|
class="h-100 my-0 mx-n2" |
|
@update-block="updateBlock" |
|
> |
|
<div v-for="stage in kanban.groupingColumnItems" :slot="stage" :key="stage" class="mx-auto"> |
|
<enum-cell :value="stage" :column="groupingFieldColumn" /> |
|
</div> |
|
<div v-for="(block) in kanban.blocks" :slot="block.id" :key="block.c_pk" class="caption"> |
|
<v-hover v-slot="{hover}"> |
|
<v-card |
|
class="h-100" |
|
:elevation="hover ? 4 : 1" |
|
@click="$emit('expandKanbanForm', {rowIdx: block.c_pk})" |
|
> |
|
<v-card-text> |
|
<v-container> |
|
<v-row class=""> |
|
<v-col |
|
v-for="(col) in fields" |
|
v-show="showFields[col.title]" |
|
:key="col.title" |
|
class="kanban-col col-12" |
|
> |
|
<label :for="`data-table-form-${col.title}`" class="body-2 text-capitalize caption grey--text"> |
|
<virtual-header-cell |
|
v-if="col.virtual" |
|
:column="col" |
|
:nodes="nodes" |
|
:is-form="true" |
|
:meta="meta" |
|
/> |
|
<header-cell |
|
v-else |
|
:is-form="true" |
|
:value="col.title" |
|
:column="col" |
|
/> |
|
</label> |
|
<virtual-cell |
|
v-if="col.virtual" |
|
ref="virtual" |
|
:column="col" |
|
:row="block" |
|
:nodes="nodes" |
|
:meta="meta" |
|
/> |
|
<table-cell |
|
v-else |
|
:value="block[col.title]" |
|
:column="col" |
|
:sql-ui="sqlUi" |
|
class="xc-input body-2" |
|
:meta="meta" |
|
/> |
|
</v-col> |
|
</v-row> |
|
</v-container> |
|
</v-card-text> |
|
</v-card> |
|
</v-hover> |
|
</div> |
|
<div v-for="stage in kanban.groupingColumnItems" :key="stage" :slot="`footer-${stage}`" class="kanban-footer"> |
|
<x-btn |
|
v-if="stage" |
|
outlined |
|
tooltip="Add a new record" |
|
color="primary" |
|
class="primary" |
|
x-small |
|
fab |
|
@click="insertNewRow(true, true, {[groupingField]: stage})" |
|
> |
|
<v-icon small> |
|
mdi-plus |
|
</v-icon> |
|
</x-btn> |
|
<!-- <x-btn |
|
v-else |
|
outlined |
|
tooltip="New Stack" |
|
color="primary" |
|
class="primary" |
|
small |
|
@click="insertNewRow(true, true, {[groupingField]: stage})" |
|
> |
|
<v-icon small left> |
|
mdi-plus |
|
</v-icon> |
|
New Stack |
|
</x-btn> --> |
|
<div class="record-cnt caption grey--text"> |
|
{{ kanban.recordCnt[stage] }} / {{ kanban.recordTotalCnt[stage] }} |
|
{{ kanban.recordTotalCnt[stage] > 1 ? "records" : "record" }} |
|
</div> |
|
</div> |
|
</kanban-board> |
|
</v-container> |
|
</template> |
|
|
|
<script> |
|
import VirtualHeaderCell from '../components/VirtualHeaderCell' |
|
import HeaderCell from '../components/HeaderCell' |
|
import VirtualCell from '../components/VirtualCell' |
|
import TableCell from '../components/Cell' |
|
import EnumCell from '../components/cell/EnumCell' |
|
export default { |
|
name: 'KanbanView', |
|
components: { TableCell, VirtualCell, HeaderCell, VirtualHeaderCell, EnumCell }, |
|
props: [ |
|
'nodes', |
|
'table', |
|
'showFields', |
|
'availableColumns', |
|
'meta', |
|
'kanban', |
|
'primaryValueColumn', |
|
'showSystemFields', |
|
'sqlUi', |
|
'groupingField', |
|
'api' |
|
], |
|
computed: { |
|
fields() { |
|
if (this.availableColumns) { |
|
return this.availableColumns |
|
} |
|
if (this.showSystemFields) { |
|
return this.meta.columns || [] |
|
} |
|
const hideCols = ['created_at', 'updated_at'] |
|
return this.meta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.column_name) && |
|
!((this.meta.v || []).some(v => v.bt && v.bt.column_name === c.column_name)) |
|
) || [] |
|
}, |
|
groupingFieldColumn() { |
|
return this.fields.filter(f => f.title === this.groupingField)[0] |
|
} |
|
}, |
|
mounted() { |
|
const kbListElements = document.querySelectorAll('.drag-inner-list') |
|
kbListElements.forEach((kbListEle) => { |
|
kbListEle.addEventListener('scroll', async(e) => { |
|
if (kbListEle.scrollTop + kbListEle.clientHeight >= kbListEle.scrollHeight) { |
|
const groupingFieldVal = kbListEle.getAttribute('data-status') |
|
this.$emit('loadMoreKanbanData', groupingFieldVal) |
|
} |
|
}) |
|
}) |
|
}, |
|
methods: { |
|
async updateBlock(c_pk, status) { |
|
try { |
|
if (!this.api) { |
|
this.$toast.error('API not found', { |
|
position: 'bottom-center' |
|
}).goAway(3000) |
|
return |
|
} |
|
|
|
// update kanban block |
|
const targetBlock = this.kanban.blocks.find(b => b.c_pk === c_pk) |
|
if (!targetBlock) { |
|
this.$toast.error(`Block with ID ${c_pk} not found`, { |
|
position: 'bottom-center' |
|
}).goAway(3000) |
|
return |
|
} |
|
|
|
if (targetBlock.status === status) { |
|
// no change |
|
return |
|
} |
|
|
|
const uncategorized = 'Uncategorized' |
|
const prevStatus = targetBlock.status |
|
await this.api.update(c_pk, |
|
{ [this.groupingField]: status === uncategorized ? null : status }, // new data |
|
{ [this.groupingField]: prevStatus }) // old data |
|
|
|
this.$set(targetBlock, 'status', status) |
|
this.$set(targetBlock, this.groupingField, status === uncategorized ? null : status) |
|
|
|
// update kanban data |
|
const kanbanRow = this.kanban.data.find(d => d.row.c_pk === c_pk) |
|
if (kanbanRow) { |
|
this.$set(kanbanRow.row, this.groupingField, status === uncategorized ? null : status) |
|
} |
|
this.$set(this.kanban.recordCnt, prevStatus, this.kanban.recordCnt[prevStatus] - 1) |
|
this.$set(this.kanban.recordCnt, status, this.kanban.recordCnt[status] + 1) |
|
this.$set(this.kanban.recordTotalCnt, prevStatus, this.kanban.recordTotalCnt[prevStatus] - 1) |
|
this.$set(this.kanban.recordTotalCnt, status, this.kanban.recordTotalCnt[status] + 1) |
|
this.$forceUpdate() |
|
this.$toast.success(`Moved block from ${prevStatus} to ${status ?? uncategorized} successfully.`, { |
|
position: 'bottom-center' |
|
}).goAway(3000) |
|
} catch (e) { |
|
if (e.response && e.response.data && e.response.data.msg) { |
|
this.$toast.error(e.response.data.msg, { |
|
position: 'bottom-center' |
|
}).goAway(3000) |
|
} else { |
|
this.$toast.error(`Failed to update block : ${e.message}`, { |
|
position: 'bottom-center' |
|
}).goAway(3000) |
|
} |
|
} |
|
}, |
|
insertNewRow(atEnd = false, expand = false, presetValues = {}) { |
|
this.$emit('insertNewRow', atEnd, expand, presetValues) |
|
} |
|
} |
|
} |
|
</script> |
|
|
|
<style scoped lang="scss"> |
|
::v-deep { |
|
.v-card { |
|
border: 1px solid rgba(0, 0, 0, 0.2); |
|
} |
|
|
|
ul.drag-inner-list { |
|
overflow-y: scroll; |
|
} |
|
|
|
ul.drag-list, ul.drag-inner-list { |
|
list-style-type: none; |
|
margin: 0; |
|
padding: 0; |
|
} |
|
|
|
.drag-container { |
|
//max-width: 1000px; |
|
margin: 20px 0px; |
|
} |
|
|
|
.drag-list { |
|
display: flex; |
|
align-items: flex-start; |
|
} |
|
|
|
@media (max-width: 690px) { |
|
.drag-list { |
|
display: block; |
|
} |
|
} |
|
|
|
.drag-column { |
|
flex: 1; |
|
margin: 0 10px; |
|
position: relative; |
|
background: var(--v-backgroundColor-base); //rgba(256, 256, 256, 0.2); |
|
border-radius: 6px; |
|
max-width: 240px; |
|
} |
|
|
|
.drag-column-footer { |
|
padding: 20px 10px 10px 10px; |
|
text-align: center; |
|
} |
|
|
|
/* .drag-column-footer .v-btn { |
|
border-radius: 50%; |
|
border: 2px solid; |
|
padding: 0px 0px 0px 6px; |
|
min-width: 40px; |
|
min-height: 38px; |
|
}*/ |
|
|
|
.drag-column-footer .record-cnt { |
|
height: 38px; |
|
line-height: 38px; |
|
font-size: 15px; |
|
} |
|
|
|
.drag-column-footer .v-btn .mdi-plug::before { |
|
font-weight: bold; |
|
} |
|
|
|
@media (max-width: 690px) { |
|
.drag-column { |
|
margin-bottom: 30px; |
|
} |
|
} |
|
|
|
.drag-column h2 { |
|
font-size: 0.8rem; |
|
margin: 0; |
|
text-transform: uppercase; |
|
font-weight: 600; |
|
} |
|
|
|
.drag-column-header { |
|
display: flex; |
|
align-items: center; |
|
justify-content: space-between; |
|
width: 240px; |
|
} |
|
|
|
.drag-column-header .set-item { |
|
margin-top: 20px !important; |
|
} |
|
|
|
.drag-item { |
|
margin: 10px; |
|
background: var(--v-backgroundColor-lighten2); |
|
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); |
|
border-radius: 4px; |
|
} |
|
|
|
.drag-item .container { |
|
padding: 0px; |
|
} |
|
|
|
.drag-item.is-moving { |
|
transform: scale(1.1); |
|
background: var(--v-backgroundColor-darken1); |
|
} |
|
|
|
.drag-header-more { |
|
cursor: pointer; |
|
} |
|
|
|
.drag-options { |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
transform: translateX(100%); |
|
opacity: 0; |
|
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); |
|
} |
|
|
|
.drag-options.active { |
|
transform: translateX(0); |
|
opacity: 1; |
|
} |
|
|
|
.drag-options-label { |
|
display: block; |
|
margin: 0 0 5px 0; |
|
} |
|
|
|
.drag-options-label input { |
|
opacity: 0.6; |
|
} |
|
|
|
.drag-options-label span { |
|
display: inline-block; |
|
font-size: 0.9rem; |
|
font-weight: 400; |
|
margin-left: 5px; |
|
} |
|
|
|
/* Dragula CSS */ |
|
.gu-mirror { |
|
position: fixed !important; |
|
margin: 0 !important; |
|
z-index: 9999 !important; |
|
opacity: 0.8; |
|
list-style-type: none; |
|
} |
|
|
|
.gu-hide { |
|
display: none !important; |
|
} |
|
|
|
.gu-unselectable { |
|
-webkit-user-select: none !important; |
|
-moz-user-select: none !important; |
|
-ms-user-select: none !important; |
|
user-select: none !important; |
|
} |
|
|
|
.gu-transit { |
|
opacity: 0.2; |
|
} |
|
|
|
.kanban-col { |
|
padding: 10px; |
|
} |
|
|
|
.drag-container { |
|
display: inline-block; |
|
.drag-list { |
|
height: 100%; |
|
display: inline-flex; |
|
|
|
.drag-column { |
|
display: flex; |
|
flex-direction: column; |
|
max-height: max(400px,100%); |
|
|
|
.drag-inner-list { |
|
overflow-y: auto; |
|
overflow-x: hidden; |
|
min-height: 200px; |
|
flex-grow: 1 |
|
} |
|
} |
|
} |
|
} |
|
|
|
} |
|
</style> |
|
|
|
<!-- |
|
/** |
|
* @copyright Copyright (c) 2021, Xgene Cloud Ltd |
|
* |
|
* @author Wing-Kam Wong <wingkwong.code@gmail.com> |
|
* |
|
* @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/>. |
|
* |
|
*/ |
|
-->
|
|
|