Browse Source

Merge pull request #6416 from nocodb/feat/gallery-ui

feat: gallery ui revamp
pull/6447/head
Raju Udava 12 months ago committed by GitHub
parent
commit
eebfd7a356
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      packages/nc-gui/components.d.ts
  2. 12
      packages/nc-gui/components/cell/Checkbox.vue
  3. 14
      packages/nc-gui/components/cell/attachment/index.vue
  4. 130
      packages/nc-gui/components/smartsheet/Gallery.vue
  5. 2
      packages/nc-gui/components/virtual-cell/Formula.vue
  6. 2
      packages/nc-gui/components/virtual-cell/Links.vue
  7. 7
      packages/nc-gui/components/virtual-cell/QrCode.vue
  8. 16
      packages/nc-gui/components/virtual-cell/barcode/JsBarcodeWrapper.vue
  9. 16
      packages/nocodb/src/helpers/getAst.ts
  10. 2
      tests/playwright/pages/Dashboard/ViewSidebar/index.ts

1
packages/nc-gui/components.d.ts vendored

@ -120,6 +120,7 @@ declare module '@vue/runtime-core' {
MdiChatProcessingOutline: typeof import('~icons/mdi/chat-processing-outline')['default']
MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
MdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default']
MdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
MdiCircleMedium: typeof import('~icons/mdi/circle-medium')['default']
MdiClose: typeof import('~icons/mdi/close')['default']

12
packages/nc-gui/components/cell/Checkbox.vue

@ -36,6 +36,8 @@ const isForm = inject(IsFormInj)
const isEditColumnMenu = inject(EditColumnInj, ref(false))
const isGallery = inject(IsGalleryInj, ref(false))
const readOnly = inject(ReadonlyInj)
const checkboxMeta = computed(() => {
@ -80,18 +82,14 @@ useSelectedCellKeyupListener(active, (e) => {
<div
class="flex cursor-pointer w-full h-full"
:class="{
'justify-center': !isForm,
'w-full': isForm,
'justify-center': !isForm || !isGallery,
'w-full flex-start': isForm || isGallery,
'nc-cell-hover-show': !vModel && !readOnly,
'opacity-0': readOnly && !vModel,
}"
@click="onClick(false, $event)"
>
<div
class="items-center"
:class="{ '!ml-[-8px]': readOnly, 'w-full justify-start': isEditColumnMenu }"
@click="onClick(true)"
>
<div class="items-center" :class="{ 'w-full justify-start': isEditColumnMenu || isGallery }" @click="onClick(true)">
<div :class="{ 'bg-gray-100 rounded-full ': !vModel }">
<Transition name="layout" mode="out-in" :duration="100">
<component

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

@ -7,6 +7,7 @@ import {
CurrentCellInj,
DropZoneRef,
IsExpandedFormOpenInj,
IsGalleryInj,
RowHeightInj,
iconMap,
inject,
@ -43,6 +44,8 @@ const currentCellRef = inject(CurrentCellInj, dropZoneInjection.value)
const isLockedMode = inject(IsLockedInj, ref(false))
const isGallery = inject(IsGalleryInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const { isSharedForm } = useSmartsheetStoreOrThrow()!
@ -200,8 +203,8 @@ const rowHeight = inject(RowHeightInj, ref())
<template v-if="visibleItems.length">
<div
ref="sortableRef"
:class="{ dragging, 'justify-center': !isExpandedForm }"
class="flex cursor-pointer items-center flex-wrap gap-2 py-1.5 scrollbar-thin-dull overflow-hidden mt-0 items-start"
:class="{ 'justify-center': !isExpandedForm && !isGallery }"
class="flex cursor-pointer w-full items-center flex-wrap gap-2 py-1.5 scrollbar-thin-dull overflow-hidden mt-0 items-start"
:style="{
maxHeight: isForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,
}"
@ -215,7 +218,12 @@ const rowHeight = inject(RowHeightInj, ref())
<div
class="nc-attachment flex items-center flex-col flex-wrap justify-center"
:class="{ 'ml-2': active }"
@click.stop="selectedImage = item"
@click="
() => {
if (isGallery) return
selectedImage = item
}
"
>
<LazyCellAttachmentImage
:alt="item.title || `#${i}`"

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

@ -19,7 +19,7 @@ import {
extractPkFromRow,
inject,
isImage,
isLTAR,
isPrimary,
nextTick,
provide,
ref,
@ -59,6 +59,8 @@ provide(IsFormInj, ref(false))
provide(IsGalleryInj, ref(true))
provide(IsGridInj, ref(false))
provide(RowHeightInj, ref(1 as const))
const isPublic = inject(IsPublicInj, ref(false))
const fields = inject(FieldsInj, ref([]))
@ -69,7 +71,9 @@ const router = useRouter()
const { getPossibleAttachmentSrc } = useAttachment()
const fieldsWithoutCover = computed(() => fields.value.filter((f) => f.id !== galleryData.value?.fk_cover_image_col_id))
const fieldsWithoutDisplay = computed(() => fields.value.filter((f) => !isPrimary(f)))
const displayField = computed(() => meta.value?.columns?.find((c) => c.pv) ?? null)
const coverImageColumn: any = computed(() =>
meta.value?.columnsById
@ -142,9 +146,8 @@ const expandForm = (row: RowType, state?: Record<string, any>) => {
const expandFormClick = async (e: MouseEvent, row: RowType) => {
const target = e.target as HTMLElement
if (target && !target.closest('.gallery-carousel')) {
expandForm(row)
}
if (target.closest('.arrow') || target.closest('.slick-dots')) return
expandForm(row)
}
openNewRecordFormHook?.on(async () => {
@ -229,7 +232,7 @@ watch(
</template>
<div
class="flex flex-col w-full nc-gallery nc-scrollbar-md"
class="flex flex-col w-full nc-gallery nc-scrollbar-md bg-[#fbfbfb]"
data-testid="nc-gallery-wrapper"
style="height: calc(100% - var(--topbar-height) + 0.7rem)"
:class="{
@ -241,19 +244,23 @@ watch(
<a-skeleton-input v-for="index of Array(20)" :key="index" class="!min-w-60.5 !h-96 !rounded-md overflow-hidden" />
</div>
</div>
<div v-else class="nc-gallery-container grid gap-2 my-4 px-3">
<div v-else class="nc-gallery-container grid gap-3 my-4 px-3">
<div v-for="(record, rowIndex) in data" :key="`record-${record.row.id}`">
<LazySmartsheetRow :row="record">
<a-card
hoverable
class="!rounded-lg h-full overflow-hidden break-all max-w-[450px]"
class="!rounded-lg h-full border-gray-200 border-1 group overflow-hidden break-all max-w-[450px] shadow-sm hover:shadow-md"
:body-style="{ padding: '0px' }"
:data-testid="`nc-gallery-card-${record.row.id}`"
:style="isPublic ? { cursor: 'default' } : { cursor: 'pointer' }"
@click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, { row: rowIndex })"
>
<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"
class="gallery-carousel !border-b-1 !border-gray-200"
arrows
>
<template #customPaging>
<a>
<div class="pt-[12px]">
@ -263,41 +270,73 @@ watch(
</template>
<template #prevArrow>
<div style="z-index: 1"></div>
<div class="z-10 arrow">
<MdiChevronLeft
class="text-gray-700 w-6 h-6 absolute left-1.5 bottom-[-90px] !opacity-0 !group-hover:opacity-100 !bg-white border-1 border-gray-200 rounded-md transition"
/>
</div>
</template>
<template #nextArrow>
<div style="z-index: 1"></div>
<div class="z-10 arrow">
<MdiChevronRight
class="text-gray-700 w-6 h-6 absolute right-1.5 bottom-[-90px] !opacity-0 !group-hover:opacity-100 !bg-white border-1 border-gray-200 rounded-md transition"
/>
</div>
</template>
<template v-for="(attachment, index) in attachments(record)">
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="`carousel-${record.row.id}-${index}`"
class="h-52 object-contain"
class="h-52 object-cover"
:srcs="getPossibleAttachmentSrc(attachment)"
@click="expandFormClick($event, record)"
/>
</template>
</a-carousel>
<div v-else class="h-52 w-full !flex flex-row items-center justify-center">
<div v-else class="h-52 w-full !flex flex-row !border-b-1 !border-gray-200 items-center justify-center">
<img class="object-contain w-[48px] h-[48px]" src="~assets/icons/FileIconImageBox.png" />
</div>
</template>
<div v-for="col in fieldsWithoutCover" :key="`record-${record.row.id}-${col.id}`">
<div
v-if="!isRowEmpty(record, col) || isLTAR(col.uidt, col.colOptions)"
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="w-full text-gray-600">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="true" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
<h2 v-if="displayField" class="text-base mt-6 mx-3 font-bold">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(displayField)"
v-model="record.row[displayField.title]"
class="!text-gray-600"
:column="displayField"
:row="record"
/>
<LazySmartsheetCell
v-else
v-model="record.row[displayField.title]"
class="!text-gray-600"
:column="displayField"
:edit-enabled="false"
:read-only="true"
/>
</h2>
<div v-for="col in fieldsWithoutDisplay" :key="`record-${record.row.id}-${col.id}`">
<div class="flex flex-col ml-2 !pr-3.5 !mb-[0.75rem] rounded-lg w-full">
<div class="flex flex-row w-full justify-start scale-75">
<div class="w-full pb-1 text-gray-300">
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(col)"
:column="col"
:hide-menu="true"
:hide-icon="true"
/>
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="true" :hide-icon="true" />
</div>
</div>
<div class="flex flex-row w-full pb-3 pt-2 pl-2 items-center justify-start">
<div
v-if="!isRowEmpty(record, col)"
class="flex flex-row w-full text-gray-700 px-1 mt-[-0.25rem] items-center justify-start"
>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(col)"
v-model="record.row[col.title]"
@ -313,6 +352,9 @@ watch(
:read-only="true"
/>
</div>
<div v-else class="flex flex-row w-full h-[1.375rem] pl-1 items-center justify-start">
<span class="bg-gray-200 h-2 w-16 rounded-md"></span>
</div>
</div>
</div>
</a-card>
@ -352,51 +394,35 @@ watch(
<style scoped>
.nc-gallery-container {
grid-auto-rows: 1fr;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
@apply auto-rows-[1fr] grid-cols-[repeat(auto-fit,minmax(250px,1fr))];
}
:deep(.slick-dots li button) {
background-color: black;
@apply !bg-black;
}
.ant-carousel.gallery-carousel :deep(.slick-dots) {
position: relative;
@apply !w-auto absolute h-auto bottom-[-15px] absolute h-auto;
height: auto;
bottom: 0;
}
.ant-carousel.gallery-carousel :deep(.slick-dots li div > div) {
background: #000;
border: 0;
border-radius: 1px;
color: transparent;
cursor: pointer;
display: block;
@apply rounded-full border-0 cursor-pointer block opacity-100 p-0 outline-none transition-all duration-500 text-transparent h-2 w-2 bg-[#d9d9d9];
font-size: 0;
height: 3px;
opacity: 0.3;
outline: none;
padding: 0;
transition: all 0.5s;
width: 100%;
}
.ant-carousel.gallery-carousel :deep(.slick-dots li.slick-active div > div) {
opacity: 1;
@apply bg-brand-500 opacity-100;
}
.ant-carousel.gallery-carousel :deep(.slick-dots li) {
@apply !w-auto;
}
.ant-carousel.gallery-carousel :deep(.slick-prev) {
left: 0;
height: 100%;
top: 12px;
width: 50%;
@apply left-0;
}
.ant-carousel.gallery-carousel :deep(.slick-next) {
right: 0;
height: 100%;
top: 12px;
width: 50%;
@apply right-0;
}
</style>

2
packages/nc-gui/components/virtual-cell/Formula.vue

@ -29,7 +29,7 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
<span>ERR!</span>
</a-tooltip>
<div v-else class="p-2" @dblclick="activateShowEditNonEditableFieldWarning">
<div v-else class="py-2" @dblclick="activateShowEditNonEditableFieldWarning">
<div v-if="urls" v-html="urls" />
<div v-else>{{ result }}</div>

2
packages/nc-gui/components/virtual-cell/Links.vue

@ -97,7 +97,7 @@ const localCellValue = computed<any[]>(() => {
<component
:is="isLocked || isUnderLookup ? 'span' : 'a'"
:title="textVal"
class="text-center pl-3 nc-datatype-link underline-transparent"
class="text-center nc-datatype-link underline-transparent"
:class="{ '!text-gray-300': !textVal }"
@click.stop.prevent="openChildList"
>

7
packages/nc-gui/components/virtual-cell/QrCode.vue

@ -1,12 +1,14 @@
<script setup lang="ts">
import { useQRCode } from '@vueuse/integrations/useQRCode'
import type QRCode from 'qrcode'
import { RowHeightInj, computed, inject, ref } from '#imports'
import { IsGalleryInj, RowHeightInj, computed, inject, ref } from '#imports'
const maxNumberOfAllowedCharsForQrValue = 2000
const cellValue = inject(CellValueInj)
const isGallery = inject(IsGalleryInj, ref(false))
const qrValue = computed(() => String(cellValue?.value))
const tooManyCharsForQrCode = computed(() => qrValue?.value.length > maxNumberOfAllowedCharsForQrValue)
@ -36,6 +38,7 @@ const qrCodeLarge = useQRCode(qrValue, {
const modalVisible = ref(false)
const showQrModal = (ev: MouseEvent) => {
if (isGallery.value) return
ev.stopPropagation()
modalVisible.value = true
}
@ -65,7 +68,7 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = us
</div>
<img
v-if="showQrCode && rowHeight"
class="mx-auto"
:class="{ 'mx-auto': !isGallery }"
:style="{ height: rowHeight ? `${rowHeight * 1.4}rem` : `1.4rem` }"
:src="qrCode"
alt="QR Code"

16
packages/nc-gui/components/virtual-cell/barcode/JsBarcodeWrapper.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import JsBarcode from 'jsbarcode'
import { onMounted } from '#imports'
import { IsGalleryInj, onMounted } from '#imports'
const props = defineProps({
barcodeValue: { type: String, required: true },
@ -10,6 +10,8 @@ const props = defineProps({
const emit = defineEmits(['onClickBarcode'])
const isGallery = inject(IsGalleryInj, ref(false))
const barcodeSvgRef = ref<HTMLElement>()
const errorForCurrentInput = ref(false)
@ -33,6 +35,7 @@ const generate = () => {
}
const onBarcodeClick = (ev: MouseEvent) => {
if (isGallery.value) return
ev.stopPropagation()
emit('onClickBarcode')
}
@ -42,6 +45,15 @@ onMounted(generate)
</script>
<template>
<svg v-show="!errorForCurrentInput" ref="barcodeSvgRef" class="w-full" data-testid="barcode" @click="onBarcodeClick"></svg>
<svg
v-show="!errorForCurrentInput"
ref="barcodeSvgRef"
:class="{
'w-full': !isGallery,
'w-auto': isGallery,
}"
data-testid="barcode"
@click="onBarcodeClick"
></svg>
<slot v-if="errorForCurrentInput" name="barcodeRenderError" />
</template>

16
packages/nocodb/src/helpers/getAst.ts

@ -5,7 +5,7 @@ import type {
LookupColumn,
Model,
} from '~/models';
import { View } from '~/models';
import { GalleryView, View } from '~/models';
const getAst = async ({
query,
@ -32,6 +32,14 @@ const getAst = async ({
dependencyFields.nested = dependencyFields.nested || {};
dependencyFields.fieldsSet = dependencyFields.fieldsSet || new Set();
let coverImageId;
if (view) {
const gallery = await GalleryView.get(view.id);
if (gallery) {
coverImageId = gallery.fk_cover_image_col_id;
}
}
if (!model.columns?.length) await model.getColumns();
// extract only pk and pv
@ -59,7 +67,7 @@ const getAst = async ({
}
let allowedCols = null;
if (view)
if (view) {
allowedCols = (await View.getColumns(view.id)).reduce(
(o, c) => ({
...o,
@ -67,6 +75,10 @@ const getAst = async ({
}),
{},
);
if (coverImageId) {
allowedCols[coverImageId] = 1;
}
}
const ast = await model.columns.reduce(async (obj, col: Column) => {
let value: number | boolean | { [key: string]: any } = 1;

2
tests/playwright/pages/Dashboard/ViewSidebar/index.ts

@ -59,7 +59,7 @@ export class ViewSidebarPage extends BasePage {
await this.rootPage.locator('input[id="form_item_title"]:visible').waitFor({ state: 'visible' });
await this.rootPage.locator('input[id="form_item_title"]:visible').fill(title);
const submitAction = () =>
this.rootPage.locator('.ant-modal-content').locator('button.ant-btn.ant-btn-primary').click();
this.rootPage.locator('.ant-modal-content').locator('button.ant-btn.ant-btn-primary').click({ force: true });
await this.waitForResponse({
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: '/api/v1/db/meta/tables/',

Loading…
Cancel
Save