Browse Source

Merge pull request #4003 from nocodb/feat/kanban-cover-image

feat: kanban cover image
pull/4010/head
աɨռɢӄաօռɢ 2 years ago committed by GitHub
parent
commit
359622ad2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/nc-gui/components/cell/attachment/index.vue
  2. 2
      packages/nc-gui/components/smartsheet/Gallery.vue
  3. 67
      packages/nc-gui/components/smartsheet/Kanban.vue
  4. 52
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  5. 1
      packages/nocodb-sdk/src/lib/Api.ts
  6. 4
      packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
  7. 41
      packages/nocodb/src/lib/migrations/v2/nc_022_add_kanban_fk_cover_image_col_id.ts
  8. 10
      packages/nocodb/src/lib/models/KanbanView.ts
  9. 21
      packages/nocodb/src/lib/models/View.ts
  10. 3
      scripts/sdk/swagger.json

2
packages/nc-gui/components/cell/attachment/index.vue

@ -197,7 +197,7 @@ watch(
</template> </template>
<template v-if="isImage(item.title, item.mimetype ?? item.type) && (item.url || item.data)"> <template v-if="isImage(item.title, item.mimetype ?? item.type) && (item.url || item.data)">
<div class="nc-attachment flex items-center justify-center" @click="selectedImage = item"> <div class="nc-attachment flex items-center justify-center" @click.stop="selectedImage = item">
<LazyNuxtImg <LazyNuxtImg
quality="75" quality="75"
placeholder placeholder

2
packages/nc-gui/components/smartsheet/Gallery.vue

@ -177,7 +177,7 @@ watch(view, async (nextView) => {
class="!rounded-lg h-full overflow-hidden break-all max-w-[450px]" class="!rounded-lg h-full overflow-hidden break-all max-w-[450px]"
@click="expandFormClick($event, record)" @click="expandFormClick($event, record)"
> >
<template #cover> <template v-if="galleryData.fk_cover_image_col_id" #cover>
<a-carousel v-if="!reloadAttachments && attachments(record).length" autoplay class="gallery-carousel" arrows> <a-carousel v-if="!reloadAttachments && attachments(record).length" autoplay class="gallery-carousel" arrows>
<template #customPaging> <template #customPaging>
<a> <a>

67
packages/nc-gui/components/smartsheet/Kanban.vue

@ -21,6 +21,10 @@ import {
} from '#imports' } from '#imports'
import type { Row as RowType } from '~/lib' import type { Row as RowType } from '~/lib'
interface Attachment {
url: string
}
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref()) const view = inject(ActiveViewInj, ref())
@ -87,6 +91,16 @@ const hasEditPermission = $computed(() => isUIAllowed('xcDatatableEditable'))
const fields = inject(FieldsInj, ref([])) const fields = inject(FieldsInj, ref([]))
const fieldsWithoutCover = computed(() => fields.value.filter((f) => f.id !== kanbanMetaData.value?.fk_cover_image_col_id))
const coverImageColumn: any = $(
computed(() =>
meta.value?.columnsById
? meta.value.columnsById[kanbanMetaData.value?.fk_cover_image_col_id as keyof typeof meta.value.columnsById]
: {},
),
)
const kanbanContainerRef = ref() const kanbanContainerRef = ref()
const selectedStackTitle = ref('') const selectedStackTitle = ref('')
@ -103,8 +117,24 @@ reloadViewDataHook?.on(async () => {
await loadKanbanData() await loadKanbanData()
}) })
const attachments = (record: any): Attachment[] => {
try {
return coverImageColumn?.title && record.row[coverImageColumn.title] ? JSON.parse(record.row[coverImageColumn.title]) : []
} catch (e) {
return []
}
}
const reloadAttachments = ref(false)
reloadViewMetaHook?.on(async () => { reloadViewMetaHook?.on(async () => {
await loadKanbanMeta() await loadKanbanMeta()
reloadAttachments.value = true
nextTick(() => {
reloadAttachments.value = false
})
}) })
const expandForm = (row: RowType, state?: Record<string, any>) => { const expandForm = (row: RowType, state?: Record<string, any>) => {
@ -400,8 +430,43 @@ watch(view, async (nextView) => {
@click="expandFormClick($event, record)" @click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, record)" @contextmenu="showContextMenu($event, record)"
> >
<template v-if="kanbanMetaData.fk_cover_image_col_id" #cover>
<a-carousel
v-if="!reloadAttachments && attachments(record).length"
autoplay
class="gallery-carousel"
arrows
>
<template #customPaging>
<a>
<div class="pt-[12px]">
<div></div>
</div>
</a>
</template>
<template #prevArrow>
<div style="z-index: 1"></div>
</template>
<template #nextArrow>
<div style="z-index: 1"></div>
</template>
<LazyNuxtImg
v-for="(attachment, index) in attachments(record)"
:key="`carousel-${record.row.id}-${index}`"
quality="90"
placeholder
class="h-52 object-cover"
:src="attachment.url"
/>
</a-carousel>
<MdiFileImageBox v-else class="w-full h-48 my-4 text-cool-gray-200" />
</template>
<div <div
v-for="col in fields" v-for="col in fieldsWithoutCover"
:key="`record-${record.row.id}-${col.id}`" :key="`record-${record.row.id}-${col.id}`"
class="flex flex-col rounded-lg w-full" class="flex flex-col rounded-lg w-full"
> >

52
packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType, GalleryType } from 'nocodb-sdk' import type { ColumnType, GalleryType, KanbanType } from 'nocodb-sdk'
import { UITypes, ViewTypes, isVirtualCol } from 'nocodb-sdk' import { UITypes, ViewTypes, isVirtualCol } from 'nocodb-sdk'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import type { SelectProps } from 'ant-design-vue' import type { SelectProps } from 'ant-design-vue'
@ -75,30 +75,44 @@ const onMove = (_event: { moved: { newIndex: number } }) => {
const coverImageColumnId = computed({ const coverImageColumnId = computed({
get: () => get: () =>
activeView.value?.type === ViewTypes.GALLERY && activeView.value?.view (activeView.value?.type === ViewTypes.GALLERY || activeView.value?.type === ViewTypes.KANBAN) && activeView.value?.view
? (activeView.value?.view as GalleryType).fk_cover_image_col_id ? (activeView.value?.view as GalleryType).fk_cover_image_col_id
: undefined, : undefined,
set: async (val) => { set: async (val) => {
if (val && activeView.value?.type === ViewTypes.GALLERY && activeView.value?.id && activeView.value?.view) { if (
await $api.dbView.galleryUpdate(activeView.value?.id, { (activeView.value?.type === ViewTypes.GALLERY || activeView.value?.type === ViewTypes.KANBAN) &&
...activeView.value?.view, activeView.value?.id &&
fk_cover_image_col_id: val, activeView.value?.view
}) ) {
;(activeView.value.view as GalleryType).fk_cover_image_col_id = val if (activeView.value?.type === ViewTypes.GALLERY) {
await $api.dbView.galleryUpdate(activeView.value?.id, {
...activeView.value?.view,
fk_cover_image_col_id: val,
})
;(activeView.value.view as GalleryType).fk_cover_image_col_id = val
} else if (activeView.value?.type === ViewTypes.KANBAN) {
await $api.dbView.kanbanUpdate(activeView.value?.id, {
...activeView.value?.view,
fk_cover_image_col_id: val,
})
;(activeView.value.view as KanbanType).fk_cover_image_col_id = val
}
reloadViewMetaHook.trigger() reloadViewMetaHook.trigger()
} }
}, },
}) })
const coverOptions = computed<SelectProps['options']>(() => { const coverOptions = computed<SelectProps['options']>(() => {
return fields.value const filterFields =
?.filter((el) => el.fk_column_id && metaColumnById.value[el.fk_column_id].uidt === UITypes.Attachment) fields.value
.map((field) => { ?.filter((el) => el.fk_column_id && metaColumnById.value[el.fk_column_id].uidt === UITypes.Attachment)
return { .map((field) => {
value: field.fk_column_id, return {
label: field.title, value: field.fk_column_id,
} label: field.title,
}) }
}) ?? []
return [{ value: null, label: 'No Image' }, ...filterFields]
}) })
const getIcon = (c: ColumnType) => const getIcon = (c: ColumnType) =>
@ -127,7 +141,11 @@ const getIcon = (c: ColumnType) =>
class="p-3 min-w-[280px] bg-gray-50 shadow-lg nc-table-toolbar-menu max-h-[max(80vh,500px)] overflow-auto !border" class="p-3 min-w-[280px] bg-gray-50 shadow-lg nc-table-toolbar-menu max-h-[max(80vh,500px)] overflow-auto !border"
@click.stop @click.stop
> >
<a-card v-if="activeView.type === ViewTypes.GALLERY" size="small" title="Cover image"> <a-card
v-if="activeView.type === ViewTypes.GALLERY || activeView.type === ViewTypes.KANBAN"
size="small"
title="Cover image"
>
<a-select <a-select
v-model:value="coverImageColumnId" v-model:value="coverImageColumnId"
class="w-full" class="w-full"

1
packages/nocodb-sdk/src/lib/Api.ts

@ -383,6 +383,7 @@ export interface KanbanType {
columns?: KanbanColumnType[]; columns?: KanbanColumnType[];
fk_model_id?: string; fk_model_id?: string;
fk_grp_col_id?: string | null; fk_grp_col_id?: string | null;
fk_cover_image_col_id?: string;
meta?: string | object; meta?: string | object;
} }

4
packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts

@ -9,6 +9,7 @@ import * as nc_018_add_meta_in_view from './v2/nc_018_add_meta_in_view';
import * as nc_019_add_meta_in_meta_tables from './v2/nc_019_add_meta_in_meta_tables'; import * as nc_019_add_meta_in_meta_tables from './v2/nc_019_add_meta_in_meta_tables';
import * as nc_020_add_kanban_meta_col from './v2/nc_020_add_kanban_meta_col'; import * as nc_020_add_kanban_meta_col from './v2/nc_020_add_kanban_meta_col';
import * as nc_021_rename_kanban_grp_col_id from './v2/nc_021_rename_kanban_grp_col_id'; import * as nc_021_rename_kanban_grp_col_id from './v2/nc_021_rename_kanban_grp_col_id';
import * as nc_022_add_kanban_fk_cover_image_col_id from './v2/nc_022_add_kanban_fk_cover_image_col_id';
// Create a custom migration source class // Create a custom migration source class
export default class XcMigrationSourcev2 { export default class XcMigrationSourcev2 {
@ -29,6 +30,7 @@ export default class XcMigrationSourcev2 {
'nc_019_add_meta_in_meta_tables', 'nc_019_add_meta_in_meta_tables',
'nc_020_add_kanban_meta_col', 'nc_020_add_kanban_meta_col',
'nc_021_rename_kanban_grp_col_id', 'nc_021_rename_kanban_grp_col_id',
'nc_022_add_kanban_fk_cover_image_col_id'
]); ]);
} }
@ -60,6 +62,8 @@ export default class XcMigrationSourcev2 {
return nc_020_add_kanban_meta_col; return nc_020_add_kanban_meta_col;
case 'nc_021_rename_kanban_grp_col_id': case 'nc_021_rename_kanban_grp_col_id':
return nc_021_rename_kanban_grp_col_id; return nc_021_rename_kanban_grp_col_id;
case 'nc_022_add_kanban_fk_cover_image_col_id':
return nc_022_add_kanban_fk_cover_image_col_id;
} }
} }
} }

41
packages/nocodb/src/lib/migrations/v2/nc_022_add_kanban_fk_cover_image_col_id.ts

@ -0,0 +1,41 @@
import Knex from 'knex';
import { MetaTable } from '../../utils/globals';
const up = async (knex: Knex) => {
await knex.schema.alterTable(MetaTable.KANBAN_VIEW, (table) => {
table.string('fk_cover_image_col_id', 20);
table
.foreign('fk_cover_image_col_id')
.references(`${MetaTable.COLUMNS}.id`);
});
};
const down = async (knex) => {
await knex.schema.alterTable(MetaTable.KANBAN_VIEW, (table) => {
table.dropColumns('fk_cover_image_col_id');
});
};
export { up, down };
/**
* @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/>.
*
*/

10
packages/nocodb/src/lib/models/KanbanView.ts

@ -1,5 +1,5 @@
import Noco from '../Noco'; import Noco from '../Noco';
import { KanbanType } from 'nocodb-sdk'; import { KanbanType, UITypes } from 'nocodb-sdk';
import { CacheGetType, CacheScope, MetaTable } from '../utils/globals'; import { CacheGetType, CacheScope, MetaTable } from '../utils/globals';
import View from './View'; import View from './View';
import NocoCache from '../cache/NocoCache'; import NocoCache from '../cache/NocoCache';
@ -10,6 +10,7 @@ export default class KanbanView implements KanbanType {
project_id?: string; project_id?: string;
base_id?: string; base_id?: string;
fk_grp_col_id?: string; fk_grp_col_id?: string;
fk_cover_image_col_id?: string;
meta?: string | object; meta?: string | object;
// below fields are not in use at this moment // below fields are not in use at this moment
@ -58,11 +59,18 @@ export default class KanbanView implements KanbanType {
} }
static async insert(view: Partial<KanbanView>, ncMeta = Noco.ncMeta) { static async insert(view: Partial<KanbanView>, ncMeta = Noco.ncMeta) {
const columns = await View.get(view.fk_view_id, ncMeta)
.then((v) => v?.getModel(ncMeta))
.then((m) => m.getColumns(ncMeta));
const insertObj = { const insertObj = {
project_id: view.project_id, project_id: view.project_id,
base_id: view.base_id, base_id: view.base_id,
fk_view_id: view.fk_view_id, fk_view_id: view.fk_view_id,
fk_grp_col_id: view.fk_grp_col_id, fk_grp_col_id: view.fk_grp_col_id,
fk_cover_image_col_id:
view?.fk_cover_image_col_id ||
columns?.find((c) => c.uidt === UITypes.Attachment)?.id,
meta: view.meta, meta: view.meta,
}; };

21
packages/nocodb/src/lib/models/View.ts

@ -355,8 +355,7 @@ export default class View implements ViewType {
{ {
let order = 1; let order = 1;
let galleryShowLimit = 0; let galleryShowLimit = 0;
let kanbanShowCount = 0; let kanbanShowLimit = 0;
let kanbanAttachmentCount = 0;
if (view.type === ViewTypes.KANBAN && !copyFromView) { if (view.type === ViewTypes.KANBAN && !copyFromView) {
// sort by primary value & attachment first, then by singleLineText & Number // sort by primary value & attachment first, then by singleLineText & Number
@ -401,22 +400,14 @@ export default class View implements ViewType {
if (vCol.id === kanbanView?.fk_grp_col_id) { if (vCol.id === kanbanView?.fk_grp_col_id) {
// include grouping field if it exists // include grouping field if it exists
show = true; show = true;
} else if (vCol.pv) { } else if (vCol.id === kanbanView.fk_cover_image_col_id || vCol.pv) {
// Show primary key // Show cover image or primary key
show = true; show = true;
kanbanShowCount++; kanbanShowLimit++;
} else if ( } else if (kanbanShowLimit < 3 && !isSystemColumn(vCol)) {
vCol.uidt === UITypes.Attachment &&
kanbanAttachmentCount < 1
) {
// Show at most 1 attachment
show = true;
kanbanAttachmentCount++;
kanbanShowCount++;
} else if (kanbanShowCount < 3 && !isSystemColumn(vCol)) {
// show at most 3 non-system columns // show at most 3 non-system columns
show = true; show = true;
kanbanShowCount++; kanbanShowLimit++;
} else { } else {
// other columns will be hidden // other columns will be hidden
show = false; show = false;

3
scripts/sdk/swagger.json

@ -7919,6 +7919,9 @@
"null" "null"
] ]
}, },
"fk_cover_image_col_id": {
"type": "string"
},
"meta": { "meta": {
"type": [ "type": [
"string", "string",

Loading…
Cancel
Save