diff --git a/packages/nc-gui/components/smartsheet/Form.vue b/packages/nc-gui/components/smartsheet/Form.vue index 30b7da12c7..19a9482c63 100644 --- a/packages/nc-gui/components/smartsheet/Form.vue +++ b/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) { - 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() { diff --git a/packages/nc-gui/components/smartsheet/VirtualCell.vue b/packages/nc-gui/components/smartsheet/VirtualCell.vue index dad0b33311..4ec2045752 100644 --- a/packages/nc-gui/components/smartsheet/VirtualCell.vue +++ b/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) { + diff --git a/packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue b/packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue new file mode 100644 index 0000000000..1ef0bcc5c3 --- /dev/null +++ b/packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue @@ -0,0 +1,120 @@ + + + diff --git a/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue b/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue index cfe70bae07..8211e94759 100644 --- a/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue +++ b/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue @@ -171,6 +171,7 @@ useEventListener('keydown', (e: KeyboardEvent) => { + diff --git a/packages/nc-gui/components/smartsheet/column/FormulaOptions.vue b/packages/nc-gui/components/smartsheet/column/FormulaOptions.vue index 2721028f22..1dea646550 100644 --- a/packages/nc-gui/components/smartsheet/column/FormulaOptions.vue +++ b/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) diff --git a/packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue b/packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue index ecab893a65..172829fa26 100644 --- a/packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue +++ b/packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue @@ -1,6 +1,6 @@ + + diff --git a/packages/nc-gui/components/virtual-cell/barcode/JsBarcodeWrapper.vue b/packages/nc-gui/components/virtual-cell/barcode/JsBarcodeWrapper.vue new file mode 100644 index 0000000000..50e999286e --- /dev/null +++ b/packages/nc-gui/components/virtual-cell/barcode/JsBarcodeWrapper.vue @@ -0,0 +1,39 @@ + + + diff --git a/packages/nc-gui/composables/useSharedView.ts b/packages/nc-gui/composables/useSharedView.ts index cb0c5ea8c5..ac56e40d97 100644 --- a/packages/nc-gui/composables/useSharedView.ts +++ b/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, b: Record) => a.order - b.order) diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts index aa3c30391e..643d23100a 100644 --- a/packages/nc-gui/composables/useViewData.ts +++ b/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 ') diff --git a/packages/nc-gui/lang/de.json b/packages/nc-gui/lang/de.json index 7304e360f8..3e9ff3f6c3 100644 --- a/packages/nc-gui/lang/de.json +++ b/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": { diff --git a/packages/nc-gui/lang/en.json b/packages/nc-gui/lang/en.json index dace266243..ef3048a4d0 100644 --- a/packages/nc-gui/lang/en.json +++ b/packages/nc-gui/lang/en.json @@ -249,7 +249,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", @@ -498,6 +501,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." diff --git a/packages/nc-gui/package-lock.json b/packages/nc-gui/package-lock.json index 5c9d20e54f..d36ae912a6 100644 --- a/packages/nc-gui/package-lock.json +++ b/packages/nc-gui/package-lock.json @@ -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", @@ -10416,6 +10417,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", @@ -25432,6 +25493,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", diff --git a/packages/nc-gui/package.json b/packages/nc-gui/package.json index 981fe722b9..5fb880206b 100644 --- a/packages/nc-gui/package.json +++ b/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", diff --git a/packages/nc-gui/utils/columnUtils.ts b/packages/nc-gui/utils/columnUtils.ts index 54569a8cea..eebd3514a5 100644 --- a/packages/nc-gui/utils/columnUtils.ts +++ b/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, diff --git a/packages/nc-gui/utils/virtualCell.ts b/packages/nc-gui/utils/virtualCell.ts index c5f705a0ee..92ab9eb7cb 100644 --- a/packages/nc-gui/utils/virtualCell.ts +++ b/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 diff --git a/packages/noco-docs/content/en/setup-and-usages/column-types.md b/packages/noco-docs/content/en/setup-and-usages/column-types.md index afeeaa55c6..97f44dd51d 100644 --- a/packages/noco-docs/content/en/setup-and-usages/column-types.md +++ b/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 diff --git a/packages/nocodb-sdk/src/lib/UITypes.ts b/packages/nocodb-sdk/src/lib/UITypes.ts index e8d069bf81..efef013b65 100644 --- a/packages/nocodb-sdk/src/lib/UITypes.ts +++ b/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, diff --git a/packages/nocodb-sdk/src/lib/columnRules/QrCodeRules.ts b/packages/nocodb-sdk/src/lib/columnRules/QrAndBarcodeRules.ts similarity index 78% rename from packages/nocodb-sdk/src/lib/columnRules/QrCodeRules.ts rename to packages/nocodb-sdk/src/lib/columnRules/QrAndBarcodeRules.ts index bf2e1f77e1..e6f05c322e 100644 --- a/packages/nocodb-sdk/src/lib/columnRules/QrCodeRules.ts +++ b/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, diff --git a/packages/nocodb-sdk/src/lib/columnRules/index.ts b/packages/nocodb-sdk/src/lib/columnRules/index.ts index 30acec12c9..4f08acd8b3 100644 --- a/packages/nocodb-sdk/src/lib/columnRules/index.ts +++ b/packages/nocodb-sdk/src/lib/columnRules/index.ts @@ -1 +1 @@ -export * from './QrCodeRules'; +export * from './QrAndBarcodeRules'; diff --git a/packages/nocodb-sdk/src/lib/enums.ts b/packages/nocodb-sdk/src/lib/enums.ts index c5b1888256..cabdc2ff90 100644 --- a/packages/nocodb-sdk/src/lib/enums.ts +++ b/packages/nocodb-sdk/src/lib/enums.ts @@ -3,3 +3,10 @@ export enum OrgUserRoles { CREATOR = 'org-level-creator', VIEWER = 'org-level-viewer', } +export enum ProjectRoles { + OWNER = 'owner', + CREATOR = 'creator', + EDITOR = 'editor', + COMMENTER = 'commenter', + VIEWER = 'viewer', +} diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts index d789d893a0..1ea5d068b1 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts +++ b/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(); + 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()); diff --git a/packages/nocodb/src/lib/meta/api/attachmentApis.ts b/packages/nocodb/src/lib/meta/api/attachmentApis.ts index 4eb3f7597e..53506414cf 100644 --- a/packages/nocodb/src/lib/meta/api/attachmentApis.ts +++ b/packages/nocodb/src/lib/meta/api/attachmentApis.ts @@ -2,15 +2,45 @@ import { Request, Response, Router } from 'express'; import multer from 'multer'; import { nanoid } from 'nanoid'; +import { OrgUserRoles, ProjectRoles } from 'nocodb-sdk'; import path from 'path'; import slash from 'slash'; +import Noco from '../../Noco'; +import { MetaTable } from '../../utils/globals'; import mimetypes, { mimeIcons } from '../../utils/mimeTypes'; import { Tele } from 'nc-help'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import catchError from '../helpers/catchError'; +import extractProjectIdAndAuthenticate from '../helpers/extractProjectIdAndAuthenticate'; +import catchError, { NcError } from '../helpers/catchError'; import NcPluginMgrv2 from '../helpers/NcPluginMgrv2'; import { NC_ATTACHMENT_FIELD_SIZE } from '../../constants'; +const isUploadAllowed = async (req: Request, _res: Response, next: any) => { + if (!req['user']?.id) { + NcError.unauthorized('Unauthorized'); + } + + try { + // check user is super admin or creator + if ( + req['user'].roles?.includes(OrgUserRoles.SUPER_ADMIN) || + req['user'].roles?.includes(OrgUserRoles.CREATOR) || + // if viewer then check at-least one project have editor or higher role + // todo: cache + !!(await Noco.ncMeta + .knex(MetaTable.PROJECT_USERS) + .where(function () { + this.where('roles', ProjectRoles.OWNER); + this.orWhere('roles', ProjectRoles.CREATOR); + this.orWhere('roles', ProjectRoles.EDITOR); + }) + .andWhere('fk_user_id', req['user'].id) + .first()) + ) + return next(); + } catch {} + NcError.badRequest('Upload not allowed'); +}; + // const storageAdapter = new Local(); export async function upload(req: Request, res: Response) { const filePath = sanitizeUrlPath( @@ -156,11 +186,20 @@ router.post( fieldSize: NC_ATTACHMENT_FIELD_SIZE, }, }).any(), - ncMetaAclMw(upload, 'upload') + [ + extractProjectIdAndAuthenticate, + catchError(isUploadAllowed), + catchError(upload), + ] ); router.post( '/api/v1/db/storage/upload-by-url', - ncMetaAclMw(uploadViaURL, 'uploadViaURL') + + [ + extractProjectIdAndAuthenticate, + catchError(isUploadAllowed), + catchError(uploadViaURL), + ] ); router.get(/^\/download\/(.+)$/, catchError(fileRead)); diff --git a/packages/nocodb/src/lib/meta/api/columnApis.ts b/packages/nocodb/src/lib/meta/api/columnApis.ts index 00551e639f..d09d1fc534 100644 --- a/packages/nocodb/src/lib/meta/api/columnApis.ts +++ b/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) { 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) { 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) { case UITypes.Lookup: case UITypes.Rollup: case UITypes.QrCode: + case UITypes.Barcode: case UITypes.Formula: await Column.delete(req.params.columnId); break; diff --git a/packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts b/packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts index 0dc39df339..8cf290e503 100644 --- a/packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts +++ b/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; } } } diff --git a/packages/nocodb/src/lib/migrations/v2/nc_024_barcode_column_type.ts b/packages/nocodb/src/lib/migrations/v2/nc_024_barcode_column_type.ts new file mode 100644 index 0000000000..7ddbc78c91 --- /dev/null +++ b/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 }; diff --git a/packages/nocodb/src/lib/models/BarcodeColumn.ts b/packages/nocodb/src/lib/models/BarcodeColumn.ts new file mode 100644 index 0000000000..6f274f3f86 --- /dev/null +++ b/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) { + Object.assign(this, data); + } + + public static async insert( + data: Partial, + 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, + 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); + } +} diff --git a/packages/nocodb/src/lib/models/Base.ts b/packages/nocodb/src/lib/models/Base.ts index 8dc170eeae..2255ad6571 100644 --- a/packages/nocodb/src/lib/models/Base.ts +++ b/packages/nocodb/src/lib/models/Base.ts @@ -108,7 +108,7 @@ export default class Base implements BaseType { 'order', 'enabled', ]); - + if (updateObj.config) { updateObj.config = CryptoJS.AES.encrypt( JSON.stringify(base.config), @@ -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,11 +217,11 @@ export default class Base implements BaseType { null, MetaTable.BASES, { - order: b.order + order: b.order, }, b.id ); - + await NocoCache.appendToList( CacheScope.BASE, [b.project_id], diff --git a/packages/nocodb/src/lib/models/Column.ts b/packages/nocodb/src/lib/models/Column.ts index ec1a872b9e..69b20dfe9a 100644 --- a/packages/nocodb/src/lib/models/Column.ts +++ b/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 implements ColumnType { public fk_model_id: string; @@ -234,6 +235,17 @@ export default class Column 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 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 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 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 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 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 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 diff --git a/packages/nocodb/src/lib/utils/globals.ts b/packages/nocodb/src/lib/utils/globals.ts index ea037b1f10..4acdfabec5 100644 --- a/packages/nocodb/src/lib/utils/globals.ts +++ b/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', diff --git a/packages/nocodb/tests/unit/factory/column.ts b/packages/nocodb/tests/unit/factory/column.ts index c2843f070a..fe9c3fa4b8 100644 --- a/packages/nocodb/tests/unit/factory/column.ts +++ b/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, diff --git a/packages/nocodb/tests/unit/rest/index.test.ts b/packages/nocodb/tests/unit/rest/index.test.ts index b95637a096..8227a1b5e4 100644 --- a/packages/nocodb/tests/unit/rest/index.test.ts +++ b/packages/nocodb/tests/unit/rest/index.test.ts @@ -6,6 +6,7 @@ import columnTypeSpecificTests from './tests/columnTypeSpecific.test'; import tableTests from './tests/table.test'; import tableRowTests from './tests/tableRow.test'; import viewRowTests from './tests/viewRow.test'; +import attachmentTests from './tests/attachment.test'; function restTests() { authTests(); @@ -15,6 +16,7 @@ function restTests() { tableRowTests(); viewRowTests(); columnTypeSpecificTests(); + attachmentTests(); } export default function () { diff --git a/packages/nocodb/tests/unit/rest/tests/attachment.test.ts b/packages/nocodb/tests/unit/rest/tests/attachment.test.ts new file mode 100644 index 0000000000..29922c76ba --- /dev/null +++ b/packages/nocodb/tests/unit/rest/tests/attachment.test.ts @@ -0,0 +1,176 @@ +import { expect } from 'chai' +import fs from 'fs' +import { OrgUserRoles, ProjectRoles } from 'nocodb-sdk' +import path from 'path' +import 'mocha' +import request from 'supertest' +import { createProject } from '../../factory/project' +import init from '../../init' + +const FILE_PATH = path.join(__dirname, 'test.txt') + +function attachmentTests() { + let context + + + beforeEach(async function() { + context = await init() + fs.writeFileSync(FILE_PATH, 'test', `utf-8`) + context = await init() + }) + + + afterEach(function() { + fs.unlinkSync(FILE_PATH) + }) + + + it('Upload file - Super admin', async () => { + const response = await request(context.app) + .post('/api/v1/db/storage/upload') + .attach('files', FILE_PATH) + .set('xc-auth', context.token) + .expect(200) + + + const attachments = response.body + expect(attachments).to.be.an('array') + expect(attachments[0].title).to.be.eq(path.basename(FILE_PATH)) + }) + + it('Upload file - Without token', async () => { + const response = await request(context.app) + .post('/api/v1/db/storage/upload') + .attach('files', FILE_PATH) + .expect(401) + + const msg = response.body.msg + expect(msg).to.be.eq('Unauthorized') + }) + + it('Upload file - Org level viewer', async () => { + + // signup a user + const args = { + email: 'dummyuser@example.com', + password: 'A1234abh2@dsad', + } + + const signupResponse = await request(context.app) + .post('/api/v1/auth/user/signup') + .send(args) + .expect(200) + + const response = await request(context.app) + .post('/api/v1/db/storage/upload') + .attach('files', FILE_PATH) + .set('xc-auth', signupResponse.body.token) + .expect(400) + + const msg = response.body.msg + expect(msg).to.be.eq('Upload not allowed') + }) + + + it('Upload file - Org level creator', async () => { + + // signup a user + const args = { + email: 'dummyuser@example.com', + password: 'A1234abh2@dsad', + } + + + await request(context.app) + .post('/api/v1/auth/user/signup') + .send(args) + .expect(200) + + // update user role to creator + const usersListResponse = await request(context.app) + .get('/api/v1/users') + .set('xc-auth', context.token) + .expect(200) + + const user = usersListResponse.body.list.find(u => u.email === args.email) + + expect(user).to.have.property('roles').to.be.equal(OrgUserRoles.VIEWER) + + + await request(context.app) + .patch('/api/v1/users/' + user.id) + .set('xc-auth', context.token) + .send({ roles: OrgUserRoles.CREATOR }) + .expect(200) + + + const signinResponse = await request(context.app) + .post('/api/v1/auth/user/signin') + // pass empty data in await request + .send(args) + .expect(200) + + const response = await request(context.app) + .post('/api/v1/db/storage/upload') + .attach('files', FILE_PATH) + .set('xc-auth', signinResponse.body.token) + .expect(200) + + const attachments = response.body + expect(attachments).to.be.an('array') + expect(attachments[0].title).to.be.eq(path.basename(FILE_PATH)) + }) + + + it('Upload file - Org level viewer with editor role in a project', async () => { + + // signup a new user + const args = { + email: 'dummyuser@example.com', + password: 'A1234abh2@dsad', + } + + await request(context.app) + .post('/api/v1/auth/user/signup') + .send(args) + .expect(200) + + const newProject = await createProject(context, { + title: 'NewTitle1', + }) + + // invite user to project with editor role + await request(context.app) + .post(`/api/v1/db/meta/projects/${newProject.id}/users`) + .set('xc-auth', context.token) + .send({ + roles: ProjectRoles.EDITOR, + email: args.email, + project_id: newProject.id, + projectName: newProject.title, + }) + .expect(200) + + // signin to get user token + const signinResponse = await request(context.app) + .post('/api/v1/auth/user/signin') + // pass empty data in await request + .send(args) + .expect(200) + + const response = await request(context.app) + .post('/api/v1/db/storage/upload') + .attach('files', FILE_PATH) + .set('xc-auth', signinResponse.body.token) + .expect(200) + + const attachments = response.body + expect(attachments).to.be.an('array') + expect(attachments[0].title).to.be.eq(path.basename(FILE_PATH)) + }) + +} + +export default function() { + describe('Attachment', attachmentTests) +} diff --git a/tests/playwright/pages/Dashboard/BarcodeOverlay/index.ts b/tests/playwright/pages/Dashboard/BarcodeOverlay/index.ts new file mode 100644 index 0000000000..ce70f07bcc --- /dev/null +++ b/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(); + } +} diff --git a/tests/playwright/pages/Dashboard/Grid/Column/index.ts b/tests/playwright/pages/Dashboard/Grid/Column/index.ts index 15bd509e27..1f1b9ecea7 100644 --- a/tests/playwright/pages/Dashboard/Grid/Column/index.ts +++ b/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(); diff --git a/tests/playwright/pages/Dashboard/Grid/index.ts b/tests/playwright/pages/Dashboard/Grid/index.ts index 39da3232a3..74c7b60078 100644 --- a/tests/playwright/pages/Dashboard/Grid/index.ts +++ b/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); diff --git a/tests/playwright/pages/Dashboard/common/Cell/index.ts b/tests/playwright/pages/Dashboard/common/Cell/index.ts index a277d31ee6..708768ae6f 100644 --- a/tests/playwright/pages/Dashboard/common/Cell/index.ts +++ b/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 diff --git a/tests/playwright/tests/columnBarcode.spec.ts b/tests/playwright/tests/columnBarcode.spec.ts new file mode 100644 index 0000000000..52cc5e5be2 --- /dev/null +++ b/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: + 'A Corua (La Corua)', + }, + { + referencedValue: 'Abha', + barcodeSvg: + 'Abha', + }, + ]; + + const barcodeCellValuesForBerlin = { + referencedValue: 'Berlin', + barcodeSvg: + 'Berlin', + }; + + const barcodeCellValuesForIstanbul = { + referencedValue: 'Istanbul', + barcodeSvg: + 'Istanbul', + }; + + const barcodeCode39SvgForBerlin = + 'BERLIN'; + + 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 }]); + }); + }); +});