Browse Source

Merge pull request #4840 from nocodb/feat/row-height

feat: row height for grid view
pull/4925/head
Raju Udava 2 years ago committed by GitHub
parent
commit
63cac57ba2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      packages/nc-gui/assets/nc-icons/row-height-extra-tall.svg
  2. 4
      packages/nc-gui/assets/nc-icons/row-height-medium.svg
  3. 4
      packages/nc-gui/assets/nc-icons/row-height-short.svg
  4. 4
      packages/nc-gui/assets/nc-icons/row-height-tall.svg
  5. 5
      packages/nc-gui/components.d.ts
  6. 24
      packages/nc-gui/components/cell/ClampedText.vue
  7. 6
      packages/nc-gui/components/cell/MultiSelect.vue
  8. 2
      packages/nc-gui/components/cell/Text.vue
  9. 24
      packages/nc-gui/components/cell/TextArea.vue
  10. 24
      packages/nc-gui/components/smartsheet/Cell.vue
  11. 27
      packages/nc-gui/components/smartsheet/Grid.vue
  12. 2
      packages/nc-gui/components/smartsheet/Toolbar.vue
  13. 87
      packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue
  14. 2
      packages/nc-gui/components/virtual-cell/HasMany.vue
  15. 2
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  16. 23
      packages/nc-gui/components/virtual-cell/QrCode.vue
  17. 23
      packages/nc-gui/components/virtual-cell/barcode/Barcode.vue
  18. 12
      packages/nc-gui/components/virtual-cell/barcode/JsBarcodeWrapper.vue
  19. 5
      packages/nc-gui/nuxt.config.ts
  20. 35
      packages/nc-gui/package-lock.json
  21. 1
      packages/nc-gui/package.json
  22. 6
      packages/nc-gui/plugins/clamp.ts
  23. 19
      packages/nocodb-sdk/src/lib/Api.ts
  24. 11
      packages/nocodb/src/lib/meta/api/gridViewApis.ts
  25. 4
      packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
  26. 16
      packages/nocodb/src/lib/migrations/v2/nc_025_add_row_height.ts
  27. 41
      packages/nocodb/src/lib/models/GridView.ts
  28. 1
      packages/nocodb/src/lib/models/View.ts
  29. 41
      scripts/sdk/swagger.json
  30. 26
      tests/playwright/pages/Dashboard/Grid/Row.ts
  31. 3
      tests/playwright/pages/Dashboard/Grid/index.ts
  32. 19
      tests/playwright/pages/Dashboard/common/Toolbar/RowHeight.ts
  33. 8
      tests/playwright/pages/Dashboard/common/Toolbar/index.ts
  34. 25
      tests/playwright/tests/toolbarOperations.spec.ts

4
packages/nc-gui/assets/nc-icons/row-height-extra-tall.svg

@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6H12.75M3 6V5H12.75V6M3 6V7M12.75 6V7M3 10.5H12.75M3 10.5V9.625M3 10.5V11.625M12.75 10.5V9.625M12.75 10.5V11.625M3 15H12.75M3 15V14M3 15V16.125M12.75 15V13.875M12.75 15V16.125M3 7H12.75M3 7V7.875M12.75 7V7.875M12.75 8.75H3M12.75 8.75V7.875M12.75 8.75V9.625M3 8.75V7.875M3 8.75V9.625M3 7.875H12.75M12.75 9.625H3M12.75 12.75H3M12.75 12.75V11.625M12.75 12.75V13.875M3 12.75V11.625M3 12.75V14M3 11.625H12.75M12.75 13.875L3 14M12.75 17.25H3M12.75 17.25V16.125M12.75 17.25V18.375M3 17.25V16.125M3 17.25V18.375M3 16.125H12.75M12.75 18.375V19.5H3V18.375M12.75 18.375H3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5 5V19.5M18.5 5L15.5 8M18.5 5L21.5 8M18.5 19.5L15.5 16.5M18.5 19.5L21.5 16.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 925 B

4
packages/nc-gui/assets/nc-icons/row-height-medium.svg

@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6H12.75M3 6V5H12.75V6M3 6V7M12.75 6V7M3 15H12.75M3 19.5H12.75M3 7H12.75M3 7V7.875M12.75 7V7.875M12.75 8.75H3M12.75 8.75V7.875M12.75 8.75V9.625M3 8.75V7.875M3 8.75V9.625M3 7.875H12.75M12.75 9.625V10.5H3V9.625M12.75 9.625H3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5 5V19.5M18.5 5L15.5 8M18.5 5L21.5 8M18.5 19.5L15.5 16.5M18.5 19.5L21.5 16.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 586 B

4
packages/nc-gui/assets/nc-icons/row-height-short.svg

@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6H12.75M3 6V5H12.75V6M3 6V7H12.75V6M3 10.5H12.75M3 15H12.75M3 19.5H12.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5 5V19.5M18.5 5L15.5 8M18.5 5L21.5 8M18.5 19.5L15.5 16.5M18.5 19.5L21.5 16.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 437 B

4
packages/nc-gui/assets/nc-icons/row-height-tall.svg

@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6H12.75M3 6V5H12.75V6M3 6V7M12.75 6V7M3 10.5H12.75M3 10.5V9.625M3 10.5V11.625M12.75 10.5V9.625M12.75 10.5V11.625M3 19.5H12.75M3 7H12.75M3 7V7.875M12.75 7V7.875M12.75 8.75H3M12.75 8.75V7.875M12.75 8.75V9.625M3 8.75V7.875M3 8.75V9.625M3 7.875H12.75M12.75 9.625H3M12.75 12.75H3M12.75 12.75V11.625M12.75 12.75V13.875M3 12.75V11.625M3 12.75V14M3 11.625H12.75M12.75 13.875V15H3V14M12.75 13.875L3 14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5 5V19.5M18.5 5L15.5 8M18.5 5L21.5 8M18.5 19.5L15.5 16.5M18.5 19.5L21.5 16.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 757 B

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

@ -244,8 +244,13 @@ declare module '@vue/runtime-core' {
MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default']
MdiXml: typeof import('~icons/mdi/xml')['default']
MiCircleWarning: typeof import('~icons/mi/circle-warning')['default']
NcIconsRowHeightExtraTall: typeof import('~icons/nc-icons/row-height-extra-tall')['default']
NcIconsRowHeightMedium: typeof import('~icons/nc-icons/row-height-medium')['default']
NcIconsRowHeightShort: typeof import('~icons/nc-icons/row-height-short')['default']
NcIconsRowHeightTall: typeof import('~icons/nc-icons/row-height-tall')['default']
PhCloudLightningDuotone: typeof import('~icons/ph/cloud-lightning-duotone')['default']
PhFileCsv: typeof import('~icons/ph/file-csv')['default']
RiLineHeight: typeof import('~icons/ri/line-height')['default']
RiTeamFill: typeof import('~icons/ri/team-fill')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

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

@ -0,0 +1,24 @@
<script setup lang="ts">
const props = defineProps<{
value?: string | number | null
lines?: number
}>()
const wrapper = ref()
const key = ref(0)
onMounted(() => {
const observer = new ResizeObserver(() => {
key.value++
})
observer.observe(wrapper.value)
})
</script>
<template>
<div ref="wrapper">
<text-clamp :key="key" class="w-full h-full break-all" :text="props.value || ''" :max-lines="props.lines" />
</div>
</template>

6
packages/nc-gui/components/cell/MultiSelect.vue

@ -281,7 +281,7 @@ const onTagClick = (e: Event, onClose: Function) => {
v-model:value="vModel"
v-model:open="isOpen"
mode="multiple"
class="w-full"
class="w-full overflow-hidden"
:bordered="false"
clear-icon
show-search
@ -402,4 +402,8 @@ const onTagClick = (e: Event, onClose: Function) => {
:deep(.ant-select-selection-overflow-item) {
@apply "flex overflow-hidden";
}
:deep(.ant-select-selection-overflow) {
@apply flex-nowrap;
}
</style>

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

@ -38,5 +38,5 @@ const focus: VNodeRef = (el) => {
@mousedown.stop
/>
<span v-else>{{ vModel }}</span>
<LazyCellClampedText v-else :value="vModel" :lines="1" />
</template>

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

@ -1,6 +1,7 @@
<script setup lang="ts">
import type { GridType } from 'nocodb-sdk'
import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, inject, useVModel } from '#imports'
import { ActiveViewInj, EditModeInj, inject, useVModel } from '#imports'
const props = defineProps<{
modelValue?: string | number
@ -10,9 +11,28 @@ const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj)
const view = inject(ActiveViewInj, ref())
const vModel = useVModel(props, 'modelValue', emits, { defaultValue: '' })
const focus: VNodeRef = (el) => (el as HTMLTextAreaElement)?.focus()
const rowHeight = computed(() => {
if ((view.value?.view as GridType)?.row_height !== undefined) {
switch ((view.value?.view as GridType)?.row_height) {
case 0:
return 1
case 1:
return 2
case 2:
return 4
case 3:
return 6
default:
return 1
}
}
})
</script>
<template>
@ -35,5 +55,7 @@ const focus: VNodeRef = (el) => (el as HTMLTextAreaElement)?.focus()
@mousedown.stop
/>
<LazyCellClampedText v-else-if="rowHeight" :value="vModel" :lines="rowHeight" />
<span v-else>{{ vModel }}</span>
</template>

24
packages/nc-gui/components/smartsheet/Cell.vue

@ -1,8 +1,9 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import type { ColumnType, GridType } from 'nocodb-sdk'
import { isSystemColumn } from 'nocodb-sdk'
import {
ActiveCellInj,
ActiveViewInj,
ColumnInj,
EditModeInj,
IsFormInj,
@ -60,6 +61,8 @@ const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'save', 'navigate', 'update:editEnabled'])
const view = inject(ActiveViewInj, ref())
const column = toRef(props, 'column')
const active = toRef(props, 'active', false)
@ -124,6 +127,23 @@ const syncAndNavigate = (dir: NavigateDir, e: KeyboardEvent) => {
if (!isForm.value) e.stopImmediatePropagation()
}
const rowHeight = computed(() => {
if ((view.value?.view as GridType)?.row_height !== undefined) {
switch ((view.value?.view as GridType)?.row_height) {
case 0:
return 1
case 1:
return 2
case 2:
return 4
case 3:
return 6
default:
return 1
}
}
})
</script>
<template>
@ -132,6 +152,8 @@ const syncAndNavigate = (dir: NavigateDir, e: KeyboardEvent) => {
:class="[
`nc-cell-${(column?.uidt || 'default').toLowerCase()}`,
{ 'text-blue-600': isPrimary(column) && !props.virtual && !isForm },
{ 'm-y-auto !h-auto': !rowHeight || rowHeight === 1 },
{ '!h-full': rowHeight && rowHeight !== 1 },
]"
@keydown.enter.exact="syncAndNavigate(NavigateDir.NEXT, $event)"
@keydown.shift.enter.exact="syncAndNavigate(NavigateDir.PREV, $event)"

27
packages/nc-gui/components/smartsheet/Grid.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { ColumnReqType, ColumnType, TableType, ViewType } from 'nocodb-sdk'
import type { ColumnReqType, ColumnType, GridType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import {
ActiveViewInj,
@ -660,6 +660,23 @@ const closeAddColumnDropdown = () => {
columnOrder.value = null
addColumnDropdown.value = false
}
const rowHeight = computed(() => {
if ((view.value?.view as GridType)?.row_height !== undefined) {
switch ((view.value?.view as GridType)?.row_height) {
case 0:
return 1
case 1:
return 2
case 2:
return 4
case 3:
return 6
default:
return 1
}
}
})
</script>
<template>
@ -749,7 +766,11 @@ const closeAddColumnDropdown = () => {
<tbody ref="tbodyEl">
<LazySmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row">
<template #default="{ state }">
<tr class="nc-grid-row" :data-testid="`grid-row-${rowIndex}`">
<tr
class="nc-grid-row"
:style="{ height: rowHeight ? `${rowHeight * 1.5}rem` : `1.5rem` }"
:data-testid="`grid-row-${rowIndex}`"
>
<td key="row-index" class="caption nc-grid-cell pl-5 pr-1" :data-testid="`cell-Id-${rowIndex}`">
<div class="items-center flex gap-1 min-w-[55px]">
<div
@ -961,7 +982,7 @@ const closeAddColumnDropdown = () => {
td:not(:first-child) > div {
overflow: hidden;
@apply flex items-center h-auto px-1;
@apply flex px-1;
}
table,

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

@ -35,6 +35,8 @@ const { allowCSVDownload } = useSharedView()
<LazySmartsheetToolbarSortListMenu v-if="isGrid || isGallery || isKanban" />
<LazySmartsheetToolbarRowHeight v-if="isGrid" />
<LazySmartsheetToolbarShareView v-if="(isForm || isGrid || isKanban || isGallery) && !isPublic" />
<LazySmartsheetToolbarExport v-if="(!isPublic && !isUIAllowed('dataInsert')) || (isPublic && allowCSVDownload)" />

87
packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue

@ -0,0 +1,87 @@
<script setup lang="ts">
import type { GridType } from 'nocodb-sdk'
import { ActiveViewInj, IsLockedInj, inject, ref, useMenuCloseOnEsc } from '#imports'
const { isSharedBase } = useProject()
const view = inject(ActiveViewInj, ref())
const isPublic = inject(IsPublicInj, ref(false))
const isLocked = inject(IsLockedInj, ref(false))
const { $api } = useNuxtApp()
const open = ref(false)
const updateRowHeight = async (rh: number) => {
if (view.value?.id) {
if (rh === (view.value.view as GridType).row_height) return
try {
if (!isPublic.value && !isSharedBase.value) {
await $api.dbView.gridUpdate(view.value.id, {
row_height: rh,
})
message.success('View updated successfully!')
}
;(view.value.view as GridType).row_height = rh
open.value = false
} catch (e) {
message.error('There was an error while updating view!')
}
}
}
useMenuCloseOnEsc(open)
</script>
<template>
<a-dropdown v-model:visible="open" offset-y class="" :trigger="['click']" overlay-class-name="nc-dropdown-height-menu">
<div>
<a-button v-e="['c:row-height']" class="nc-height-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-1">
<RiLineHeight />
<!-- Row Height -->
<MdiMenuDown class="text-grey" />
</div>
</a-button>
</div>
<template #overlay>
<div class="w-full bg-gray-50 shadow-lg menu-filter-dropdown !border" data-testid="nc-height-menu">
<div class="text-gray-500 !text-xs px-4 py-2">Select a row height</div>
<div class="flex flex-col w-full text-sm" @click.stop>
<div class="nc-row-height-option" @click="updateRowHeight(0)">
<NcIconsRowHeightShort class="nc-row-height-icon" />
Short
</div>
<div class="nc-row-height-option" @click="updateRowHeight(1)">
<NcIconsRowHeightMedium class="nc-row-height-icon" />
Medium
</div>
<div class="nc-row-height-option" @click="updateRowHeight(2)">
<NcIconsRowHeightTall class="nc-row-height-icon" />
Tall
</div>
<div class="nc-row-height-option" @click="updateRowHeight(3)">
<NcIconsRowHeightExtraTall class="nc-row-height-icon" />
Extra
</div>
</div>
</div>
</template>
</a-dropdown>
</template>
<style scoped>
.nc-row-height-option {
@apply flex items-center py-1 px-2 justify-start hover:bg-gray-200 cursor-pointer;
}
.nc-row-height-icon {
@apply text-gray-600 mx-4 text-base;
}
</style>

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

@ -94,7 +94,7 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</script>
<template>
<div class="flex items-center items-center gap-1 w-full chips-wrapper">
<div class="flex items-center gap-1 w-full chips-wrapper">
<template v-if="!isForm">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">

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

@ -96,7 +96,7 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</script>
<template>
<div class="flex items-center gap-1 w-full h-full chips-wrapper">
<div class="flex items-center gap-1 w-full chips-wrapper">
<template v-if="!isForm">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">

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

@ -1,8 +1,12 @@
<script setup lang="ts">
import { useQRCode } from '@vueuse/integrations/useQRCode'
import { GridType } from 'nocodb-sdk'
import { ActiveViewInj } from '#imports'
const maxNumberOfAllowedCharsForQrValue = 2000
const view = inject(ActiveViewInj, ref())
const cellValue = inject(CellValueInj)
const qrValue = computed(() => String(cellValue?.value))
@ -11,6 +15,23 @@ const tooManyCharsForQrCode = computed(() => qrValue?.value.length > maxNumberOf
const showQrCode = computed(() => qrValue?.value?.length > 0 && !tooManyCharsForQrCode.value)
const rowHeight = computed(() => {
if ((view.value?.view as GridType)?.row_height !== undefined) {
switch ((view.value?.view as GridType)?.row_height) {
case 0:
return 1
case 1:
return 2
case 2:
return 4
case 3:
return 6
default:
return 1
}
}
})
const qrCode = useQRCode(qrValue, {
width: 150,
})
@ -47,7 +68,7 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = us
<div v-if="tooManyCharsForQrCode" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('labels.qrCodeValueTooLong') }}
</div>
<img v-if="showQrCode" :src="qrCode" alt="QR Code" @click="showQrModal" />
<img v-if="showQrCode" class="mx-auto" :style="{ height: rowHeight ? `${rowHeight * 1.4}rem` : `1.4rem` }" :src="qrCode" alt="QR Code" @click="showQrModal" />
<div v-if="showEditNonEditableFieldWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('msg.warning.nonEditableFields.computedFieldUnableToClear') }}
</div>

23
packages/nc-gui/components/virtual-cell/barcode/Barcode.vue

@ -1,8 +1,13 @@
<script setup lang="ts">
import JsBarcodeWrapper from './JsBarcodeWrapper.vue'
import { ComputedRef } from 'vue'
import { GridType } from 'nocodb-sdk'
import { ActiveViewInj } from '#imports'
const maxNumberOfAllowedCharsForBarcodeValue = 100
const view = inject(ActiveViewInj, ref())
const cellValue = inject(CellValueInj)
const column = inject(ColumnInj)
@ -29,6 +34,23 @@ const handleModalOkClick = () => (modalVisible.value = false)
const showBarcode = computed(() => barcodeValue?.value.length > 0 && !tooManyCharsForBarcode.value)
const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = useShowNotEditableWarning()
const rowHeight = computed(() => {
if ((view.value?.view as GridType)?.row_height !== undefined) {
switch ((view.value?.view as GridType)?.row_height) {
case 0:
return 1
case 1:
return 2
case 2:
return 4
case 3:
return 6
default:
return 1
}
}
})
</script>
<template>
@ -46,6 +68,7 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = us
v-if="showBarcode"
:barcode-value="barcodeValue"
:barcode-format="barcodeMeta.barcodeFormat"
:custom-style="{ height: rowHeight ? `${rowHeight * 1.4}rem` : `1.4rem` }"
@on-click-barcode="showBarcodeModal"
>
<template #barcodeRenderError>

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

@ -5,11 +5,12 @@ import { onMounted } from '#imports'
const props = defineProps({
barcodeValue: { type: String, required: true },
barcodeFormat: { type: String, required: true },
customStyle: { type: Object, required: false },
})
const emit = defineEmits(['onClickBarcode'])
const barcodeSvgRef = ref(null)
const barcodeSvgRef = ref<HTMLElement>()
const errorForCurrentInput = ref(false)
const generate = () => {
@ -17,6 +18,13 @@ const generate = () => {
JsBarcode(barcodeSvgRef.value, String(props.barcodeValue), {
format: props.barcodeFormat,
})
if (props.customStyle) {
if (barcodeSvgRef.value) {
for (const key in props.customStyle) {
barcodeSvgRef.value.style.setProperty(key, props.customStyle[key])
}
}
}
errorForCurrentInput.value = false
} catch (e) {
console.log('e', e)
@ -29,7 +37,7 @@ const onBarcodeClick = (ev: MouseEvent) => {
emit('onClickBarcode')
}
watch([() => props.barcodeValue, () => props.barcodeFormat], generate)
watch([() => props.barcodeValue, () => props.barcodeFormat, () => props.customStyle], generate)
onMounted(generate)
</script>

5
packages/nc-gui/nuxt.config.ts

@ -6,6 +6,7 @@ import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import monacoEditorPlugin from 'vite-plugin-monaco-editor'
import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfill'
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
import PurgeIcons from 'vite-plugin-purge-icons'
@ -107,6 +108,9 @@ export default defineNuxtConfig({
autoInstall: false,
compiler: 'vue3',
defaultClass: 'nc-icon',
customCollections: {
'nc-icons': FileSystemIconLoader('./assets/nc-icons', (svg) => svg.replace(/^<svg /, '<svg fill="currentColor" ')),
},
}),
Components({
resolvers: [
@ -133,6 +137,7 @@ export default defineNuxtConfig({
'system-uicons',
'vscode-icons',
'simple-icons',
'nc-icons',
],
}),
],

35
packages/nc-gui/package-lock.json generated

@ -41,6 +41,7 @@
"vue-dompurify-html": "^3.0.0",
"vue-github-button": "^3.0.3",
"vue-i18n": "^9.2.2",
"vue3-text-clamp": "^0.1.1",
"vuedraggable": "^4.1.0",
"xlsx": "^0.18.5"
},
@ -95,7 +96,7 @@
}
},
"../nocodb-sdk": {
"version": "0.101.1",
"version": "0.101.2",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -14208,6 +14209,11 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true
},
"node_modules/resize-detector": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/resize-detector/-/resize-detector-0.3.0.tgz",
"integrity": "sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ=="
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
@ -17156,6 +17162,19 @@
"vue": "^3.0.0"
}
},
"node_modules/vue3-text-clamp": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/vue3-text-clamp/-/vue3-text-clamp-0.1.1.tgz",
"integrity": "sha512-l/30RvXLkw50axAjswAK1DmvbUc5Oyhq9GkvD98p8pykrLkIajRi3evVsMnahMBK0O7+EGIK9RbIOKPyRfuw7w==",
"dependencies": {
"resize-detector": "^0.3.0",
"vue": "^3.2.37"
},
"peerDependencies": {
"resize-detector": "^0.3.0",
"vue": "^3.2.37"
}
},
"node_modules/vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
@ -28072,6 +28091,11 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true
},
"resize-detector": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/resize-detector/-/resize-detector-0.3.0.tgz",
"integrity": "sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ=="
},
"resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
@ -30124,6 +30148,15 @@
"is-plain-object": "3.0.1"
}
},
"vue3-text-clamp": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/vue3-text-clamp/-/vue3-text-clamp-0.1.1.tgz",
"integrity": "sha512-l/30RvXLkw50axAjswAK1DmvbUc5Oyhq9GkvD98p8pykrLkIajRi3evVsMnahMBK0O7+EGIK9RbIOKPyRfuw7w==",
"requires": {
"resize-detector": "^0.3.0",
"vue": "^3.2.37"
}
},
"vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",

1
packages/nc-gui/package.json

@ -64,6 +64,7 @@
"vue-dompurify-html": "^3.0.0",
"vue-github-button": "^3.0.3",
"vue-i18n": "^9.2.2",
"vue3-text-clamp": "^0.1.1",
"vuedraggable": "^4.1.0",
"xlsx": "^0.18.5"
},

6
packages/nc-gui/plugins/clamp.ts

@ -0,0 +1,6 @@
import TextClamp from 'vue3-text-clamp'
import { defineNuxtPlugin } from 'nuxt/app'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(TextClamp)
})

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

@ -340,6 +340,7 @@ export interface GridType {
deleted?: boolean;
order?: number;
lock_type?: 'collaborative' | 'locked' | 'personal';
row_height?: number;
}
export interface GalleryType {
@ -2499,6 +2500,24 @@ export class Api<
...params,
}),
/**
* No description
*
* @tags DB view
* @name GridUpdate
* @request PATCH:/api/v1/db/meta/grids/{viewId}
* @response `200` `any` OK
*/
gridUpdate: (viewId: string, data: GridType, params: RequestParams = {}) =>
this.request<any, any>({
path: `/api/v1/db/meta/grids/${viewId}`,
method: 'PATCH',
body: data,
type: ContentType.Json,
format: 'json',
...params,
}),
/**
* No description
*

11
packages/nocodb/src/lib/meta/api/gridViewApis.ts

@ -12,6 +12,7 @@ import Project from '../../models/Project';
import View from '../../models/View';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { metaApiMetrics } from '../helpers/apiMetrics';
import GridView from '../../models/GridView';
// @ts-ignore
export async function gridViewCreate(req: Request<any, any>, res) {
@ -25,10 +26,20 @@ export async function gridViewCreate(req: Request<any, any>, res) {
res.json(view);
}
export async function gridViewUpdate(req, res) {
Tele.emit('evt', { evt_type: 'view:updated', type: 'grid' });
res.json(await GridView.update(req.params.viewId, req.body));
}
const router = Router({ mergeParams: true });
router.post(
'/api/v1/db/meta/tables/:tableId/grids/',
metaApiMetrics,
ncMetaAclMw(gridViewCreate, 'gridViewCreate')
);
router.patch(
'/api/v1/db/meta/grids/:viewId',
metaApiMetrics,
ncMetaAclMw(gridViewUpdate, 'gridViewUpdate')
);
export default router;

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

@ -12,6 +12,7 @@ import * as nc_021_add_fields_in_token from './v2/nc_021_add_fields_in_token';
import * as nc_022_qr_code_column_type from './v2/nc_022_qr_code_column_type';
import * as nc_023_multiple_source from './v2/nc_023_multiple_source';
import * as nc_024_barcode_column_type from './v2/nc_024_barcode_column_type';
import * as nc_025_add_row_height from './v2/nc_025_add_row_height';
// Create a custom migration source class
export default class XcMigrationSourcev2 {
@ -35,6 +36,7 @@ export default class XcMigrationSourcev2 {
'nc_022_qr_code_column_type',
'nc_023_multiple_source',
'nc_024_barcode_column_type',
'nc_025_add_row_height',
]);
}
@ -72,6 +74,8 @@ export default class XcMigrationSourcev2 {
return nc_023_multiple_source;
case 'nc_024_barcode_column_type':
return nc_024_barcode_column_type;
case 'nc_025_add_row_height':
return nc_025_add_row_height;
}
}
}

16
packages/nocodb/src/lib/migrations/v2/nc_025_add_row_height.ts

@ -0,0 +1,16 @@
import { Knex } from 'knex';
import { MetaTable } from '../../utils/globals';
const up = async (knex: Knex) => {
await knex.schema.alterTable(MetaTable.GRID_VIEW, (table) => {
table.integer('row_height');
});
};
const down = async (knex) => {
await knex.schema.alterTable(MetaTable.GRID_VIEW, (table) => {
table.dropColumns('row_height');
});
};
export { up, down };

41
packages/nocodb/src/lib/models/GridView.ts

@ -5,18 +5,15 @@ import View from './View';
import NocoCache from '../cache/NocoCache';
export default class GridView {
title: string;
show: boolean;
is_default: boolean;
order: number;
fk_view_id: string;
columns?: GridViewColumn[];
project_id?: string;
base_id?: string;
meta?: string;
row_height?: number;
columns?: GridViewColumn[];
constructor(data: GridView) {
Object.assign(this, data);
}
@ -47,6 +44,7 @@ export default class GridView {
fk_view_id: view.fk_view_id,
project_id: view.project_id,
base_id: view.base_id,
row_height: view.row_height,
};
if (!(view.project_id && view.base_id)) {
const viewRef = await View.get(view.fk_view_id, ncMeta);
@ -63,4 +61,31 @@ export default class GridView {
const view = await this.get(id, ncMeta);
return view;
}
static async update(
viewId: string,
body: Partial<GridView>,
ncMeta = Noco.ncMeta
) {
// get existing cache
const key = `${CacheScope.GRID_VIEW}:${viewId}`;
const o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
if (o) {
o.row_height = body.row_height;
// set cache
await NocoCache.set(key, o);
}
// update meta
return await ncMeta.metaUpdate(
null,
null,
MetaTable.GRID_VIEW,
{
row_height: body.row_height,
},
{
fk_view_id: viewId,
}
);
}
}

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

@ -299,6 +299,7 @@ export default class View implements ViewType {
case ViewTypes.GRID:
await GridView.insert(
{
...(copyFromView?.view as GridView || {}),
...(view as GridView),
fk_view_id: view_id,
},

41
scripts/sdk/swagger.json

@ -3024,6 +3024,44 @@
}
}
},
"/api/v1/db/meta/grids/{viewId}": {
"parameters": [
{
"schema": {
"type": "string"
},
"name": "viewId",
"in": "path",
"required": true
}
],
"patch": {
"summary": "",
"operationId": "db-view-grid-update",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {}
}
}
}
},
"tags": [
"DB view"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Grid"
}
}
}
}
}
},
"/api/v1/db/meta/grids/{gridId}/grid-columns": {
"parameters": [
{
@ -8508,6 +8546,9 @@
"locked",
"personal"
]
},
"row_height": {
"type": "number"
}
},
"description": ""

26
tests/playwright/pages/Dashboard/Grid/Row.ts

@ -0,0 +1,26 @@
import BasePage from '../../Base';
import { GridPage } from './index';
export class RowPageObject extends BasePage {
readonly grid: GridPage;
constructor(grid: GridPage) {
super(grid.rootPage);
this.grid = grid;
}
get() {
return this.rootPage.locator('tr.nc-grid-row');
}
async getRecord(index: number) {
return this.get().nth(index);
}
// style="height: 3rem;"
async getRecordHeight(index: number) {
const record = await this.getRecord(index);
const style = await record.getAttribute('style');
return style.split(':')[1].split(';')[0].trim();
}
}

3
tests/playwright/pages/Dashboard/Grid/index.ts

@ -7,6 +7,7 @@ import { ToolbarPage } from '../common/Toolbar';
import { ProjectMenuObject } from '../common/ProjectMenu';
import { QrCodeOverlay } from '../QrCodeOverlay';
import { BarcodeOverlay } from '../BarcodeOverlay';
import { RowPageObject } from './Row';
export class GridPage extends BasePage {
readonly dashboard: DashboardPage;
@ -18,6 +19,7 @@ export class GridPage extends BasePage {
readonly cell: CellPageObject;
readonly toolbar: ToolbarPage;
readonly projectMenu: ProjectMenuObject;
readonly rowPage: RowPageObject;
constructor(dashboardPage: DashboardPage) {
super(dashboardPage.rootPage);
@ -29,6 +31,7 @@ export class GridPage extends BasePage {
this.cell = new CellPageObject(this);
this.toolbar = new ToolbarPage(this);
this.projectMenu = new ProjectMenuObject(this);
this.rowPage = new RowPageObject(this);
}
get() {

19
tests/playwright/pages/Dashboard/common/Toolbar/RowHeight.ts

@ -0,0 +1,19 @@
import BasePage from '../../../Base';
import { ToolbarPage } from './index';
export class RowHeight extends BasePage {
readonly toolbar: ToolbarPage;
constructor(toolbar: ToolbarPage) {
super(toolbar.rootPage);
this.toolbar = toolbar;
}
get() {
return this.rootPage.locator(`[data-testid="nc-height-menu"]`);
}
click({ title }: { title: string }) {
return this.get().locator(`.nc-row-height-option:has-text("${title}")`).click();
}
}

8
tests/playwright/pages/Dashboard/common/Toolbar/index.ts

@ -14,6 +14,7 @@ import { FormPage } from '../../Form';
import { ToolbarStackbyPage } from './StackBy';
import { ToolbarAddEditStackPage } from './AddEditKanbanStack';
import { ToolbarSearchDataPage } from './SearchData';
import { RowHeight } from './RowHeight';
export class ToolbarPage extends BasePage {
readonly parent: GridPage | GalleryPage | FormPage | KanbanPage;
@ -26,6 +27,7 @@ export class ToolbarPage extends BasePage {
readonly stackBy: ToolbarStackbyPage;
readonly addEditStack: ToolbarAddEditStackPage;
readonly searchData: ToolbarSearchDataPage;
readonly rowHeight: RowHeight;
constructor(parent: GridPage | GalleryPage | FormPage | KanbanPage) {
super(parent.rootPage);
@ -39,6 +41,7 @@ export class ToolbarPage extends BasePage {
this.stackBy = new ToolbarStackbyPage(this);
this.addEditStack = new ToolbarAddEditStackPage(this);
this.searchData = new ToolbarSearchDataPage(this);
this.rowHeight = new RowHeight(this);
}
get() {
@ -136,6 +139,11 @@ export class ToolbarPage extends BasePage {
await expect(file).toEqual(expectedData);
}
async clickRowHeight() {
// ant-btn nc-height-menu-btn nc-toolbar-btn
await this.get().locator(`.nc-toolbar-btn.nc-height-menu-btn`).click();
}
async verifyStackByButton({ title }: { title: string }) {
await this.get().locator(`.nc-toolbar-btn.nc-kanban-stacked-by-menu-btn`).waitFor({ state: 'visible' });
await expect(

25
tests/playwright/tests/toolbarOperations.spec.ts

@ -1,4 +1,4 @@
import { test } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { DashboardPage } from '../pages/Dashboard';
import { ToolbarPage } from '../pages/Dashboard/common/Toolbar';
import setup from '../setup';
@ -73,4 +73,27 @@ test.describe('Toolbar operations (GRID)', () => {
await dashboard.closeTab({ title: 'Country' });
});
test('row height', async () => {
// define an array of row heights
const rowHeight = [
{ title: 'Short', height: '1.5rem' },
{ title: 'Medium', height: '3rem' },
{ title: 'Tall', height: '6rem' },
{ title: 'Extra', height: '9rem' },
];
// close 'Team & Auth' tab
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.openTable({ title: 'Country' });
// set row height & verify
for (let i = 0; i < rowHeight.length; i++) {
await toolbar.clickRowHeight();
await toolbar.rowHeight.click({ title: rowHeight[i].title });
await dashboard.grid.rowPage.getRecordHeight(0).then(height => {
expect(height).toBe(rowHeight[i].height);
});
}
});
});

Loading…
Cancel
Save