Browse Source

Merge pull request #3379 from nocodb/feat/gallery-rework

feat: gallery rework
pull/3408/head
mertmit 2 years ago committed by GitHub
parent
commit
ec52d4e849
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  2. 127
      packages/nc-gui-v2/components/smartsheet/Gallery.vue
  3. 2
      packages/nocodb-sdk/src/lib/Api.ts
  4. 11
      packages/nocodb/src/lib/models/View.ts
  5. 16
      scripts/sdk/swagger.json

36
packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue

@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType, GalleryType } from 'nocodb-sdk'
import { 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 { import {
ActiveViewInj, ActiveViewInj,
FieldsInj, FieldsInj,
@ -31,7 +32,7 @@ const isLocked = inject(IsLockedInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const { $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { const {
showSystemFields, showSystemFields,
@ -82,6 +83,32 @@ const onMove = (event: { moved: { newIndex: number } }) => {
$e('a:fields:reorder') $e('a:fields:reorder')
} }
const coverImageColumnId = computed({
get: () =>
activeView.value?.type === ViewTypes.GALLERY ? (activeView.value?.view as GalleryType).fk_cover_image_col_id : undefined,
set: async (val) => {
if (val && activeView.value.type === ViewTypes.GALLERY && activeView.value.id && activeView.value.view) {
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
reloadDataHook.trigger()
}
},
})
const coverOptions = computed<SelectProps['options']>(() => {
return fields.value
?.filter((el) => el.fk_column_id && metaColumnById.value[el.fk_column_id].uidt === UITypes.Attachment)
.map((field) => {
return {
value: field.fk_column_id,
label: field.title,
}
})
})
const getIcon = (c: ColumnType) => const getIcon = (c: ColumnType) =>
h(isVirtualCol(c) ? VirtualCellIcon : CellIcon, { h(isVirtualCol(c) ? VirtualCellIcon : CellIcon, {
columnMeta: c, columnMeta: c,
@ -107,6 +134,9 @@ 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-select v-model:value="coverImageColumnId" class="w-full" :options="coverOptions" @click.stop></a-select>
</a-card>
<div class="p-1" @click.stop> <div class="p-1" @click.stop>
<a-input v-model:value="filterQuery" size="small" :placeholder="$t('placeholder.searchFields')" /> <a-input v-model:value="filterQuery" size="small" :placeholder="$t('placeholder.searchFields')" />
</div> </div>

127
packages/nc-gui-v2/components/smartsheet/Gallery.vue

@ -3,7 +3,17 @@ import { isVirtualCol } from 'nocodb-sdk'
import { inject, provide, useViewData } from '#imports' import { inject, provide, useViewData } from '#imports'
import Row from '~/components/smartsheet/Row.vue' import Row from '~/components/smartsheet/Row.vue'
import type { Row as RowType } from '~/composables' import type { Row as RowType } from '~/composables'
import { ActiveViewInj, ChangePageInj, FieldsInj, IsFormInj, IsGridInj, MetaInj, PaginationDataInj, ReadonlyInj } from '~/context' import {
ActiveViewInj,
ChangePageInj,
FieldsInj,
IsFormInj,
IsGridInj,
MetaInj,
OpenNewRecordFormHookInj,
PaginationDataInj,
ReadonlyInj,
} from '~/context'
import ImageIcon from '~icons/mdi/file-image-box' import ImageIcon from '~icons/mdi/file-image-box'
interface Attachment { interface Attachment {
@ -13,12 +23,21 @@ interface Attachment {
const meta = inject(MetaInj) const meta = inject(MetaInj)
const view = inject(ActiveViewInj) const view = inject(ActiveViewInj)
const reloadViewDataHook = inject(ReloadViewDataHookInj) const reloadViewDataHook = inject(ReloadViewDataHookInj)
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook())
const expandedFormDlg = ref(false) const expandedFormDlg = ref(false)
const expandedFormRow = ref<RowType>() const expandedFormRow = ref<RowType>()
const expandedFormRowState = ref<Record<string, any>>() const expandedFormRowState = ref<Record<string, any>>()
const { loadData, paginationData, formattedData: data, loadGalleryData, galleryData, changePage } = useViewData(meta, view as any) const {
loadData,
paginationData,
formattedData: data,
loadGalleryData,
galleryData,
changePage,
addEmptyRow,
} = useViewData(meta, view as any)
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
@ -30,7 +49,15 @@ provide(ReadonlyInj, !isUIAllowed('xcDatatableEditable'))
const fields = inject(FieldsInj, ref([])) const fields = inject(FieldsInj, ref([]))
const coverImageColumn = $(computed(() => fields.value.find((col) => col.id === galleryData.value?.fk_cover_image_col_id))) const fieldsWithoutCover = computed(() => fields.value.filter((f) => f.id !== galleryData.value?.fk_cover_image_col_id))
const coverImageColumn: any = $(
computed(() =>
meta?.value.columnsById
? meta.value.columnsById[galleryData.value?.fk_cover_image_col_id as keyof typeof meta.value.columnsById]
: {},
),
)
watch( watch(
[meta, view], [meta, view],
@ -52,14 +79,21 @@ const isRowEmpty = (record: any, col: any) => {
const attachments = (record: any): Array<Attachment> => { const attachments = (record: any): Array<Attachment> => {
try { try {
return JSON.parse(record.row[coverImageColumn?.title]) ?? [] return coverImageColumn?.title && record.row[coverImageColumn.title] ? JSON.parse(record.row[coverImageColumn.title]) : []
} catch (e) { } catch (e) {
return [] return []
} }
} }
const reloadAttachments = ref(false)
reloadViewDataHook?.on(async () => { reloadViewDataHook?.on(async () => {
await loadData() await loadData()
await loadGalleryData()
reloadAttachments.value = true
nextTick(() => {
reloadAttachments.value = false
})
}) })
const expandForm = (row: RowType, state?: Record<string, any>) => { const expandForm = (row: RowType, state?: Record<string, any>) => {
@ -68,20 +102,43 @@ const expandForm = (row: RowType, state?: Record<string, any>) => {
expandedFormRowState.value = state expandedFormRowState.value = state
expandedFormDlg.value = true expandedFormDlg.value = true
} }
const expandFormClick = async (e: MouseEvent, row: RowType) => {
const target = e.target as HTMLElement
if (target && !target.closest('.gallery-carousel')) {
expandForm(row)
}
}
openNewRecordFormHook?.on(async () => {
const newRow = await addEmptyRow()
expandForm(newRow)
})
</script> </script>
<template> <template>
<div class="flex flex-col h-full w-full"> <div class="flex flex-col h-full w-full overflow-auto">
<div class="nc-gallery-container min-h-0 flex-1 grid grid-cols-4 gap-4 my-4 px-3 overflow-auto"> <div class="nc-gallery-container grid w-full min-h-0 gap-2 my-4 px-3">
<div v-for="(record, recordIndex) in data" :key="recordIndex" class="flex flex-col" @click="expandForm(record)"> <div v-for="record in data" :key="`record-${record.row.id}`">
<Row :row="record"> <Row :row="record">
<a-card hoverable class="!rounded-lg h-full"> <a-card hoverable class="!rounded-lg h-full overflow-hidden break-all" @click="expandFormClick($event, record)">
<template #cover> <template #cover>
<a-carousel v-if="attachments(record).length !== 0" autoplay> <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>
<img <img
v-for="(attachment, index) in attachments(record)" v-for="(attachment, index) in attachments(record).filter((attachment) => attachment.url)"
:key="index" :key="`carousel-${record.row.id}-${index}`"
class="h-52 rounded-t-lg" class="h-52"
:src="attachment.url" :src="attachment.url"
/> />
</a-carousel> </a-carousel>
@ -89,8 +146,8 @@ const expandForm = (row: RowType, state?: Record<string, any>) => {
</template> </template>
<div <div
v-for="(col, colIndex) in fields" v-for="col in fieldsWithoutCover"
:key="colIndex" :key="`record-${record.row.id}-${col.id}`"
class="flex flex-col space-y-1 px-4 mb-6 bg-gray-50 rounded-lg w-full" class="flex flex-col space-y-1 px-4 mb-6 bg-gray-50 rounded-lg w-full"
> >
<div class="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5"> <div class="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5">
@ -112,6 +169,7 @@ const expandForm = (row: RowType, state?: Record<string, any>) => {
</Row> </Row>
</div> </div>
</div> </div>
<div class="flex-1" />
<SmartsheetPagination /> <SmartsheetPagination />
<SmartsheetExpandedForm <SmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg" v-if="expandedFormRow && expandedFormDlg"
@ -125,6 +183,45 @@ const expandForm = (row: RowType, state?: Record<string, any>) => {
<style scoped> <style scoped>
.nc-gallery-container { .nc-gallery-container {
overflow: auto; grid-auto-rows: 1fr;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}
:depp(.slick-dots li button) {
background-color: black;
}
.ant-carousel.gallery-carousel :deep(.slick-dots) {
position: relative;
height: auto;
bottom: 0px;
}
.ant-carousel.gallery-carousel :deep(.slick-dots li div > div) {
background: #000;
border: 0;
border-radius: 1px;
color: transparent;
cursor: pointer;
display: block;
font-size: 0;
height: 3px;
opacity: .3;
outline: none;
padding: 0;
transition: all .5s;
width: 100%;
}
.ant-carousel.gallery-carousel :deep(.slick-dots li.slick-active div > div) {
opacity: 1;
}
.ant-carousel.gallery-carousel :deep(.slick-prev) {
left: 0;
height: 100%;
top: 12px;
width: 50%;
}
.ant-carousel.gallery-carousel :deep(.slick-next) {
right: 0;
height: 100%;
top: 12px;
width: 50%;
} }
</style> </style>

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

@ -134,6 +134,8 @@ export interface ViewType {
uuid?: string; uuid?: string;
show_system_fields?: boolean; show_system_fields?: boolean;
lock_type?: 'collaborative' | 'locked' | 'personal'; lock_type?: 'collaborative' | 'locked' | 'personal';
type?: number;
view?: FormType | GridType | GalleryType;
} }
export interface TableInfoType { export interface TableInfoType {

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

@ -337,9 +337,20 @@ export default class View implements ViewType {
} }
{ {
let order = 1; let order = 1;
let galleryShowLimit = 0;
for (const vCol of columns) { for (const vCol of columns) {
let show = 'show' in vCol ? vCol.show : true; let show = 'show' in vCol ? vCol.show : true;
if (view.type === ViewTypes.GALLERY) {
const galleryView = await GalleryView.get(view_id, ncMeta);
if (vCol.id === galleryView.fk_cover_image_col_id || vCol.pv || galleryShowLimit < 3) {
show = true;
galleryShowLimit++;
} else {
show = false;
}
}
// if columns is list of virtual columns then get the parent column // if columns is list of virtual columns then get the parent column
const col = vCol.fk_column_id const col = vCol.fk_column_id
? await Column.get({ colId: vCol.fk_column_id }, ncMeta) ? await Column.get({ colId: vCol.fk_column_id }, ncMeta)

16
scripts/sdk/swagger.json

@ -6183,6 +6183,22 @@
"locked", "locked",
"personal" "personal"
] ]
},
"type": {
"type": "number"
},
"view": {
"oneOf": [
{
"$ref": "#/components/schemas/Form"
},
{
"$ref": "#/components/schemas/Grid"
},
{
"$ref": "#/components/schemas/Gallery"
}
]
} }
} }
}, },

Loading…
Cancel
Save