Browse Source

Merge branch 'develop' into feat/attachments

rebase to latest develop
pull/4931/head
Raju Udava 1 year ago
parent
commit
69563cf0a9
  1. 4
      .run/test-debug.run.xml
  2. 4
      packages/nc-gui/assets/nc-icons/row-height-extra-tall.svg
  3. 4
      packages/nc-gui/assets/nc-icons/row-height-medium.svg
  4. 4
      packages/nc-gui/assets/nc-icons/row-height-short.svg
  5. 4
      packages/nc-gui/assets/nc-icons/row-height-tall.svg
  6. 5
      packages/nc-gui/components.d.ts
  7. 24
      packages/nc-gui/components/cell/ClampedText.vue
  8. 10
      packages/nc-gui/components/cell/MultiSelect.vue
  9. 4
      packages/nc-gui/components/cell/SingleSelect.vue
  10. 2
      packages/nc-gui/components/cell/Text.vue
  11. 24
      packages/nc-gui/components/cell/TextArea.vue
  12. 2
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  13. 2
      packages/nc-gui/components/dashboard/settings/Erd.vue
  14. 6
      packages/nc-gui/components/dashboard/settings/Modal.vue
  15. 30
      packages/nc-gui/components/erd/FullScreenToggle.vue
  16. 16
      packages/nc-gui/components/erd/View.vue
  17. 1
      packages/nc-gui/components/erd/utils.ts
  18. 24
      packages/nc-gui/components/smartsheet/Cell.vue
  19. 27
      packages/nc-gui/components/smartsheet/Grid.vue
  20. 2
      packages/nc-gui/components/smartsheet/Toolbar.vue
  21. 87
      packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue
  22. 2
      packages/nc-gui/components/virtual-cell/HasMany.vue
  23. 2
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  24. 23
      packages/nc-gui/components/virtual-cell/QrCode.vue
  25. 23
      packages/nc-gui/components/virtual-cell/barcode/Barcode.vue
  26. 12
      packages/nc-gui/components/virtual-cell/barcode/JsBarcodeWrapper.vue
  27. 4
      packages/nc-gui/composables/useMultiSelect/index.ts
  28. 5
      packages/nc-gui/nuxt.config.ts
  29. 35
      packages/nc-gui/package-lock.json
  30. 1
      packages/nc-gui/package.json
  31. 6
      packages/nc-gui/plugins/clamp.ts
  32. 19
      packages/nocodb-sdk/src/lib/Api.ts
  33. 11
      packages/nocodb/src/lib/meta/api/gridViewApis.ts
  34. 4
      packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
  35. 16
      packages/nocodb/src/lib/migrations/v2/nc_025_add_row_height.ts
  36. 41
      packages/nocodb/src/lib/models/GridView.ts
  37. 1
      packages/nocodb/src/lib/models/View.ts
  38. 41
      scripts/sdk/swagger.json
  39. 35
      tests/playwright/pages/Account/License.ts
  40. 3
      tests/playwright/pages/Account/index.ts
  41. 26
      tests/playwright/pages/Dashboard/Grid/Row.ts
  42. 3
      tests/playwright/pages/Dashboard/Grid/index.ts
  43. 19
      tests/playwright/pages/Dashboard/common/Toolbar/RowHeight.ts
  44. 8
      tests/playwright/pages/Dashboard/common/Toolbar/index.ts
  45. 29
      tests/playwright/tests/accountLicense.spec.ts
  46. 25
      tests/playwright/tests/toolbarOperations.spec.ts

4
.run/test-debug.run.xml

@ -1,9 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="test-debug" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/scripts/playwright/package.json" />
<package-json value="$PROJECT_DIR$/tests/playwright/package.json" />
<command value="run" />
<scripts>
<script value="test-debug" />
<script value="test:debug" />
</scripts>
<node-interpreter value="project" />
<envs />

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

@ -245,8 +245,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>

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

@ -224,7 +224,7 @@ async function addIfMissingAndSave() {
// todo: refactor and avoid repetition
if (updatedColMeta.cdf) {
// Postgres returns default value wrapped with single quotes & casted with type so we have to get value between single quotes to keep it unified for all databases
if (isPg.value) {
if (isPg(column.value.base_id)) {
updatedColMeta.cdf = updatedColMeta.cdf.substring(
updatedColMeta.cdf.indexOf(`'`) + 1,
updatedColMeta.cdf.lastIndexOf(`'`),
@ -232,7 +232,7 @@ async function addIfMissingAndSave() {
}
// Mysql escapes single quotes with backslash so we keep quotes but others have to unescaped
if (!isMysql.value) {
if (!isMysql(column.value.base_id)) {
updatedColMeta.cdf = updatedColMeta.cdf.replace(/''/g, "'")
}
}
@ -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>

4
packages/nc-gui/components/cell/SingleSelect.vue

@ -147,7 +147,7 @@ async function addIfMissingAndSave() {
// todo: refactor and avoid repetition
if (updatedColMeta.cdf) {
// Postgres returns default value wrapped with single quotes & casted with type so we have to get value between single quotes to keep it unified for all databases
if (isPg.value) {
if (isPg(column.value.base_id)) {
updatedColMeta.cdf = updatedColMeta.cdf.substring(
updatedColMeta.cdf.indexOf(`'`) + 1,
updatedColMeta.cdf.lastIndexOf(`'`),
@ -155,7 +155,7 @@ async function addIfMissingAndSave() {
}
// Mysql escapes single quotes with backslash so we keep quotes but others have to unescaped
if (!isMysql.value) {
if (!isMysql(column.value.base_id)) {
updatedColMeta.cdf = updatedColMeta.cdf.replace(/''/g, "'")
}
}

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>

2
packages/nc-gui/components/dashboard/settings/DataSources.vue

@ -381,7 +381,7 @@ watch(
<div v-else-if="vState === DataSourcesSubTab.UIAcl" class="max-h-600px overflow-y-auto">
<UIAcl :base-id="activeBaseId" />
</div>
<div v-else-if="vState === DataSourcesSubTab.ERD" class="max-h-600px overflow-y-auto">
<div v-else-if="vState === DataSourcesSubTab.ERD" class="h-full overflow-y-auto">
<Erd :base-id="activeBaseId" />
</div>
<div v-else-if="vState === DataSourcesSubTab.Edit" class="max-h-600px overflow-y-auto">

2
packages/nc-gui/components/dashboard/settings/Erd.vue

@ -5,7 +5,7 @@ const props = defineProps<{
</script>
<template>
<div class="w-full h-full !p-0 h-70vh">
<div class="w-full h-full !p-0">
<ErdView :base-id="props.baseId" />
</div>
</template>

6
packages/nc-gui/components/dashboard/settings/Modal.vue

@ -170,13 +170,11 @@ watch(
<a-button
type="text"
class="!rounded-md border-none -mt-1.5 -mr-1"
class="!rounded-md border-none !px-1.5"
data-testid="settings-modal-close-button"
@click="vModel = false"
>
<template #icon>
<MdiClose class="cursor-pointer mt-1 nc-modal-close" />
</template>
<MdiClose class="cursor-pointer nc-modal-close w-4" />
</a-button>
</div>

30
packages/nc-gui/components/erd/FullScreenToggle.vue

@ -0,0 +1,30 @@
<script lang="ts" setup>
import { Panel, PanelPosition } from '@vue-flow/additional-components'
import type { ERDConfig } from './utils'
import MiFullscreen from '~icons/material-symbols/fullscreen'
import MiFullscreenExit from '~icons/material-symbols/fullscreen-exit'
const props = defineProps<{
config: ERDConfig
}>()
const emit = defineEmits(['toggleFullScreen'])
const { config } = toRefs(props)
const toggleFullScreen = () => {
emit('toggleFullScreen')
}
</script>
<template>
<Panel
class="text-xs bg-white border-1 rounded-md p-0.5 border-gray-50 z-50 nc-erd-histogram cursor-pointer hover:bg-gray-100"
:position="PanelPosition.TopLeft"
>
<div class="flex">
<MiFullscreenExit v-if="config.isFullScreen" class="h-5 w-5" @click="toggleFullScreen" />
<MiFullscreen v-else class="h-5 w-5" @click="toggleFullScreen" />
</div>
</Panel>
</template>

16
packages/nc-gui/components/erd/View.vue

@ -21,6 +21,7 @@ const config = reactive<ERDConfig>({
singleTableMode: !!props.table,
showMMTables: false,
showJunctionTableNames: false,
isFullScreen: false,
})
const loadMetaOfTablesNotInMetas = async (localTables: TableType[]) => {
@ -65,6 +66,10 @@ const populateTables = async () => {
isLoading = false
}
const toggleFullScreen = () => {
config.isFullScreen = !config.isFullScreen
}
watch([metas, projectTables], populateTables, {
flush: 'post',
immediate: true,
@ -86,7 +91,15 @@ watch(
</script>
<template>
<div class="w-full" style="height: inherit" :class="[`nc-erd-vue-flow${config.singleTableMode ? '-single-table' : ''}`]">
<div
class="w-full bg-white"
:class="{
'z-100 h-screen w-screen fixed top-0 left-0 right-0 bottom-0': config.isFullScreen,
'nc-erd-vue-flow-single-table': config.singleTableMode,
'nc-erd-vue-flow': !config.singleTableMode,
}"
:style="!config.isFullScreen ? 'height: inherit' : ''"
>
<div class="relative h-full">
<LazyErdFlow :tables="filteredTables" :config="config">
<GeneralOverlay v-model="isLoading" inline class="bg-gray-300/50">
@ -95,6 +108,7 @@ watch(
</div>
</GeneralOverlay>
<ErdFullScreenToggle :config="config" @toggle-full-screen="toggleFullScreen" />
<ErdConfigPanel :config="config" />
<ErdHistogramPanel v-if="!config.singleTableMode" />
</LazyErdFlow>

1
packages/nc-gui/components/erd/utils.ts

@ -15,6 +15,7 @@ export interface ERDConfig {
singleTableMode: boolean
showJunctionTableNames: boolean
showMMTables: boolean
isFullScreen: boolean
}
export interface NodeData {

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>

4
packages/nc-gui/composables/useMultiSelect/index.ts

@ -304,7 +304,7 @@ export function useMultiSelect(
column: columnObj,
appInfo: unref(appInfo),
},
isMysql.value,
isMysql(meta.value?.base_id),
)
e.preventDefault()
@ -338,7 +338,7 @@ export function useMultiSelect(
column: columnObj,
appInfo: unref(appInfo),
},
isMysql.value,
isMysql(meta.value?.base_id),
)
e.preventDefault()
syncCellData?.(activeCell)

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 {
@ -2501,6 +2502,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": ""

35
tests/playwright/pages/Account/License.ts

@ -0,0 +1,35 @@
import BasePage from '../Base';
import { AccountPage } from './index';
export class AccountLicensePage extends BasePage {
private accountPage: AccountPage;
constructor(accountPage: AccountPage) {
super(accountPage.rootPage);
this.accountPage = accountPage;
}
async goto() {
await this.rootPage.goto('/#/account/license', { waitUntil: 'networkidle' });
}
async waitUntilContentLoads() {
return this.rootPage.waitForResponse(resp => resp.url().includes('api/v1/license') && resp.status() === 200);
}
// License TextBox
get() {
return this.accountPage.get().locator(`textarea[placeholder="License key"]`);
}
// Save button
getSaveButton() {
return this.accountPage.get().locator(`button.ant-btn-primary:has-text("Save license key")`);
}
async saveLicenseKey(licenseKey: string) {
await this.get().fill(licenseKey);
await this.getSaveButton().click();
await this.verifyToast({ message: 'License key updated' });
}
}

3
tests/playwright/pages/Account/index.ts

@ -4,12 +4,14 @@ import { AccountSettingsPage } from './Settings';
import { AccountTokenPage } from './Token';
import { AccountUsersPage } from './Users';
import { AccountAppStorePage } from './AppStore';
import { AccountLicensePage } from './License';
export class AccountPage extends BasePage {
readonly settings: AccountSettingsPage;
readonly token: AccountTokenPage;
readonly users: AccountUsersPage;
readonly appStore: AccountAppStorePage;
readonly license: AccountLicensePage;
constructor(page: Page) {
super(page);
@ -17,6 +19,7 @@ export class AccountPage extends BasePage {
this.token = new AccountTokenPage(this);
this.users = new AccountUsersPage(this);
this.appStore = new AccountAppStorePage(this);
this.license = new AccountLicensePage(this);
}
get() {

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(

29
tests/playwright/tests/accountLicense.spec.ts

@ -0,0 +1,29 @@
import { test } from '@playwright/test';
import { AccountPage } from '../pages/Account';
import setup from '../setup';
import { AccountLicensePage } from '../pages/Account/License';
import { DashboardPage } from '../pages/Dashboard';
test.describe('Enterprise License', () => {
// @ts-ignore
let dashboard: DashboardPage;
// @ts-ignore
let accountLicensePage: AccountLicensePage, accountPage: AccountPage, context: any;
test.beforeEach(async ({ page }) => {
context = await setup({ page });
accountPage = new AccountPage(page);
accountLicensePage = new AccountLicensePage(accountPage);
dashboard = new DashboardPage(page, context.project);
});
test('Update license key & verify if enterprise features enabled', async () => {
test.slow();
await accountLicensePage.goto();
await accountLicensePage.saveLicenseKey('1234567890');
await dashboard.goto();
// presence of snowflake icon indicates enterprise features are enabled
await dashboard.treeView.quickImport({ title: 'Snowflake' });
});
});

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