Browse Source

Merge pull request #4641 from humannocode/barcode

Barcode
pull/4706/head
աɨռɢӄաօռɢ 2 years ago committed by GitHub
parent
commit
8bcb6041fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      packages/nc-gui/components/smartsheet/Form.vue
  2. 2
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  3. 120
      packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue
  4. 1
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  5. 2
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  6. 5
      packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue
  7. 3
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  8. 2
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  9. 6
      packages/nc-gui/components/virtual-cell/QrCode.vue
  10. 68
      packages/nc-gui/components/virtual-cell/barcode/Barcode.vue
  11. 39
      packages/nc-gui/components/virtual-cell/barcode/JsBarcodeWrapper.vue
  12. 1
      packages/nc-gui/composables/useSharedView.ts
  13. 1
      packages/nc-gui/composables/useViewData.ts
  14. 3
      packages/nc-gui/lang/de.json
  15. 6
      packages/nc-gui/lang/en.json
  16. 66
      packages/nc-gui/package-lock.json
  17. 1
      packages/nc-gui/package.json
  18. 6
      packages/nc-gui/utils/columnUtils.ts
  19. 1
      packages/nc-gui/utils/virtualCell.ts
  20. 13
      packages/noco-docs/content/en/setup-and-usages/column-types.md
  21. 1
      packages/nocodb-sdk/src/lib/UITypes.ts
  22. 2
      packages/nocodb-sdk/src/lib/columnRules/QrAndBarcodeRules.ts
  23. 2
      packages/nocodb-sdk/src/lib/columnRules/index.ts
  24. 113
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  25. 11
      packages/nocodb/src/lib/meta/api/columnApis.ts
  26. 6
      packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
  27. 26
      packages/nocodb/src/lib/migrations/v2/nc_024_barcode_column_type.ts
  28. 69
      packages/nocodb/src/lib/models/BarcodeColumn.ts
  29. 7
      packages/nocodb/src/lib/models/Base.ts
  30. 61
      packages/nocodb/src/lib/models/Column.ts
  31. 2
      packages/nocodb/src/lib/utils/globals.ts
  32. 31
      packages/nocodb/tests/unit/factory/column.ts
  33. 25
      tests/playwright/pages/Dashboard/BarcodeOverlay/index.ts
  34. 34
      tests/playwright/pages/Dashboard/Grid/Column/index.ts
  35. 3
      tests/playwright/pages/Dashboard/Grid/index.ts
  36. 42
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  37. 174
      tests/playwright/tests/columnBarcode.spec.ts

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

@ -33,7 +33,7 @@ provide(IsGalleryInj, ref(false))
// todo: generate hideCols based on default values
const hiddenCols = ['created_at', 'updated_at']
const hiddenColTypes = [UITypes.Rollup, UITypes.Lookup, UITypes.Formula, UITypes.QrCode, UITypes.SpecificDBType]
const hiddenColTypes = [UITypes.Rollup, UITypes.Lookup, UITypes.Formula, UITypes.QrCode, UITypes.Barcode, UITypes.SpecificDBType]
const state = useGlobal()
@ -229,7 +229,9 @@ async function addAllColumns() {
}
function shouldSkipColumn(col: Record<string, any>) {
return isDbRequired(col) || !!col.required || (!!col.rqd && !col.cdf) || col.uidt === UITypes.QrCode
return (
isDbRequired(col) || !!col.required || (!!col.rqd && !col.cdf) || col.uidt === UITypes.QrCode || col.uidt === UITypes.Barcode
)
}
async function removeAllColumns() {

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

@ -7,6 +7,7 @@ import {
IsFormInj,
RowInj,
inject,
isBarcode,
isBt,
isCount,
isFormula,
@ -59,6 +60,7 @@ function onNavigate(dir: NavigateDir, e: KeyboardEvent) {
<LazyVirtualCellRollup v-else-if="isRollup(column)" />
<LazyVirtualCellFormula v-else-if="isFormula(column)" />
<LazyVirtualCellQrCode v-else-if="isQrCode(column)" />
<LazyVirtualCellBarcode v-else-if="isBarcode(column)" />
<LazyVirtualCellCount v-else-if="isCount(column)" />
<LazyVirtualCellLookup v-else-if="isLookup(column)" />
</div>

120
packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue

@ -0,0 +1,120 @@
<script setup lang="ts">
import type { UITypes } from 'nocodb-sdk'
import { AllowedColumnTypesForQrAndBarcodes } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue'
import { onMounted, useVModel, watch } 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 columnsAllowedAsBarcodeValue = computed<SelectProps['options']>(() => {
return fields.value
?.filter(
(el) =>
el.fk_column_id && AllowedColumnTypesForQrAndBarcodes.includes(metaColumnById.value[el.fk_column_id].uidt as UITypes),
)
.map((field) => {
return {
value: field.fk_column_id,
label: field.title,
}
})
})
const supportedBarcodeFormats = [
{ value: 'CODE128', label: 'CODE128' },
{ value: 'upc', label: 'UPC' },
{ value: 'EAN13', label: 'EAN-13' },
{ value: 'EAN8', label: 'EAN-8' },
{ value: 'EAN5', label: 'EAN-5' },
{ value: 'EAN2', label: 'EAN-2' },
{ value: 'CODE39', label: 'CODE39' },
{ value: 'ITF14', label: 'ITF-14' },
{ value: 'MSI', label: 'MSI' },
{ value: 'PHARMACODE', label: 'pharmacode' },
{ value: 'CODABAR', label: 'codabar' },
]
onMounted(() => {
// set default value
vModel.value.meta = {
barcodeFormat: supportedBarcodeFormats[0].value,
...vModel.value.meta,
}
vModel.value.fk_barcode_value_column_id =
(column?.value?.colOptions as Record<string, any>)?.fk_barcode_value_column_id || columnsAllowedAsBarcodeValue.value?.[0]
})
watch(columnsAllowedAsBarcodeValue, (newColumnsAllowedAsBarcodeValue) => {
if (vModel.value.fk_barcode_value_column_id == null) {
vModel.value.fk_barcode_value_column_id = newColumnsAllowedAsBarcodeValue?.[0]?.value
}
})
setAdditionalValidations({
fk_barcode_value_column_id: [{ required: true, message: 'Required' }],
barcode_format: [{ required: true, message: 'Required' }],
})
const showBarcodeValueColumnInfoIcon = computed(() => !columnsAllowedAsBarcodeValue.value?.length)
</script>
<template>
<a-row>
<a-col :span="24">
<a-form-item
class="flex pb-2 nc-barcode-value-column-select flex-row"
:label="$t('labels.barcodeValueColumn')"
v-bind="validateInfos.fk_barcode_value_column_id"
>
<div class="flex w-1/2 flex-row items-center">
<a-select
v-model:value="vModel.fk_barcode_value_column_id"
:options="columnsAllowedAsBarcodeValue"
placeholder="Select a column for the Barcode value"
not-found-content="No valid Column Type can be found."
@click.stop
/>
<div v-if="showBarcodeValueColumnInfoIcon" class="pl-2">
<a-tooltip placement="bottom">
<template #title>
<span>
The valid Column Types for a Barcode Column are: Number, Single Line Text, Long Text, Phone Number, URL, Email,
Decimal. Please create one first.
</span>
</template>
<mdi-information class="cursor-pointer" />
</a-tooltip>
</div>
</div>
</a-form-item>
<a-form-item
class="flex w-1/2 pb-2 nc-barcode-format-select"
:label="$t('labels.barcodeFormat')"
v-bind="validateInfos.barcode_format"
>
<a-select
v-model:value="vModel.meta.barcodeFormat"
:options="supportedBarcodeFormats"
placeholder="Select a Barcode format"
@click.stop
/>
</a-form-item>
</a-col>
</a-row>
</template>

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

@ -171,6 +171,7 @@ useEventListener('keydown', (e: KeyboardEvent) => {
<LazySmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" v-model:value="formState" />
<LazySmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" />
<LazySmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" />
<LazySmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" 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" />

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

@ -26,7 +26,7 @@ const props = defineProps<{
const emit = defineEmits(['update:value'])
const uiTypesNotSupportedInFormulas = [UITypes.QrCode]
const uiTypesNotSupportedInFormulas = [UITypes.QrCode, UITypes.Barcode]
const vModel = useVModel(props, 'value', emit)

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { UITypes } from 'nocodb-sdk'
import { AllowedColumnTypesForQrCode } from 'nocodb-sdk'
import { AllowedColumnTypesForQrAndBarcodes } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue'
import { useVModel } from '#imports'
@ -25,7 +25,8 @@ const { setAdditionalValidations, validateInfos, column } = useColumnCreateStore
const columnsAllowedAsQrValue = computed<SelectProps['options']>(() => {
return fields.value
?.filter(
(el) => el.fk_column_id && AllowedColumnTypesForQrCode.includes(metaColumnById.value[el.fk_column_id].uidt as UITypes),
(el) =>
el.fk_column_id && AllowedColumnTypesForQrAndBarcodes.includes(metaColumnById.value[el.fk_column_id].uidt as UITypes),
)
.map((field) => {
return {

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

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

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

@ -21,7 +21,7 @@ const localValue = computed({
const options = computed<SelectProps['options']>(() =>
meta.value?.columns
?.filter((c: ColumnType) => {
if (c.uidt === UITypes.QrCode) {
if (c.uidt === UITypes.QrCode || c.uidt === UITypes.Barcode) {
return false
} else if (isSort) {
/** ignore hasmany and manytomany relations if it's using within sort menu */

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

@ -9,6 +9,8 @@ const qrValue = computed(() => String(cellValue?.value))
const tooManyCharsForQrCode = computed(() => qrValue?.value.length > maxNumberOfAllowedCharsForQrValue)
const showQrCode = computed(() => qrValue?.value?.length > 0 && !tooManyCharsForQrCode.value)
const qrCode = useQRCode(qrValue, {
width: 150,
})
@ -40,12 +42,12 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = us
<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" />
<img v-if="showQrCode" :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" />
<img v-if="showQrCode" :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>

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

@ -0,0 +1,68 @@
<script setup lang="ts">
import JsBarcodeWrapper from './JsBarcodeWrapper.vue'
const maxNumberOfAllowedCharsForBarcodeValue = 100
const cellValue = inject(CellValueInj)
const column = inject(ColumnInj)
const barcodeValue: ComputedRef<string> = computed(() => String(cellValue?.value || ''))
const tooManyCharsForBarcode = computed(() => barcodeValue.value.length > maxNumberOfAllowedCharsForBarcodeValue)
const modalVisible = ref(false)
const showBarcodeModal = () => {
modalVisible.value = true
}
const barcodeMeta = $computed(() => {
return {
barcodeFormat: 'CODE128',
...(column?.value?.meta || {}),
}
})
const handleModalOkClick = () => (modalVisible.value = false)
const showBarcode = computed(() => barcodeValue?.value.length > 0 && !tooManyCharsForBarcode.value)
const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = useShowNotEditableWarning()
</script>
<template>
<a-modal
v-model:visible="modalVisible"
:class="{ active: modalVisible }"
wrap-class-name="nc-barcode-large"
:body-style="{ padding: '0px' }"
:footer="null"
@ok="handleModalOkClick"
>
<JsBarcodeWrapper v-if="showBarcode" :barcode-value="barcodeValue" :barcode-format="barcodeMeta.barcodeFormat" />
</a-modal>
<JsBarcodeWrapper
v-if="showBarcode"
:barcode-value="barcodeValue"
:barcode-format="barcodeMeta.barcodeFormat"
class="nc-barcode-svg"
@on-click-barcode="showBarcodeModal"
>
<template #barcodeRenderError>
<div class="text-left text-wrap mt-2 text-[#e65100] text-xs" data-testid="barcode-invalid-input-message">
{{ $t('msg.warning.barcode.renderError') }}
</div>
</template>
</JsBarcodeWrapper>
<div v-if="tooManyCharsForBarcode" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('labels.barcodeValueTooLong') }}
</div>
<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.barcodeFieldsCannotBeDirectlyChanged') }}
</div>
</template>

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

@ -0,0 +1,39 @@
<script lang="ts" setup>
import JsBarcode from 'jsbarcode'
import { onMounted } from '#imports'
const props = defineProps({
barcodeValue: { type: String, required: true },
barcodeFormat: { type: String, required: true },
})
const emit = defineEmits(['onClickBarcode'])
const barcodeSvgRef = ref(null)
const errorForCurrentInput = ref(false)
const generate = () => {
try {
JsBarcode(barcodeSvgRef.value, String(props.barcodeValue), {
format: props.barcodeFormat,
})
errorForCurrentInput.value = false
} catch (e) {
console.log('e', e)
errorForCurrentInput.value = true
}
}
const onBarcodeClick = (ev: MouseEvent) => {
ev.stopPropagation()
emit('onClickBarcode')
}
watch([() => props.barcodeValue, () => props.barcodeFormat], generate)
onMounted(generate)
</script>
<template>
<svg v-show="!errorForCurrentInput" ref="barcodeSvgRef" class="w-full" data-testid="barcode" @click="onBarcodeClick"></svg>
<slot v-if="errorForCurrentInput" name="barcodeRenderError" />
</template>

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

@ -42,6 +42,7 @@ export function useSharedView() {
f.uidt !== UITypes.Rollup &&
f.uidt !== UITypes.Lookup &&
f.uidt !== UITypes.Formula &&
f.uidt !== UITypes.Barcode &&
f.uidt !== UITypes.QrCode,
)
.sort((a: Record<string, any>, b: Record<string, any>) => a.order - b.order)

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

@ -292,6 +292,7 @@ export function useViewData(
if (
col.uidt === UITypes.Formula ||
col.uidt === UITypes.QrCode ||
col.uidt === UITypes.Barcode ||
col.uidt === UITypes.Rollup ||
col.au ||
col.cdf?.includes(' on update ')

3
packages/nc-gui/lang/de.json

@ -498,7 +498,8 @@
"warning": {
"nonEditableFields": {
"computedFieldUnableToClear": "Warning: Computed field - unable to clear text",
"qrFieldsCannotBeDirectlyChanged": "Warning: QR fields cannot be directly changed."
"qrFieldsCannotBeDirectlyChanged": "Warning: QR fields cannot be directly changed.",
"barcodeFieldsCannotBeDirectlyChanged": "Warning: Barcode fields cannot be directly changed."
}
},
"info": {

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

@ -248,7 +248,10 @@
"sqlOutput": "SQL Output",
"addOption": "Add option",
"qrCodeValueColumn": "Column with QR code value",
"barcodeValueColumn": "Column with Barcode value",
"barcodeFormat": "Barcode format",
"qrCodeValueTooLong": "Too many characters for a QR code",
"barcodeValueTooLong": "Too many characters for a barcode",
"aggregateFunction": "Aggregate function",
"dbCreateIfNotExists": "Database : create if not exists",
"clientKey": "Client Key",
@ -496,6 +499,9 @@
},
"msg": {
"warning": {
"barcode": {
"renderError": "Barcode error - please check compatibility between input and barcode type"
},
"nonEditableFields": {
"computedFieldUnableToClear": "Warning: Computed field - unable to clear text",
"qrFieldsCannotBeDirectlyChanged": "Warning: QR fields cannot be directly changed."

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

@ -23,6 +23,7 @@
"dayjs": "^1.11.3",
"file-saver": "^2.0.5",
"httpsnippet": "^2.0.0",
"jsbarcode": "^3.11.5",
"jsep": "^1.3.6",
"just-clone": "^6.1.1",
"jwt-decode": "^3.1.2",
@ -10336,6 +10337,66 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsbarcode": {
"version": "3.11.5",
"resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.11.5.tgz",
"integrity": "sha512-zv3KsH51zD00I/LrFzFSM6dst7rDn0vIMzaiZFL7qusTjPZiPtxg3zxetp0RR7obmjTw4f6NyGgbdkBCgZUIrA==",
"bin": {
"auto.js": "bin/barcodes/CODE128/auto.js",
"Barcode.js": "bin/barcodes/Barcode.js",
"barcodes": "bin/barcodes",
"canvas.js": "bin/renderers/canvas.js",
"checksums.js": "bin/barcodes/MSI/checksums.js",
"codabar": "bin/barcodes/codabar",
"CODE128": "bin/barcodes/CODE128",
"CODE128_AUTO.js": "bin/barcodes/CODE128/CODE128_AUTO.js",
"CODE128.js": "bin/barcodes/CODE128/CODE128.js",
"CODE128A.js": "bin/barcodes/CODE128/CODE128A.js",
"CODE128B.js": "bin/barcodes/CODE128/CODE128B.js",
"CODE128C.js": "bin/barcodes/CODE128/CODE128C.js",
"CODE39": "bin/barcodes/CODE39",
"constants.js": "bin/barcodes/ITF/constants.js",
"defaults.js": "bin/options/defaults.js",
"EAN_UPC": "bin/barcodes/EAN_UPC",
"EAN.js": "bin/barcodes/EAN_UPC/EAN.js",
"EAN13.js": "bin/barcodes/EAN_UPC/EAN13.js",
"EAN2.js": "bin/barcodes/EAN_UPC/EAN2.js",
"EAN5.js": "bin/barcodes/EAN_UPC/EAN5.js",
"EAN8.js": "bin/barcodes/EAN_UPC/EAN8.js",
"encoder.js": "bin/barcodes/EAN_UPC/encoder.js",
"ErrorHandler.js": "bin/exceptions/ErrorHandler.js",
"exceptions": "bin/exceptions",
"exceptions.js": "bin/exceptions/exceptions.js",
"fixOptions.js": "bin/help/fixOptions.js",
"GenericBarcode": "bin/barcodes/GenericBarcode",
"getOptionsFromElement.js": "bin/help/getOptionsFromElement.js",
"getRenderProperties.js": "bin/help/getRenderProperties.js",
"help": "bin/help",
"index.js": "bin/renderers/index.js",
"index.tmp.js": "bin/barcodes/index.tmp.js",
"ITF": "bin/barcodes/ITF",
"ITF.js": "bin/barcodes/ITF/ITF.js",
"ITF14.js": "bin/barcodes/ITF/ITF14.js",
"JsBarcode.js": "bin/JsBarcode.js",
"linearizeEncodings.js": "bin/help/linearizeEncodings.js",
"merge.js": "bin/help/merge.js",
"MSI": "bin/barcodes/MSI",
"MSI.js": "bin/barcodes/MSI/MSI.js",
"MSI10.js": "bin/barcodes/MSI/MSI10.js",
"MSI1010.js": "bin/barcodes/MSI/MSI1010.js",
"MSI11.js": "bin/barcodes/MSI/MSI11.js",
"MSI1110.js": "bin/barcodes/MSI/MSI1110.js",
"object.js": "bin/renderers/object.js",
"options": "bin/options",
"optionsFromStrings.js": "bin/help/optionsFromStrings.js",
"pharmacode": "bin/barcodes/pharmacode",
"renderers": "bin/renderers",
"shared.js": "bin/renderers/shared.js",
"svg.js": "bin/renderers/svg.js",
"UPC.js": "bin/barcodes/EAN_UPC/UPC.js",
"UPCE.js": "bin/barcodes/EAN_UPC/UPCE.js"
}
},
"node_modules/jsdom": {
"version": "20.0.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.0.tgz",
@ -25172,6 +25233,11 @@
"argparse": "^2.0.1"
}
},
"jsbarcode": {
"version": "3.11.5",
"resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.11.5.tgz",
"integrity": "sha512-zv3KsH51zD00I/LrFzFSM6dst7rDn0vIMzaiZFL7qusTjPZiPtxg3zxetp0RR7obmjTw4f6NyGgbdkBCgZUIrA=="
},
"jsdom": {
"version": "20.0.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.0.tgz",

1
packages/nc-gui/package.json

@ -46,6 +46,7 @@
"dayjs": "^1.11.3",
"file-saver": "^2.0.5",
"httpsnippet": "^2.0.0",
"jsbarcode": "^3.11.5",
"jsep": "^1.3.6",
"just-clone": "^6.1.1",
"jwt-decode": "^3.1.2",

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 LinkVariant from '~icons/mdi/link-variant'
import QrCodeScan from '~icons/mdi/qrcode-scan'
import BarcodeScan from '~icons/mdi/barcode-scan'
import FormatColorText from '~icons/mdi/format-color-text'
import TextSubject from '~icons/mdi/text-subject'
import JSONIcon from '~icons/mdi/code-json'
@ -131,6 +132,11 @@ const uiTypes = [
icon: QrCodeScan,
virtual: 1,
},
{
name: UITypes.Barcode,
icon: BarcodeScan,
virtual: 1,
},
{
name: UITypes.Geometry,
icon: RulerSquareCompass,

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

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

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

@ -33,6 +33,7 @@ menuTitle: 'Column Types'
|[Rating](#rating)| Rating |
|[Formula](#formula)| Formula based generated column |
|[QR Code](#qr-code)| QR Code visualization of another referenced column |
|[Barcode](#barcode)| Barcode visualization of another referenced column |
| [Count](#count) | |
|[DateTime](#datetime)| Date & Time selector |
|[CreateTime](#createtime)| |
@ -279,6 +280,18 @@ Encodes the value of a reference column as QR code. The following column types a
* Email
Since it's a virtual column, the cell content (QR code) cannot be changed directly.
### Barcode
Encodes the value of a reference column as Barcode. Supported barcode formats: CODE128, EAN, EAN-13, EAN-8, EAN-5, EAN-2, UPC (A), CODE39, ITF-14, MSI, Pharmacode, Codabar. 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 (Barcode) cannot be changed directly.
### Count
#### Available Database Types

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

@ -52,6 +52,7 @@ export function isVirtualCol(
UITypes.LinkToAnotherRecord,
UITypes.Formula,
UITypes.QrCode,
UITypes.Barcode,
UITypes.Rollup,
UITypes.Lookup,
// UITypes.Count,

2
packages/nocodb-sdk/src/lib/columnRules/QrCodeRules.ts → packages/nocodb-sdk/src/lib/columnRules/QrAndBarcodeRules.ts

@ -1,6 +1,6 @@
import UITypes from '../UITypes';
export const AllowedColumnTypesForQrCode = [
export const AllowedColumnTypesForQrAndBarcodes = [
UITypes.Formula,
UITypes.SingleLineText,
UITypes.LongText,

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

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

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

@ -45,6 +45,7 @@ import { customAlphabet } from 'nanoid';
import DOMPurify from 'isomorphic-dompurify';
import { sanitize, unsanitize } from './helpers/sanitize';
import QrCodeColumn from '../../../../models/QrCodeColumn';
import BarcodeColumn from '../../../../models/BarcodeColumn';
const GROUP_COL = '__nc_group_id';
@ -100,7 +101,7 @@ class BaseModelSqlv2 {
qb.where(_wherePk(this.model.primaryKeys, id));
let data = (await this.execAndParse(qb))?.[0];
const data = (await this.execAndParse(qb))?.[0];
if (data) {
const proto = await this.getProto();
@ -158,7 +159,7 @@ class BaseModelSqlv2 {
qb.orderBy(this.model.primaryKey.column_name);
}
let data = await qb.first();
const data = await qb.first();
if (data) {
const proto = await this.getProto();
@ -252,7 +253,7 @@ class BaseModelSqlv2 {
if (!ignoreViewFilterAndSort) applyPaginate(qb, rest);
const proto = await this.getProto();
let data = await this.execAndParse(qb);
const data = await this.execAndParse(qb);
return data?.map((d) => {
d.__proto__ = proto;
@ -320,7 +321,8 @@ class BaseModelSqlv2 {
as: 'count',
}).first();
const res = (await this.dbDriver.raw(unsanitize(qb.toQuery()))) as any;
return ((this.isPg || this.isSnowflake) ? res.rows[0] : res[0][0] ?? res[0]).count;
return (this.isPg || this.isSnowflake ? res.rows[0] : res[0][0] ?? res[0])
.count;
}
// todo: add support for sortArrJson and filterArrJson
@ -423,7 +425,7 @@ class BaseModelSqlv2 {
.as('list')
);
let children = await this.execAndParse(childQb, childTable);
const children = await this.execAndParse(childQb, childTable);
const proto = await (
await Model.getBaseModelSQL({
id: childTable.id,
@ -550,7 +552,7 @@ class BaseModelSqlv2 {
await childModel.selectObject({ qb });
let children = await this.execAndParse(qb, childTable);
const children = await this.execAndParse(qb, childTable);
const proto = await (
await Model.getBaseModelSQL({
@ -735,7 +737,7 @@ class BaseModelSqlv2 {
qb.limit(+rest?.limit || 25);
qb.offset(+rest?.offset || 0);
let children = await this.execAndParse(qb, childTable);
const children = await this.execAndParse(qb, childTable);
const proto = await (
await Model.getBaseModelSQL({ id: rtnId, dbDriver: this.dbDriver })
).getProto();
@ -961,7 +963,7 @@ class BaseModelSqlv2 {
applyPaginate(qb, rest);
const proto = await childModel.getProto();
let data = await qb;
const data = await qb;
return data.map((c) => {
c.__proto__ = proto;
return c;
@ -1075,7 +1077,7 @@ class BaseModelSqlv2 {
applyPaginate(qb, rest);
const proto = await childModel.getProto();
let data = await this.execAndParse(qb, childTable);
const data = await this.execAndParse(qb, childTable);
return data.map((c) => {
c.__proto__ = proto;
@ -1193,7 +1195,7 @@ class BaseModelSqlv2 {
applyPaginate(qb, rest);
const proto = await parentModel.getProto();
let data = await this.execAndParse(qb, childTable);
const data = await this.execAndParse(qb, childTable);
return data.map((c) => {
c.__proto__ = proto;
@ -1460,6 +1462,40 @@ class BaseModelSqlv2 {
break;
}
case 'Barcode': {
const barcodeColumn = await column.getColOptions<BarcodeColumn>();
const barcodeValueColumn = await Column.get({
colId: barcodeColumn.fk_barcode_value_column_id,
});
// If the referenced value cannot be found: cancel current iteration
if (barcodeValueColumn == null) {
break;
}
switch (barcodeValueColumn.uidt) {
case UITypes.Formula:
try {
const selectQb = await this.getSelectQueryBuilderForFormula(
barcodeValueColumn
);
qb.select({
[column.column_name]: selectQb.builder,
});
} catch {
continue;
}
break;
default: {
qb.select({
[column.column_name]: barcodeValueColumn.column_name,
});
break;
}
}
break;
}
case 'Formula':
{
try {
@ -1558,10 +1594,11 @@ class BaseModelSqlv2 {
.max(ai.column_name, { as: 'id' })
)[0].id;
} else if (this.isSnowflake) {
id = ((
await this.dbDriver(this.tnPath)
.max(ai.column_name, { as: 'id' })
) as any)[0].id;
id = (
(await this.dbDriver(this.tnPath).max(ai.column_name, {
as: 'id',
})) as any
)[0].id;
}
response = await this.readByPk(id);
} else {
@ -1677,7 +1714,11 @@ class BaseModelSqlv2 {
if (this.isMssql && schema) {
return this.dbDriver.raw('??.??', [schema, tb.table_name]);
} else if (this.isSnowflake) {
return [this.dbDriver.client.config.connection.database, this.dbDriver.client.config.connection.schema, tb.table_name].join('.');
return [
this.dbDriver.client.config.connection.database,
this.dbDriver.client.config.connection.schema,
tb.table_name,
].join('.');
} else {
return tb.table_name;
}
@ -1817,10 +1858,11 @@ class BaseModelSqlv2 {
.max(ai.column_name, { as: 'id' })
)[0].id;
} else if (this.isSnowflake) {
id = ((
await this.dbDriver(this.tnPath)
.max(ai.column_name, { as: 'id' })
) as any).rows[0].id;
id = (
(await this.dbDriver(this.tnPath).max(ai.column_name, {
as: 'id',
})) as any
).rows[0].id;
}
response = await this.readByPk(id);
} else {
@ -2010,9 +2052,7 @@ class BaseModelSqlv2 {
const res = [];
for (const d of deleteIds) {
if (Object.keys(d).length) {
const response = await transaction(this.tnPath)
.del()
.where(d);
const response = await transaction(this.tnPath).del().where(d);
res.push(response);
}
}
@ -2234,6 +2274,7 @@ class BaseModelSqlv2 {
f.uidt !== UITypes.Lookup &&
f.uidt !== UITypes.Formula &&
f.uidt !== UITypes.QrCode &&
f.uidt !== UITypes.Barcode &&
f.uidt !== UITypes.SpecificDBType
)
.sort(
@ -2383,20 +2424,19 @@ class BaseModelSqlv2 {
if (this.isSnowflake) {
const parentPK = this.dbDriver(parentTn)
.select(parentColumn.column_name)
.where(_wherePk(parentTable.primaryKeys, childId))
.first();
.select(parentColumn.column_name)
.where(_wherePk(parentTable.primaryKeys, childId))
.first();
const childPK = this.dbDriver(childTn)
.select(childColumn.column_name)
.where(_wherePk(childTable.primaryKeys, rowId))
.first();
.select(childColumn.column_name)
.where(_wherePk(childTable.primaryKeys, rowId))
.first();
await this.dbDriver.raw(`INSERT INTO ?? (??, ??) SELECT (${parentPK.toQuery()}), (${childPK.toQuery()})`, [
vTn,
vParentCol.column_name,
vChildCol.column_name,
])
await this.dbDriver.raw(
`INSERT INTO ?? (??, ??) SELECT (${parentPK.toQuery()}), (${childPK.toQuery()})`,
[vTn, vParentCol.column_name, vChildCol.column_name]
);
} else {
await this.dbDriver(vTn).insert({
[vParentCol.column_name]: this.dbDriver(parentTn)
@ -2697,7 +2737,7 @@ class BaseModelSqlv2 {
const proto = await this.getProto();
let data = await groupedQb;
const data = await groupedQb;
const result = data?.map((d) => {
d.__proto__ = proto;
return d;
@ -2800,10 +2840,7 @@ class BaseModelSqlv2 {
return await qb;
}
private async execAndParse(
qb: Knex.QueryBuilder,
childTable?: Model
) {
private async execAndParse(qb: Knex.QueryBuilder, childTable?: Model) {
let query = qb.toQuery();
if (!this.isPg && !this.isMssql && !this.isSnowflake) {
query = unsanitize(qb.toQuery());

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

@ -512,6 +512,12 @@ export async function columnAdd(
fk_model_id: table.id,
});
break;
case UITypes.Barcode:
await Column.insert({
...colBody,
fk_model_id: table.id,
});
break;
case UITypes.Formula:
colBody.formula = await substituteColumnAliasWithIdInFormula(
colBody.formula_raw || colBody.formula,
@ -738,11 +744,12 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
UITypes.LinkToAnotherRecord,
UITypes.Formula,
UITypes.QrCode,
UITypes.Barcode,
UITypes.ForeignKey,
].includes(column.uidt)
) {
if (column.uidt === colBody.uidt) {
if (column.uidt === UITypes.QrCode) {
if ([UITypes.QrCode, UITypes.Barcode].includes(column.uidt)) {
await Column.update(column.id, {
...column,
...colBody,
@ -774,6 +781,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
UITypes.LinkToAnotherRecord,
UITypes.Formula,
UITypes.QrCode,
UITypes.Barcode,
UITypes.ForeignKey,
].includes(colBody.uidt)
) {
@ -1460,6 +1468,7 @@ export async function columnDelete(req: Request, res: Response<TableType>) {
case UITypes.Lookup:
case UITypes.Rollup:
case UITypes.QrCode:
case UITypes.Barcode:
case UITypes.Formula:
await Column.delete(req.params.columnId);
break;

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

@ -11,6 +11,7 @@ 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_022_qr_code_column_type from './v2/nc_022_qr_code_column_type';
import * as nc_023_multiple_source from './v2/nc_023_multiple_source';
import * as nc_024_barcode_column_type from './v2/nc_024_barcode_column_type';
// Create a custom migration source class
export default class XcMigrationSourcev2 {
@ -32,7 +33,8 @@ export default class XcMigrationSourcev2 {
'nc_020_kanban_view',
'nc_021_add_fields_in_token',
'nc_022_qr_code_column_type',
'nc_023_multiple_source'
'nc_023_multiple_source',
'nc_024_barcode_column_type',
]);
}
@ -68,6 +70,8 @@ export default class XcMigrationSourcev2 {
return nc_022_qr_code_column_type;
case 'nc_023_multiple_source':
return nc_023_multiple_source;
case 'nc_024_barcode_column_type':
return nc_024_barcode_column_type;
}
}
}

26
packages/nocodb/src/lib/migrations/v2/nc_024_barcode_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_BARCODE, (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_barcode_value_column_id', 20);
table
.foreign('fk_barcode_value_column_id')
.references(`${MetaTable.COLUMNS}.id`);
table.string('barcode_format', 15);
table.boolean('deleted');
table.timestamps(true, true);
});
};
const down = async (knex: Knex) => {
await knex.schema.dropTable(MetaTable.COL_BARCODE);
};
export { up, down };

69
packages/nocodb/src/lib/models/BarcodeColumn.ts

@ -0,0 +1,69 @@
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 BarcodeColumn {
id: string;
fk_column_id: string;
fk_barcode_value_column_id: string;
barcode_format: string;
constructor(data: Partial<BarcodeColumn>) {
Object.assign(this, data);
}
public static async insert(
data: Partial<BarcodeColumn>,
ncMeta = Noco.ncMeta
) {
await ncMeta.metaInsert2(null, null, MetaTable.COL_BARCODE, {
fk_column_id: data.fk_column_id,
fk_barcode_value_column_id: data.fk_barcode_value_column_id,
barcode_format: data.barcode_format,
});
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_BARCODE}:${columnId}`,
CacheGetType.TYPE_OBJECT
));
if (!column) {
column = await ncMeta.metaGet2(
null, //,
null, //model.db_alias,
MetaTable.COL_BARCODE,
{ fk_column_id: columnId }
);
await NocoCache.set(`${CacheScope.COL_BARCODE}:${columnId}`, column);
}
return column ? new BarcodeColumn(column) : null;
}
static async update(
id: string,
barcode: Partial<BarcodeColumn>,
ncMeta = Noco.ncMeta
) {
const updateObj = extractProps(barcode, [
'fk_column_id',
'fk_barcode_value_column_id',
'barcode_format',
]);
// get existing cache
const key = `${CacheScope.COL_BARCODE}:${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_BARCODE, updateObj, id);
}
}

7
packages/nocodb/src/lib/models/Base.ts

@ -164,9 +164,7 @@ export default class Base implements BaseType {
await NocoCache.setList(CacheScope.BASE, [args.projectId], baseDataList);
}
baseDataList.sort(
(a, b) => (a.order ?? Infinity) - (b.order ?? Infinity)
);
baseDataList.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity));
return baseDataList?.map((baseData) => {
return new Base(baseData);
@ -206,7 +204,6 @@ export default class Base implements BaseType {
// update order for bases
for (const [i, b] of Object.entries(bases)) {
await NocoCache.deepDel(
CacheScope.BASE,
`${CacheScope.BASE}:${b.id}`,
@ -220,7 +217,7 @@ export default class Base implements BaseType {
null,
MetaTable.BASES,
{
order: b.order
order: b.order,
},
b.id
);

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

@ -6,8 +6,8 @@ import SelectOption from './SelectOption';
import Model from './Model';
import NocoCache from '../cache/NocoCache';
import {
AllowedColumnTypesForQrAndBarcodes,
ColumnReqType,
AllowedColumnTypesForQrCode,
ColumnType,
UITypes,
} from 'nocodb-sdk';
@ -24,6 +24,7 @@ import Filter from './Filter';
import addFormulaErrorIfMissingColumn from '../meta/helpers/addFormulaErrorIfMissingColumn';
import { NcError } from '../meta/helpers/catchError';
import QrCodeColumn from './QrCodeColumn';
import BarcodeColumn from './BarcodeColumn';
export default class Column<T = any> implements ColumnType {
public fk_model_id: string;
@ -234,6 +235,17 @@ export default class Column<T = any> implements ColumnType {
);
break;
}
case UITypes.Barcode: {
await BarcodeColumn.insert(
{
fk_column_id: colId,
fk_barcode_value_column_id: column.fk_barcode_value_column_id,
barcode_format: column.barcode_format,
},
ncMeta
);
break;
}
case UITypes.Formula: {
await FormulaColumn.insert(
{
@ -413,6 +425,9 @@ export default class Column<T = any> implements ColumnType {
case UITypes.QrCode:
res = await QrCodeColumn.read(this.id, ncMeta);
break;
case UITypes.Barcode:
res = await BarcodeColumn.read(this.id, ncMeta);
break;
// default:
// res = await DbColumn.read(this.id);
// break;
@ -590,6 +605,20 @@ export default class Column<T = any> implements ColumnType {
}
}
{
const barcodeCols = await ncMeta.metaList2(
null,
null,
MetaTable.COL_BARCODE,
{
condition: { fk_barcode_value_column_id: id },
}
);
for (const barcodeCol of barcodeCols) {
await Column.delete(barcodeCol.fk_column_id, ncMeta);
}
}
// get lookup columns and delete
{
let lookups = await NocoCache.getList(CacheScope.COL_LOOKUP, [id]);
@ -728,6 +757,10 @@ export default class Column<T = any> implements ColumnType {
colOptionTableName = MetaTable.COL_QRCODE;
cacheScopeName = CacheScope.COL_QRCODE;
break;
case UITypes.Barcode:
colOptionTableName = MetaTable.COL_BARCODE;
cacheScopeName = CacheScope.COL_BARCODE;
break;
}
if (colOptionTableName && cacheScopeName) {
@ -905,6 +938,19 @@ export default class Column<T = any> implements ColumnType {
break;
}
case UITypes.Barcode: {
await ncMeta.metaDelete(null, null, MetaTable.COL_BARCODE, {
fk_column_id: colId,
});
await NocoCache.deepDel(
CacheScope.COL_BARCODE,
`${CacheScope.COL_BARCODE}:${colId}`,
CacheDelDirection.CHILD_TO_PARENT
);
break;
}
case UITypes.MultiSelect:
case UITypes.SingleSelect: {
await ncMeta.metaDelete(null, null, MetaTable.COL_SELECT_OPTIONS, {
@ -954,7 +1000,7 @@ export default class Column<T = any> implements ColumnType {
}
// get qr code columns and delete if target type is not supported by QR code column type
if (!AllowedColumnTypesForQrCode.includes(updateObj.uidt)) {
if (!AllowedColumnTypesForQrAndBarcodes.includes(updateObj.uidt)) {
const qrCodeCols = await ncMeta.metaList2(
null,
null,
@ -963,9 +1009,20 @@ export default class Column<T = any> implements ColumnType {
condition: { fk_qr_value_column_id: colId },
}
);
const barcodeCols = await ncMeta.metaList2(
null,
null,
MetaTable.COL_BARCODE,
{
condition: { fk_barcode_value_column_id: colId },
}
);
for (const qrCodeCol of qrCodeCols) {
await Column.delete(qrCodeCol.fk_column_id, ncMeta);
}
for (const barcodeCol of barcodeCols) {
await Column.delete(barcodeCol.fk_column_id, ncMeta);
}
}
// get existing cache

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

@ -11,6 +11,7 @@ export enum MetaTable {
COL_ROLLUP = 'nc_col_rollup_v2',
COL_FORMULA = 'nc_col_formula_v2',
COL_QRCODE = 'nc_col_qrcode_v2',
COL_BARCODE = 'nc_col_barcode_v2',
FILTER_EXP = 'nc_filter_exp_v2',
// HOOK_FILTER_EXP = 'nc_hook_filter_exp_v2',
SORT = 'nc_sort_v2',
@ -114,6 +115,7 @@ export enum CacheScope {
COL_ROLLUP = 'colRollup',
COL_FORMULA = 'colFormula',
COL_QRCODE = 'colQRCode',
COL_BARCODE = 'colBarcode',
FILTER_EXP = 'filterExp',
SORT = 'sort',
SHARED_VIEW = 'sharedView',

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

@ -184,6 +184,36 @@ const createQrCodeColumn = async (
return qrCodeColumn;
};
const createBarcodeColumn = async (
context,
{
title,
table,
referencedBarcodeValueTableColumnTitle,
}: {
title: string;
table: Model;
referencedBarcodeValueTableColumnTitle: string;
}
) => {
const referencedBarcodeValueTableColumnId = await table
.getColumns()
.then(
(cols) =>
cols.find(
(column) => column.title == referencedBarcodeValueTableColumnTitle
)['id']
);
const barcodeColumn = await createColumn(context, table, {
title: title,
uidt: UITypes.Barcode,
column_name: title,
fk_barcode_value_column_id: referencedBarcodeValueTableColumnId,
});
return barcodeColumn;
};
const createLtarColumn = async (
context,
{
@ -232,6 +262,7 @@ export {
defaultColumns,
createColumn,
createQrCodeColumn,
createBarcodeColumn,
createRollupColumn,
createLookupColumn,
createLtarColumn,

25
tests/playwright/pages/Dashboard/BarcodeOverlay/index.ts

@ -0,0 +1,25 @@
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 BarcodeOverlay extends BasePage {
constructor(parent: GridPage | GalleryPage | KanbanPage | FormPage) {
super(parent.rootPage);
}
get() {
return this.rootPage.locator(`.nc-barcode-large`);
}
async verifyBarcodeSvgValue(expectedValue: string) {
const foundBarcodeSvg = await this.get().getByTestId('barcode').innerHTML();
await expect(foundBarcodeSvg).toContain(expectedValue);
}
async clickCloseButton() {
await this.get().locator('.ant-modal-close-x').click();
}
}

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

@ -30,6 +30,8 @@ export class ColumnPageObject extends BasePage {
type = 'SingleLineText',
formula = '',
qrCodeValueColumnTitle = '',
barcodeValueColumnTitle = '',
barcodeFormat = '',
childTable = '',
childColumn = '',
relationType = '',
@ -44,6 +46,8 @@ export class ColumnPageObject extends BasePage {
type?: string;
formula?: string;
qrCodeValueColumnTitle?: string;
barcodeValueColumnTitle?: string;
barcodeFormat?: string;
childTable?: string;
childColumn?: string;
relationType?: string;
@ -113,6 +117,14 @@ export class ColumnPageObject extends BasePage {
})
.click();
break;
case 'Barcode':
await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage
.locator(`.ant-select-item`, {
hasText: new RegExp(`^${barcodeValueColumnTitle}$`),
})
.click();
break;
case 'Lookup':
await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage
@ -218,6 +230,28 @@ export class ColumnPageObject extends BasePage {
await this.save();
}
async changeReferencedColumnForBarcode({ titleOfReferencedColumn }: { titleOfReferencedColumn: string }) {
await this.get().locator('.nc-barcode-value-column-select .ant-select-single').click();
await this.rootPage
.locator(`.ant-select-item`, {
hasText: titleOfReferencedColumn,
})
.click();
await this.save();
}
async changeBarcodeFormat({ barcodeFormatName }: { barcodeFormatName: string }) {
await this.get().locator('.nc-barcode-format-select .ant-select-single').click();
await this.rootPage
.locator(`.ant-select-item`, {
hasText: barcodeFormatName,
})
.click();
await this.save();
}
async delete({ title }: { title: string }) {
await this.getColumnHeader(title).locator('svg.ant-dropdown-trigger').click();
// await this.rootPage.locator('li[role="menuitem"]:has-text("Delete")').waitFor();

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

@ -6,12 +6,14 @@ import { ColumnPageObject } from './Column';
import { ToolbarPage } from '../common/Toolbar';
import { ProjectMenuObject } from '../common/ProjectMenu';
import { QrCodeOverlay } from '../QrCodeOverlay';
import { BarcodeOverlay } from '../BarcodeOverlay';
export class GridPage extends BasePage {
readonly dashboard: DashboardPage;
readonly addNewTableButton: Locator;
readonly dashboardPage: DashboardPage;
readonly qrCodeOverlay: QrCodeOverlay;
readonly barcodeOverlay: BarcodeOverlay;
readonly column: ColumnPageObject;
readonly cell: CellPageObject;
readonly toolbar: ToolbarPage;
@ -22,6 +24,7 @@ export class GridPage extends BasePage {
this.dashboard = dashboardPage;
this.addNewTableButton = dashboardPage.get().locator('.nc-add-new-table');
this.qrCodeOverlay = new QrCodeOverlay(this);
this.barcodeOverlay = new BarcodeOverlay(this);
this.column = new ColumnPageObject(this);
this.cell = new CellPageObject(this);
this.toolbar = new ToolbarPage(this);

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

@ -156,6 +156,48 @@ export class CellPageObject extends BasePage {
await _verify(expectedSrcValue);
}
async verifyBarcodeCellShowsInvalidInputMessage({ index, columnHeader }: { index: number; columnHeader: string }) {
const _verify = async expectedInvalidInputMessage => {
await expect
.poll(async () => {
const barcodeCell = await this.get({
index,
columnHeader,
});
const barcodeInvalidInputMessage = await barcodeCell.getByTestId('barcode-invalid-input-message');
return await barcodeInvalidInputMessage.textContent();
})
.toEqual(expectedInvalidInputMessage);
};
await _verify('Barcode error - please check compatibility between input and barcode type');
}
async verifyBarcodeCell({
index,
columnHeader,
expectedSvgValue,
}: {
index: number;
columnHeader: string;
expectedSvgValue: string;
}) {
const _verify = async expectedBarcodeSvg => {
await expect
.poll(async () => {
const barcodeCell = await this.get({
index,
columnHeader,
});
const barcodeSvg = await barcodeCell.getByTestId('barcode');
return await barcodeSvg.innerHTML();
})
.toEqual(expectedBarcodeSvg);
};
await _verify(expectedSvgValue);
}
// todo: Improve param names (i.e value => values)
// verifyVirtualCell
// : virtual relational cell- HM, BT, MM

174
tests/playwright/tests/columnBarcode.spec.ts

@ -0,0 +1,174 @@
import { expect, test } from '@playwright/test';
import { DashboardPage } from '../pages/Dashboard';
import setup from '../setup';
import { GridPage } from '../pages/Dashboard/Grid';
interface ExpectedBarcodeData {
referencedValue: string;
barcodeSvg: 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('Barcode Column', () => {
const initiallyExpectedBarcodeCellValues: ExpectedBarcodeData[] = [
{
referencedValue: 'A Corua (La Corua)',
barcodeSvg:
'<rect x="0" y="0" width="486" height="142" style="fill:#ffffff;"></rect><g transform="translate(10, 10)" style="fill:#000000;"><rect x="0" y="0" width="4" height="100"></rect><rect x="6" y="0" width="2" height="100"></rect><rect x="12" y="0" width="2" height="100"></rect><rect x="22" y="0" width="2" height="100"></rect><rect x="26" y="0" width="2" height="100"></rect><rect x="34" y="0" width="4" height="100"></rect><rect x="44" y="0" width="4" height="100"></rect><rect x="50" y="0" width="4" height="100"></rect><rect x="58" y="0" width="4" height="100"></rect><rect x="66" y="0" width="2" height="100"></rect><rect x="74" y="0" width="2" height="100"></rect><rect x="82" y="0" width="4" height="100"></rect><rect x="88" y="0" width="2" height="100"></rect><rect x="96" y="0" width="8" height="100"></rect><rect x="106" y="0" width="2" height="100"></rect><rect x="110" y="0" width="2" height="100"></rect><rect x="116" y="0" width="2" height="100"></rect><rect x="122" y="0" width="8" height="100"></rect><rect x="132" y="0" width="2" height="100"></rect><rect x="138" y="0" width="8" height="100"></rect><rect x="150" y="0" width="2" height="100"></rect><rect x="154" y="0" width="2" height="100"></rect><rect x="160" y="0" width="2" height="100"></rect><rect x="164" y="0" width="4" height="100"></rect><rect x="176" y="0" width="4" height="100"></rect><rect x="182" y="0" width="4" height="100"></rect><rect x="190" y="0" width="4" height="100"></rect><rect x="198" y="0" width="2" height="100"></rect><rect x="206" y="0" width="4" height="100"></rect><rect x="214" y="0" width="2" height="100"></rect><rect x="220" y="0" width="2" height="100"></rect><rect x="228" y="0" width="4" height="100"></rect><rect x="234" y="0" width="6" height="100"></rect><rect x="242" y="0" width="2" height="100"></rect><rect x="248" y="0" width="2" height="100"></rect><rect x="252" y="0" width="4" height="100"></rect><rect x="264" y="0" width="4" height="100"></rect><rect x="270" y="0" width="4" height="100"></rect><rect x="278" y="0" width="4" height="100"></rect><rect x="286" y="0" width="2" height="100"></rect><rect x="294" y="0" width="2" height="100"></rect><rect x="302" y="0" width="4" height="100"></rect><rect x="308" y="0" width="2" height="100"></rect><rect x="316" y="0" width="8" height="100"></rect><rect x="326" y="0" width="2" height="100"></rect><rect x="330" y="0" width="2" height="100"></rect><rect x="336" y="0" width="2" height="100"></rect><rect x="342" y="0" width="8" height="100"></rect><rect x="352" y="0" width="2" height="100"></rect><rect x="358" y="0" width="8" height="100"></rect><rect x="370" y="0" width="2" height="100"></rect><rect x="374" y="0" width="2" height="100"></rect><rect x="380" y="0" width="2" height="100"></rect><rect x="384" y="0" width="4" height="100"></rect><rect x="396" y="0" width="4" height="100"></rect><rect x="404" y="0" width="2" height="100"></rect><rect x="410" y="0" width="2" height="100"></rect><rect x="418" y="0" width="6" height="100"></rect><rect x="428" y="0" width="4" height="100"></rect><rect x="436" y="0" width="2" height="100"></rect><rect x="440" y="0" width="4" height="100"></rect><rect x="450" y="0" width="6" height="100"></rect><rect x="458" y="0" width="2" height="100"></rect><rect x="462" y="0" width="4" height="100"></rect><text style="font: 20px monospace" text-anchor="middle" x="233" y="122">A Corua (La Corua)</text></g>',
},
{
referencedValue: 'Abha',
barcodeSvg:
'<rect x="0" y="0" width="178" height="142" style="fill:#ffffff;"></rect><g transform="translate(10, 10)" style="fill:#000000;"><rect x="0" y="0" width="4" height="100"></rect><rect x="6" y="0" width="2" height="100"></rect><rect x="12" y="0" width="2" height="100"></rect><rect x="22" y="0" width="2" height="100"></rect><rect x="26" y="0" width="2" height="100"></rect><rect x="34" y="0" width="4" height="100"></rect><rect x="44" y="0" width="2" height="100"></rect><rect x="50" y="0" width="2" height="100"></rect><rect x="60" y="0" width="4" height="100"></rect><rect x="66" y="0" width="2" height="100"></rect><rect x="72" y="0" width="4" height="100"></rect><rect x="84" y="0" width="2" height="100"></rect><rect x="88" y="0" width="2" height="100"></rect><rect x="94" y="0" width="2" height="100"></rect><rect x="98" y="0" width="4" height="100"></rect><rect x="110" y="0" width="6" height="100"></rect><rect x="118" y="0" width="2" height="100"></rect><rect x="124" y="0" width="4" height="100"></rect><rect x="132" y="0" width="4" height="100"></rect><rect x="142" y="0" width="6" height="100"></rect><rect x="150" y="0" width="2" height="100"></rect><rect x="154" y="0" width="4" height="100"></rect><text style="font: 20px monospace" text-anchor="middle" x="79" y="122">Abha</text></g>',
},
];
const barcodeCellValuesForBerlin = {
referencedValue: 'Berlin',
barcodeSvg:
'<rect x="0" y="0" width="222" height="142" style="fill:#ffffff;"></rect><g transform="translate(10, 10)" style="fill:#000000;"><rect x="0" y="0" width="4" height="100"></rect><rect x="6" y="0" width="2" height="100"></rect><rect x="12" y="0" width="2" height="100"></rect><rect x="22" y="0" width="2" height="100"></rect><rect x="30" y="0" width="2" height="100"></rect><rect x="34" y="0" width="4" height="100"></rect><rect x="44" y="0" width="2" height="100"></rect><rect x="48" y="0" width="4" height="100"></rect><rect x="56" y="0" width="2" height="100"></rect><rect x="66" y="0" width="2" height="100"></rect><rect x="72" y="0" width="2" height="100"></rect><rect x="78" y="0" width="8" height="100"></rect><rect x="88" y="0" width="4" height="100"></rect><rect x="96" y="0" width="2" height="100"></rect><rect x="100" y="0" width="2" height="100"></rect><rect x="110" y="0" width="2" height="100"></rect><rect x="120" y="0" width="4" height="100"></rect><rect x="126" y="0" width="2" height="100"></rect><rect x="132" y="0" width="4" height="100"></rect><rect x="144" y="0" width="2" height="100"></rect><rect x="148" y="0" width="2" height="100"></rect><rect x="154" y="0" width="4" height="100"></rect><rect x="164" y="0" width="2" height="100"></rect><rect x="170" y="0" width="2" height="100"></rect><rect x="176" y="0" width="4" height="100"></rect><rect x="186" y="0" width="6" height="100"></rect><rect x="194" y="0" width="2" height="100"></rect><rect x="198" y="0" width="4" height="100"></rect><text style="font: 20px monospace" text-anchor="middle" x="101" y="122">Berlin</text></g>',
};
const barcodeCellValuesForIstanbul = {
referencedValue: 'Istanbul',
barcodeSvg:
'<rect x="0" y="0" width="266" height="142" style="fill:#ffffff;"></rect><g transform="translate(10, 10)" style="fill:#000000;"><rect x="0" y="0" width="4" height="100"></rect><rect x="6" y="0" width="2" height="100"></rect><rect x="12" y="0" width="2" height="100"></rect><rect x="22" y="0" width="4" height="100"></rect><rect x="32" y="0" width="2" height="100"></rect><rect x="40" y="0" width="2" height="100"></rect><rect x="44" y="0" width="2" height="100"></rect><rect x="48" y="0" width="8" height="100"></rect><rect x="60" y="0" width="2" height="100"></rect><rect x="66" y="0" width="2" height="100"></rect><rect x="72" y="0" width="8" height="100"></rect><rect x="82" y="0" width="2" height="100"></rect><rect x="88" y="0" width="2" height="100"></rect><rect x="94" y="0" width="2" height="100"></rect><rect x="98" y="0" width="4" height="100"></rect><rect x="110" y="0" width="4" height="100"></rect><rect x="122" y="0" width="2" height="100"></rect><rect x="126" y="0" width="2" height="100"></rect><rect x="132" y="0" width="2" height="100"></rect><rect x="138" y="0" width="2" height="100"></rect><rect x="148" y="0" width="4" height="100"></rect><rect x="154" y="0" width="2" height="100"></rect><rect x="160" y="0" width="8" height="100"></rect><rect x="172" y="0" width="2" height="100"></rect><rect x="176" y="0" width="4" height="100"></rect><rect x="184" y="0" width="2" height="100"></rect><rect x="188" y="0" width="2" height="100"></rect><rect x="198" y="0" width="4" height="100"></rect><rect x="204" y="0" width="4" height="100"></rect><rect x="214" y="0" width="4" height="100"></rect><rect x="220" y="0" width="4" height="100"></rect><rect x="230" y="0" width="6" height="100"></rect><rect x="238" y="0" width="2" height="100"></rect><rect x="242" y="0" width="4" height="100"></rect><text style="font: 20px monospace" text-anchor="middle" x="123" y="122">Istanbul</text></g>',
};
const barcodeCode39SvgForBerlin =
'<rect x="0" y="0" width="276" height="142" style="fill:#ffffff;"></rect><g transform="translate(10, 10)" style="fill:#000000;"><rect x="0" y="0" width="2" height="100"></rect><rect x="8" y="0" width="2" height="100"></rect><rect x="12" y="0" width="6" height="100"></rect><rect x="20" y="0" width="6" height="100"></rect><rect x="28" y="0" width="2" height="100"></rect><rect x="32" y="0" width="2" height="100"></rect><rect x="36" y="0" width="6" height="100"></rect><rect x="44" y="0" width="2" height="100"></rect><rect x="52" y="0" width="2" height="100"></rect><rect x="56" y="0" width="6" height="100"></rect><rect x="64" y="0" width="6" height="100"></rect><rect x="72" y="0" width="2" height="100"></rect><rect x="76" y="0" width="6" height="100"></rect><rect x="88" y="0" width="2" height="100"></rect><rect x="92" y="0" width="2" height="100"></rect><rect x="96" y="0" width="6" height="100"></rect><rect x="104" y="0" width="2" height="100"></rect><rect x="108" y="0" width="2" height="100"></rect><rect x="112" y="0" width="6" height="100"></rect><rect x="124" y="0" width="2" height="100"></rect><rect x="128" y="0" width="2" height="100"></rect><rect x="132" y="0" width="6" height="100"></rect><rect x="140" y="0" width="2" height="100"></rect><rect x="144" y="0" width="2" height="100"></rect><rect x="152" y="0" width="6" height="100"></rect><rect x="160" y="0" width="2" height="100"></rect><rect x="164" y="0" width="6" height="100"></rect><rect x="172" y="0" width="2" height="100"></rect><rect x="180" y="0" width="6" height="100"></rect><rect x="188" y="0" width="2" height="100"></rect><rect x="192" y="0" width="2" height="100"></rect><rect x="196" y="0" width="2" height="100"></rect><rect x="200" y="0" width="6" height="100"></rect><rect x="208" y="0" width="2" height="100"></rect><rect x="216" y="0" width="6" height="100"></rect><rect x="224" y="0" width="2" height="100"></rect><rect x="232" y="0" width="2" height="100"></rect><rect x="236" y="0" width="6" height="100"></rect><rect x="244" y="0" width="6" height="100"></rect><rect x="252" y="0" width="2" height="100"></rect><text style="font: 20px monospace" text-anchor="middle" x="128" y="122">BERLIN</text></g>';
const expectedBarcodeCellValuesAfterCityNameChange = [
barcodeCellValuesForBerlin,
...initiallyExpectedBarcodeCellValues.slice(1),
];
async function barcodeColumnVerify(barcodeColumnTitle: string, expectedBarcodeCodeData: ExpectedBarcodeData[]) {
for (let i = 0; i < expectedBarcodeCodeData.length; i++) {
await grid.cell.verifyBarcodeCell({
index: i,
columnHeader: barcodeColumnTitle,
expectedSvgValue: expectedBarcodeCodeData[i].barcodeSvg,
});
}
}
test('creation, showing, updating value and change barcode column title and reference column', async () => {
// Add barcode 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
*/
// close 'Team & Auth' tab
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.openTable({ title: 'City' });
await grid.column.create({
title: 'Barcode1',
type: 'Barcode',
barcodeValueColumnTitle: 'City',
});
await barcodeColumnVerify('Barcode1', initiallyExpectedBarcodeCellValues);
await grid.cell.fillText({ columnHeader: 'City', index: 0, text: 'Berlin' });
await barcodeColumnVerify('Barcode1', expectedBarcodeCellValuesAfterCityNameChange);
await grid.cell.get({ columnHeader: 'Barcode1', index: 0 }).click();
const barcodeGridOverlay = grid.barcodeOverlay;
await barcodeGridOverlay.verifyBarcodeSvgValue(barcodeCellValuesForBerlin.barcodeSvg);
await barcodeGridOverlay.clickCloseButton();
// Change the barcode column title
await grid.column.openEdit({ title: 'Barcode1' });
await grid.column.fillTitle({ title: 'Barcode1 Renamed' });
await grid.column.save({ isUpdated: true });
await barcodeColumnVerify('Barcode1 Renamed', expectedBarcodeCellValuesAfterCityNameChange);
// 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 barcodeColumnVerify('Barcode1 Renamed', expectedBarcodeCellValuesAfterCityNameChange);
// Change the referenced column
await grid.column.create({ title: 'New City Column' });
await grid.cell.fillText({ columnHeader: 'New City Column', index: 0, text: 'Istanbul' });
await grid.column.openEdit({ title: 'Barcode1 Renamed' });
await grid.column.changeReferencedColumnForBarcode({ titleOfReferencedColumn: 'New City Column' });
await barcodeColumnVerify('Barcode1 Renamed', [barcodeCellValuesForIstanbul]);
await dashboard.closeTab({ title: 'City' });
});
test('deletion of the barcode column: a) directly and b) indirectly when the reference value column is deleted', async () => {
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: 'Barcode2',
type: 'Barcode',
barcodeValueColumnTitle: 'column_name_a',
});
await grid.column.verify({ title: 'Barcode2', isVisible: true });
await grid.column.delete({ title: 'Barcode2' });
await grid.column.verify({ title: 'Barcode2', isVisible: false });
await grid.column.create({
title: 'Barcode2',
type: 'Barcode',
barcodeValueColumnTitle: 'column_name_a',
});
await grid.column.verify({ title: 'Barcode2', isVisible: true });
await grid.column.delete({ title: 'column_name_a' });
await grid.column.verify({ title: 'Barcode2', isVisible: false });
await dashboard.closeTab({ title: 'City' });
});
test('a) showing an error message for non-compatible barcode input and b) changing the format of the Barcode is reflected in the change of the actual rendered barcode', async () => {
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.openTable({ title: 'City' });
await grid.column.create({
title: 'Barcode1',
type: 'Barcode',
barcodeValueColumnTitle: 'City',
});
await grid.column.openEdit({
title: 'Barcode1',
});
await grid.column.changeBarcodeFormat({ barcodeFormatName: 'CODE39' });
await grid.cell.verifyBarcodeCellShowsInvalidInputMessage({
index: 0,
columnHeader: 'Barcode1',
});
await grid.cell.fillText({ columnHeader: 'City', index: 0, text: 'Berlin' });
await barcodeColumnVerify('Barcode1', [{ referencedValue: 'Berlin', barcodeSvg: barcodeCode39SvgForBerlin }]);
});
});
});
Loading…
Cancel
Save