Browse Source

feat: kanban view (#903)

* feat: enable kanban button on nav drawer

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* refak: move conditions to isKanban

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* wip: add basic layout & integrate with view data

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: add missing components

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: emit expandForm

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: add boolean cell for kanban card

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: add :disable to boollean cell

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* style: kanban card

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: enable kanban share view on drawer

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: share links

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* wip: kanban shared page

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* wip: implement updateBlock logic

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* wip: reload kanban view after drag n drop

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* refak: kanban view

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: update CsvExportImport path

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: add grouping field for kanban view

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: add uncategorized stack

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* style: match stack title color with that of grouping field

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* style: kanban view

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: implement groupingField change logic

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: watch data prop change

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* style: fix center title n padding

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* wip: add footer

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* test(cypress): include kanban viewTest

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: add insertNewRow logic to kanban view

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: add new stack column

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: include loadTableData

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: reload issue

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* wip: fetch data using api

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: add api filtering logic

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: inconsistent content

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* chore: merge from upstream master

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* chore: hide new stack button

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: populate the grouping field to new record for kanban

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* chore: hide pagination in kanban view

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* style: display add new record as an icon

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: add numbers of records under each stack

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: missing uncategorized records

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: kanban preset value issue

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: kanban layout & 0 record issue

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* chore: set toast position

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: lint

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: add v-skeleton-loader

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* chore: rm toast if there is no grouping column

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: remove :loading

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* refak: move kanban logic to rowsXcDataTable

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* refak: update kanban block logic

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* refak: remove unused code

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: update kanban ui for status and grouping field

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* refak: move set kanban logic to rowsXcDataTable

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: new kanban record issue

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: infinite scrolling on kanban view

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* refak: remove updateKanbanBlock

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: splash issue

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: empty kanban view

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* chore: move skeleton-loader to rowsXcDataTable.vue

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: avoid reload after saving in expanded form

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* refak: remove unused clonedBlock

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: handle composite primary key

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: kanban refresh issue

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: update reload logic for kanban

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* feat: add total number of records for each kanban stack

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: kanban display issue

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* wip: kanban share view

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: insert new row issue

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* fix: missing data after reload

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* chore: temporarily disable share view for kanban view

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>

* chore: disable kanban viewTest temporarily

Signed-off-by: Wing-Kam Wong <wingkwong.code@gmail.com>
pull/914/head
աɨռɢӄաօռɢ 3 years ago committed by GitHub
parent
commit
cfabc58456
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      packages/nc-gui/components/project/spreadsheet/components/cell.vue
  2. 49
      packages/nc-gui/components/project/spreadsheet/components/cell/booleanCell.vue
  3. 10
      packages/nc-gui/components/project/spreadsheet/components/expandedForm.vue
  4. 48
      packages/nc-gui/components/project/spreadsheet/components/fieldsMenu.vue
  5. 26
      packages/nc-gui/components/project/spreadsheet/components/sharedViewsList.vue
  6. 143
      packages/nc-gui/components/project/spreadsheet/components/spreadsheetNavDrawer.vue
  7. 15
      packages/nc-gui/components/project/spreadsheet/dialog/createViewDialog.vue
  8. 5
      packages/nc-gui/components/project/spreadsheet/mixins/spreadsheet.js
  9. 369
      packages/nc-gui/components/project/spreadsheet/public/xcKanban.vue
  10. 248
      packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue
  11. 371
      packages/nc-gui/components/project/spreadsheet/views/kanbanView.vue
  12. 44
      packages/nc-gui/pages/nc/kanban/_id.vue
  13. 2
      packages/nocodb/src/lib/noco/meta/NcMetaMgrEE.ts
  14. 4
      scripts/cypress/integration/common/4a_table_view_grid_gallery_form.js

5
packages/nc-gui/components/project/spreadsheet/components/cell.vue

@ -15,6 +15,7 @@
<date-cell v-else-if="isDate" :value="value" />
<date-time-cell v-else-if="isDateTime" :value="value" />
<time-cell v-else-if="isTime" :value="value" />
<boolean-cell v-else-if="isBoolean" :value="value" />
<span v-else :class="{'long-text-cell' : isTextArea}" :title="title">{{ value }}</span>
</template>
@ -28,10 +29,11 @@ import cell from '@/components/project/spreadsheet/mixins/cell'
import SetListCell from '@/components/project/spreadsheet/components/cell/setListCell'
import EnumCell from '@/components/project/spreadsheet/components/cell/enumCell'
import EditableAttachmentCell from '@/components/project/spreadsheet/components/editableCell/editableAttachmentCell'
import BooleanCell from '@/components/project/spreadsheet/components/cell/booleanCell'
export default {
name: 'TableCell',
components: { TimeCell, DateTimeCell, DateCell, JsonCell, UrlCell, EditableAttachmentCell, EnumCell, SetListCell },
components: { TimeCell, DateTimeCell, DateCell, JsonCell, UrlCell, EditableAttachmentCell, EnumCell, SetListCell, BooleanCell },
mixins: [cell],
props: ['value', 'dbAlias', 'isLocked', 'selected'],
computed: {
@ -54,6 +56,7 @@ export default {
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*

49
packages/nc-gui/components/project/spreadsheet/components/cell/booleanCell.vue

@ -0,0 +1,49 @@
<template>
<div class="d-flex align-center ">
<input v-model="localState" type="checkbox" :disabled="true">
</div>
</template>
<script>
export default {
name: 'BooleanCell',
props: ['value'],
computed: {
localState: {
get() {
return this.value
}
}
}
}
</script>
<style scoped>
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
* @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/>.
*
*/
-->

10
packages/nc-gui/components/project/spreadsheet/components/expandedForm.vue

@ -301,7 +301,8 @@ export default {
},
availableColumns: [Object, Array],
queryParams: Object,
meta: Object
meta: Object,
presetValues: Object
},
data: () => ({
showborder: false,
@ -395,6 +396,13 @@ export default {
try {
const id = this.meta.columns.filter(c => c.pk).map(c => this.localState[c._cn]).join('___')
if (this.presetValues) {
// cater presetValues
for (const k in this.presetValues) {
this.$set(this.changedColumns, k, true)
}
}
const updatedObj = Object.keys(this.changedColumns).reduce((obj, col) => {
obj[col] = this.localState[col]
return obj

48
packages/nc-gui/components/project/spreadsheet/components/fieldsMenu.vue

@ -33,7 +33,7 @@
<v-select
v-model="coverImageFieldLoc"
label="Cover Image"
class="caption cover-image"
class="caption field-caption"
dense
outlined
:items="attachmentFields"
@ -43,7 +43,7 @@
@click.stop
>
<template #prepend-inner>
<v-icon small class="cover-image-icon">
<v-icon small class="field-icon">
mdi-image
</v-icon>
</template>
@ -52,6 +52,30 @@
<v-divider />
</template>
<template v-if="isKanban">
<div class="pa-2">
<v-select
v-model="groupingFieldLoc"
label="Grouping Field"
class="caption field-caption"
dense
outlined
:items="singleSelectFields"
item-text="alias"
item-value="_cn"
hide-details
@click.stop
>
<template #prepend-inner>
<v-icon small class="field-icon">
mdi-select-group
</v-icon>
</template>
</v-select>
</div>
<v-divider />
</template>
<v-list-item
dense
class=""
@ -139,7 +163,9 @@ export default {
},
props: {
coverImageField: String,
groupingField: String,
isGallery: Boolean,
isKanban: Boolean,
sqlUi: [Object, Function],
meta: Object,
fieldsOrder: [Array],
@ -164,6 +190,12 @@ export default {
_cn: ''
}]
},
singleSelectFields() {
return [...(this.meta && this.meta.columns ? this.meta.columns.filter(f => f.uidt === 'SingleSelect') : []), {
alias: 'None',
_cn: ''
}]
},
coverImageFieldLoc: {
get() {
return this.coverImageField
@ -172,6 +204,14 @@ export default {
this.$emit('update:coverImageField', val)
}
},
groupingFieldLoc: {
get() {
return this.groupingField
},
set(val) {
this.$emit('update:groupingField', val)
}
},
columnMeta() {
return this.meta && this.meta.columns ? this.meta.columns.reduce((o, c) => ({ ...o, [c._cn]: c }), {}) : {}
},
@ -244,7 +284,7 @@ export default {
font-size: 12px !important;
}
.cover-image {
.field-caption {
.v-input__append-inner {
margin-top: 4px !important;
}
@ -257,7 +297,7 @@ export default {
max-height: 20px !important;
}
.cover-image-icon{
.field-icon{
margin-top: 2px;
}
}

26
packages/nc-gui/components/project/spreadsheet/components/sharedViewsList.vue

@ -28,8 +28,8 @@
{{ currentView.view_name }}
</td>
<td class="caption text-left">
<nuxt-link :to="`/nc/${currentView.view_type === 'form' ? 'form' : 'view'}/${currentView.view_id}`">
{{ `${dashboardUrl}#/nc/${currentView.view_type === 'form' ? 'form' : 'view'}/${currentView.view_id}` }}
<nuxt-link :to="sharedViewUrl(currentView)">
{{ `${dashboardUrl}#${sharedViewUrl(currentView)}` }}
</nuxt-link>
</td>
<td class="caption">
@ -65,8 +65,8 @@
{{ link.view_name }}
</td>
<td class="caption text-left">
<nuxt-link :to="`/nc/${link.view_type === 'form' ? 'form' : 'view'}/${link.view_id}`">
{{ `${dashboardUrl}#/nc/${link.view_type === 'form' ? 'form' : 'view'}/${link.view_id}` }}
<nuxt-link :to="sharedViewUrl(link)">
{{ `${dashboardUrl}#${sharedViewUrl(link)}` }}
</nuxt-link>
</td>
<td class="caption">
@ -125,12 +125,10 @@ export default {
this.loadSharedViewsList()
},
methods: {
copyLink(view) {
this.$clipboard(`${this.dashboardUrl}#/nc/${view.view_type === 'form' ? 'form' : 'view'}/${view.view_id}`)
this.$clipboard(`${this.dashboardUrl}#${this.sharedViewUrl(view)}`)
this.$toast.info('Copied to clipboard').goAway(1000)
},
async loadSharedViewsList() {
const viewsList = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: this.nodes.dbAlias }, 'listSharedViewLinks', {
model_name: this.modelName
@ -163,6 +161,20 @@ export default {
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}
},
sharedViewUrl(view) {
let viewType
switch (view.view_type) {
case 'form':
viewType = 'form'
break
case 'kanban':
viewType = 'kanban'
break
default:
viewType = 'view'
}
return `/nc/${viewType}/${view.view_id}`
}
}
}

143
packages/nc-gui/components/project/spreadsheet/components/spreadsheetNavDrawer.vue

@ -199,7 +199,6 @@
<v-list-item-title>
<span class="font-weight-regular">
<!-- Gallery -->
{{ $t('nav_drawer.virtual_views.gallery.title') }}
</span>
</v-list-item-title>
@ -213,65 +212,63 @@
<!-- Add Gallery View -->
{{ $t('nav_drawer.virtual_views.gallery.create') }}
</v-tooltip>
<!-- <v-tooltip bottom>
<template #activator="{ on }">
<v-list-item
dense
class="body-2"
v-on="on"
@click="enableDummyFeat ? openCreateViewDlg('calendar') : comingSoon()"
>
<v-list-item-icon class="mr-n1">
<v-icon x-small>
mdi-calendar
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<span class="font-weight-regular">
&lt;!&ndash; Calendar &ndash;&gt;
{{ $t('nav_drawer.virtual_views.calendar.title') }}
</span>
</v-list-item-title>
<!-- <v-tooltip bottom>-->
<!-- <template #activator="{ on }">-->
<!-- <v-list-item-->
<!-- dense-->
<!-- class="body-2"-->
<!-- v-on="on"-->
<!-- @click="enableDummyFeat ? openCreateViewDlg('calendar') : comingSoon()"-->
<!-- >-->
<!-- <v-list-item-icon class="mr-n1">-->
<!-- <v-icon x-small>-->
<!-- mdi-calendar-->
<!-- </v-icon>-->
<!-- </v-list-item-icon>-->
<!-- <v-list-item-title>-->
<!-- <span class="font-weight-regular">-->
<!-- &lt;!&ndash; Calendar &ndash;&gt;-->
<!-- {{ $t('nav_drawer.virtual_views.calendar.title') }}-->
<!-- </span>-->
<!-- </v-list-item-title>-->
<!-- <v-spacer />-->
<!-- <v-icon class="mr-1" small>-->
<!-- mdi-plus-->
<!-- </v-icon>-->
<!-- </v-list-item>-->
<!-- </template>-->
<!-- &lt;!&ndash; Add Calendar View &ndash;&gt;-->
<!-- {{ $t('nav_drawer.virtual_views.calendar.create') }}-->
<!-- </v-tooltip>-->
<!-- <v-tooltip bottom>-->
<!-- <template #activator="{ on }">-->
<!-- <v-list-item-->
<!-- dense-->
<!-- open-class="body-2"-->
<!-- v-on="on"-->
<!-- @click="enableDummyFeat ? openCreateViewDlg('kanban') : comingSoon()"-->
<!-- >-->
<!-- <v-list-item-icon class="mr-n1">-->
<!-- <v-icon x-small>-->
<!-- mdi-tablet-dashboard-->
<!-- </v-icon>-->
<!-- </v-list-item-icon>-->
<!-- <v-list-item-title>-->
<!-- <span class="font-weight-regular">-->
<!-- &lt;!&ndash; Kanban &ndash;&gt;-->
<!-- {{ $t('nav_drawer.virtual_views.kanban.title') }}-->
<!-- </span>-->
<!-- </v-list-item-title>-->
<!-- <v-spacer />-->
<!-- <v-icon class="mr-1" small>-->
<!-- mdi-plus-->
<!-- </v-icon>-->
<!-- </v-list-item>-->
<!-- </template>-->
<!-- &lt;!&ndash; Add Kanban View &ndash;&gt;-->
<!-- {{ $t('nav_drawer.virtual_views.kanban.create') }}-->
<!-- </v-tooltip>-->
<v-spacer />
<v-icon class="mr-1" small>
mdi-plus
</v-icon>
</v-list-item>
</template>
&lt;!&ndash; Add Calendar View &ndash;&gt;
{{ $t('nav_drawer.virtual_views.calendar.create') }}
</v-tooltip> -->
<v-tooltip bottom>
<template #activator="{ on }">
<v-list-item
dense
open-class="body-2"
v-on="on"
@click="openCreateViewDlg('kanban')"
>
<v-list-item-icon class="mr-n1">
<v-icon x-small>
mdi-tablet-dashboard
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<span class="font-weight-regular">
<!-- Kanban -->
{{ $t('nav_drawer.virtual_views.kanban.title') }}
</span>
</v-list-item-title>
<v-spacer />
<v-icon class="mr-1" small>
mdi-plus
</v-icon>
</v-list-item>
</template>
Add Kanban View
{{ $t('nav_drawer.virtual_views.kanban.create') }}
</v-tooltip>
<v-tooltip
bottom
>
@ -372,9 +369,13 @@
<!-- <v-menu offset-x left>-->
<!-- <template v-slot:activator="{on}">-->
<!--
TODO:
- Add selectedView.show_as === 'kanban' when it is ready
-->
<v-list-item
v-show="
selectedView && (selectedView.type === 'view' || selectedView.type === 'table' || selectedView.show_as === 'form' ||selectedView.show_as === 'grid' )
selectedView && (selectedView.type === 'view' || selectedView.type === 'table' || selectedView.show_as === 'form' || selectedView.show_as === 'grid' )
"
v-if="_isUIAllowed('shareview')"
@click="genShareLink"
@ -454,10 +455,10 @@
<!-- People with private link can only see cells visible in this view -->
</p>
<div style="border-radius: 4px" class="share-link-box body-2 pa-2 d-flex align-center">
{{ `${dashboardUrl}#/nc/${shareLink.view_type === 'form' ? 'form' : 'view'}/${shareLink.view_id}` }}
{{ sharedViewUrl }}
<v-spacer />
<a
:href=" `${dashboardUrl}#/nc/${shareLink.view_type === 'form' ? 'form' : 'view'}/${shareLink.view_id}`"
:href="`${sharedViewUrl}`"
style="text-decoration: none"
target="_blank"
>
@ -553,6 +554,7 @@ export default {
viewStatus: Object,
columnsWidth: Object,
coverImageField: String,
groupingField: String,
showSystemFields: Boolean
},
data: () => ({
@ -610,6 +612,20 @@ export default {
}
return id
}
},
sharedViewUrl() {
let viewType
switch (this.shareLink.view_type) {
case 'form':
viewType = 'form'
break
case 'kanban':
viewType = 'kanban'
break
default:
viewType = 'view'
}
return `${this.dashboardUrl}#/nc/${viewType}/${this.shareLink.view_id}`
}
},
watch: {
@ -666,6 +682,7 @@ export default {
this.$emit('update:columnsWidth', queryParams.columnsWidth || {})
this.$emit('update:extraViewParams', queryParams.extraViewParams || {})
this.$emit('update:coverImageField', queryParams.coverImageField)
this.$emit('update:groupingField', queryParams.groupingField)
this.$emit('update:showSystemFields', queryParams.showSystemFields)
if (queryParams.showFields) {
this.$emit('update:showFields', queryParams.showFields)
@ -871,7 +888,7 @@ export default {
this.$toast.info('Copied to clipboard').goAway(1000)
},
copyShareUrlToClipboard() {
this.clipboard(`${this.dashboardUrl}#/nc/${this.shareLink.view_type === 'form' ? 'form' : 'view'}/${this.shareLink.view_id}`)
this.clipboard(this.sharedViewUrl)
this.clipboardSuccessHandler()
}
}

15
packages/nc-gui/components/project/spreadsheet/dialog/createViewDialog.vue

@ -77,6 +77,7 @@ export default {
if (!this.valid) { return }
let showFields = null
let attachmentCol
let singleSelectCol
if (this.show_as === 'gallery') {
showFields = { [this.primaryValueColumn]: true }
attachmentCol = this.meta.columns.find(c => c.uidt === 'Attachment')
@ -90,6 +91,19 @@ export default {
})
}
if (this.show_as === 'kanban') {
showFields = { [this.primaryValueColumn]: true }
singleSelectCol = this.meta.columns.find(c => c.uidt === 'SingleSelect')
if (singleSelectCol) {
showFields[singleSelectCol.cn] = true
}
this.meta.columns.forEach((c) => {
if (c.pk) {
showFields[c.cn] = true
}
})
}
this.loading = true
try {
const viewMeta = await this.sqlOp({
@ -99,6 +113,7 @@ export default {
query_params: {
showFields,
coverImageField: attachmentCol ? attachmentCol._cn : '',
groupingField: singleSelectCol ? singleSelectCol._cn : '',
...this.queryParams
},
parent_model_title: this.table,

5
packages/nc-gui/components/project/spreadsheet/mixins/spreadsheet.js

@ -322,6 +322,11 @@ export default {
this.syncDataDebounce(this)
}
},
groupingField(v) {
if (!this.loadingMeta || !this.loadingData) {
this.syncDataDebounce(this)
}
},
fieldsOrder: {
handler(v) {
if (!this.loadingMeta || !this.loadingData) {

369
packages/nc-gui/components/project/spreadsheet/public/xcKanban.vue

@ -0,0 +1,369 @@
<template>
<v-container
class="h-100 j-excel-container "
:class="{
'pa-0 ma-0': ! notFound
}"
fluid
>
<v-alert v-if="notFound" type="warning" class="mx-auto mt-10" outlined max-width="300">
Not found
</v-alert>
<template v-else>
<div v-if="viewName" class="model-name">
<span class="font-weight-bold"> {{ viewName }}</span> <span class="font-weight-regular ml-1" />
</div>
<v-toolbar v-if="meta" height="40" dense class="elevation-0 xc-toolbar xc-border-bottom" style="z-index: 7;border-radius: 4px">
<v-spacer />
<v-btn outlined small text @click="reload">
<v-icon small class="mr-1" color="grey darken-3">
mdi-reload
</v-icon>
Reload
</v-btn>
<fields-menu v-model="showFields" :field-list="fieldList" is-public />
<sort-list-menu v-model="sortList" :field-list="realFieldList" />
<column-filter-menu v-model="filters" :field-list="realFieldList" />
<csv-export-import :query-params="{...queryParams, showFields}" :public-view-id="$route.params.id" :meta="meta" />
</v-toolbar>
<div
v-if="meta"
class="nc-grid-wrapper d-flex"
:class="`cell-height-${cellHeight}`"
style="overflow:auto;transition: width 500ms "
>
<v-container fluid v-if="loadingData">
<v-row>
<v-col v-for="idx in 5" :key="idx">
<v-skeleton-loader type="image@3"></v-skeleton-loader>
</v-col>
</v-row>
</v-container>
<kanban-view
v-if="!loadingData && kanban.data.length"
:nodes="nodes"
:table="table"
:show-fields="showFields"
:available-columns="availableColumns"
:meta="meta"
:kanban="kanban"
:sql-ui="sqlUi"
:primary-value-column="primaryValueColumn"
:grouping-field.sync="groupingField"
:api="api"
@loadMoreKanbanData="(groupingFieldVal) => loadMoreKanbanData(groupingFieldVal)"
/>
</div>
</template>
</v-container>
</template>
<script>
/* eslint-disable camelcase */
import spreadsheet from '../mixins/spreadsheet'
import FieldsMenu from '../components/fieldsMenu'
import SortListMenu from '../components/sortListMenu'
import ColumnFilterMenu from '../components/columnFilterMenu'
import CsvExportImport from '~/components/project/spreadsheet/components/exportImport'
import KanbanView from '@/components/project/spreadsheet/views/kanbanView'
export default {
name: 'XcKanban',
components: { CsvExportImport, ColumnFilterMenu, SortListMenu, FieldsMenu, KanbanView },
mixins: [spreadsheet],
props: {
env: String,
nodes: Object,
addNewRelationTab: Function,
relationType: String,
relation: Object,
relationIdValue: [String, Number],
refTable: String,
relationPrimaryValue: [String, Number]
},
data: () => ({
notFound: false,
viewName: null,
viewType: null,
columnsWidth: {},
metas: {},
fieldsOrder: [],
password: null,
showPasswordModal: false,
client: 'mysql',
shareLink: null,
showShareModel: false,
loadingMeta: true,
loadingData: true,
toggleDrawer: false,
selectedItem: 0,
searchField: null,
searchQuery: '',
showExpandModal: false,
selectedExpandRowIndex: null,
selectedExpandRowMeta: null,
meta: null,
navDrawer: true,
selected: {
row: null,
col: null
},
editEnabled: {
row: null,
col: null
},
page: 1,
count: 0,
size: 25,
xWhere: '',
sort: '',
cellHeight: 'small',
isAnyFieldHidden: false,
opList: ['is equal', 'is not equal', 'is like', 'is not like', 'is empty', 'is not empty', 'is null', 'is not null'],
fieldFilter: '',
filters: [],
sortList: [],
data: [],
spreadsheet: null,
options: {
allowToolbar: true,
columnSorting: false
},
filteredData: [],
showFields: {},
// fieldList: [],
cellHeights: [{
size: 'small',
icon: 'mdi-view-headline'
}, {
size: 'medium',
icon: 'mdi-view-sequential'
}, {
size: 'large',
icon: 'mdi-view-stream'
}, {
size: 'xlarge',
icon: 'mdi-card'
}],
rowContextMenu: null,
modelName: null,
kanban: {
data: [],
stages: [],
blocks: [],
recordCnt: {},
recordTotalCnt: {},
groupingColumnItems: [],
loadingData : true,
selectedExpandRow: null,
selectedExpandOldRow: null,
selectedExpandRowMeta: null,
},
}),
computed: {
},
async mounted() {
try {
await this.loadMetaData()
if (!this.showPasswordModal && !this.notFound) {
await this.loadKanbanData()
}
} catch (e) {
console.log(e)
}
this.searchField = this.primaryValueColumn
},
methods: {
async loadMetaData() {
this.loading = true
try {
// eslint-disable-next-line camelcase
const {
meta,
// model_name,
view_name,
view_type,
client,
query_params: qp = {},
db_alias: dbAlias = '_noco',
relatedTableMetas = {}
} = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'sharedViewGet', {
view_id: this.$route.params.id,
password: this.password
}])
this.fieldsOrder = qp.fieldsOrder || []
this.viewName = view_name
this.viewType = view_type
this.columnsWidth = qp.columnsWidth || {}
this.client = client
this.meta = meta
this.query_params = qp
this.dbAlias = dbAlias
this.metas = relatedTableMetas
this.sortList = qp.sortList || []
this.showFields = this.query_params.showFields || {}
// this.fieldList = Object.keys(this.showFields)
let fields = this.query_params.fieldsOrder || []
if (!fields.length) { fields = Object.keys(this.showFields) }
// eslint-disable-next-line camelcase
let columns = this.meta.columns
if (this.meta && this.meta.v) {
columns = [...columns, ...this.meta.v.map(v => ({ ...v, virtual: 1 }))]
}
{
const _ref = {}
columns.forEach((c) => {
if (c.virtual && c.bt) {
c.prop = `${c.bt.rtn}Read`
}
if (c.virtual && c.mm) {
c.prop = `${c.mm.rtn}MMList`
}
if (c.virtual && c.hm) {
c.prop = `${c.hm.tn}List`
}
c.alias = c._cn
if (c.alias in _ref) {
c.alias += _ref[c.alias]++
} else {
_ref[c.alias] = 1
}
})
}
} catch (e) {
if (e.message === 'Not found' || e.message === 'Meta not found') {
this.notFound = true
} else if (e.message === 'Invalid password') {
this.showPasswordModal = true
} else {
console.log(e)
}
}
this.loadingData = false
},
async loadKanbanData() {
this.loadingData = true
try {
// TODO
} catch (e) {
this.showPasswordModal = true
}
this.loadingData = false
},
async unlock() {
this.showPasswordModal = false
await this.reload()
},
async reload() {
await this.loadMetaData()
await this.loadKanbanData()
}
}
}
</script>
<style scoped>
/deep/ .v-input__control .v-input__slot .v-input--selection-controls__input {
transform: scale(.85);
margin-right: 0;
}
/deep/ .xc-toolbar .v-input__slot, .navigation .v-input__slot {
box-shadow: none !important;
}
/deep/ .navigation .v-input__slot input::placeholder {
font-size: .8rem;
}
/deep/ .v-btn {
text-transform: capitalize;
}
/deep/ .xc-bt-chip {
margin-right: 12px;
transition: .4s margin-right, .4s padding-right;
}
/deep/ .xc-border.search-box {
overflow: visible;
border-radius: 4px;
}
/deep/ .xc-border.search-box .v-input {
transition: .4s border-color;
}
/deep/ .xc-border.search-box .v-input--is-focused {
border: 1px solid var(--v-primary-base) !important;
margin: -1px;
}
/deep/ .search-field.v-text-field.v-text-field--solo.v-input--dense > .v-input__control {
min-height: auto;
}
.model-name {
position: fixed;
top: 5px;
pointer-events: none;
left: 0;
right: 0;
text-align: center;
z-index: 9999;
width: 100%;
font-size: 1.2rem;
color: white;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.nc-grid-wrapper{
height:calc(100vh - 120px)
}
.nc-grid{
height: calc(100% - 34px)
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
* @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/>.
*
*/
-->

248
packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue

@ -194,7 +194,9 @@
:sql-ui="sqlUi"
:show-system-fields.sync="showSystemFields"
:cover-image-field.sync="coverImageField"
:grouping-field.sync="groupingField"
:is-gallery="isGallery"
:is-kanban="isKanban"
/>
<sort-list
@ -326,17 +328,29 @@
@expandForm="({rowIndex,rowMeta}) => expandRow(rowIndex,rowMeta)"
/>
</template>
<template v-else-if="selectedView && selectedView.show_as === 'kanban' ">
<template v-else-if="isKanban">
<v-container fluid v-if="kanban.loadingData">
<v-row>
<v-col v-for="idx in 5" :key="idx">
<v-skeleton-loader type="image@3"></v-skeleton-loader>
</v-col>
</v-row>
</v-container>
<kanban-view
v-if="!kanban.loadingData && kanban.data.length"
:nodes="nodes"
:table="table"
:show-fields="showFields"
:available-columns="availableColumns"
:meta="meta"
:data="data"
:kanban="kanban"
:sql-ui="sqlUi"
:primary-value-column="primaryValueColumn"
@expandForm="({rowIndex,rowMeta}) => expandRow(rowIndex,rowMeta)"
:grouping-field.sync="groupingField"
:api="api"
@expandKanbanForm="({rowIdx}) => expandKanbanForm(rowIdx)"
@insertNewRow="insertNewRow"
@loadMoreKanbanData="(groupingFieldVal) => loadMoreKanbanData(groupingFieldVal)"
/>
</template>
<template v-else-if="selectedView && selectedView.show_as === 'calendar' ">
@ -374,7 +388,7 @@
/>
</template>
</div>
<template v-if="data && !isForm">
<template v-if="data && !isForm && !isKanban">
<pagination
v-model="page"
:count="count"
@ -393,6 +407,7 @@
:meta="meta"
:selected-view-id.sync="selectedViewId"
:cover-image-field.sync="coverImageField"
:grouping-field.sync="groupingField"
:selected-view.sync="selectedView"
:primary-value-column="primaryValueColumn"
:concatenated-x-where="concatenatedXWhere"
@ -548,7 +563,33 @@
class=" mx-auto"
>
<expanded-form
v-if="selectedExpandRowIndex != null && data[selectedExpandRowIndex]"
v-if="isKanban && kanban.selectedExpandRow"
:key="kanban.selectedExpandRow.id"
v-model="kanban.selectedExpandRow"
:db-alias="nodes.dbAlias"
:has-many="hasMany"
:belongs-to="belongsTo"
:table="table"
:old-row.sync="kanban.selectedExpandOldRow"
:is-new="kanban.selectedExpandRowMeta.new"
:selected-row-meta="kanban.selectedExpandRowMeta"
:meta="meta"
:sql-ui="sqlUi"
:primary-value-column="primaryValueColumn"
:api="api"
:available-columns="availableColumns"
:nodes="nodes"
:query-params="queryParams"
:show-next-prev="false"
:preset-values="presetValues"
@cancel="showExpandModal = false;"
@input="showExpandModal = false; (kanban.selectedExpandRow && kanban.selectedExpandRow.rowMeta && delete kanban.selectedExpandRow.rowMeta.new) ; loadKanbanData(false)"
@commented="reloadComments"
@next="loadNext"
@prev="loadPrev"
/>
<expanded-form
v-if="!isKanban && (selectedExpandRowIndex != null && data[selectedExpandRowIndex])"
:key="selectedExpandRowIndex"
v-model="data[selectedExpandRowIndex].row"
:db-alias="nodes.dbAlias"
@ -566,6 +607,7 @@
:nodes="nodes"
:query-params="queryParams"
:show-next-prev="true"
:preset-values="presetValues"
@cancel="showExpandModal = false;"
@input="showExpandModal = false; (data[selectedExpandRowIndex] && data[selectedExpandRowIndex].rowMeta && delete data[selectedExpandRowIndex].rowMeta.new) ; loadTableData()"
@commented="reloadComments"
@ -661,6 +703,7 @@ export default {
},
fieldsOrder: [],
coverImageField: null,
groupingField: null,
showSystemFields: false,
showAdvanceOptions: false,
loadViews: false,
@ -720,7 +763,20 @@ export default {
size: 'xlarge',
icon: 'mdi-ca rd'
}],
rowContextMenu: null
rowContextMenu: null,
presetValues: {},
kanban: {
data: [],
stages: [],
blocks: [],
recordCnt: {},
recordTotalCnt: {},
groupingColumnItems: [],
loadingData : true,
selectedExpandRow: null,
selectedExpandOldRow: null,
selectedExpandRowMeta: null,
},
}),
watch: {
isActive(n, o) {
@ -746,6 +802,12 @@ export default {
// key: 'selectedViewId',
// val: id
// })
},
async groupingField(newVal) {
this.groupingField = newVal
if (this.selectedView && this.selectedView.show_as === 'kanban') {
await this.loadKanbanData()
}
}
},
async mounted() {
@ -763,7 +825,6 @@ export default {
this.loadingMeta = true
await this.loadMeta(false)
this.loadingMeta = false
if (this.relationType === 'hm') {
this.filters.push({
field: this.meta.columns.find(c => c.cn === this.relation.cn)._cn,
@ -781,7 +842,11 @@ export default {
})
} else {
// await this.$refs.drawer.loadViews();
await this.loadTableData()
if (this.selectedView && this.selectedView.show_as === 'kanban') {
await this.loadKanbanData()
} else {
await this.loadTableData()
}
}
// this.mapFieldsAndShowFields()
} catch (e) {
@ -789,7 +854,6 @@ export default {
}
this.searchField = this.primaryValueColumn
this.dataLoaded = true
// await this.loadViews();
},
methods: {
@ -823,7 +887,11 @@ export default {
tn: this.table,
force: true
})
await this.loadTableData()
if (this.selectedView && this.selectedView.show_as === 'kanban') {
await this.loadKanbanData()
} else {
await this.loadTableData()
}
this.key = Math.random()
},
reloadComments() {
@ -851,6 +919,10 @@ export default {
queryParams.coverImageField = this.coverImageField
}
if (this.isKanban) {
queryParams.groupingField = this.groupingField
}
this.$set(this.selectedView, 'query_params', JSON.stringify(queryParams))
if (!this._isUIAllowed('xcVirtualTableUpdate')) {
@ -998,7 +1070,6 @@ export default {
if (oldRow[column._cn] === rowObj[column._cn]) {
return
}
const id = this.meta.columns.filter(c => c.pk).map(c => rowObj[c._cn]).join('___')
if (!id) {
@ -1080,28 +1151,36 @@ export default {
this.$set(this.data[index].row, col._cn, null)
await this.onCellValueChange(colIndex, index, col)
},
async insertNewRow(atEnd = false, expand = false) {
async insertNewRow(atEnd = false, expand = false, presetValues = {}) {
const focusRow = atEnd ? this.rowLength : this.rowContextMenu.index + 1
const focusCol = this.availableColumns.findIndex(c => !c.ai)
this.data.splice(focusRow, 0, {
const isKanban = this.selectedView && this.selectedView.show_as === 'kanban'
const data = isKanban ? this.kanban.data : this.data
data.splice(focusRow, 0, {
row: this.relationType === 'hm'
? {
...this.fieldList.reduce((o, f) => ({ ...o, [f]: null }), {}),
...this.fieldList.reduce((o, f) => ({ ...o, [f]: presetValues[f] ?? null }), {}),
[this.relation.cn]: this.relationIdValue
}
: this.fieldList.reduce((o, f) => ({ ...o, [f]: null }), {}),
: this.fieldList.reduce((o, f) => ({ ...o, [f]: presetValues[f] ?? null }), {}),
rowMeta: {
new: true
},
oldRow: {}
})
this.selected = { row: focusRow, col: focusCol }
this.editEnabled = { row: focusRow, col: focusCol }
this.presetValues = presetValues
if (expand) {
const { rowMeta } = this.data[this.data.length - 1]
this.expandRow(this.data.length - 1, rowMeta)
if (isKanban) {
this.expandKanbanForm(-1, data[data.length - 1])
} else {
const { rowMeta } = data[data.length - 1]
this.expandRow(data.length - 1, rowMeta)
}
}
// this.save()
},
@ -1225,7 +1304,132 @@ export default {
}
this.$refs.csvExportImport.onCsvFileSelection(file)
}
},
// Kanban
async loadKanbanData(initKanbanProps = true) {
try {
const kanban = {
data: [],
stages: [],
blocks: [],
recordCnt: {},
recordTotalCnt: {},
groupingColumnItems: [],
loadingData: true,
selectedExpandRow: null,
selectedExpandOldRow: null,
selectedExpandRowMeta: null
}
if (initKanbanProps) {
this.kanban = kanban
}
if (this.api) {
const groupingColumn = this.meta.columns.find(c => c._cn === this.groupingField)
if (!groupingColumn) {
return
}
const initialLimit = 10
const uncategorized = 'Uncategorized'
kanban.groupingColumnItems = groupingColumn.dtxp.split(',').map((c) => {
const trimCol = c.replace(/'/g, '')
kanban.recordCnt[trimCol] = 0
return trimCol
}).sort()
kanban.groupingColumnItems.unshift(uncategorized)
kanban.recordCnt[uncategorized] = 0
for (const groupingColumnItem of kanban.groupingColumnItems) {
// enrich Kanban data
var {
data
} = await this.api.get(`/nc/${this.$store.state.project.projectId}/api/v1/${this.$route.query.name}`, {
limit: initialLimit,
where: groupingColumnItem === uncategorized ? `(${this.groupingField},is,null)` : `(${this.groupingField},eq,${groupingColumnItem})`
})
data.map((d) => {
// handle composite primary key
d.c_pk = this.meta.columns.filter(c => c.pk).map(c => d[c._cn]).join('___')
if (!d.id) {
// id is required for <kanban-board/>
d.id = d.c_pk
}
kanban.data.push({
row: d,
oldRow: d,
rowMeta: {}
})
kanban.recordCnt[groupingColumnItem] += 1
kanban.blocks.push({
status: groupingColumnItem,
...d
})
})
// enrich recordTotalCnt
var {
data
} = await this.api.get(`/nc/${this.$store.state.project.projectId}/api/v1/${this.$route.query.name}/count`, {
where: groupingColumnItem === uncategorized ? `(${this.groupingField},is,null)` : `(${this.groupingField},eq,${groupingColumnItem})`
})
kanban.recordTotalCnt[groupingColumnItem] = data.count
}
}
this.kanban = kanban
} 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(`Error occurred : ${e.message}`, {
position: 'bottom-center'
}).goAway(3000)
}
} finally {
this.kanban.loadingData = false
}
},
async loadMoreKanbanData(groupingFieldVal) {
const uncategorized = "uncategorized"
const {
data
} = await this.api.get(`/nc/${this.$store.state.project.projectId}/api/v1/${this.$route.query.name}`, {
limit: 5,
where: groupingFieldVal === uncategorized ? `(${this.groupingField},is,null)` : `(${this.groupingField},eq,${groupingFieldVal})`,
offset: this.kanban.recordCnt[groupingFieldVal]
})
data.map(d => {
// handle composite primary key
d.c_pk = this.meta.columns.filter(c => c.pk).map(c => d[c._cn]).join('___')
if (!d.id) {
// id is required for <kanban-board/>
d.id = d.c_pk
}
this.kanban.data.push({
row: d,
oldRow: d,
rowMeta: {},
})
this.kanban.blocks.push({
status: groupingFieldVal,
...d
})
})
this.kanban.recordCnt[groupingFieldVal] += data.length
},
expandKanbanForm(rowIdx, data) {
if (rowIdx != -1) {
// not a new record -> find the target record
data = this.kanban.data.filter(o => o.row.c_pk == rowIdx)[0]
}
this.showExpandModal = true
this.kanban.selectedExpandRow = data.row
this.kanban.selectedExpandOldRow = data.oldRow
this.kanban.selectedExpandRowMeta = data.rowMeta
},
},
computed: {
tabsState() {
@ -1246,6 +1450,9 @@ export default {
isForm() {
return this.selectedView && this.selectedView.show_as === 'form'
},
isKanban() {
return this.selectedView && this.selectedView.show_as === 'kanban'
},
meta() {
return this.$store.state.meta.metas[this.table]
},
@ -1265,7 +1472,7 @@ export default {
table: this.meta.tn
})
// return this.meta && this.meta._tn ? ApiFactory.create(this.$store.getters['project/GtrProjectType'], this.meta && this.meta._tn, this.meta && this.meta.columns, this, this.meta) : null
}
},
}
}
</script>
@ -1357,6 +1564,7 @@ export default {
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*

371
packages/nc-gui/components/project/spreadsheet/views/kanbanView.vue

@ -1,17 +1,93 @@
<template>
<v-container fluid>
<kanban-board :stages="stages" :blocks="blocks" @update-block="updateBlock">
<div v-for="(stage,i) in stages" :slot="stage" :key="stage" class="mx-auto">
<v-chip :color="stagesColors[i]" class="text-uppercase caption font-weight-bold">
{{ stage }}
</v-chip>
<kanban-board :stages="kanban.groupingColumnItems" :blocks="kanban.blocks" @update-block="updateBlock">
<div v-for="stage in this.kanban.groupingColumnItems" :slot="stage" :key="stage" class="mx-auto">
<enum-cell :value="stage" :column="groupingFieldColumn" />
</div>
<div v-for="block in blocks" :slot="block.id" :key="block.id" class="caption">
<!-- <div>-->
<!-- {{ block.id }}-->
<!-- </div>-->
<div>
{{ block.title }}
<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.alias|| col._cn]"
:key="col.alias || col._cn"
class="kanban-col col-12"
>
<label :for="`data-table-form-${col._cn}`" 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._cn"
: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._cn]"
: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"
small
@click="insertNewRow(true, true, {[groupingField]: stage})"
>
<v-icon small left>
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">
{{ kanban.recordCnt[stage] }} / {{ kanban.recordTotalCnt[stage] }} {{ kanban.recordTotalCnt[stage] > 1 ? "records" : "record" }}
</div>
</div>
</kanban-board>
@ -19,103 +95,127 @@
</template>
<script>
// import "vue-kanban/src/assets/kanban.css";
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',
data: () => ({
stages: ['on-hold', 'in-progress', 'needs-review', 'approved'],
stagesColors: ['error', 'primary', 'warning', 'success'],
blocks: [
{
id: 1,
status: 'on-hold',
title: 'More language options'
},
{
id: 2,
status: 'on-hold',
title: 'Customizable links'
},
{
id: 3,
status: 'on-hold',
title: 'Emoji support'
},
{
id: 4,
status: 'on-hold',
title: 'Video embedding'
},
{
id: 5,
status: 'on-hold',
title: 'Photo gallery support'
},
{
id: 6,
status: 'on-hold',
title: 'Starred cards'
},
{
id: 7,
status: 'in-progress',
title: 'Reporting'
},
{
id: 8,
status: 'in-progress',
title: 'Marks card as done'
},
{
id: 9,
status: 'in-progress',
title: 'Multiple due dates'
},
{
id: 10,
status: 'in-progress',
title: 'Expand notifications'
},
{
id: 11,
status: 'needs-review',
title: '"Heart" commad'
},
{
id: 12,
status: 'needs-review',
title: 'Assign checklist item'
},
{
id: 13,
status: 'approved',
title: 'Delete permissions'
},
{
id: 14,
status: 'approved',
title: '3rd party calendar support'
},
{
id: 15,
status: 'approved',
title: '2FA'
},
{
id: 16,
status: 'approved',
title: 'Additional sticker packs'
components: { TableCell, VirtualCell, HeaderCell, VirtualHeaderCell, EnumCell },
props: [
'nodes',
'table',
'showFields',
'availableColumns',
'meta',
'kanban',
'primaryValueColumn',
'showSystemFields',
'sqlUi',
'groupingField',
'api',
],
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)
}
})
})
},
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.cn) &&
!((this.meta.v || []).some(v => v.bt && v.bt.cn === c.cn))
) || []
},
groupingFieldColumn() {
return this.fields.filter(o => o.alias === this.groupingField)[0]
},
},
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.$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 {
height: 400px;
overflow-y: scroll;
}
ul.drag-list, ul.drag-inner-list {
list-style-type: none;
@ -125,7 +225,7 @@ export default {
.drag-container {
max-width: 1000px;
margin: 20px auto;
margin: 20px 0px;
}
.drag-list {
@ -144,8 +244,31 @@ export default {
margin: 0 10px;
position: relative;
background: var(--v-backgroundColor-base); //rgba(256, 256, 256, 0.2);
overflow: hidden;
border-radius: 4px;
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) {
@ -165,23 +288,24 @@ export default {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
width: 240px;
}
.drag-inner-list {
min-height: 20px;
//color: white;
.drag-column-header .set-item {
margin-top: 20px !important;
}
.drag-item {
padding: 10px;
margin: 10px;
//height: 100px;
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);
@ -192,12 +316,9 @@ export default {
}
.drag-options {
position: absolute;
top: 44px;
left: 0;
width: 100%;
height: 100%;
padding: 10px;
transform: translateX(100%);
opacity: 0;
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
@ -248,5 +369,33 @@ export default {
opacity: 0.2;
}
.kanban-col {
padding: 10px;
}
}
</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/>.
*
*/
-->

44
packages/nc-gui/pages/nc/kanban/_id.vue

@ -0,0 +1,44 @@
<template>
<xc-kanban />
</template>
<script>
import XcKanban from '../../../components/project/spreadsheet/public/xcKanban'
export default {
components: { XcKanban },
layout: 'public',
data: () => ({
data: null
})
}
</script>
<style scoped>
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
* @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/>.
*
*/
-->

2
packages/nocodb/src/lib/noco/meta/NcMetaMgrEE.ts

@ -271,6 +271,8 @@ export default class NcMetaMgrEE extends NcMetaMgr {
sharedView.url = `${req.ncSiteUrl}${this.config.dashboardPath}#/nc/form/${sharedView.view_id}`;
} else if (args.args.show_as === 'gallery') {
sharedView.url = `${req.ncSiteUrl}${this.config.dashboardPath}#/nc/gallery/${sharedView.view_id}`;
} else if(args.args.show_as === 'kanban') {
sharedView.url = `${req.ncSiteUrl}${this.config.dashboardPath}#/nc/kanban/${sharedView.view_id}`;
} else {
sharedView.url = `${req.ncSiteUrl}${this.config.dashboardPath}#/nc/view/${sharedView.view_id}`;
}

4
scripts/cypress/integration/common/4a_table_view_grid_gallery_form.js

@ -65,10 +65,11 @@ export const genTest = (type, xcdb) => {
});
};
// below two scenario's will be invoked twice, once for rest & then for graphql
// below four scenario's will be invoked twice, once for rest & then for graphql
viewTest("grid");
viewTest("gallery");
viewTest("form");
// viewTest("kanban");
});
};
@ -77,6 +78,7 @@ export const genTest = (type, xcdb) => {
*
* @author Pranav C Balan <pranavxc@gmail.com>
* @author Raju Udava <sivadstala@gmail.com>
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*

Loading…
Cancel
Save