Browse Source

Nc Refactor: Gallery view (#8674)

* fix(nc-gui): introduce header icon in gallery view card and update style

* fix(nc-gui): field modal width issue if it is rich text

* fix(nc-gui): hide longtext expanded icon on gallery & kanban view card hove

* fix(nc-gui): date field alignment issue

* fix(nc-gui): udpate kanban view card

* fix(nc-gui): udpate gallery & kanban view card display value style

* fix(nocodb): hide cover image in new gallery, kanban view if it is not pv column

* feat(nc-gui): change cover image object fit property change support

* fix(nc-gui): virtual cell card value alignment issue

* fix(nc-gui): gallery view card image navigation issue

* fix(nc-gui): gallerym, kanban card cover image dots navigation overflow issue

* fix(nocodb): use optional chaining to access nested variable

* chore(nc-gui): lint

* fix(nc-gui): long text max line shuld be 4 in card

* test: update open expanded form in gallery test

* fix(nc-gui): add empty card in gallery view if cards length is less than 4

* fix(nc-gui): update gallery view card min width

* fix(nocodb): small changes

* fix(nc-gui): review changes

* fix(nc-gui): add input shadow effect

* fix(nc-gui): update card image navigation buttons icon

* fix(nc-gui): udpate gallery view bg color

* fix(nc-gui): update email, url, phone cell height from card

* fix(nc-gui): update isEmptyRow function logic

* fix(nc-gui): some review changes

* fix(nc-gui): card display value color

* fix(nc-gui): udpate gallery view card min width

* fix(nc-gui): update card shadow & border on hover

* fix(nc-gui): update gallery loader card width

* fix(nc-gui): add min height for card image

* chore(nc-gui): lint

* fix(nc-gui): card rich text height

* fix(nc-gui): align record count in right side in gallery view

* fix(nc-gui): review changes

* fix(nc-gui): shared view show & hide field issue

* chore(nc-gui): lint

* fix(nc-gui): link record test fail issue
nc-oss/f4dcddf8
Ramesh Mane 6 months ago committed by GitHub
parent
commit
99ecc40f9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      packages/nc-gui/components/cell/Checkbox.vue
  2. 15
      packages/nc-gui/components/cell/DateTimePicker.vue
  3. 16
      packages/nc-gui/components/cell/RichText.vue
  4. 24
      packages/nc-gui/components/cell/TextArea.vue
  5. 2
      packages/nc-gui/components/cell/attachment/index.vue
  6. 3
      packages/nc-gui/components/notification/Item/ProjectInvite.vue
  7. 274
      packages/nc-gui/components/smartsheet/Gallery.vue
  8. 258
      packages/nc-gui/components/smartsheet/Kanban.vue
  9. 7
      packages/nc-gui/components/smartsheet/SharedMapMarkerPopup.vue
  10. 1
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  11. 187
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  12. 3
      packages/nc-gui/components/virtual-cell/QrCode.vue
  13. 1
      packages/nc-gui/components/virtual-cell/barcode/JsBarcodeWrapper.vue
  14. 9
      packages/nc-gui/components/virtual-cell/components/ListItem.vue
  15. 45
      packages/nc-gui/composables/useViewColumns.ts
  16. 5
      packages/nc-gui/lang/en.json
  17. 5
      packages/nc-gui/lib/enums.ts
  18. 3
      packages/nc-gui/utils/dataUtils.ts
  19. 4
      packages/nc-gui/utils/formValidations.ts
  20. 2
      packages/nc-gui/windi.config.ts
  21. 15
      packages/nocodb/src/models/GalleryView.ts
  22. 14
      packages/nocodb/src/models/KanbanView.ts
  23. 18
      packages/nocodb/src/models/View.ts
  24. 4
      tests/playwright/pages/Dashboard/Gallery/index.ts

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

@ -114,7 +114,7 @@ useSelectedCellKeyupListener(active, (e) => {
'justify-center': !isEditColumnMenu && !isGallery && !isForm, 'justify-center': !isEditColumnMenu && !isGallery && !isForm,
'py-2': isEditColumnMenu, 'py-2': isEditColumnMenu,
}" }"
@click="onClick(true)" @click.stop="onClick(true)"
> >
<Transition name="layout" mode="out-in" :duration="100"> <Transition name="layout" mode="out-in" :duration="100">
<component <component

15
packages/nc-gui/components/cell/DateTimePicker.vue

@ -440,14 +440,18 @@ const cellValue = computed(
> >
<div <div
:title="localState?.format(dateTimeFormat)" :title="localState?.format(dateTimeFormat)"
class="nc-date-picker ant-picker-input flex justify-between gap-2 relative !w-auto" class="nc-date-picker ant-picker-input flex relative !w-auto gap-2"
:class="{
'justify-between': !isColDisabled,
}"
> >
<div <div
class="flex-none hover:bg-gray-100 px-1 rounded-md box-border w-[60%] max-w-[110px]" class="flex-none rounded-md box-border w-[60%] max-w-[110px]"
:class="{ :class="{
'py-0': isForm, 'py-0': isForm,
'py-0.5': !isForm, 'py-0.5': !isForm && !isColDisabled,
'bg-gray-100': isDatePicker && isOpen, 'bg-gray-100': isDatePicker && isOpen,
'hover:bg-gray-100 px-1': !isColDisabled,
}" }"
> >
<input <input
@ -465,13 +469,14 @@ const cellValue = computed(
/> />
</div> </div>
<div <div
class="flex-none hover:bg-gray-100 px-1 rounded-md box-border flex-1" class="flex-none rounded-md box-border flex-1"
:class="[ :class="[
`${timeCellMaxWidth}`, `${timeCellMaxWidth}`,
{ {
'py-0': isForm, 'py-0': isForm,
'py-0.5': !isForm, 'py-0.5': !isForm && !isColDisabled,
'bg-gray-100': !isDatePicker && isOpen, 'bg-gray-100': !isDatePicker && isOpen,
'hover:bg-gray-100 px-1': !isColDisabled,
}, },
]" ]"
> >

16
packages/nc-gui/components/cell/RichText.vue

@ -48,10 +48,20 @@ const isGrid = inject(IsGridInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false)) const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isGallery = inject(IsGalleryInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false))
const isFocused = ref(false) const isFocused = ref(false)
const keys = useMagicKeys() const keys = useMagicKeys()
const localRowHeight = computed(() => {
if (readOnlyCell.value && !isExpandedFormOpen.value && (isGallery.value || isKanban.value)) return 6
return rowHeight.value
})
const shouldShowLinkOption = computed(() => { const shouldShowLinkOption = computed(() => {
return isFormField.value ? isFocused.value : true return isFormField.value ? isFocused.value : true
}) })
@ -157,7 +167,7 @@ const editor = useEditor({
.turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />')) .turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />'))
.replaceAll(/\n\n<br \/>\n\n/g, '<br>\n\n') .replaceAll(/\n\n<br \/>\n\n/g, '<br>\n\n')
vModel.value = isFormField.value && markdown === '<br />' ? '' : markdown vModel.value = markdown === '<br />' ? '' : markdown
}, },
editable: !props.readOnly, editable: !props.readOnly,
autofocus: props.autofocus, autofocus: props.autofocus,
@ -322,8 +332,8 @@ onClickOutside(editorDom, (e) => {
'mt-2.5 flex-grow': fullMode, 'mt-2.5 flex-grow': fullMode,
'scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent': !fullMode || (!fullMode && isExpandedFormOpen), 'scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent': !fullMode || (!fullMode && isExpandedFormOpen),
'flex-grow': isExpandedFormOpen, 'flex-grow': isExpandedFormOpen,
[`!overflow-hidden nc-truncate nc-line-clamp-${rowHeightTruncateLines(rowHeight)}`]: [`!overflow-hidden nc-truncate nc-line-clamp-${rowHeightTruncateLines(localRowHeight)}`]:
!fullMode && readOnly && rowHeight && !isExpandedFormOpen && !isForm, !fullMode && readOnly && localRowHeight && !isExpandedFormOpen && !isForm,
}" }"
@keydown.alt.enter.stop @keydown.alt.enter.stop
@keydown.shift.enter.stop @keydown.shift.enter.stop

24
packages/nc-gui/components/cell/TextArea.vue

@ -21,6 +21,10 @@ const isForm = inject(IsFormInj, ref(false))
const isGrid = inject(IsGridInj, ref(false)) const isGrid = inject(IsGridInj, ref(false))
const isGallery = inject(IsGalleryInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false))
const readOnly = inject(ReadonlyInj, ref(false)) const readOnly = inject(ReadonlyInj, ref(false))
const { showNull } = useGlobal() const { showNull } = useGlobal()
@ -60,6 +64,12 @@ const height = computed(() => {
return rowHeight.value * 36 return rowHeight.value * 36
}) })
const localRowHeight = computed(() => {
if (readOnly.value && !isExpandedFormOpen.value && (isGallery.value || isKanban.value)) return 6
return rowHeight.value
})
const isVisible = ref(false) const isVisible = ref(false)
const inputWrapperRef = ref<HTMLElement | null>(null) const inputWrapperRef = ref<HTMLElement | null>(null)
@ -222,11 +232,11 @@ watch(inputWrapperRef, () => {
class="w-full cursor-pointer nc-readonly-rich-text-wrapper" class="w-full cursor-pointer nc-readonly-rich-text-wrapper"
:class="{ :class="{
'nc-readonly-rich-text-grid ': !isExpandedFormOpen && !isForm, 'nc-readonly-rich-text-grid ': !isExpandedFormOpen && !isForm,
'nc-readonly-rich-text-sort-height': rowHeight === 1 && !isExpandedFormOpen && !isForm, 'nc-readonly-rich-text-sort-height': localRowHeight === 1 && !isExpandedFormOpen && !isForm,
}" }"
:style="{ :style="{
maxHeight: isForm ? undefined : isExpandedFormOpen ? `${height}px` : `${21 * rowHeightTruncateLines(rowHeight)}px`, maxHeight: isForm ? undefined : isExpandedFormOpen ? `${height}px` : `${21 * rowHeightTruncateLines(localRowHeight)}px`,
minHeight: isForm ? undefined : isExpandedFormOpen ? `${height}px` : `${21 * rowHeightTruncateLines(rowHeight)}px`, minHeight: isForm ? undefined : isExpandedFormOpen ? `${height}px` : `${21 * rowHeightTruncateLines(localRowHeight)}px`,
}" }"
@dblclick="onExpand" @dblclick="onExpand"
@keydown.enter="onExpand" @keydown.enter="onExpand"
@ -265,12 +275,12 @@ watch(inputWrapperRef, () => {
<LazyCellClampedText <LazyCellClampedText
v-else-if="rowHeight" v-else-if="rowHeight"
:value="vModel" :value="vModel"
:lines="rowHeightTruncateLines(rowHeight)" :lines="rowHeightTruncateLines(localRowHeight)"
class="nc-text-area-clamped-text" class="nc-text-area-clamped-text"
:style="{ :style="{
'word-break': 'break-word', 'word-break': 'break-word',
'max-height': `${25 * rowHeightTruncateLines(rowHeight)}px`, 'max-height': `${25 * rowHeightTruncateLines(localRowHeight)}px`,
'my-auto': rowHeightTruncateLines(rowHeight) === 1, 'my-auto': rowHeightTruncateLines(localRowHeight) === 1,
}" }"
@click="onTextClick" @click="onTextClick"
/> />
@ -280,7 +290,7 @@ watch(inputWrapperRef, () => {
<NcTooltip <NcTooltip
v-if="!isVisible && !isForm" v-if="!isVisible && !isForm"
placement="bottom" placement="bottom"
class="!absolute !hidden nc-text-area-expand-btn group-hover:block z-3" class="nc-action-icon !absolute !hidden nc-text-area-expand-btn group-hover:block z-3"
:class="{ :class="{
'right-1': isForm, 'right-1': isForm,
'right-0': !isForm, 'right-0': !isForm,

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

@ -157,7 +157,7 @@ const onExpand = () => {
const onImageClick = (item: any) => { const onImageClick = (item: any) => {
if (isMobileMode.value && !isExpandedForm.value) return if (isMobileMode.value && !isExpandedForm.value) return
if (!isMobileMode.value && (isGallery.value || (isKanban.value && !isExpandedForm.value))) return if (!isMobileMode.value && (isGallery.value || isKanban.value) && !isExpandedForm.value) return
selectedImage.value = item selectedImage.value = item
} }

3
packages/nc-gui/components/notification/Item/ProjectInvite.vue

@ -14,8 +14,7 @@ const item = toRef(props, 'item')
<NotificationItemWrapper :item="item" @click="navigateToProject({ baseId: item.body.base.id })"> <NotificationItemWrapper :item="item" @click="navigateToProject({ baseId: item.body.base.id })">
<div> <div>
<span class="font-semibold">{{ item.body.user.display_name ?? item.body.user.email }}</span> has invited you to collaborate <span class="font-semibold">{{ item.body.user.display_name ?? item.body.user.email }}</span> has invited you to collaborate
on on <span class="font-semibold">{{ item.body.base.title }}</span> base.
<span class="font-semibold">{{ item.body.base.title }}</span> base.
</div> </div>
</NotificationItemWrapper> </NotificationItemWrapper>
</template> </template>

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

@ -56,12 +56,12 @@ const coverImageColumn: any = computed(() =>
: {}, : {},
) )
const isRowEmpty = (record: any, col: any) => { const coverImageObjectFitClass = computed(() => {
const val = record.row[col.title] const fk_cover_image_object_fit = parseProp(galleryData.value?.meta)?.fk_cover_image_object_fit || CoverImageObjectFit.FIT
if (!val) return true
return Array.isArray(val) && val.length === 0 if (fk_cover_image_object_fit === CoverImageObjectFit.FIT) return '!object-contain'
} if (fk_cover_image_object_fit === CoverImageObjectFit.COVER) return '!object-cover'
})
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const hasEditPermission = computed(() => isUIAllowed('dataEdit')) const hasEditPermission = computed(() => isUIAllowed('dataEdit'))
@ -205,7 +205,7 @@ watch(
</template> </template>
<div <div
class="flex flex-col w-full nc-gallery nc-scrollbar-md bg-[#fbfbfb]" class="flex flex-col w-full nc-gallery nc-scrollbar-md bg-gray-50"
data-testid="nc-gallery-wrapper" data-testid="nc-gallery-wrapper"
style="height: calc(100% - var(--topbar-height) + 0.7rem)" style="height: calc(100% - var(--topbar-height) + 0.7rem)"
:class="{ :class="{
@ -213,16 +213,16 @@ watch(
}" }"
> >
<div v-if="isViewDataLoading" class="flex flex-col h-full"> <div v-if="isViewDataLoading" class="flex flex-col h-full">
<div class="flex flex-row p-3 !pr-1 gap-x-2 flex-wrap gap-y-2"> <div class="nc-gallery-container-skeleton grid gap-3 p-3">
<a-skeleton-input v-for="index of Array(20)" :key="index" class="!min-w-60.5 !h-96 !rounded-md overflow-hidden" /> <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> </div>
<div v-else class="nc-gallery-container grid gap-3 my-4 px-3"> <div v-else class="nc-gallery-container grid gap-3 p-3">
<div v-for="(record, rowIndex) in data" :key="`record-${record.row.id}`"> <div v-for="(record, rowIndex) in data" :key="`record-${record.row.id}`">
<LazySmartsheetRow :row="record"> <LazySmartsheetRow :row="record">
<a-card <a-card
class="!rounded-lg h-full border-gray-200 border-1 group overflow-hidden break-all max-w-[450px] shadow-sm hover:shadow-md cursor-pointer" class="!rounded-xl h-full border-gray-200 border-1 group overflow-hidden break-all max-w-[450px] cursor-pointer"
:body-style="{ padding: '0px' }" :body-style="{ padding: '16px !important' }"
:data-testid="`nc-gallery-card-${record.row.id}`" :data-testid="`nc-gallery-card-${record.row.id}`"
@click="expandFormClick($event, record)" @click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, { row: rowIndex })" @contextmenu="showContextMenu($event, { row: rowIndex })"
@ -230,12 +230,12 @@ watch(
<template v-if="galleryData?.fk_cover_image_col_id" #cover> <template v-if="galleryData?.fk_cover_image_col_id" #cover>
<a-carousel <a-carousel
v-if="!reloadAttachments && attachments(record).length" v-if="!reloadAttachments && attachments(record).length"
class="gallery-carousel !border-b-1 !border-gray-200" class="gallery-carousel !border-b-1 !border-gray-200 min-h-52"
arrows arrows
> >
<template #customPaging> <template #customPaging>
<a> <a>
<div class="pt-[12px]"> <div>
<div></div> <div></div>
</div> </div>
</a> </a>
@ -243,17 +243,25 @@ watch(
<template #prevArrow> <template #prevArrow>
<div class="z-10 arrow"> <div class="z-10 arrow">
<MdiChevronLeft <NcButton
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" type="secondary"
/> size="xsmall"
class="!absolute !left-1.5 !bottom-[-90px] !opacity-0 !group-hover:opacity-100 !rounded-lg cursor-pointer"
>
<GeneralIcon icon="arrowLeft" class="text-gray-700 w-4 h-4" />
</NcButton>
</div> </div>
</template> </template>
<template #nextArrow> <template #nextArrow>
<div class="z-10 arrow"> <div class="z-10 arrow">
<MdiChevronRight <NcButton
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" type="secondary"
/> size="xsmall"
class="!absolute !right-1.5 !bottom-[-90px] !opacity-0 !group-hover:opacity-100 !rounded-lg cursor-pointer"
>
<GeneralIcon icon="arrowRight" class="text-gray-700 w-4 h-4" />
</NcButton>
</div> </div>
</template> </template>
@ -261,7 +269,8 @@ watch(
<LazyCellAttachmentImage <LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)" v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="`carousel-${record.row.id}-${index}`" :key="`carousel-${record.row.id}-${index}`"
class="h-52 !object-contain" class="h-52"
:class="[`${coverImageObjectFitClass}`]"
:srcs="getPossibleAttachmentSrc(attachment)" :srcs="getPossibleAttachmentSrc(attachment)"
@click="expandFormClick($event, record)" @click="expandFormClick($event, record)"
/> />
@ -271,70 +280,81 @@ watch(
<img class="object-contain w-[48px] h-[48px]" src="~assets/icons/FileIconImageBox.png" /> <img class="object-contain w-[48px] h-[48px]" src="~assets/icons/FileIconImageBox.png" />
</div> </div>
</template> </template>
<h2 v-if="displayField" class="text-base mt-3 mx-3 font-bold"> <div class="flex flex-col gap-3 !children:pointer-events-none">
<LazySmartsheetVirtualCell <h2 v-if="displayField" class="nc-card-display-value-wrapper">
v-if="isVirtualCol(displayField)" <template v-if="!isRowEmpty(record, displayField)">
v-model="record.row[displayField.title]"
class="!text-brand-500"
:column="displayField"
:row="record"
/>
<LazySmartsheetCell
v-else
v-model="record.row[displayField.title]"
class="!text-brand-500"
: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 first:mt-3 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
v-if="!isRowEmpty(record, col)"
class="flex flex-row w-full text-gray-700 px-1 mt-[-0.25rem] items-center justify-start"
>
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
v-if="isVirtualCol(col)" v-if="isVirtualCol(displayField)"
v-model="record.row[col.title]" v-model="record.row[displayField.title]"
:column="col" class="!text-brand-500"
:column="displayField"
:row="record" :row="record"
/> />
<LazySmartsheetCell <LazySmartsheetCell
v-else v-else
v-model="record.row[col.title]" v-model="record.row[displayField.title]"
:column="col" class="!text-brand-500"
:column="displayField"
:edit-enabled="false" :edit-enabled="false"
:read-only="true" :read-only="true"
/> />
</template>
<template v-else> - </template>
</h2>
<div v-for="col in fieldsWithoutDisplay" :key="`record-${record.row.id}-${col.id}`">
<div class="flex flex-col rounded-lg w-full">
<div class="flex flex-row w-full justify-start">
<div class="nc-card-col-header w-full text-gray-500 uppercase">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="true" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
</div>
</div>
<div
v-if="!isRowEmpty(record, col)"
class="flex flex-row w-full text-gray-800 items-center justify-start min-h-7 py-1"
>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(col)"
v-model="record.row[col.title]"
:column="col"
:row="record"
class="!text-gray-800"
/>
<LazySmartsheetCell
v-else
v-model="record.row[col.title]"
:column="col"
:edit-enabled="false"
:read-only="true"
class="!text-gray-800"
/>
</div>
<div v-else class="flex flex-row w-full h-7 pl-1 items-center justify-start">-</div>
</div> </div>
<div v-else class="flex flex-row w-full h-[1.375rem] pl-1 items-center justify-start">-</div>
</div> </div>
</div> </div>
</a-card> </a-card>
</LazySmartsheetRow> </LazySmartsheetRow>
</div> </div>
<template v-if="data.length <= 4">
<div v-for="index of Array(8 - data.length)" :key="index" class="nc-empty-card"></div>
</template>
</div> </div>
</div> </div>
</a-dropdown> </a-dropdown>
<LazySmartsheetPagination v-model:pagination-data="paginationData" show-api-timing :change-page="changePage" /> <LazySmartsheetPagination
v-model:pagination-data="paginationData"
align-count-on-right
show-api-timing
:change-page="changePage"
/>
<Suspense> <Suspense>
<LazySmartsheetExpandedForm <LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg" v-if="expandedFormRow && expandedFormDlg"
@ -364,8 +384,9 @@ watch(
</Suspense> </Suspense>
</template> </template>
<style scoped> <style lang="scss" scoped>
.nc-gallery-container { .nc-gallery-container,
.nc-gallery-container-skeleton {
@apply auto-rows-[1fr] grid-cols-[repeat(auto-fit,minmax(250px,1fr))]; @apply auto-rows-[1fr] grid-cols-[repeat(auto-fit,minmax(250px,1fr))];
} }
@ -374,8 +395,7 @@ watch(
} }
.ant-carousel.gallery-carousel :deep(.slick-dots) { .ant-carousel.gallery-carousel :deep(.slick-dots) {
@apply !w-auto absolute h-auto bottom-[-15px] absolute h-auto; @apply !w-full max-w-[calc(100%_-_36%)] absolute left-0 right-0 bottom-[-18px] h-6 overflow-x-auto nc-scrollbar-thin !mx-auto;
height: auto;
} }
.ant-carousel.gallery-carousel :deep(.slick-dots li div > div) { .ant-carousel.gallery-carousel :deep(.slick-dots li div > div) {
@ -399,8 +419,122 @@ watch(
} }
:deep(.ant-card) { :deep(.ant-card) {
&:hover .nc-action-icon { @apply transition-all duration-0.3s;
@apply invisible;
box-shadow: 0px 2px 4px -2px rgba(0, 0, 0, 0.06), 0px 4px 4px -2px rgba(0, 0, 0, 0.02);
&:hover {
@apply !border-gray-300;
box-shadow: 0px 0px 24px 0px rgba(0, 0, 0, 0.1), 0px 0px 8px 0px rgba(0, 0, 0, 0.04);
.nc-action-icon {
@apply invisible;
}
}
}
.nc-card-display-value-wrapper {
@apply my-0 text-xl leading-8 text-gray-600;
.nc-cell,
.nc-virtual-cell {
@apply text-xl leading-8;
:deep(.nc-cell-field),
:deep(input),
:deep(textarea),
:deep(.nc-cell-field-link) {
@apply !text-xl leading-8 text-gray-600;
}
}
}
.nc-card-col-header {
:deep(.nc-cell-icon),
:deep(.nc-virtual-cell-icon) {
@apply ml-0;
}
}
:deep(.nc-cell),
:deep(.nc-virtual-cell) {
@apply text-small leading-[18px];
.nc-cell-field,
input,
textarea,
.nc-cell-field-link {
@apply !text-small !leading-[18px];
}
}
:deep(.nc-cell) {
&.nc-cell-longtext {
.long-text-wrapper {
@apply min-h-1;
.nc-readonly-rich-text-wrapper {
@apply !min-h-1;
}
.nc-rich-text {
@apply pl-0;
.tiptap.ProseMirror {
@apply -ml-1 min-h-1;
}
}
}
}
&.nc-cell-checkbox {
@apply children:pl-0;
}
&.nc-cell-singleselect .nc-cell-field > div {
@apply flex items-center;
}
&.nc-cell-multiselect .nc-cell-field > div {
@apply h-5;
}
&.nc-cell-email,
&.nc-cell-phonenumber {
@apply flex items-center;
}
&.nc-cell-email,
&.nc-cell-phonenumber,
&.nc-cell-url {
.nc-cell-field-link {
@apply py-0;
}
}
}
:deep(.nc-virtual-cell) {
.nc-links-wrapper {
@apply py-0 children:min-h-4;
}
&.nc-virtual-cell-linktoanotherrecord {
.chips-wrapper {
@apply min-h-4 !children:min-h-4;
.chip.group {
@apply my-0;
}
}
}
&.nc-virtual-cell-lookup {
.nc-lookup-cell {
@apply !h-5.5;
.nc-cell-lookup-scroll {
@apply py-0 h-auto;
}
}
}
&.nc-virtual-cell-formula {
.nc-cell-field {
@apply py-0;
}
}
&.nc-virtual-cell-qrcode,
&.nc-virtual-cell-barcode {
@apply children:justify-start;
} }
} }
</style> </style>

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

@ -96,16 +96,16 @@ const coverImageColumn: any = computed(() =>
: {}, : {},
) )
const kanbanContainerRef = ref() const coverImageObjectFitClass = computed(() => {
const fk_cover_image_object_fit = parseProp(kanbanMetaData.value?.meta)?.fk_cover_image_object_fit || CoverImageObjectFit.FIT
const selectedStackTitle = ref('') if (fk_cover_image_object_fit === CoverImageObjectFit.FIT) return '!object-contain'
if (fk_cover_image_object_fit === CoverImageObjectFit.COVER) return '!object-cover'
})
const isRowEmpty = (record: any, col: any) => { const kanbanContainerRef = ref()
const val = record.row[col.title]
if (!val) return true
return Array.isArray(val) && val.length === 0 const selectedStackTitle = ref('')
}
reloadViewDataHook?.on(async () => { reloadViewDataHook?.on(async () => {
await loadKanbanMeta() await loadKanbanMeta()
@ -512,8 +512,8 @@ const getRowId = (row: RowType) => {
<LazySmartsheetRow :row="record"> <LazySmartsheetRow :row="record">
<a-card <a-card
:key="`${getRowId(record)}-${index}`" :key="`${getRowId(record)}-${index}`"
class="!rounded-lg h-full border-gray-200 border-1 group overflow-hidden break-all max-w-[450px] shadow-sm hover:shadow-md cursor-pointer children:pointer-events-none" class="!rounded-xl h-full border-gray-200 border-1 group overflow-hidden break-all max-w-[450px] cursor-pointer"
:body-style="{ padding: '0px' }" :body-style="{ padding: '16px !important' }"
:data-stack="stack.title" :data-stack="stack.title"
:data-testid="`nc-gallery-card-${record.row.id}`" :data-testid="`nc-gallery-card-${record.row.id}`"
:class="{ :class="{
@ -528,10 +528,11 @@ const getRowId = (row: RowType) => {
<a-carousel <a-carousel
:key="attachments(record).reduce((acc, curr) => acc + curr?.path, '')" :key="attachments(record).reduce((acc, curr) => acc + curr?.path, '')"
class="gallery-carousel !border-b-1 !border-gray-200" class="gallery-carousel !border-b-1 !border-gray-200"
arrows
> >
<template #customPaging> <template #customPaging>
<a> <a>
<div class="pt-[12px]"> <div>
<div></div> <div></div>
</div> </div>
</a> </a>
@ -539,17 +540,25 @@ const getRowId = (row: RowType) => {
<template #prevArrow> <template #prevArrow>
<div class="z-10 arrow"> <div class="z-10 arrow">
<MdiChevronLeft <NcButton
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" type="secondary"
/> size="xsmall"
class="!absolute !left-1.5 !bottom-[-90px] !opacity-0 !group-hover:opacity-100 !rounded-lg cursor-pointer"
>
<GeneralIcon icon="arrowLeft" class="text-gray-700 w-4 h-4" />
</NcButton>
</div> </div>
</template> </template>
<template #nextArrow> <template #nextArrow>
<div class="z-10 arrow"> <div class="z-10 arrow">
<MdiChevronRight <NcButton
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" type="secondary"
/> size="xsmall"
class="!absolute !right-1.5 !bottom-[-90px] !opacity-0 !group-hover:opacity-100 !rounded-lg cursor-pointer"
>
<GeneralIcon icon="arrowRight" class="text-gray-700 w-4 h-4" />
</NcButton>
</div> </div>
</template> </template>
@ -557,7 +566,8 @@ const getRowId = (row: RowType) => {
<LazyCellAttachmentImage <LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)" v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="attachment.path" :key="attachment.path"
class="h-52 object-cover" class="h-52"
:class="[`${coverImageObjectFitClass}`]"
:srcs="getPossibleAttachmentSrc(attachment)" :srcs="getPossibleAttachmentSrc(attachment)"
/> />
</template> </template>
@ -570,60 +580,66 @@ const getRowId = (row: RowType) => {
<img class="object-contain w-[48px] h-[48px]" src="~assets/icons/FileIconImageBox.png" /> <img class="object-contain w-[48px] h-[48px]" src="~assets/icons/FileIconImageBox.png" />
</div> </div>
</template> </template>
<h2 v-if="displayField" class="text-base mt-3 mx-3 font-bold"> <div class="flex flex-col gap-3 !children:pointer-events-none">
<LazySmartsheetVirtualCell <h2 v-if="displayField" class="nc-card-display-value-wrapper">
v-if="isVirtualCol(displayField)" <template v-if="!isRowEmpty(record, displayField)">
v-model="record.row[displayField.title]"
class="!text-brand-500"
:column="displayField"
:row="record"
/>
<LazySmartsheetCell
v-else
v-model="record.row[displayField.title]"
class="!text-brand-500"
: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 first:mt-3 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
v-if="!isRowEmpty(record, col)"
class="flex flex-row w-full text-gray-700 px-1 mt-[-0.25rem] items-center justify-start"
>
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
v-if="isVirtualCol(col)" v-if="isVirtualCol(displayField)"
v-model="record.row[col.title]" v-model="record.row[displayField.title]"
:column="col" class="!text-brand-500"
:column="displayField"
:row="record" :row="record"
/> />
<LazySmartsheetCell <LazySmartsheetCell
v-else v-else
v-model="record.row[col.title]" v-model="record.row[displayField.title]"
:column="col" class="!text-brand-500"
:column="displayField"
:edit-enabled="false" :edit-enabled="false"
:read-only="true" :read-only="true"
/> />
</template>
<template v-else> - </template>
</h2>
<div v-for="col in fieldsWithoutDisplay" :key="`record-${record.row.id}-${col.id}`">
<div class="flex flex-col rounded-lg w-full">
<div class="flex flex-row w-full justify-start">
<div class="nc-card-col-header w-full text-gray-500 uppercase">
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(col)"
:column="col"
:hide-menu="true"
/>
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
</div>
</div>
<div
v-if="!isRowEmpty(record, col)"
class="flex flex-row w-full text-gray-800 items-center justify-start min-h-7 py-1"
>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(col)"
v-model="record.row[col.title]"
:column="col"
:row="record"
class="!text-gray-800"
/>
<LazySmartsheetCell
v-else
v-model="record.row[col.title]"
:column="col"
:edit-enabled="false"
:read-only="true"
class="!text-gray-800"
/>
</div>
<div v-else class="flex flex-row w-full h-7 pl-1 items-center justify-start">-</div>
</div> </div>
<div v-else class="flex flex-row w-full h-[1.375rem] pl-1 items-center justify-start">-</div>
</div> </div>
</div> </div>
</a-card> </a-card>
@ -783,8 +799,7 @@ const getRowId = (row: RowType) => {
} }
.ant-carousel.gallery-carousel :deep(.slick-dots) { .ant-carousel.gallery-carousel :deep(.slick-dots) {
@apply !w-auto absolute h-auto bottom-[-15px] absolute h-auto; @apply !w-full max-w-[calc(100%_-_36%)] absolute left-0 right-0 bottom-[-18px] h-6 overflow-x-auto nc-scrollbar-thin !mx-auto;
height: auto;
} }
.ant-carousel.gallery-carousel :deep(.slick-dots li div > div) { .ant-carousel.gallery-carousel :deep(.slick-dots li div > div) {
@ -812,8 +827,121 @@ const getRowId = (row: RowType) => {
} }
:deep(.ant-card) { :deep(.ant-card) {
&:hover .nc-action-icon { @apply transition-shadow duration-0.3s;
@apply invisible;
box-shadow: 0px 2px 4px -2px rgba(0, 0, 0, 0.06), 0px 4px 4px -2px rgba(0, 0, 0, 0.02);
&:hover {
box-shadow: 0px 12px 16px -4px rgba(0, 0, 0, 0.1), 0px 4px 6px -2px rgba(0, 0, 0, 0.06);
.nc-action-icon {
@apply invisible;
}
}
}
.nc-card-display-value-wrapper {
@apply my-0 text-xl leading-8 text-gray-600;
.nc-cell,
.nc-virtual-cell {
@apply text-xl leading-8;
:deep(.nc-cell-field),
:deep(input),
:deep(textarea),
:deep(.nc-cell-field-link) {
@apply !text-xl leading-8 text-gray-600;
}
}
}
.nc-card-col-header {
:deep(.nc-cell-icon),
:deep(.nc-virtual-cell-icon) {
@apply ml-0;
}
}
:deep(.nc-cell),
:deep(.nc-virtual-cell) {
@apply text-small leading-[18px];
.nc-cell-field,
input,
textarea,
.nc-cell-field-link {
@apply !text-small !leading-[18px];
}
}
:deep(.nc-cell) {
&.nc-cell-longtext {
.long-text-wrapper {
@apply min-h-1;
.nc-readonly-rich-text-wrapper {
@apply !min-h-1;
}
.nc-rich-text {
@apply pl-0;
.tiptap.ProseMirror {
@apply -ml-1 min-h-1;
}
}
}
}
&.nc-cell-checkbox {
@apply children:pl-0;
}
&.nc-cell-singleselect .nc-cell-field > div {
@apply flex items-center;
}
&.nc-cell-multiselect .nc-cell-field > div {
@apply h-5;
}
&.nc-cell-email,
&.nc-cell-phonenumber {
@apply flex items-center;
}
&.nc-cell-email,
&.nc-cell-phonenumber,
&.nc-cell-url {
.nc-cell-field-link {
@apply py-0;
}
}
}
:deep(.nc-virtual-cell) {
.nc-links-wrapper {
@apply py-0 children:min-h-4;
}
&.nc-virtual-cell-linktoanotherrecord {
.chips-wrapper {
@apply min-h-4 !children:min-h-4;
.chip.group {
@apply my-0;
}
}
}
&.nc-virtual-cell-lookup {
.nc-lookup-cell {
@apply !h-5.5;
.nc-cell-lookup-scroll {
@apply py-0 h-auto;
}
}
}
&.nc-virtual-cell-formula {
.nc-cell-field {
@apply py-0;
}
}
&.nc-virtual-cell-qrcode,
&.nc-virtual-cell-barcode {
@apply children:justify-start;
} }
} }
</style> </style>

7
packages/nc-gui/components/smartsheet/SharedMapMarkerPopup.vue

@ -15,13 +15,6 @@ const { loadData } = useViewData(meta, view)
provide(IsFormInj, ref(false)) provide(IsFormInj, ref(false))
provide(IsGridInj, ref(false)) provide(IsGridInj, ref(false))
const isRowEmpty = (record: any, col: any) => {
const val = record.row[col.title]
if (!val) return true
return Array.isArray(val) && val.length === 0
}
reloadViewDataHook?.on(async () => { reloadViewDataHook?.on(async () => {
await loadData() await loadData()
}) })

1
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -299,7 +299,6 @@ const filterOption = (input: string, option: { value: UITypes }) => {
'bg-white': !props.fromTableExplorer, 'bg-white': !props.fromTableExplorer,
'w-[384px]': !props.embedMode, 'w-[384px]': !props.embedMode,
'min-w-500px': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links, 'min-w-500px': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links,
'!w-146': isTextArea(formState) && formState.meta?.richMode,
'!w-116 overflow-visible': formState.uidt === UITypes.Formula && !props.embedMode, '!w-116 overflow-visible': formState.uidt === UITypes.Formula && !props.embedMode,
'!w-[500px]': formState.uidt === UITypes.Attachment && !props.embedMode && !appInfo.ee, '!w-[500px]': formState.uidt === UITypes.Attachment && !props.embedMode && !appInfo.ee,
'!w-[600px]': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links, '!w-[600px]': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links,

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

@ -19,6 +19,8 @@ const isPublic = inject(IsPublicInj, ref(false))
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { t } = useI18n()
const { const {
showSystemFields, showSystemFields,
fields, fields,
@ -194,6 +196,80 @@ const coverImageColumnId = computed({
}, },
}) })
const updateCoverImageObjectFit = async (val: string) => {
if (
![ViewTypes.GALLERY, ViewTypes.KANBAN].includes(activeView.value?.type as ViewTypes) ||
!activeView.value?.id ||
!activeView.value?.view
) {
return
}
if (activeView.value?.type === ViewTypes.GALLERY) {
const payload = {
...parseProp((activeView.value?.view as GalleryType)?.meta),
fk_cover_image_object_fit: val,
}
await $api.dbView.galleryUpdate(activeView.value?.id, {
meta: payload,
})
;(activeView.value.view as GalleryType).meta = payload
} else if (activeView.value?.type === ViewTypes.KANBAN) {
const payload = {
...parseProp((activeView.value?.view as KanbanType)?.meta),
fk_cover_image_object_fit: val,
}
await $api.dbView.kanbanUpdate(activeView.value?.id, {
meta: payload,
})
;(activeView.value.view as KanbanType).meta = payload
}
await reloadViewMetaHook?.trigger()
}
const coverImageObjectFitOptions = [
{ value: CoverImageObjectFit.FIT, label: t('labels.fitImage') },
{ value: CoverImageObjectFit.COVER, label: t('labels.coverImageArea') },
]
const coverImageObjectFitDropdown = ref<{
isOpen: boolean
isSaving: keyof typeof CoverImageObjectFit | null
}>({
isOpen: false,
isSaving: null,
})
const coverImageObjectFit = computed({
get: () => {
return [ViewTypes.GALLERY, ViewTypes.KANBAN].includes(activeView.value?.type as ViewTypes) && activeView.value?.view
? parseProp(activeView.value?.view?.meta)?.fk_cover_image_object_fit || CoverImageObjectFit.FIT
: undefined
},
set: async (val) => {
if (val !== coverImageObjectFit.value) {
coverImageObjectFitDropdown.value.isSaving = val
addUndo({
undo: {
fn: updateCoverImageObjectFit,
args: [coverImageObjectFit.value],
},
redo: {
fn: updateCoverImageObjectFit,
args: [val],
},
scope: defineViewScope({ view: activeView.value }),
})
await updateCoverImageObjectFit(val)
}
coverImageObjectFitDropdown.value.isSaving = null
coverImageObjectFitDropdown.value.isOpen = false
},
})
const onShowAll = () => { const onShowAll = () => {
addUndo({ addUndo({
undo: { undo: {
@ -341,18 +417,80 @@ useMenuCloseOnEsc(open)
> >
<div <div
v-if="!isPublic && (activeView?.type === ViewTypes.GALLERY || activeView?.type === ViewTypes.KANBAN)" v-if="!isPublic && (activeView?.type === ViewTypes.GALLERY || activeView?.type === ViewTypes.KANBAN)"
class="flex flex-col gap-y-2 px-2 mb-6" class="flex flex-col gap-y-2 px-2 mb-3"
> >
<div class="flex text-sm select-none">Select cover image field</div> <div class="flex text-sm select-none text-gray-600">{{ $t('labels.coverImageField') }}</div>
<a-select
v-model:value="coverImageColumnId" <div
:options="coverOptions" class="nc-dropdown-cover-image-wrapper flex items-stretch border-1 border-gray-200 rounded-lg transition-all duration-0.3s"
class="w-full"
dropdown-class-name="nc-dropdown-cover-image !rounded-lg"
@click.stop
> >
<template #suffixIcon><GeneralIcon class="text-gray-700" icon="arrowDown" /></template> <a-select
</a-select> v-model:value="coverImageColumnId"
class="w-full"
dropdown-class-name="nc-dropdown-cover-image !rounded-lg"
:bordered="false"
@click.stop
>
<template #suffixIcon><GeneralIcon class="text-gray-700" icon="arrowDown" /></template>
<a-select-option v-for="option of coverOptions" :key="option.value" :value="option.value">
<div class="w-full flex gap-2 items-center justify-between">
<div class="flex items-center gap-1">
<component
:is="getIcon(metaColumnById[option.value])"
v-if="option.value"
class="!w-3.5 !h-3.5 !text-gray-700 !ml-0"
/>
<span> {{ option.label }} </span>
</div>
<GeneralIcon
v-if="coverImageColumnId === option.value"
id="nc-selected-item-icon"
icon="check"
class="flex-none text-primary w-4 h-4"
/>
</div>
</a-select-option>
</a-select>
<NcDropdown v-if="coverImageObjectFit" v-model:visible="coverImageObjectFitDropdown.isOpen" placement="bottomRight">
<button class="flex items-center px-2 border-l-1 border-gray-200 cursor-pointer">
<GeneralIcon
icon="settings"
class="h-4 w-4"
:class="{
'!text-brand-500': coverImageObjectFitDropdown.isOpen,
}"
/>
</button>
<template #overlay>
<NcMenu class="nc-cover-image-object-fit-dropdown-menu min-w-[168px]">
<NcMenuItem
v-for="option in coverImageObjectFitOptions"
:key="option.value"
class="!children:w-full"
@click.stop="coverImageObjectFit = option.value"
>
<span>
{{ option.label }}
</span>
<GeneralLoader
v-if="option.value === coverImageObjectFitDropdown.isSaving"
size="regular"
class="flex-none"
/>
<GeneralIcon
v-else-if="option.value === coverImageObjectFit"
icon="check"
class="flex-none text-primary w-4 h-4"
/>
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</div>
</div> </div>
<div class="px-2" @click.stop> <div class="px-2" @click.stop>
@ -469,7 +607,7 @@ useMenuCloseOnEsc(open)
</div> </div>
<div v-if="!filterQuery" class="flex px-2 gap-2 py-2"> <div v-if="!filterQuery" class="flex px-2 gap-2 py-2">
<NcButton class="nc-fields-show-all-fields" size="small" type="ghost" @click="showAllColumns = !showAllColumns"> <NcButton class="nc-fields-show-all-fields" size="small" type="ghost" @click="showAllColumns = !showAllColumns">
{{ showAllColumns ? 'Hide all' : 'Show all' }} fields {{ showAllColumns ? $t('general.hideAll') : $t('general.showAll') }} {{ $t('objects.fields') }}
</NcButton> </NcButton>
<NcButton <NcButton
v-if="!isPublic" v-if="!isPublic"
@ -478,7 +616,7 @@ useMenuCloseOnEsc(open)
type="ghost" type="ghost"
@click="showSystemField = !showSystemField" @click="showSystemField = !showSystemField"
> >
{{ showSystemField ? 'Hide system fields' : 'Show system fields' }} {{ showSystemField ? $t('title.hideSystemFields') : $t('activity.showSystemFields') }}
</NcButton> </NcButton>
</div> </div>
</div> </div>
@ -499,4 +637,29 @@ useMenuCloseOnEsc(open)
.nc-fields-show-system-fields { .nc-fields-show-system-fields {
@apply !text-xs !w-1/2 !text-gray-500 !border-none bg-gray-100 hover:(!text-gray-600 bg-gray-200); @apply !text-xs !w-1/2 !text-gray-500 !border-none bg-gray-100 hover:(!text-gray-600 bg-gray-200);
} }
.nc-cover-image-object-fit-dropdown-menu {
:deep(.nc-menu-item-inner) {
@apply !w-full flex items-center justify-between;
}
}
.nc-dropdown-cover-image-wrapper {
@apply h-8;
&:not(:focus-within) {
@apply shadow-default hover:shadow-hover;
}
&:focus-within {
@apply shadow-selected border-brand-500;
}
}
:deep(.ant-input-affix-wrapper) {
&:not(.ant-input-affix-wrapper-disabled):not(.ant-input-affix-wrapper-focused):not(:focus) {
@apply shadow-default hover:(shadow-hover border-gray-200);
}
&.ant-input-affix-wrapper-focused,
&:focus {
@apply border-brand-500 shadow-selected;
}
}
</style> </style>

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

@ -6,8 +6,6 @@ const maxNumberOfAllowedCharsForQrValue = 2000
const cellValue = inject(CellValueInj) const cellValue = inject(CellValueInj)
const isGallery = inject(IsGalleryInj, ref(false))
const qrValue = computed(() => String(cellValue?.value || '')) const qrValue = computed(() => String(cellValue?.value || ''))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false)) const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
@ -39,7 +37,6 @@ const qrCodeLarge = useQRCode(qrValue, {
const modalVisible = ref(false) const modalVisible = ref(false)
const showQrModal = (ev: MouseEvent) => { const showQrModal = (ev: MouseEvent) => {
if (isGallery.value) return
ev.stopPropagation() ev.stopPropagation()
modalVisible.value = true modalVisible.value = true
} }

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

@ -42,7 +42,6 @@ const downloadSvg = () => {
} }
const onBarcodeClick = (ev: MouseEvent) => { const onBarcodeClick = (ev: MouseEvent) => {
if (isGallery.value) return
ev.stopPropagation() ev.stopPropagation()
emit('onClickBarcode') emit('onClickBarcode')
} }

9
packages/nc-gui/components/virtual-cell/components/ListItem.vue

@ -41,13 +41,6 @@ interface Attachment {
mimetype: string mimetype: string
} }
const isRowEmpty = (row: any, col: any) => {
const val = row[col.title]
if (!val) return true
return Array.isArray(val) && val.length === 0
}
const attachments: ComputedRef<Attachment[]> = computed(() => { const attachments: ComputedRef<Attachment[]> = computed(() => {
try { try {
if (props.attachment && row.value[props.attachment.title]) { if (props.attachment && row.value[props.attachment.title]) {
@ -124,7 +117,7 @@ const displayValue = computed(() => {
class="flex ml-[-0.25rem] sm:flex-row xs:(flex-col mt-2) gap-4 min-h-5" class="flex ml-[-0.25rem] sm:flex-row xs:(flex-col mt-2) gap-4 min-h-5"
> >
<div v-for="field in fields" :key="field.id" class="sm:(w-1/3 max-w-1/3 overflow-hidden)"> <div v-for="field in fields" :key="field.id" class="sm:(w-1/3 max-w-1/3 overflow-hidden)">
<div v-if="!isRowEmpty(row, field)" class="flex flex-col gap-[-1]"> <div v-if="!isRowEmpty({ row }, field)" class="flex flex-col gap-[-1]">
<NcTooltip class="z-10 flex" placement="bottomLeft" :arrow-point-at-center="false"> <NcTooltip class="z-10 flex" placement="bottomLeft" :arrow-point-at-center="false">
<template #title> <template #title>
<LazySmartsheetHeaderVirtualCell <LazySmartsheetHeaderVirtualCell

45
packages/nc-gui/composables/useViewColumns.ts

@ -113,10 +113,30 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
const showAll = async (ignoreIds?: any) => { const showAll = async (ignoreIds?: any) => {
if (isLocalMode.value) { if (isLocalMode.value) {
const fieldById = (fields.value || []).reduce<Record<string, any>>((acc, curr) => {
if (curr.fk_column_id) {
curr.show = true
acc[curr.fk_column_id] = curr
}
return acc
}, {})
fields.value = fields.value?.map((field: Field) => ({ fields.value = fields.value?.map((field: Field) => ({
...field, ...field,
show: true, show: fieldById[field.fk_column_id!]?.show,
})) }))
meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => {
if (fieldById[column.id!]) {
return {
...column,
...fieldById[column.id!],
id: fieldById[column.id!].fk_column_id,
}
}
return column
})
reloadData?.() reloadData?.()
return return
} }
@ -137,10 +157,30 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
} }
const hideAll = async (ignoreIds?: any) => { const hideAll = async (ignoreIds?: any) => {
if (isLocalMode.value) { if (isLocalMode.value) {
const fieldById = (fields.value || []).reduce<Record<string, any>>((acc, curr) => {
if (curr.fk_column_id) {
curr.show = !!curr.isViewEssentialField
acc[curr.fk_column_id] = curr
}
return acc
}, {})
fields.value = fields.value?.map((field: Field) => ({ fields.value = fields.value?.map((field: Field) => ({
...field, ...field,
show: !!field.isViewEssentialField, show: fieldById[field.fk_column_id!]?.show,
})) }))
meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => {
if (fieldById[column.id!]) {
return {
...column,
...fieldById[column.id!],
id: fieldById[column.id!].fk_column_id,
}
}
return column
})
reloadData?.() reloadData?.()
return return
} }
@ -268,6 +308,7 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
const toggleFieldVisibility = (checked: boolean, field: any) => { const toggleFieldVisibility = (checked: boolean, field: any) => {
const fieldIndex = fields.value?.findIndex((f) => f.fk_column_id === field.fk_column_id) const fieldIndex = fields.value?.findIndex((f) => f.fk_column_id === field.fk_column_id)
if (!fieldIndex && fieldIndex !== 0) return if (!fieldIndex && fieldIndex !== 0) return
addUndo({ addUndo({
undo: { undo: {

5
packages/nc-gui/lang/en.json

@ -777,7 +777,10 @@
"displayAsProgress": "Display as progress", "displayAsProgress": "Display as progress",
"relationType": "Relation type", "relationType": "Relation type",
"showThousandsSeparator": "Show thousands separator", "showThousandsSeparator": "Show thousands separator",
"signUpForFree": "Sign up for free" "signUpForFree": "Sign up for free",
"coverImageField": "Cover image field",
"fitImage": "Fit image",
"coverImageArea": "Cover image"
}, },
"activity": { "activity": {
"renameBase": "Rename Base", "renameBase": "Rename Base",

5
packages/nc-gui/lib/enums.ts

@ -163,3 +163,8 @@ export enum RichTextBubbleMenuOptions {
taskList = 'taskList', taskList = 'taskList',
link = 'link', link = 'link',
} }
export enum CoverImageObjectFit {
FIT = 'fit',
COVER = 'cover',
}

3
packages/nc-gui/utils/dataUtils.ts

@ -119,8 +119,9 @@ export const rowDefaultData = (columns: ColumnType[] = []) => {
export const isRowEmpty = (record: any, col: any) => { export const isRowEmpty = (record: any, col: any) => {
if (!record || !col) return true if (!record || !col) return true
const val = record.row[col.title] const val = record.row[col.title]
if (!val) return true if (val === null || val === undefined || val === '') return true
return Array.isArray(val) && val.length === 0 return Array.isArray(val) && val.length === 0
} }

4
packages/nc-gui/utils/formValidations.ts

@ -141,8 +141,6 @@ export const extractFieldValidator = (_validators: Validation[], element: Column
return rules return rules
} }
export const getValidFieldName = (title: string, uniqueFieldNames: Set<string>) => { export const getValidFieldName = (title: string, uniqueFieldNames: Set<string>) => {
title = title.replace(/\./g, '_') title = title.replace(/\./g, '_')
let counter = 1 let counter = 1
@ -154,4 +152,4 @@ export const getValidFieldName = (title: string, uniqueFieldNames: Set<string>)
} }
uniqueFieldNames.add(newTitle) uniqueFieldNames.add(newTitle)
return newTitle return newTitle
} }

2
packages/nc-gui/windi.config.ts

@ -112,6 +112,8 @@ export default defineConfig({
accent: 'rgba(var(--color-accent), var(--tw-ring-opacity))', accent: 'rgba(var(--color-accent), var(--tw-ring-opacity))',
}, },
boxShadow: { boxShadow: {
default: '0px 0px 4px 0px rgba(0, 0, 0, 0.08)',
hover: '0px 0px 4px 0px rgba(0, 0, 0, 0.24)',
selected: '0px 0px 0px 2px var(--ant-primary-color-outline)', selected: '0px 0px 0px 2px var(--ant-primary-color-outline)',
}, },
colors: { colors: {

15
packages/nocodb/src/models/GalleryView.ts

@ -11,7 +11,12 @@ import Noco from '~/Noco';
import NocoCache from '~/cache/NocoCache'; import NocoCache from '~/cache/NocoCache';
import { extractProps } from '~/helpers/extractProps'; import { extractProps } from '~/helpers/extractProps';
import { CacheGetType, CacheScope, MetaTable } from '~/utils/globals'; import { CacheGetType, CacheScope, MetaTable } from '~/utils/globals';
import { prepareForDb, prepareForResponse } from '~/utils/modelUtils'; import {
parseMetaProp,
prepareForDb,
prepareForResponse,
stringifyMetaProp,
} from '~/utils/modelUtils';
export default class GalleryView implements GalleryType { export default class GalleryView implements GalleryType {
fk_view_id?: string; fk_view_id?: string;
@ -86,12 +91,20 @@ export default class GalleryView implements GalleryType {
'restrict_types', 'restrict_types',
'restrict_size', 'restrict_size',
'restrict_number', 'restrict_number',
'meta',
]); ]);
insertObj.fk_cover_image_col_id = insertObj.fk_cover_image_col_id =
view?.fk_cover_image_col_id || view?.fk_cover_image_col_id ||
columns?.find((c) => c.uidt === UITypes.Attachment)?.id; columns?.find((c) => c.uidt === UITypes.Attachment)?.id;
insertObj.meta = {
fk_cover_image_object_fit:
parseMetaProp(insertObj)?.fk_cover_image_object_fit || 'fit',
};
insertObj.meta = stringifyMetaProp(insertObj);
const viewRef = await View.get(context, insertObj.fk_view_id, ncMeta); const viewRef = await View.get(context, insertObj.fk_view_id, ncMeta);
if (!insertObj.source_id) { if (!insertObj.source_id) {

14
packages/nocodb/src/models/KanbanView.ts

@ -6,7 +6,12 @@ import Noco from '~/Noco';
import NocoCache from '~/cache/NocoCache'; import NocoCache from '~/cache/NocoCache';
import { extractProps } from '~/helpers/extractProps'; import { extractProps } from '~/helpers/extractProps';
import { CacheGetType, CacheScope, MetaTable } from '~/utils/globals'; import { CacheGetType, CacheScope, MetaTable } from '~/utils/globals';
import { prepareForDb, prepareForResponse } from '~/utils/modelUtils'; import {
parseMetaProp,
prepareForDb,
prepareForResponse,
stringifyMetaProp,
} from '~/utils/modelUtils';
export default class KanbanView implements KanbanType { export default class KanbanView implements KanbanType {
fk_view_id: string; fk_view_id: string;
@ -102,6 +107,13 @@ export default class KanbanView implements KanbanType {
view?.fk_cover_image_col_id || view?.fk_cover_image_col_id ||
columns?.find((c) => c.uidt === UITypes.Attachment)?.id; columns?.find((c) => c.uidt === UITypes.Attachment)?.id;
insertObj.meta = {
fk_cover_image_object_fit:
parseMetaProp(insertObj)?.fk_cover_image_object_fit || 'cover',
};
insertObj.meta = stringifyMetaProp(insertObj);
const viewRef = await View.get(context, insertObj.fk_view_id, ncMeta); const viewRef = await View.get(context, insertObj.fk_view_id, ncMeta);
if (!insertObj.source_id) { if (!insertObj.source_id) {

18
packages/nocodb/src/models/View.ts

@ -1841,9 +1841,9 @@ export default class View implements ViewType {
if (view.type === ViewTypes.GALLERY) { if (view.type === ViewTypes.GALLERY) {
const galleryView = await GalleryView.get(context, view.id, ncMeta); const galleryView = await GalleryView.get(context, view.id, ncMeta);
if ( if (
column.id === galleryView.fk_cover_image_col_id || (column.id === galleryView.fk_cover_image_col_id && column.pv) ||
column.pv || (column.id !== galleryView.fk_cover_image_col_id &&
galleryShowLimit < 3 (column.pv || galleryShowLimit < 3))
) { ) {
show = true; show = true;
galleryShowLimit++; galleryShowLimit++;
@ -1856,13 +1856,17 @@ export default class View implements ViewType {
// include grouping field if it exists // include grouping field if it exists
show = true; show = true;
} else if ( } else if (
column.id === kanbanView.fk_cover_image_col_id || (column.id === kanbanView.fk_cover_image_col_id && column.pv) ||
column.pv (column.id !== kanbanView.fk_cover_image_col_id && column.pv)
) { ) {
// Show cover image or primary key // Show primary key and cover image if it is a primary value; otherwise, it will be redundant.
show = true; show = true;
kanbanShowLimit++; kanbanShowLimit++;
} else if (kanbanShowLimit < 3 && !isSystemColumn(column)) { } else if (
column.id !== kanbanView.fk_cover_image_col_id &&
kanbanShowLimit < 3 &&
!isSystemColumn(column)
) {
// show at most 3 non-system columns // show at most 3 non-system columns
show = true; show = true;
kanbanShowLimit++; kanbanShowLimit++;

4
tests/playwright/pages/Dashboard/Gallery/index.ts

@ -26,8 +26,8 @@ export class GalleryPage extends BasePage {
async openExpandedRow({ index }: { index: number }) { async openExpandedRow({ index }: { index: number }) {
await this.card(index).click({ await this.card(index).click({
position: { position: {
x: 1, x: 5,
y: 1, y: 5,
}, },
}); });
} }

Loading…
Cancel
Save