Browse Source

Merge branch 'develop' into l10n_develop_2

pull/4534/head
Raju Udava 2 years ago committed by GitHub
parent
commit
a808c60968
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .github/workflows/ci-cd.yml
  2. 14
      .github/workflows/playwright-test-workflow.yml
  3. 29
      packages/nc-gui/components/smartsheet/Form.vue
  4. 3
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  5. 1
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  6. 34
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  7. 4
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  8. 60
      packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue
  9. 3
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  10. 20
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  11. 25
      packages/nc-gui/components/virtual-cell/Formula.vue
  12. 50
      packages/nc-gui/components/virtual-cell/QrCode.vue
  13. 6
      packages/nc-gui/composables/useSharedView.ts
  14. 21
      packages/nc-gui/composables/useShowNotEditableWarning.ts
  15. 18
      packages/nc-gui/composables/useViewData.ts
  16. 397
      packages/nc-gui/package-lock.json
  17. 1
      packages/nc-gui/package.json
  18. 3
      packages/nc-gui/utils/cell.ts
  19. 6
      packages/nc-gui/utils/columnUtils.ts
  20. 1
      packages/nc-gui/utils/virtualCell.ts
  21. 12
      packages/noco-docs/content/en/setup-and-usages/column-types.md
  22. 1192
      packages/nocodb-sdk/package-lock.json
  23. 1
      packages/nocodb-sdk/src/index.ts
  24. 2
      packages/nocodb-sdk/src/lib/UITypes.ts
  25. 10
      packages/nocodb-sdk/src/lib/columnRules/QrCodeRules.ts
  26. 1
      packages/nocodb-sdk/src/lib/columnRules/index.ts
  27. 61
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  28. 16
      packages/nocodb/src/lib/meta/api/columnApis.ts
  29. 4
      packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
  30. 26
      packages/nocodb/src/lib/migrations/v2/nc_022_qr_code_column_type.ts
  31. 62
      packages/nocodb/src/lib/models/Column.ts
  32. 67
      packages/nocodb/src/lib/models/QrCodeColumn.ts
  33. 2
      packages/nocodb/src/lib/utils/globals.ts
  34. 52
      packages/nocodb/tests/unit/factory/column.ts
  35. 2
      packages/nocodb/tests/unit/rest/index.test.ts
  36. 96
      packages/nocodb/tests/unit/rest/tests/columnTypeSpecific.test.ts
  37. 1
      tests/playwright/.gitignore
  38. 21
      tests/playwright/pages/Dashboard/Grid/Column/index.ts
  39. 3
      tests/playwright/pages/Dashboard/Grid/index.ts
  40. 27
      tests/playwright/pages/Dashboard/QrCodeOverlay/index.ts
  41. 26
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  42. 34
      tests/playwright/scripts/stressTestNewlyAddedTest.js
  43. 151
      tests/playwright/tests/columnQrCode.spec.ts
  44. 14
      tests/playwright/tests/tableColumnOperation.spec.ts

1
.github/workflows/ci-cd.yml

@ -10,6 +10,7 @@ on:
- "packages/nc-gui/**" - "packages/nc-gui/**"
- "packages/nocodb/**" - "packages/nocodb/**"
- ".github/workflows/ci-cd.yml" - ".github/workflows/ci-cd.yml"
- "tests/playwright/**"
pull_request: pull_request:
types: [opened, reopened, synchronize, ready_for_review, labeled] types: [opened, reopened, synchronize, ready_for_review, labeled]
branches: [develop] branches: [develop]

14
.github/workflows/playwright-test-workflow.yml

@ -98,6 +98,14 @@ jobs:
working-directory: ./tests/playwright working-directory: ./tests/playwright
run: E2E_DB_TYPE=${{ inputs.db }} npm run ci:test:shard:${{ inputs.shard }} run: E2E_DB_TYPE=${{ inputs.db }} npm run ci:test:shard:${{ inputs.shard }}
# Stress test added/modified tests
- name: Fetch develop branch
working-directory: ./tests/playwright
run: git fetch origin develop
- name: Stress test
working-directory: ./tests/playwright
run: E2E_DB_TYPE=${{ inputs.db }} node ./scripts/stressTestNewlyAddedTest.js
# Quick tests (pg on sqlite shard 0 and sqlite on sqlite shard 1) # Quick tests (pg on sqlite shard 0 and sqlite on sqlite shard 1)
- name: Run quick server and tests (pg) - name: Run quick server and tests (pg)
if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }} if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }}
@ -139,6 +147,12 @@ jobs:
name: playwright-report-${{ inputs.db }}-${{ inputs.shard }} name: playwright-report-${{ inputs.db }}-${{ inputs.shard }}
path: ./tests/playwright/playwright-report/ path: ./tests/playwright/playwright-report/
retention-days: 2 retention-days: 2
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report-stress-${{ inputs.db }}-${{ inputs.shard }}
path: ./tests/playwright/playwright-report-stress/
retention-days: 2
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: always() if: always()
with: with:

29
packages/nc-gui/components/smartsheet/Form.vue

@ -33,6 +33,8 @@ provide(IsGalleryInj, ref(false))
// todo: generate hideCols based on default values // todo: generate hideCols based on default values
const hiddenCols = ['created_at', 'updated_at'] const hiddenCols = ['created_at', 'updated_at']
const hiddenColTypes = [UITypes.Rollup, UITypes.Lookup, UITypes.Formula, UITypes.QrCode, UITypes.SpecificDBType]
const state = useGlobal() const state = useGlobal()
const formRef = ref() const formRef = ref()
@ -227,7 +229,7 @@ async function addAllColumns() {
} }
function shouldSkipColumn(col: Record<string, any>) { function shouldSkipColumn(col: Record<string, any>) {
return isDbRequired(col) || !!col.required || (!!col.rqd && !col.cdf) return isDbRequired(col) || !!col.required || (!!col.rqd && !col.cdf) || col.uidt === UITypes.QrCode
} }
async function removeAllColumns() { async function removeAllColumns() {
@ -256,7 +258,7 @@ async function checkSMTPStatus() {
} }
function setFormData() { function setFormData() {
const col = (formColumnData as Record<string, any>)?.value const col = formColumnData?.value || []
formViewData.value = { formViewData.value = {
...formViewData.value, ...formViewData.value,
@ -273,27 +275,14 @@ function setFormData() {
emailMe.value = data[state.user.value?.email as string] emailMe.value = data[state.user.value?.email as string]
localColumns.value = col localColumns.value = col
.filter( .filter((f) => f.show && !hiddenColTypes.includes(f.uidt))
(f: Record<string, any>) => .sort((a, b) => a.order - b.order)
f.show && .map((c) => ({ ...c, required: !!c.required }))
f.uidt !== UITypes.Rollup &&
f.uidt !== UITypes.Lookup &&
f.uidt !== UITypes.Formula &&
f.uidt !== UITypes.SpecificDBType,
)
.sort((a: Record<string, any>, b: Record<string, any>) => a.order - b.order)
.map((c: Record<string, any>) => ({ ...c, required: !!(c.required || 0) }))
systemFieldsIds.value = getSystemColumns(col).map((c: Record<string, any>) => c.fk_column_id) systemFieldsIds.value = getSystemColumns(col).map((c) => c.fk_column_id)
hiddenColumns.value = col.filter( hiddenColumns.value = col.filter(
(f: Record<string, any>) => (f) => !f.show && !systemFieldsIds.value.includes(f.fk_column_id) && !hiddenColTypes.includes(f.uidt),
!f.show &&
!systemFieldsIds.value.includes(f.fk_column_id) &&
f.uidt !== UITypes.Rollup &&
f.uidt !== UITypes.Lookup &&
f.uidt !== UITypes.Formula &&
f.uidt !== UITypes.SpecificDBType,
) )
} }

3
packages/nc-gui/components/smartsheet/VirtualCell.vue

@ -13,6 +13,7 @@ import {
isHm, isHm,
isLookup, isLookup,
isMm, isMm,
isQrCode,
isRollup, isRollup,
provide, provide,
toRef, toRef,
@ -39,7 +40,6 @@ provide(RowInj, row)
provide(CellValueInj, toRef(props, 'modelValue')) provide(CellValueInj, toRef(props, 'modelValue'))
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
function onNavigate(dir: NavigateDir, e: KeyboardEvent) { function onNavigate(dir: NavigateDir, e: KeyboardEvent) {
emit('navigate', dir) emit('navigate', dir)
@ -58,6 +58,7 @@ function onNavigate(dir: NavigateDir, e: KeyboardEvent) {
<LazyVirtualCellBelongsTo v-else-if="isBt(column)" /> <LazyVirtualCellBelongsTo v-else-if="isBt(column)" />
<LazyVirtualCellRollup v-else-if="isRollup(column)" /> <LazyVirtualCellRollup v-else-if="isRollup(column)" />
<LazyVirtualCellFormula v-else-if="isFormula(column)" /> <LazyVirtualCellFormula v-else-if="isFormula(column)" />
<LazyVirtualCellQrCode v-else-if="isQrCode(column)" />
<LazyVirtualCellCount v-else-if="isCount(column)" /> <LazyVirtualCellCount v-else-if="isCount(column)" />
<LazyVirtualCellLookup v-else-if="isLookup(column)" /> <LazyVirtualCellLookup v-else-if="isLookup(column)" />
</div> </div>

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

@ -165,6 +165,7 @@ useEventListener('keydown', (e: KeyboardEvent) => {
</a-form-item> </a-form-item>
<LazySmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" v-model:value="formState" /> <LazySmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" v-model:value="formState" />
<LazySmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" />
<LazySmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" /> <LazySmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" /> <LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" /> <LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />

34
packages/nc-gui/components/smartsheet/column/FormulaOptions.vue

@ -26,6 +26,8 @@ const props = defineProps<{
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const uiTypesNotSupportedInFormulas = [UITypes.QrCode]
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const { setAdditionalValidations, validateInfos, sqlUi, column } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, sqlUi, column } = useColumnCreateStoreOrThrow()
@ -44,7 +46,9 @@ enum JSEPNode {
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const columns = computed(() => meta?.value?.columns || []) const supportedColumns = computed(
() => meta?.value?.columns?.filter((col) => !uiTypesNotSupportedInFormulas.includes(col.uidt as UITypes)) || [],
)
const validators = { const validators = {
formula_raw: [ formula_raw: [
@ -97,8 +101,8 @@ const suggestionsList = computed(() => {
syntax: formulas[fn].syntax, syntax: formulas[fn].syntax,
examples: formulas[fn].examples, examples: formulas[fn].examples,
})), })),
...columns.value ...supportedColumns.value
.filter((c: Record<string, any>) => { .filter((c) => {
// skip system LTAR columns // skip system LTAR columns
if (c.uidt === UITypes.LinkToAnotherRecord && c.system) return false if (c.uidt === UITypes.LinkToAnotherRecord && c.system) return false
// v1 logic? skip the current column // v1 logic? skip the current column
@ -237,11 +241,7 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
errors = new Set([...errors, ...typeErrors]) errors = new Set([...errors, ...typeErrors])
} else if (parsedTree.type === JSEPNode.IDENTIFIER) { } else if (parsedTree.type === JSEPNode.IDENTIFIER) {
if ( if (supportedColumns.value.filter((c) => !column || column.value?.id !== c.id).every((c) => c.title !== parsedTree.name)) {
columns.value
.filter((c: Record<string, any>) => !column || column.value?.id !== c.id)
.every((c: Record<string, any>) => c.title !== parsedTree.name)
) {
errors.add(`Column '${parsedTree.name}' is not available`) errors.add(`Column '${parsedTree.name}' is not available`)
} }
@ -249,8 +249,8 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
// e.g. formula1 -> formula2 -> formula1 should return circular reference error // e.g. formula1 -> formula2 -> formula1 should return circular reference error
// get all formula columns excluding itself // get all formula columns excluding itself
const formulaPaths = columns.value const formulaPaths = supportedColumns.value
.filter((c: Record<string, any>) => c.id !== column.value?.id && c.uidt === UITypes.Formula) .filter((c) => c.id !== column.value?.id && c.uidt === UITypes.Formula)
.reduce((res: Record<string, any>[], c: Record<string, any>) => { .reduce((res: Record<string, any>[], c: Record<string, any>) => {
// in `formula`, get all the (unique) target neighbours // in `formula`, get all the (unique) target neighbours
// i.e. all column id (e.g. cl_xxxxxxxxxxxxxx) with formula type // i.e. all column id (e.g. cl_xxxxxxxxxxxxxx) with formula type
@ -258,7 +258,7 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
...new Set( ...new Set(
(c.colOptions.formula.match(/cl_\w{14}/g) || []).filter( (c.colOptions.formula.match(/cl_\w{14}/g) || []).filter(
(colId: string) => (colId: string) =>
columns.value.filter((col: ColumnType) => col.id === colId && col.uidt === UITypes.Formula).length, supportedColumns.value.filter((col: ColumnType) => col.id === colId && col.uidt === UITypes.Formula).length,
), ),
), ),
] ]
@ -269,7 +269,9 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
return res return res
}, []) }, [])
// include target formula column (i.e. the one to be saved if applicable) // include target formula column (i.e. the one to be saved if applicable)
const targetFormulaCol = columns.value.find((c: ColumnType) => c.title === parsedTree.name && c.uidt === UITypes.Formula) const targetFormulaCol = supportedColumns.value.find(
(c: ColumnType) => c.title === parsedTree.name && c.uidt === UITypes.Formula,
)
if (targetFormulaCol && column.value?.id) { if (targetFormulaCol && column.value?.id) {
formulaPaths.push({ formulaPaths.push({
@ -362,7 +364,7 @@ function validateAgainstType(parsedTree: any, expectedType: string, func: any, t
} }
} }
} else if (parsedTree.type === JSEPNode.IDENTIFIER) { } else if (parsedTree.type === JSEPNode.IDENTIFIER) {
const col = columns.value.find((c) => c.title === parsedTree.name) const col = supportedColumns.value.find((c) => c.title === parsedTree.name)
if (col === undefined) { if (col === undefined) {
return return
@ -432,6 +434,7 @@ function validateAgainstType(parsedTree: any, expectedType: string, func: any, t
case UITypes.Button: case UITypes.Button:
case UITypes.Checkbox: case UITypes.Checkbox:
case UITypes.Collaborator: case UITypes.Collaborator:
case UITypes.QrCode:
default: default:
typeErrors.add(`Not supported to reference column '${parsedTree.name}'`) typeErrors.add(`Not supported to reference column '${parsedTree.name}'`)
break break
@ -455,7 +458,7 @@ function getRootDataType(parsedTree: any): any {
if (parsedTree.type === JSEPNode.CALL_EXP) { if (parsedTree.type === JSEPNode.CALL_EXP) {
return formulas[parsedTree.callee.name].type return formulas[parsedTree.callee.name].type
} else if (parsedTree.type === JSEPNode.IDENTIFIER) { } else if (parsedTree.type === JSEPNode.IDENTIFIER) {
const col = columns.value.find((c) => c.title === parsedTree.name) as Record<string, any> const col = supportedColumns.value.find((c) => c.title === parsedTree.name) as Record<string, any>
if (col?.uidt === UITypes.Formula) { if (col?.uidt === UITypes.Formula) {
return getRootDataType(jsep(col?.formula_raw)) return getRootDataType(jsep(col?.formula_raw))
} else { } else {
@ -500,6 +503,7 @@ function getRootDataType(parsedTree: any): any {
case UITypes.Button: case UITypes.Button:
case UITypes.Checkbox: case UITypes.Checkbox:
case UITypes.Collaborator: case UITypes.Collaborator:
case UITypes.QrCode:
default: default:
return 'N/A' return 'N/A'
} }
@ -561,7 +565,7 @@ function handleInput() {
.complete(wordToComplete.value) .complete(wordToComplete.value)
?.sort((x: Record<string, any>, y: Record<string, any>) => sortOrder[x.type] - sortOrder[y.type]) ?.sort((x: Record<string, any>, y: Record<string, any>) => sortOrder[x.type] - sortOrder[y.type])
if (!isCurlyBracketBalanced()) { if (!isCurlyBracketBalanced()) {
suggestion.value = suggestion.value.filter((v: Record<string, any>) => v.type === 'column') suggestion.value = suggestion.value.filter((v) => v.type === 'column')
} }
autocomplete.value = !!suggestion.value.length autocomplete.value = !!suggestion.value.length
} }

4
packages/nc-gui/components/smartsheet/column/LookupOptions.vue

@ -54,7 +54,9 @@ const columns = $computed(() => {
return [] return []
} }
return metas[selectedTable.id].columns.filter((c: any) => !isSystemColumn(c)) return metas[selectedTable.id].columns.filter((c: any) => {
return !(isSystemColumn(c) || c.uidt === UITypes.QrCode)
})
}) })
</script> </script>

60
packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue

@ -0,0 +1,60 @@
<script setup lang="ts">
import { AllowedColumnTypesForQrCode, UITypes } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue'
import { useVModel } from '#imports'
const props = defineProps<{
modelValue: any
}>()
const emit = defineEmits(['update:modelValue'])
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
const { fields, metaColumnById } = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
const vModel = useVModel(props, 'modelValue', emit)
const { setAdditionalValidations, validateInfos, column } = useColumnCreateStoreOrThrow()
const columnsAllowedAsQrValue = computed<SelectProps['options']>(() => {
return fields.value
?.filter(
(el) =>
el.fk_column_id &&
// AllowedColumnTypesForQrCode.map((el) => el.toString()).includes(metaColumnById.value[el.fk_column_id].uidt),
AllowedColumnTypesForQrCode.includes(metaColumnById.value[el.fk_column_id].uidt as UITypes),
)
.map((field) => {
return {
value: field.fk_column_id,
label: field.title,
}
})
})
onMounted(() => {
// set default value
vModel.value.fk_qr_value_column_id = (column?.value?.colOptions as Record<string, any>)?.fk_qr_value_column_id || ''
})
setAdditionalValidations({
fk_qr_value_column_id: [{ required: true, message: 'Required' }],
})
</script>
<template>
<a-row>
<a-col :span="24">
<a-form-item class="flex w-1/2 pb-2 nc-qr-code-value-column-select" :label="$t('labels.qrCodeValueColumn')"
v-bind="validateInfos.fk_qr_value_column_id">
<a-select v-model:value="vModel.fk_qr_value_column_id" :options="columnsAllowedAsQrValue"
placeholder="Select a column for the QR code value" @click.stop />
</a-form-item>
</a-col>
</a-row>
</template>

3
packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts

@ -8,6 +8,7 @@ import HMIcon from '~icons/mdi/table-arrow-right'
import BTIcon from '~icons/mdi/table-arrow-left' import BTIcon from '~icons/mdi/table-arrow-left'
import MMIcon from '~icons/mdi/table-network' import MMIcon from '~icons/mdi/table-network'
import FormulaIcon from '~icons/mdi/math-integral' import FormulaIcon from '~icons/mdi/math-integral'
import QrCodeScan from '~icons/mdi/qrcode-scan'
import RollupIcon from '~icons/mdi/movie-roll' import RollupIcon from '~icons/mdi/movie-roll'
import CountIcon from '~icons/mdi/counter' import CountIcon from '~icons/mdi/counter'
import SpecificDBTypeIcon from '~icons/mdi/database-settings' import SpecificDBTypeIcon from '~icons/mdi/database-settings'
@ -29,6 +30,8 @@ const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
return { icon: SpecificDBTypeIcon, color: 'text-grey' } return { icon: SpecificDBTypeIcon, color: 'text-grey' }
case UITypes.Formula: case UITypes.Formula:
return { icon: FormulaIcon, color: 'text-grey' } return { icon: FormulaIcon, color: 'text-grey' }
case UITypes.QrCode:
return { icon: QrCodeScan, color: 'text-grey' }
case UITypes.Lookup: case UITypes.Lookup:
switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) { switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY: case RelationTypes.MANY_TO_MANY:

20
packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue

@ -21,14 +21,17 @@ const localValue = computed({
const options = computed<SelectProps['options']>(() => const options = computed<SelectProps['options']>(() =>
meta.value?.columns meta.value?.columns
?.filter((c: ColumnType) => { ?.filter((c: ColumnType) => {
if (c.uidt === UITypes.QrCode) {
return false
} else if (isSort) {
/** ignore hasmany and manytomany relations if it's using within sort menu */ /** ignore hasmany and manytomany relations if it's using within sort menu */
if (isSort) {
return !( return !(
c.uidt === UITypes.LinkToAnotherRecord && (c.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO c.uidt === UITypes.LinkToAnotherRecord && (c.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO
) )
/** ignore virtual fields which are system fields ( mm relation ) */ /** ignore virtual fields which are system fields ( mm relation ) and qr code fields */
} else { } else {
return !c.colOptions || !c.system const isVirtualSystemField = c.colOptions && c.system
return !isVirtualSystemField
} }
}) })
.map((c: ColumnType) => ({ .map((c: ColumnType) => ({
@ -48,14 +51,9 @@ const filterOption = (input: string, option: any) => option.label.toLowerCase()?
</script> </script>
<template> <template>
<a-select <a-select v-model:value="localValue" :dropdown-match-select-width="false" show-search
v-model:value="localValue" :placeholder="$t('placeholder.selectField')" :filter-option="filterOption"
:dropdown-match-select-width="false" dropdown-class-name="nc-dropdown-toolbar-field-list">
show-search
:placeholder="$t('placeholder.selectField')"
:filter-option="filterOption"
dropdown-class-name="nc-dropdown-toolbar-field-list"
>
<a-select-option v-for="option in options" :key="option.value" :label="option.label" :value="option.value"> <a-select-option v-for="option in options" :key="option.value" :label="option.label" :value="option.value">
<div class="flex gap-2 items-center items-center h-full"> <div class="flex gap-2 items-center items-center h-full">
<component :is="option.icon" class="min-w-5 !mx-0" /> <component :is="option.icon" class="min-w-5 !mx-0" />

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

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { CellValueInj, ColumnInj, computed, handleTZ, inject, ref, refAutoReset, replaceUrlsWithLink, useProject } from '#imports' import { CellValueInj, ColumnInj, computed, handleTZ, inject, replaceUrlsWithLink, useProject } from '#imports'
// todo: column type doesn't have required property `error` - throws in typecheck // todo: column type doesn't have required property `error` - throws in typecheck
const column = inject(ColumnInj) as Ref<ColumnType & { colOptions: { error: any } }> const column = inject(ColumnInj) as Ref<ColumnType & { colOptions: { error: any } }>
@ -14,21 +14,8 @@ const result = computed(() => (isPg.value ? handleTZ(cellValue?.value) : cellVal
const urls = computed(() => replaceUrlsWithLink(result.value)) const urls = computed(() => replaceUrlsWithLink(result.value))
const timeout = 3000 // in ms const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activateShowEditNonEditableFieldWarning } =
useShowNotEditableWarning()
const showEditFormulaWarning = refAutoReset(false, timeout)
const showClearFormulaWarning = refAutoReset(false, timeout)
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
showEditFormulaWarning.value = true
break
case 'Delete':
showClearFormulaWarning.value = true
break
}
})
</script> </script>
<template> <template>
@ -41,16 +28,16 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
<span>ERR!</span> <span>ERR!</span>
</a-tooltip> </a-tooltip>
<div class="p-2" @dblclick="showEditFormulaWarning = true"> <div class="p-2" @dblclick="activateShowEditNonEditableFieldWarning">
<div v-if="urls" v-html="urls" /> <div v-if="urls" v-html="urls" />
<div v-else>{{ result }}</div> <div v-else>{{ result }}</div>
<div v-if="showEditFormulaWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs"> <div v-if="showEditNonEditableFieldWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
<!-- TODO: i18n --> <!-- TODO: i18n -->
Warning: Formula fields should be configured in the field menu dropdown. Warning: Formula fields should be configured in the field menu dropdown.
</div> </div>
<div v-if="showClearFormulaWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs"> <div v-if="showClearNonEditableFieldWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
<!-- TODO: i18n --> <!-- TODO: i18n -->
Warning: Computed field - unable to clear text. Warning: Computed field - unable to clear text.
</div> </div>

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

@ -0,0 +1,50 @@
<script setup lang="ts">
import { useQRCode } from '@vueuse/integrations/useQRCode'
const maxNumberOfAllowedCharsForQrValue = 2000
const cellValue = inject(CellValueInj)
const qrValue = computed(() => String(cellValue?.value))
const tooManyCharsForQrCode = computed(() => qrValue?.value.length > maxNumberOfAllowedCharsForQrValue)
const qrCode = useQRCode(qrValue, {
width: 150,
})
const qrCodeLarge = useQRCode(qrValue, {
width: 600,
})
const modalVisible = ref(false)
const showQrModal = (ev: MouseEvent) => {
ev.stopPropagation()
modalVisible.value = true
}
const handleModalOkClick = () => (modalVisible.value = false)
const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = useShowNotEditableWarning()
</script>
<template>
<a-modal v-model:visible="modalVisible" wrap-class-name="nc-qr-code-large" :body-style="{ padding: '0px' }"
@ok="handleModalOkClick">
<template #footer>
<div class="mr-4" data-testid="nc-qr-code-large-value-label">{{ qrValue }}</div>
</template>
<img v-if="qrValue && !tooManyCharsForQrCode" :src="qrCodeLarge" alt="QR Code" />
</a-modal>
<div v-if="tooManyCharsForQrCode" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('labels.qrCodeValueTooLong') }}
</div>
<img v-if="qrValue && !tooManyCharsForQrCode" :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>
<div v-if="showClearNonEditableFieldWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('msg.warning.nonEditableFields.qrFieldsCannotBeDirectlyChanged') }}
</div>
</template>

6
packages/nc-gui/composables/useSharedView.ts

@ -36,7 +36,11 @@ export function useSharedView() {
(meta.value as TableType)?.columns (meta.value as TableType)?.columns
?.filter( ?.filter(
(f: Record<string, any>) => (f: Record<string, any>) =>
f.show && f.uidt !== UITypes.Rollup && f.uidt !== UITypes.Lookup && f.uidt !== UITypes.Formula, f.show &&
f.uidt !== UITypes.Rollup &&
f.uidt !== UITypes.Lookup &&
f.uidt !== UITypes.Formula &&
f.uidt !== UITypes.QrCode,
) )
.sort((a: Record<string, any>, b: Record<string, any>) => a.order - b.order) .sort((a: Record<string, any>, b: Record<string, any>) => a.order - b.order)
.map((c: Record<string, any>) => ({ ...c, required: !!(c.required || 0) })) ?? [], .map((c: Record<string, any>) => ({ ...c, required: !!(c.required || 0) })) ?? [],

21
packages/nc-gui/composables/useShowNotEditableWarning.ts

@ -0,0 +1,21 @@
const timeout = 3000 // in ms
export default function useShowNotEditableWarning() {
const showEditNonEditableFieldWarning = refAutoReset(false, timeout)
const showClearNonEditableFieldWarning = refAutoReset(false, timeout)
const activateShowEditNonEditableFieldWarning = () => (showEditNonEditableFieldWarning.value = true)
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
showEditNonEditableFieldWarning.value = true
break
case 'Delete':
showClearNonEditableFieldWarning.value = true
break
}
})
return { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activateShowEditNonEditableFieldWarning }
}

18
packages/nc-gui/composables/useViewData.ts

@ -1,5 +1,5 @@
import { UITypes, ViewTypes } from 'nocodb-sdk' import { UITypes, ViewTypes } from 'nocodb-sdk'
import type { Api, ColumnType, FormType, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' import type { Api, ColumnType, FormColumnType, FormType, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { import {
IsPublicInj, IsPublicInj,
@ -54,7 +54,7 @@ export function useViewData(
const galleryData = ref<GalleryType>() const galleryData = ref<GalleryType>()
const formColumnData = ref<FormType>() const formColumnData = ref<Record<string, any>[]>()
const formViewData = ref<FormType>() const formViewData = ref<FormType>()
@ -289,7 +289,13 @@ export function useViewData(
Object.assign( Object.assign(
toUpdate.row, toUpdate.row,
metaValue!.columns!.reduce<Record<string, any>>((acc: Record<string, any>, col: ColumnType) => { metaValue!.columns!.reduce<Record<string, any>>((acc: Record<string, any>, col: ColumnType) => {
if (col.uidt === UITypes.Formula || col.uidt === UITypes.Rollup || col.au || col.cdf?.includes(' on update ')) if (
col.uidt === UITypes.Formula ||
col.uidt === UITypes.QrCode ||
col.uidt === UITypes.Rollup ||
col.au ||
col.cdf?.includes(' on update ')
)
acc[col.title!] = updatedRowData[col.title!] acc[col.title!] = updatedRowData[col.title!]
return acc return acc
}, {} as Record<string, any>), }, {} as Record<string, any>),
@ -412,14 +418,14 @@ export function useViewData(
async function loadFormView() { async function loadFormView() {
if (!viewMeta?.value?.id) return if (!viewMeta?.value?.id) return
try { try {
const { columns, ...view } = (await $api.dbView.formRead(viewMeta.value.id)) as Record<string, any> const { columns, ...view } = await $api.dbView.formRead(viewMeta.value.id)
const fieldById = columns.reduce( const fieldById = (columns || []).reduce(
(o: Record<string, any>, f: Record<string, any>) => ({ (o: Record<string, any>, f: Record<string, any>) => ({
...o, ...o,
[f.fk_column_id]: f, [f.fk_column_id]: f,
}), }),
{}, {} as Record<string, FormColumnType>,
) )
let order = 1 let order = 1

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

@ -28,6 +28,7 @@
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk", "nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2", "papaparse": "^5.3.2",
"qrcode": "^1.5.1",
"socket.io-client": "^4.5.1", "socket.io-client": "^4.5.1",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
@ -4391,7 +4392,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -4400,7 +4400,6 @@
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": { "dependencies": {
"color-convert": "^2.0.1" "color-convert": "^2.0.1"
}, },
@ -5478,7 +5477,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": { "dependencies": {
"color-name": "~1.1.4" "color-name": "~1.1.4"
}, },
@ -5489,8 +5487,7 @@
"node_modules/color-name": { "node_modules/color-name": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
"dev": true
}, },
"node_modules/color-string": { "node_modules/color-string": {
"version": "1.9.1", "version": "1.9.1",
@ -6209,6 +6206,14 @@
} }
} }
}, },
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/decimal.js": { "node_modules/decimal.js": {
"version": "10.3.1", "version": "10.3.1",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz",
@ -6379,6 +6384,11 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dijkstrajs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz",
"integrity": "sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg=="
},
"node_modules/dir-glob": { "node_modules/dir-glob": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -6572,6 +6582,11 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/encode-utf8": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz",
"integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw=="
},
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@ -8642,7 +8657,6 @@
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"engines": { "engines": {
"node": "6.* || 8.* || >= 10.*" "node": "6.* || 8.* || >= 10.*"
} }
@ -9859,7 +9873,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -12781,6 +12794,14 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.18", "version": "8.4.18",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
@ -13459,6 +13480,174 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/qrcode": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.1.tgz",
"integrity": "sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==",
"dependencies": {
"dijkstrajs": "^1.0.1",
"encode-utf8": "^1.0.3",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qrcode/node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/qrcode/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/qrcode/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/qrcode/node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
},
"node_modules/qrcode/node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.10.5", "version": "6.10.5",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.5.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.5.tgz",
@ -13814,11 +14003,15 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"node_modules/requires-port": { "node_modules/requires-port": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@ -14295,8 +14488,7 @@
"node_modules/set-blocking": { "node_modules/set-blocking": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
"dev": true
}, },
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
@ -14772,7 +14964,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
}, },
@ -16966,6 +17157,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q=="
},
"node_modules/wide-align": { "node_modules/wide-align": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
@ -20492,14 +20688,12 @@
"ansi-regex": { "ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
"dev": true
}, },
"ansi-styles": { "ansi-styles": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": { "requires": {
"color-convert": "^2.0.1" "color-convert": "^2.0.1"
} }
@ -21272,7 +21466,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": { "requires": {
"color-name": "~1.1.4" "color-name": "~1.1.4"
} }
@ -21280,8 +21473,7 @@
"color-name": { "color-name": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
"dev": true
}, },
"color-string": { "color-string": {
"version": "1.9.1", "version": "1.9.1",
@ -21851,6 +22043,11 @@
"ms": "2.1.2" "ms": "2.1.2"
} }
}, },
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="
},
"decimal.js": { "decimal.js": {
"version": "10.3.1", "version": "10.3.1",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz",
@ -21978,6 +22175,11 @@
"integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==",
"dev": true "dev": true
}, },
"dijkstrajs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz",
"integrity": "sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg=="
},
"dir-glob": { "dir-glob": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -22127,6 +22329,11 @@
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true "dev": true
}, },
"encode-utf8": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz",
"integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw=="
},
"encodeurl": { "encodeurl": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@ -23568,8 +23775,7 @@
"get-caller-file": { "get-caller-file": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
"dev": true
}, },
"get-func-name": { "get-func-name": {
"version": "2.0.0", "version": "2.0.0",
@ -24462,8 +24668,7 @@
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
"dev": true
}, },
"is-glob": { "is-glob": {
"version": "4.0.3", "version": "4.0.3",
@ -26663,6 +26868,11 @@
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
"dev": true "dev": true
}, },
"pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="
},
"postcss": { "postcss": {
"version": "8.4.18", "version": "8.4.18",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
@ -27118,6 +27328,134 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
}, },
"qrcode": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.1.tgz",
"integrity": "sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==",
"requires": {
"dijkstrajs": "^1.0.1",
"encode-utf8": "^1.0.3",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"dependencies": {
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"requires": {
"p-locate": "^4.1.0"
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"requires": {
"p-limit": "^2.2.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
},
"string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
}
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
},
"y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
},
"yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"requires": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
}
},
"yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
"qs": { "qs": {
"version": "6.10.5", "version": "6.10.5",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.5.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.5.tgz",
@ -27385,8 +27723,12 @@
"require-directory": { "require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
"dev": true },
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
}, },
"requires-port": { "requires-port": {
"version": "1.0.0", "version": "1.0.0",
@ -27765,8 +28107,7 @@
"set-blocking": { "set-blocking": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
"dev": true
}, },
"setprototypeof": { "setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
@ -28125,7 +28466,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"requires": { "requires": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
} }
@ -29604,6 +29944,11 @@
"is-symbol": "^1.0.3" "is-symbol": "^1.0.3"
} }
}, },
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q=="
},
"wide-align": { "wide-align": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",

1
packages/nc-gui/package.json

@ -51,6 +51,7 @@
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk", "nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2", "papaparse": "^5.3.2",
"qrcode": "^1.5.1",
"socket.io-client": "^4.5.1", "socket.io-client": "^4.5.1",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",

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

@ -46,8 +46,7 @@ export const isAutoSaved = (column: ColumnType) =>
UITypes.Duration, UITypes.Duration,
].includes(column.uidt as UITypes) ].includes(column.uidt as UITypes)
export const isManualSaved = (column: ColumnType) => export const isManualSaved = (column: ColumnType) => [UITypes.Currency].includes(column.uidt as UITypes)
[UITypes.Currency, UITypes.Year, UITypes.Time].includes(column.uidt as UITypes)
export const isPrimary = (column: ColumnType) => !!column.pv export const isPrimary = (column: ColumnType) => !!column.pv

6
packages/nc-gui/utils/columnUtils.ts

@ -2,6 +2,7 @@ import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk' import { RelationTypes, UITypes } from 'nocodb-sdk'
import LinkVariant from '~icons/mdi/link-variant' import LinkVariant from '~icons/mdi/link-variant'
import TableColumnPlusBefore from '~icons/mdi/table-column-plus-before' import TableColumnPlusBefore from '~icons/mdi/table-column-plus-before'
import QrCodeScan from '~icons/mdi/qrcode-scan'
import FormatColorText from '~icons/mdi/format-color-text' import FormatColorText from '~icons/mdi/format-color-text'
import TextSubject from '~icons/mdi/text-subject' import TextSubject from '~icons/mdi/text-subject'
import JSONIcon from '~icons/mdi/code-json' import JSONIcon from '~icons/mdi/code-json'
@ -125,6 +126,11 @@ const uiTypes = [
name: UITypes.DateTime, name: UITypes.DateTime,
icon: CalendarClock, icon: CalendarClock,
}, },
{
name: UITypes.QrCode,
icon: QrCodeScan,
virtual: 1,
},
{ {
name: UITypes.Geometry, name: UITypes.Geometry,
icon: RulerSquareCompass, icon: RulerSquareCompass,

1
packages/nc-gui/utils/virtualCell.ts

@ -16,4 +16,5 @@ export const isBt = (column: ColumnType) =>
export const isLookup = (column: ColumnType) => column.uidt === UITypes.Lookup export const isLookup = (column: ColumnType) => column.uidt === UITypes.Lookup
export const isRollup = (column: ColumnType) => column.uidt === UITypes.Rollup export const isRollup = (column: ColumnType) => column.uidt === UITypes.Rollup
export const isFormula = (column: ColumnType) => column.uidt === UITypes.Formula export const isFormula = (column: ColumnType) => column.uidt === UITypes.Formula
export const isQrCode = (column: ColumnType) => column.uidt === UITypes.QrCode
export const isCount = (column: ColumnType) => column.uidt === UITypes.Count export const isCount = (column: ColumnType) => column.uidt === UITypes.Count

12
packages/noco-docs/content/en/setup-and-usages/column-types.md

@ -32,6 +32,7 @@ menuTitle: 'Column Types'
|[Duration](#duration)| Duration | |[Duration](#duration)| Duration |
|[Rating](#rating)| Rating | |[Rating](#rating)| Rating |
|[Formula](#formula)| Formula based generated column | |[Formula](#formula)| Formula based generated column |
|[QR Code](#qr-code)| QR Code visualization of another referenced column |
| [Count](#count) | | | [Count](#count) | |
|[DateTime](#datetime)| Date & Time selector | |[DateTime](#datetime)| Date & Time selector |
|[CreateTime](#createtime)| | |[CreateTime](#createtime)| |
@ -267,6 +268,17 @@ N/A
For more about formula, please visit [here](./formulas). For more about formula, please visit [here](./formulas).
### QR-Code
Encodes the value of a reference column as QR code. The following column types are supported for the for reference column:
* Formula
* Single Line Text
* Long Text
* Phone Number
* URL
* Email
Since it's a virtual column, the cell content (QR code) cannot be changed directly.
### Count ### Count
#### Available Database Types #### Available Database Types

1192
packages/nocodb-sdk/package-lock.json generated

File diff suppressed because it is too large Load Diff

1
packages/nocodb-sdk/src/index.ts

@ -1,6 +1,7 @@
export * from './lib/XcUIBuilder'; export * from './lib/XcUIBuilder';
export * from './lib/XcNotification'; export * from './lib/XcNotification';
export * from './lib/Api'; export * from './lib/Api';
export * from './lib/columnRules';
export * from './lib/sqlUi'; export * from './lib/sqlUi';
export * from './lib/globals'; export * from './lib/globals';
export * from './lib/helperFunctions'; export * from './lib/helperFunctions';

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

@ -35,6 +35,7 @@ enum UITypes {
JSON = 'JSON', JSON = 'JSON',
SpecificDBType = 'SpecificDBType', SpecificDBType = 'SpecificDBType',
Barcode = 'Barcode', Barcode = 'Barcode',
QrCode = 'QrCode',
Button = 'Button', Button = 'Button',
} }
@ -50,6 +51,7 @@ export function isVirtualCol(
// UITypes.SpecificDBType, // UITypes.SpecificDBType,
UITypes.LinkToAnotherRecord, UITypes.LinkToAnotherRecord,
UITypes.Formula, UITypes.Formula,
UITypes.QrCode,
UITypes.Rollup, UITypes.Rollup,
UITypes.Lookup, UITypes.Lookup,
// UITypes.Count, // UITypes.Count,

10
packages/nocodb-sdk/src/lib/columnRules/QrCodeRules.ts

@ -0,0 +1,10 @@
import UITypes from '../UITypes';
export const AllowedColumnTypesForQrCode = [
UITypes.Formula,
UITypes.SingleLineText,
UITypes.LongText,
UITypes.PhoneNumber,
UITypes.URL,
UITypes.Email,
];

1
packages/nocodb-sdk/src/lib/columnRules/index.ts

@ -0,0 +1 @@
export * from './QrCodeRules';

61
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts

@ -44,6 +44,7 @@ import { NcError } from '../../../../meta/helpers/catchError';
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
import DOMPurify from 'isomorphic-dompurify'; import DOMPurify from 'isomorphic-dompurify';
import { sanitize, unsanitize } from './helpers/sanitize'; import { sanitize, unsanitize } from './helpers/sanitize';
import QrCodeColumn from '../../../../models/QrCodeColumn';
const GROUP_COL = '__nc_group_id'; const GROUP_COL = '__nc_group_id';
@ -1210,6 +1211,19 @@ class BaseModelSqlv2 {
}); });
} }
private async getSelectQueryBuilderForFormula(column: Column<any>) {
const formula = await column.getColOptions<FormulaColumn>();
if (formula.error) throw new Error(`Formula error: ${formula.error}`);
const selectQb = await formulaQueryBuilderv2(
formula.formula,
null,
this.dbDriver,
this.model
);
return selectQb;
}
async getProto() { async getProto() {
if (this._proto) { if (this._proto) {
return this._proto; return this._proto;
@ -1424,16 +1438,43 @@ class BaseModelSqlv2 {
case 'LinkToAnotherRecord': case 'LinkToAnotherRecord':
case 'Lookup': case 'Lookup':
break; break;
case 'QrCode': {
const qrCodeColumn = await column.getColOptions<QrCodeColumn>();
const qrValueColumn = await Column.get({
colId: qrCodeColumn.fk_qr_value_column_id,
});
// If the referenced value cannot be found: cancel current iteration
if (qrValueColumn == null) {
break;
}
switch (qrValueColumn.uidt) {
case UITypes.Formula:
try {
const selectQb = await this.getSelectQueryBuilderForFormula(
qrValueColumn
);
qb.select({
[column.column_name]: selectQb.builder,
});
} catch {
continue;
}
break;
default: {
qb.select({ [column.column_name]: qrValueColumn.column_name });
break;
}
}
break;
}
case 'Formula': case 'Formula':
{ {
const formula = await column.getColOptions<FormulaColumn>(); try {
if (formula.error) continue; const selectQb = await this.getSelectQueryBuilderForFormula(
const selectQb = await formulaQueryBuilderv2( column
formula.formula,
null,
this.dbDriver,
this.model
// this.aliasToColumn
); );
// todo: verify syntax of as ? / ?? // todo: verify syntax of as ? / ??
qb.select( qb.select(
@ -1442,6 +1483,9 @@ class BaseModelSqlv2 {
sanitize(column.title), sanitize(column.title),
]) ])
); );
} catch {
continue;
}
} }
break; break;
case 'Rollup': case 'Rollup':
@ -2172,6 +2216,7 @@ class BaseModelSqlv2 {
f.uidt !== UITypes.Rollup && f.uidt !== UITypes.Rollup &&
f.uidt !== UITypes.Lookup && f.uidt !== UITypes.Lookup &&
f.uidt !== UITypes.Formula && f.uidt !== UITypes.Formula &&
f.uidt !== UITypes.QrCode &&
f.uidt !== UITypes.SpecificDBType f.uidt !== UITypes.SpecificDBType
) )
.sort( .sort(

16
packages/nocodb/src/lib/meta/api/columnApis.ts

@ -490,6 +490,12 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
Tele.emit('evt', { evt_type: 'relation:created' }); Tele.emit('evt', { evt_type: 'relation:created' });
break; break;
case UITypes.QrCode:
await Column.insert({
...colBody,
fk_model_id: table.id,
});
break;
case UITypes.Formula: case UITypes.Formula:
colBody.formula = await substituteColumnAliasWithIdInFormula( colBody.formula = await substituteColumnAliasWithIdInFormula(
colBody.formula_raw || colBody.formula, colBody.formula_raw || colBody.formula,
@ -715,11 +721,17 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
UITypes.Rollup, UITypes.Rollup,
UITypes.LinkToAnotherRecord, UITypes.LinkToAnotherRecord,
UITypes.Formula, UITypes.Formula,
UITypes.QrCode,
UITypes.ForeignKey, UITypes.ForeignKey,
].includes(column.uidt) ].includes(column.uidt)
) { ) {
if (column.uidt === colBody.uidt) { if (column.uidt === colBody.uidt) {
if (column.uidt === UITypes.Formula) { if (column.uidt === UITypes.QrCode) {
await Column.update(column.id, {
...column,
...colBody,
});
} else if (column.uidt === UITypes.Formula) {
colBody.formula = await substituteColumnAliasWithIdInFormula( colBody.formula = await substituteColumnAliasWithIdInFormula(
colBody.formula_raw || colBody.formula, colBody.formula_raw || colBody.formula,
table.columns table.columns
@ -745,6 +757,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
UITypes.Rollup, UITypes.Rollup,
UITypes.LinkToAnotherRecord, UITypes.LinkToAnotherRecord,
UITypes.Formula, UITypes.Formula,
UITypes.QrCode,
UITypes.ForeignKey, UITypes.ForeignKey,
].includes(colBody.uidt) ].includes(colBody.uidt)
) { ) {
@ -1430,6 +1443,7 @@ export async function columnDelete(req: Request, res: Response<TableType>) {
switch (column.uidt) { switch (column.uidt) {
case UITypes.Lookup: case UITypes.Lookup:
case UITypes.Rollup: case UITypes.Rollup:
case UITypes.QrCode:
case UITypes.Formula: case UITypes.Formula:
await Column.delete(req.params.columnId); await Column.delete(req.params.columnId);
break; break;

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

@ -9,6 +9,7 @@ import * as nc_018_add_meta_in_view from './v2/nc_018_add_meta_in_view';
import * as nc_019_add_meta_in_meta_tables from './v2/nc_019_add_meta_in_meta_tables'; import * as nc_019_add_meta_in_meta_tables from './v2/nc_019_add_meta_in_meta_tables';
import * as nc_020_kanban_view from './v2/nc_020_kanban_view'; import * as nc_020_kanban_view from './v2/nc_020_kanban_view';
import * as nc_021_add_fields_in_token from './v2/nc_021_add_fields_in_token'; 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';
// Create a custom migration source class // Create a custom migration source class
export default class XcMigrationSourcev2 { export default class XcMigrationSourcev2 {
@ -29,6 +30,7 @@ export default class XcMigrationSourcev2 {
'nc_019_add_meta_in_meta_tables', 'nc_019_add_meta_in_meta_tables',
'nc_020_kanban_view', 'nc_020_kanban_view',
'nc_021_add_fields_in_token', 'nc_021_add_fields_in_token',
'nc_022_qr_code_column_type',
]); ]);
} }
@ -60,6 +62,8 @@ export default class XcMigrationSourcev2 {
return nc_020_kanban_view; return nc_020_kanban_view;
case 'nc_021_add_fields_in_token': case 'nc_021_add_fields_in_token':
return nc_021_add_fields_in_token; return nc_021_add_fields_in_token;
case 'nc_022_qr_code_column_type':
return nc_022_qr_code_column_type;
} }
} }
} }

26
packages/nocodb/src/lib/migrations/v2/nc_022_qr_code_column_type.ts

@ -0,0 +1,26 @@
import { MetaTable } from '../../utils/globals';
import { Knex } from 'knex';
const up = async (knex: Knex) => {
await knex.schema.createTable(MetaTable.COL_QRCODE, (table) => {
table.string('id', 20).primary().notNullable();
table.string('fk_column_id', 20);
table.foreign('fk_column_id').references(`${MetaTable.COLUMNS}.id`);
table.string('fk_qr_value_column_id', 20);
table
.foreign('fk_qr_value_column_id')
.references(`${MetaTable.COLUMNS}.id`);
table.boolean('deleted');
table.float('order');
table.timestamps(true, true);
});
};
const down = async (knex: Knex) => {
await knex.schema.dropTable(MetaTable.COL_QRCODE);
};
export { up, down };

62
packages/nocodb/src/lib/models/Column.ts

@ -5,7 +5,7 @@ import RollupColumn from './RollupColumn';
import SelectOption from './SelectOption'; import SelectOption from './SelectOption';
import Model from './Model'; import Model from './Model';
import NocoCache from '../cache/NocoCache'; import NocoCache from '../cache/NocoCache';
import { ColumnType, UITypes } from 'nocodb-sdk'; import { AllowedColumnTypesForQrCode, ColumnType, UITypes } from 'nocodb-sdk';
import { import {
CacheDelDirection, CacheDelDirection,
CacheGetType, CacheGetType,
@ -18,6 +18,7 @@ import Sort from './Sort';
import Filter from './Filter'; import Filter from './Filter';
import addFormulaErrorIfMissingColumn from '../meta/helpers/addFormulaErrorIfMissingColumn'; import addFormulaErrorIfMissingColumn from '../meta/helpers/addFormulaErrorIfMissingColumn';
import { NcError } from '../meta/helpers/catchError'; import { NcError } from '../meta/helpers/catchError';
import QrCodeColumn from './QrCodeColumn';
export default class Column<T = any> implements ColumnType { export default class Column<T = any> implements ColumnType {
public fk_model_id: string; public fk_model_id: string;
@ -219,6 +220,16 @@ export default class Column<T = any> implements ColumnType {
); );
break; break;
} }
case UITypes.QrCode: {
await QrCodeColumn.insert(
{
fk_column_id: colId,
fk_qr_value_column_id: column.fk_qr_value_column_id,
},
ncMeta
);
break;
}
case UITypes.Formula: { case UITypes.Formula: {
await FormulaColumn.insert( await FormulaColumn.insert(
{ {
@ -395,6 +406,9 @@ export default class Column<T = any> implements ColumnType {
case UITypes.Formula: case UITypes.Formula:
res = await FormulaColumn.read(this.id, ncMeta); res = await FormulaColumn.read(this.id, ncMeta);
break; break;
case UITypes.QrCode:
res = await QrCodeColumn.read(this.id, ncMeta);
break;
// default: // default:
// res = await DbColumn.read(this.id); // res = await DbColumn.read(this.id);
// break; // break;
@ -557,6 +571,21 @@ export default class Column<T = any> implements ColumnType {
// todo: or instead of delete reset related foreign key value to null and handle in BaseModel // todo: or instead of delete reset related foreign key value to null and handle in BaseModel
// get qr code columns and delete
{
const qrCodeCols = await ncMeta.metaList2(
null,
null,
MetaTable.COL_QRCODE,
{
condition: { fk_qr_value_column_id: id },
}
);
for (const qrCodeCol of qrCodeCols) {
await Column.delete(qrCodeCol.fk_column_id, ncMeta);
}
}
// get lookup columns and delete // get lookup columns and delete
{ {
let lookups = await NocoCache.getList(CacheScope.COL_LOOKUP, [id]); let lookups = await NocoCache.getList(CacheScope.COL_LOOKUP, [id]);
@ -691,6 +720,10 @@ export default class Column<T = any> implements ColumnType {
colOptionTableName = MetaTable.COL_FORMULA; colOptionTableName = MetaTable.COL_FORMULA;
cacheScopeName = CacheScope.COL_FORMULA; cacheScopeName = CacheScope.COL_FORMULA;
break; break;
case UITypes.QrCode:
colOptionTableName = MetaTable.COL_QRCODE;
cacheScopeName = CacheScope.COL_QRCODE;
break;
} }
if (colOptionTableName && cacheScopeName) { if (colOptionTableName && cacheScopeName) {
@ -855,6 +888,18 @@ export default class Column<T = any> implements ColumnType {
); );
break; break;
} }
case UITypes.QrCode: {
await ncMeta.metaDelete(null, null, MetaTable.COL_QRCODE, {
fk_column_id: colId,
});
await NocoCache.deepDel(
CacheScope.COL_QRCODE,
`${CacheScope.COL_QRCODE}:${colId}`,
CacheDelDirection.CHILD_TO_PARENT
);
break;
}
case UITypes.MultiSelect: case UITypes.MultiSelect:
case UITypes.SingleSelect: { case UITypes.SingleSelect: {
@ -904,6 +949,21 @@ export default class Column<T = any> implements ColumnType {
else updateObj.validate = JSON.stringify(column.validate); else updateObj.validate = JSON.stringify(column.validate);
} }
// get qr code columns and delete if target type is not supported by QR code column type
if (!AllowedColumnTypesForQrCode.includes(updateObj.uidt)) {
const qrCodeCols = await ncMeta.metaList2(
null,
null,
MetaTable.COL_QRCODE,
{
condition: { fk_qr_value_column_id: colId },
}
);
for (const qrCodeCol of qrCodeCols) {
await Column.delete(qrCodeCol.fk_column_id, ncMeta);
}
}
// get existing cache // get existing cache
const key = `${CacheScope.COLUMN}:${colId}`; const key = `${CacheScope.COLUMN}:${colId}`;
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT); let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);

67
packages/nocodb/src/lib/models/QrCodeColumn.ts

@ -0,0 +1,67 @@
import Noco from '../Noco';
import { CacheGetType, CacheScope, MetaTable } from '../utils/globals';
import NocoCache from '../cache/NocoCache';
import { extractProps } from '../meta/helpers/extractProps';
export default class QrCodeColumn {
fk_column_id: string;
fk_qr_value_column_id: string;
constructor(data: Partial<QrCodeColumn>) {
Object.assign(this, data);
}
public static async insert(
data: Partial<QrCodeColumn>,
ncMeta = Noco.ncMeta
) {
await ncMeta.metaInsert2(null, null, MetaTable.COL_QRCODE, {
fk_column_id: data.fk_column_id,
fk_qr_value_column_id: data.fk_qr_value_column_id,
});
return this.read(data.fk_column_id, ncMeta);
}
public static async read(columnId: string, ncMeta = Noco.ncMeta) {
let column =
columnId &&
(await NocoCache.get(
`${CacheScope.COL_QRCODE}:${columnId}`,
CacheGetType.TYPE_OBJECT
));
if (!column) {
column = await ncMeta.metaGet2(
null, //,
null, //model.db_alias,
MetaTable.COL_QRCODE,
{ fk_column_id: columnId }
);
await NocoCache.set(`${CacheScope.COL_QRCODE}:${columnId}`, column);
}
return column ? new QrCodeColumn(column) : null;
}
id: string;
static async update(
id: string,
qrCode: Partial<QrCodeColumn>,
ncMeta = Noco.ncMeta
) {
const updateObj = extractProps(qrCode, [
'fk_column_id',
'fk_qr_value_column_id',
]);
// get existing cache
const key = `${CacheScope.COL_QRCODE}:${id}`;
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
if (o) {
o = { ...o, ...updateObj };
// set cache
await NocoCache.set(key, o);
}
// set meta
await ncMeta.metaUpdate(null, null, MetaTable.COL_QRCODE, updateObj, id);
}
}

2
packages/nocodb/src/lib/utils/globals.ts

@ -10,6 +10,7 @@ export enum MetaTable {
COL_LOOKUP = 'nc_col_lookup_v2', COL_LOOKUP = 'nc_col_lookup_v2',
COL_ROLLUP = 'nc_col_rollup_v2', COL_ROLLUP = 'nc_col_rollup_v2',
COL_FORMULA = 'nc_col_formula_v2', COL_FORMULA = 'nc_col_formula_v2',
COL_QRCODE = 'nc_col_qrcode_v2',
FILTER_EXP = 'nc_filter_exp_v2', FILTER_EXP = 'nc_filter_exp_v2',
// HOOK_FILTER_EXP = 'nc_hook_filter_exp_v2', // HOOK_FILTER_EXP = 'nc_hook_filter_exp_v2',
SORT = 'nc_sort_v2', SORT = 'nc_sort_v2',
@ -112,6 +113,7 @@ export enum CacheScope {
COL_LOOKUP = 'colLookup', COL_LOOKUP = 'colLookup',
COL_ROLLUP = 'colRollup', COL_ROLLUP = 'colRollup',
COL_FORMULA = 'colFormula', COL_FORMULA = 'colFormula',
COL_QRCODE = 'colQRCode',
FILTER_EXP = 'filterExp', FILTER_EXP = 'filterExp',
SORT = 'sort', SORT = 'sort',
SHARED_VIEW = 'sharedView', SHARED_VIEW = 'sharedView',

52
packages/nocodb/tests/unit/factory/column.ts

@ -30,14 +30,16 @@ const defaultColumns = function(context) {
uidt: 'DateTime', uidt: 'DateTime',
}, },
{ {
cdf: isSqlite(context) ? 'CURRENT_TIMESTAMP': 'CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP', cdf: isSqlite(context)
? 'CURRENT_TIMESTAMP'
: 'CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP',
column_name: 'updated_at', column_name: 'updated_at',
title: 'UpdatedAt', title: 'UpdatedAt',
dtxp: '', dtxp: '',
dtxs: '', dtxs: '',
uidt: 'DateTime', uidt: 'DateTime',
}, },
] ];
}; };
const createColumn = async (context, table, columnAttr) => { const createColumn = async (context, table, columnAttr) => {
@ -152,6 +154,36 @@ const createLookupColumn = async (
return lookupColumn; return lookupColumn;
}; };
const createQrCodeColumn = async (
context,
{
title,
table,
referencedQrValueTableColumnTitle,
}: {
title: string;
table: Model;
referencedQrValueTableColumnTitle: string;
}
) => {
const referencedQrValueTableColumnId = await table
.getColumns()
.then(
(cols) =>
cols.find(
(column) => column.title == referencedQrValueTableColumnTitle
)['id']
);
const qrCodeColumn = await createColumn(context, table, {
title: title,
uidt: UITypes.QrCode,
column_name: title,
fk_qr_value_column_id: referencedQrValueTableColumnId,
});
return qrCodeColumn;
};
const createLtarColumn = async ( const createLtarColumn = async (
context, context,
{ {
@ -178,7 +210,10 @@ const createLtarColumn = async (
return ltarColumn; return ltarColumn;
}; };
const updateViewColumn = async (context, {view, column, attr}: {column: Column, view: View, attr: any}) => { const updateViewColumn = async (
context,
{ view, column, attr }: { column: Column; view: View; attr: any }
) => {
const res = await request(context.app) const res = await request(context.app)
.patch(`/api/v1/db/meta/views/${view.id}/columns/${column.id}`) .patch(`/api/v1/db/meta/views/${view.id}/columns/${column.id}`)
.set('xc-auth', context.token) .set('xc-auth', context.token)
@ -186,18 +221,19 @@ const updateViewColumn = async (context, {view, column, attr}: {column: Column,
...attr, ...attr,
}); });
const updatedColumn: FormViewColumn | GridViewColumn | GalleryViewColumn = (await view.getColumns()).find( const updatedColumn: FormViewColumn | GridViewColumn | GalleryViewColumn = (
(column) => column.id === column.id await view.getColumns()
)!; ).find((column) => column.id === column.id)!;
return updatedColumn; return updatedColumn;
} };
export { export {
defaultColumns, defaultColumns,
createColumn, createColumn,
createQrCodeColumn,
createRollupColumn, createRollupColumn,
createLookupColumn, createLookupColumn,
createLtarColumn, createLtarColumn,
updateViewColumn updateViewColumn,
}; };

2
packages/nocodb/tests/unit/rest/index.test.ts

@ -2,6 +2,7 @@ import 'mocha';
import authTests from './tests/auth.test'; import authTests from './tests/auth.test';
import orgTests from './tests/org.test'; import orgTests from './tests/org.test';
import projectTests from './tests/project.test'; import projectTests from './tests/project.test';
import columnTypeSpecificTests from './tests/columnTypeSpecific.test';
import tableTests from './tests/table.test'; import tableTests from './tests/table.test';
import tableRowTests from './tests/tableRow.test'; import tableRowTests from './tests/tableRow.test';
import viewRowTests from './tests/viewRow.test'; import viewRowTests from './tests/viewRow.test';
@ -13,6 +14,7 @@ function restTests() {
tableTests(); tableTests();
tableRowTests(); tableRowTests();
viewRowTests(); viewRowTests();
columnTypeSpecificTests();
} }
export default function () { export default function () {

96
packages/nocodb/tests/unit/rest/tests/columnTypeSpecific.test.ts

@ -0,0 +1,96 @@
import 'mocha';
import init from '../../init';
import { createProject, createSakilaProject } from '../../factory/project';
import request from 'supertest';
import { UITypes } from 'nocodb-sdk';
import { createQrCodeColumn, createColumn } from '../../factory/column';
import { getTable } from '../../factory/table';
import Model from '../../../../src/lib/models/Model';
import Project from '../../../../src/lib/models/Project';
import { expect } from 'chai';
import Column from '../../../../src/lib/models/Column';
import { title } from 'process';
function columnTypeSpecificTests() {
let context;
let project: Project;
let sakilaProject: Project;
let customerTable: Model;
let qrValueReferenceColumn: Column;
const qrValueReferenceColumnTitle = 'Qr Value Column';
const qrCodeReferenceColumnTitle = 'Qr Code Column';
beforeEach(async function () {
context = await init();
sakilaProject = await createSakilaProject(context);
project = await createProject(context);
customerTable = await getTable({
project: sakilaProject,
name: 'customer',
});
});
describe('Qr Code Column', () => {
beforeEach(async function () {
qrValueReferenceColumn = await createColumn(context, customerTable, {
title: qrValueReferenceColumnTitle,
uidt: UITypes.SingleLineText,
table_name: customerTable.table_name,
column_name: title,
});
});
describe('adding a QR code column which references another column ', async () => {
beforeEach(async function () {
await createQrCodeColumn(context, {
title: qrCodeReferenceColumnTitle,
table: customerTable,
referencedQrValueTableColumnTitle: qrValueReferenceColumnTitle,
});
});
it('delivers the same cell values as the referenced column', async () => {
const resp = await request(context.app)
.get(`/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}`)
.set('xc-auth', context.token)
.expect(200);
expect(resp.body.list[0][qrValueReferenceColumnTitle]).to.eql(
resp.body.list[0][qrCodeReferenceColumnTitle]
);
expect(
resp.body.list.map((row) => row[qrValueReferenceColumnTitle])
).to.eql(resp.body.list.map((row) => row[qrCodeReferenceColumnTitle]));
});
it('gets deleted if the referenced column gets deleted', async () => {
// delete referenced value column
const columnsBeforeReferencedColumnDeleted =
await customerTable.getColumns();
expect(
columnsBeforeReferencedColumnDeleted.some(
(col) => col['title'] === qrCodeReferenceColumnTitle
)
).to.eq(true);
const response = await request(context.app)
.delete(`/api/v1/db/meta/columns/${qrValueReferenceColumn.id}`)
.set('xc-auth', context.token)
.send({});
const columnsAfterReferencedColumnDeleted =
await customerTable.getColumns();
expect(
columnsAfterReferencedColumnDeleted.some(
(col) => col['title'] === qrCodeReferenceColumnTitle
)
).to.eq(false);
});
});
});
}
export default function () {
describe('Column types specific behavior', columnTypeSpecificTests);
}

1
tests/playwright/.gitignore vendored

@ -3,6 +3,7 @@ node_modules/
/playwright-report/ /playwright-report/
/playwright-report copy/ /playwright-report copy/
/playwright-report-quick/ /playwright-report-quick/
/playwright-report-stress/
/playwright/.cache/ /playwright/.cache/
.env .env
output output

21
tests/playwright/pages/Dashboard/Grid/Column/index.ts

@ -21,6 +21,7 @@ export class ColumnPageObject extends BasePage {
title, title,
type = 'SingleLineText', type = 'SingleLineText',
formula = '', formula = '',
qrCodeValueColumnTitle = '',
childTable = '', childTable = '',
childColumn = '', childColumn = '',
relationType = '', relationType = '',
@ -30,6 +31,7 @@ export class ColumnPageObject extends BasePage {
title: string; title: string;
type?: string; type?: string;
formula?: string; formula?: string;
qrCodeValueColumnTitle?: string;
childTable?: string; childTable?: string;
childColumn?: string; childColumn?: string;
relationType?: string; relationType?: string;
@ -70,6 +72,14 @@ export class ColumnPageObject extends BasePage {
case 'Formula': case 'Formula':
await this.get().locator('.nc-formula-input').fill(formula); await this.get().locator('.nc-formula-input').fill(formula);
break; break;
case 'QrCode':
await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage
.locator(`.ant-select-item`, {
hasText: qrCodeValueColumnTitle,
})
.click();
break;
case 'Lookup': case 'Lookup':
await this.get().locator('.ant-select-single').nth(1).click(); await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage await this.rootPage
@ -140,6 +150,17 @@ export class ColumnPageObject extends BasePage {
await this.rootPage.locator(`text=${type}`).nth(1).click(); await this.rootPage.locator(`text=${type}`).nth(1).click();
} }
async changeReferencedColumnForQrCode({ titleOfReferencedColumn }: { titleOfReferencedColumn: string }) {
await this.get().locator('.nc-qr-code-value-column-select .ant-select-single').click();
await this.rootPage
.locator(`.ant-select-item`, {
hasText: titleOfReferencedColumn,
})
.click();
await this.save();
}
async delete({ title }: { title: string }) { async delete({ title }: { title: string }) {
await this.grid.get().locator(`th[data-title="${title}"] >> svg.ant-dropdown-trigger`).click(); await this.grid.get().locator(`th[data-title="${title}"] >> svg.ant-dropdown-trigger`).click();
// await this.rootPage.locator('li[role="menuitem"]:has-text("Delete")').waitFor(); // await this.rootPage.locator('li[role="menuitem"]:has-text("Delete")').waitFor();

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

@ -5,11 +5,13 @@ import { CellPageObject } from '../common/Cell';
import { ColumnPageObject } from './Column'; import { ColumnPageObject } from './Column';
import { ToolbarPage } from '../common/Toolbar'; import { ToolbarPage } from '../common/Toolbar';
import { ProjectMenuObject } from '../common/ProjectMenu'; import { ProjectMenuObject } from '../common/ProjectMenu';
import { QrCodeOverlay } from '../QrCodeOverlay';
export class GridPage extends BasePage { export class GridPage extends BasePage {
readonly dashboard: DashboardPage; readonly dashboard: DashboardPage;
readonly addNewTableButton: Locator; readonly addNewTableButton: Locator;
readonly dashboardPage: DashboardPage; readonly dashboardPage: DashboardPage;
readonly qrCodeOverlay: QrCodeOverlay;
readonly column: ColumnPageObject; readonly column: ColumnPageObject;
readonly cell: CellPageObject; readonly cell: CellPageObject;
readonly toolbar: ToolbarPage; readonly toolbar: ToolbarPage;
@ -19,6 +21,7 @@ export class GridPage extends BasePage {
super(dashboardPage.rootPage); super(dashboardPage.rootPage);
this.dashboard = dashboardPage; this.dashboard = dashboardPage;
this.addNewTableButton = dashboardPage.get().locator('.nc-add-new-table'); this.addNewTableButton = dashboardPage.get().locator('.nc-add-new-table');
this.qrCodeOverlay = new QrCodeOverlay(this);
this.column = new ColumnPageObject(this); this.column = new ColumnPageObject(this);
this.cell = new CellPageObject(this); this.cell = new CellPageObject(this);
this.toolbar = new ToolbarPage(this); this.toolbar = new ToolbarPage(this);

27
tests/playwright/pages/Dashboard/QrCodeOverlay/index.ts

@ -0,0 +1,27 @@
import { expect } from '@playwright/test';
import BasePage from '../../Base';
import { FormPage } from '../Form';
import { GalleryPage } from '../Gallery';
import { GridPage } from '../Grid';
import { KanbanPage } from '../Kanban';
export class QrCodeOverlay extends BasePage {
constructor(parent: GridPage | GalleryPage | KanbanPage | FormPage) {
super(parent.rootPage);
}
get() {
return this.rootPage.locator(`.nc-qr-code-large`);
}
async verifyQrValueLabel(expectedValue: string) {
const foundQrValueLabelText = await this.get()
.locator('[data-testid="nc-qr-code-large-value-label"]')
.textContent();
await expect(foundQrValueLabelText).toContain(expectedValue);
}
async clickCloseButton() {
await this.get().locator('.ant-modal-close-x').click();
}
}

26
tests/playwright/pages/Dashboard/common/Cell/index.ts

@ -82,6 +82,32 @@ export class CellPageObject extends BasePage {
} }
} }
async verifyQrCodeCell({
index,
columnHeader,
expectedSrcValue,
}: {
index: number;
columnHeader: string;
expectedSrcValue: string;
}) {
const _verify = async expectedQrCodeImgSrc => {
await expect
.poll(async () => {
const qrCell = await this.get({
index,
columnHeader,
});
const qrImg = await qrCell.getByRole('img');
const qrImgSrc = await qrImg.getAttribute('src');
return qrImgSrc;
})
.toEqual(expectedQrCodeImgSrc);
};
await _verify(expectedSrcValue);
}
// todo: Improve param names (i.e value => values) // todo: Improve param names (i.e value => values)
// verifyVirtualCell // verifyVirtualCell
// : virtual relational cell- HM, BT, MM // : virtual relational cell- HM, BT, MM

34
tests/playwright/scripts/stressTestNewlyAddedTest.js

@ -0,0 +1,34 @@
// eslint-disable-next-line no-undef
const util = require('util');
// eslint-disable-next-line no-undef
const exec = util.promisify(require('child_process').exec);
// Get items from `git diff develop'
void (async () => {
const { stdout: allFileNames } = await exec('git diff --name-only origin/develop');
// return if no changed file ends with .js
const testFilesInChangedFiles = allFileNames.split('\n').filter(fileName => fileName.endsWith('.spec.ts'));
if (testFilesInChangedFiles.length === 0) {
console.log('No test file changed, skipping stress test');
return;
}
const { stdout } = await exec(`git diff origin/develop -- *.spec.ts **/*.spec.ts | grep test\\(`);
// eslint-disable-next-line no-undef
const dbType = process.env.E2E_DB_TYPE;
// get test names which is in the form of `+ test('test name', () => {'
const testNames = stdout
.match(/\+ {2}test\('(.*)',/g)
// extract test name by removing `+ test('` and `',*`
.map(testName => testName.replace("test('", '').trimEnd().slice(0, -2).slice(1, testName.length).trim());
console.log({ dbType, testNames });
// run all the tests by title using regex with exact match
const { stdout: pwStdout } = await exec(
`PLAYWRIGHT_HTML_REPORT=playwright-report-stress E2E_DB_TYPE=${dbType} npx playwright test --repeat-each=2 --workers=2 -g "${testNames.join(
'|'
)}"`
);
console.log('pwStdout:', pwStdout);
})();

151
tests/playwright/tests/columnQrCode.spec.ts

@ -0,0 +1,151 @@
import { test } from '@playwright/test';
import { DashboardPage } from '../pages/Dashboard';
import setup from '../setup';
import { GridPage } from '../pages/Dashboard/Grid';
type ExpectedQrCodeData = {
referencedValue: string;
base64EncodedSrc: string;
};
test.describe('Virtual Columns', () => {
let dashboard: DashboardPage;
let grid: GridPage;
let context: any;
test.beforeEach(async ({ page }) => {
context = await setup({ page });
dashboard = new DashboardPage(page, context.project);
grid = dashboard.grid;
});
test.describe('QrCode Column', () => {
async function qrCodeColumnVerify(qrColumnTitle: string, expectedQrCodeData: ExpectedQrCodeData[]) {
for (let i = 0; i < expectedQrCodeData.length; i++) {
await grid.cell.verifyQrCodeCell({
index: i,
columnHeader: qrColumnTitle,
expectedSrcValue: expectedQrCodeData[i].base64EncodedSrc,
});
}
}
test('creation, showing, updating value and change qr column title and reference column', async () => {
// Add qr code column referencing the City column
// and compare the base64 encoded codes/src attributes for the first 3 rows.
// Column data from City table (Sakila DB)
/**
* City LastUpdate Address List Country
* A Corua (La Corua) 2006-02-15 04:45:25 939 Probolinggo Loop Spain
* Abha 2006-02-15 04:45:25 733 Mandaluyong Place Saudi Arabia
* Abu Dhabi 2006-02-15 04:45:25 535 Ahmadnagar Manor United Arab Emirates
*/
const expectedQrCodeCellValues: ExpectedQrCodeData[] = [
{
referencedValue: 'A Corua (La Corua)',
base64EncodedSrc:
'data:image/png;base64,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',
},
{
referencedValue: 'Abha',
base64EncodedSrc:
'data:image/png;base64,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',
},
{
referencedValue: 'Abu Dhabi',
base64EncodedSrc:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAYAAAA8AXHiAAAAAXNSR0IArs4c6QAABtpJREFUeF7tndt22zoMBev//+icZXf12FYka7ZA6JJMn8HbYARStN3cvr6+vv74TwKDCdwUazBRu3sQUCxFaCGgWC1Y7VSxdKCFgGK1YLVTxdKBFgKK1YLVThVLB1oIKFYLVjtVLB1oIaBYLVjtVLF0oIWAYrVgtVPF0oEWAorVgtVOFUsHWggoVgtWO1UsHWghoFgtWO1UsXSghUCLWLfbrWWyWztd+r0Inedc+0rb+zpo+61rTtuN/k2NYoEMKBaANAlRLMBMsQAkxXoSoNuRYinWLAHPWOtiXPaMNXriS6jmqlAiFq1O1fUk81zXIovYY+zdzljVRFB0CTQaS+PoHJfeCs/IKFnTa6xiTchZsbaq9N5OsRTrQWB0tVQsxVIsUqST8xCNpXFkfv9iOvqk4+8x9uEVi94lzUGrnocqY9MkdryR7jU2HWcuTrEq9EBbxQKQaEhSaitVw4r1OSNVqWm+rVgTAhWpKfRqcitzrI5N16hYivUgkOwoW+X61WcsCq0jEbRPK9ZLlii0paeHJrx6xqLjJOsZ3adiKRZ1KtqOFEuxFOvVgY7/5z3ZOkY/kcnY1IQj+xzNx8M7yHr1jFVJGpjeI6T6yl+ZY3VsukavG7xuiM53inXA+Y5Cr1YNK9YByXUr/Kx3VWr68LgVuhX+nq2w8lTMPikzv8KufjvyyLfC0Xx+zVvhaHBXkaBjnpTlHmMf/lkhhUHjOqBdpc8jGU3HViyQDcUCkKbn16Nv3vMpf25xFQk65klZ7jH2bhWLLrojrvraTRNB45YO0B1rp31WX3AO2wrpAjviFGudqmKtM/oWoVjr0BRrnZFiDWS0oatHE89YE3KVj4k8Yz1htoi11fIztEvkOMN8zzoHxZrevzR8JHTW5HfOS7EUq8UvxVIsxWohoFgtWK1YinUdsapvVpWv43ZQGn0FcZ8jvZCkLKvM6Hwo35aKRWEsTbIKiS6exikWJdV8j6VYL4AX/q4QrRCUZfVhpPOhilmxACkrFoA0Paue8ftY1acvx/C5hWLlRE9ZseaWQbeEHMF7CzoOlb/6zQq6Hjpv2l81TrE2Xjco1mf1FEuxqsVptr1iKZZiTQmMfkW+90/PKm6FJ98KqwmaLo+Ksdfl7BXkv7MYPc/Dt0LFynci+vBQtooFckChW7HeCVixVuRSrCcgKxaoRPSJUizFehBInirg3/BDaMeb4tKZhrKgHzEt8aIPKeG9eKQ4+rNCCpMusgMarYLJWipyVNp2HNTncnOZt0LFehJQrBcbkg9jqURzcVasdXodjL7dJ7oVrifCrXCd0S5iJdOgSaN9ju6v676rsp3RilM981Hmu52xkgmNFmF0f4qVZPPlqqNjK0ymMlqE0f0pVpJNxdpG66VVss3Qlwzap1shSN/oCjO6PysWSOJMSMs91rap7N/qChWCzrFKj1ZBOo5iAVIUeiJBR59gKYshdD50DMUCpCh0xTrR4R3ktS2EiqBYeQqsWICZYgFIkxDFAswUC0DaQyy6xeTT3dYi+QC8IhFtm6yiwrJjPnTuLRWrAoNOPIlTrITWmFjFmnCkT/kVLmLpWsao9N6LYilWh1f+ZYopVfqUW7E++7hbxaIJqz4+ScKT2Oq8pu3p2JW4ZM6j86NYG7fCJGlzsRVhKl8SXJq3Yq1klCbs3k0SWxXJijWA4BUSpljvibZiWbFmf5RbvTv8cWJVgNCzRvWCtKMCV9Y992yNFqO6cR1+eK8AVqxn+hVr8igoVrU2/G2vWIr1IFB5oNwKXwgk5xz6DLsVuhUulurKk9sh1mipaX/JdkaZHbk9enhPMr9SgWnCq7ffdBzFak7ukdtwsjQqgmJd8IxFRaDbMO3PrRCQSi4U6dNH34TONjbA9X+IFav5YxWaDFo1aMI6rgH2Ok9RZtX50HF+9eF9DlKlglLoHdteMjat/pU+FWtCT7EqOj3bKpZiPQgkxwWinmIp1s8Ui9ifxCRvhUm/01i6ZY6uBF0vGKPneXjFqiSXHr5HQ0uSe+TYCdvR81SshP5LrBXrMzjFUizPWMQBz1iE0vcYt8IVbor1y8TattwxrarfbqBPMz13JfdG9EGhcWOIrvey2xlrfSp9EYrVx3apZ8UCzK1YANL0ornjT54kW0I+5byFFStnVm1hxQIErVgA0h4VK5+GLX4agZaK9dMguZ6cgGLlzGwBCCgWgGRITkCxcma2AAQUC0AyJCegWDkzWwACigUgGZITUKycmS0AAcUCkAzJCShWzswWgIBiAUiG5AQUK2dmC0BAsQAkQ3ICipUzswUgoFgAkiE5AcXKmdkCEFAsAMmQnIBi5cxsAQgoFoBkSE7gPxlL8reNvNsfAAAAAElFTkSuQmCC',
},
];
// close 'Team & Auth' tab
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.openTable({ title: 'City' });
await grid.column.create({
title: 'QrCode1',
type: 'QrCode',
qrCodeValueColumnTitle: 'City',
});
await qrCodeColumnVerify('QrCode1', expectedQrCodeCellValues);
// Clicking on qr code in first row and expect it shows a
// popup with an enlarged version of the qr code
await grid.cell.get({ columnHeader: 'QrCode1', index: 0 }).click();
const qrGridOverlay = grid.qrCodeOverlay;
await qrGridOverlay.verifyQrValueLabel(expectedQrCodeCellValues[0].referencedValue);
await qrGridOverlay.clickCloseButton();
// Change the value in the referenced column, first row
// and expect respective QR changes accordingly
await grid.cell.fillText({ columnHeader: 'City', index: 0, text: 'Hamburg' });
const expectedQrCodeCellValuesAfterCityNameChange = [
{
referencedValue: 'Hamburg',
base64EncodedSrc:
'data:image/png;base64,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',
},
...expectedQrCodeCellValues.slice(1),
];
await qrCodeColumnVerify('QrCode1', expectedQrCodeCellValuesAfterCityNameChange);
// Change the QR Code column title
await grid.column.openEdit({ title: 'QrCode1' });
await grid.column.fillTitle({ title: 'QrCode1 Renamed' });
await grid.column.save({ isUpdated: true });
await qrCodeColumnVerify('QrCode1 Renamed', expectedQrCodeCellValuesAfterCityNameChange);
// Change the referenced column title
await grid.column.openEdit({ title: 'City' });
await grid.column.fillTitle({ title: 'City Renamed' });
await grid.column.save({ isUpdated: true });
await qrCodeColumnVerify('QrCode1 Renamed', expectedQrCodeCellValuesAfterCityNameChange);
// Change to another referenced column
await grid.column.create({ title: 'New City Column' });
await grid.cell.fillText({ columnHeader: 'New City Column', index: 0, text: 'Hamburg' });
await grid.column.openEdit({ title: 'QrCode1 Renamed' });
await grid.column.changeReferencedColumnForQrCode({ titleOfReferencedColumn: 'New City Column' });
await qrCodeColumnVerify('QrCode1 Renamed', [
{
referencedValue: 'Hamburg',
base64EncodedSrc:
'data:image/png;base64,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',
},
]);
await dashboard.closeTab({ title: 'City' });
});
test('deletion of the QR column: directly and indirectly when the reference value column is deleted', async () => {
// close 'Team & Auth' tab
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.openTable({ title: 'City' });
await grid.column.create({ title: 'column_name_a' });
await grid.column.verify({ title: 'column_name_a' });
await grid.column.create({
title: 'QrCode2',
type: 'QrCode',
qrCodeValueColumnTitle: 'column_name_a',
});
await grid.column.verify({ title: 'QrCode2', isVisible: true });
await grid.column.delete({ title: 'QrCode2' });
await grid.column.verify({ title: 'QrCode2', isVisible: false });
await grid.column.create({
title: 'QrCode2',
type: 'QrCode',
qrCodeValueColumnTitle: 'column_name_a',
});
await grid.column.verify({ title: 'QrCode2', isVisible: true });
await grid.column.delete({ title: 'column_name_a' });
await grid.column.verify({ title: 'QrCode2', isVisible: false });
await dashboard.closeTab({ title: 'City' });
});
});
});

14
tests/playwright/tests/tableColumnOperation.spec.ts

@ -36,25 +36,13 @@ test.describe('Table Column Operations', () => {
columnTitle: 'Title', columnTitle: 'Title',
value: 'value_a', value: 'value_a',
}); });
await dashboard.expandedForm.save({ saveAndExitMode: true }); await dashboard.expandedForm.save();
await grid.cell.verify({ await grid.cell.verify({
index: 0, index: 0,
columnHeader: 'Title', columnHeader: 'Title',
value: 'value_a', value: 'value_a',
}); });
await grid.openExpandedRow({ index: 0 });
await dashboard.expandedForm.fillField({
columnTitle: 'Title',
value: 'value_a_a',
});
await dashboard.expandedForm.save({ saveAndExitMode: false });
await grid.cell.verify({
index: 0,
columnHeader: 'Title',
value: 'value_a_a',
});
await grid.deleteRow(0); await grid.deleteRow(0);
await grid.verifyRowDoesNotExist({ index: 0 }); await grid.verifyRowDoesNotExist({ index: 0 });

Loading…
Cancel
Save