Browse Source

Merge branch 'develop' into refactor/timezone-locale

pull/5719/head
Wing-Kam Wong 1 year ago
parent
commit
92383d0afb
  1. 59
      build-local-docker-image.sh
  2. 28
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  3. 12
      packages/nc-gui/composables/useSharedFormViewStore.ts
  4. 2
      packages/nc-gui/package-lock.json
  5. 2
      packages/nc-lib-gui/package.json
  6. 4
      packages/nocodb-sdk/package-lock.json
  7. 2
      packages/nocodb-sdk/package.json
  8. 2
      packages/nocodb-sdk/src/lib/formulaHelpers.ts
  9. 1
      packages/nocodb/Dockerfile.local
  10. 20
      packages/nocodb/package-lock.json
  11. 4
      packages/nocodb/package.json
  12. 9
      packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts
  13. 2
      packages/nocodb/src/db/mapFunctionName.ts
  14. 1
      packages/nocodb/webpack.local.config.js
  15. 35
      tests/playwright/pages/SharedForm/index.ts
  16. 14
      tests/playwright/tests/db/columnFormula.spec.ts
  17. 126
      tests/playwright/tests/db/viewForm.spec.ts

59
build-local-docker-image.sh

@ -8,35 +8,70 @@
# 3. build nocodb
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
LOG_FILE=${SCRIPT_DIR}/build-local-docker-image.log
ERROR=""
function build_sdk(){
#build nocodb-sdk
echo "Building nocodb-sdk"
cd ${SCRIPT_DIR}/packages/nocodb-sdk
npm ci
npm run build
npm ci || ERROR="sdk build failed"
npm run build || ERROR="sdk build failed"
}
function build_gui(){
# build nc-gui
echo "Building nc-gui"
export NODE_OPTIONS="--max_old_space_size=16384"
# generate static build of nc-gui
cd ${SCRIPT_DIR}/packages/nc-gui
npm ci
npm run generate
npm ci || ERROR="gui build failed"
npm run generate || ERROR="gui build failed"
}
function copy_gui_artifacts(){
# copy nc-gui build to nocodb dir
rsync -rvzh --delete ./dist/ ${SCRIPT_DIR}/packages/nocodb/docker/nc-gui/
rsync -rvzh --delete ./dist/ ${SCRIPT_DIR}/packages/nocodb/docker/nc-gui/ || ERROR="copy_gui_artifacts failed"
}
function package_nocodb(){
#build nocodb
# build nocodb ( pack nocodb-sdk and nc-gui )
cd ${SCRIPT_DIR}/packages/nocodb
npm install
EE=true ./node_modules/.bin/webpack --config webpack.local.config.js
# remove nocodb-sdk since it's packed with the build
npm uninstall --save nocodb-sdk
npm install || ERROR="package_nocodb failed"
EE=true ./node_modules/.bin/webpack --config webpack.local.config.js || ERROR="package_nocodb failed"
}
function build_image(){
# build docker
docker build . -f Dockerfile.local -t nocodb-local
docker build . -f Dockerfile.local -t nocodb-local || ERROR="build_image failed"
}
function log_message(){
if [[ ${ERROR} != "" ]];
then
>&2 echo "build failed, Please check build-local-docker-image.log for more details"
>&2 echo "ERROR: ${ERROR}"
exit 1
else
echo 'docker image with tag "nocodb-local" built sussessfully. Use below sample command to run the container'
echo 'docker run -d -p 3333:8080 --name nocodb-local nocodb-local '
fi
}
echo "Info: Building nocodb-sdk" | tee ${LOG_FILE}
build_sdk 1>> ${LOG_FILE} 2>> ${LOG_FILE}
echo "Info: Building nc-gui" | tee -a ${LOG_FILE}
build_gui 1>> ${LOG_FILE} 2>> ${LOG_FILE}
echo "Info: copy nc-gui build to nocodb dir" | tee -a ${LOG_FILE}
copy_gui_artifacts 1>> ${LOG_FILE} 2>> ${LOG_FILE}
echo "Info: build nocodb, package nocodb-sdk and nc-gui" | tee -a ${LOG_FILE}
package_nocodb 1>> ${LOG_FILE} 2>> ${LOG_FILE}
if [[ ${ERROR} == "" ]]; then
echo "Info: building docker image" | tee -a ${LOG_FILE}
build_image 1>> ${LOG_FILE} 2>> ${LOG_FILE}
fi
log_message | tee -a ${LOG_FILE}

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

@ -149,28 +149,29 @@ function parseAndValidateFormula(formula: string) {
function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = new Set()) {
if (parsedTree.type === JSEPNode.CALL_EXP) {
const calleeName = parsedTree.callee.name.toUpperCase()
// validate function name
if (!availableFunctions.includes(parsedTree.callee.name)) {
errors.add(`'${parsedTree.callee.name}' function is not available`)
if (!availableFunctions.includes(calleeName)) {
errors.add(`'${calleeName}' function is not available`)
}
// validate arguments
const validation = formulas[parsedTree.callee.name] && formulas[parsedTree.callee.name].validation
const validation = formulas[calleeName] && formulas[calleeName].validation
if (validation && validation.args) {
if (validation.args.rqd !== undefined && validation.args.rqd !== parsedTree.arguments.length) {
errors.add(`'${parsedTree.callee.name}' required ${validation.args.rqd} arguments`)
errors.add(`'${calleeName}' required ${validation.args.rqd} arguments`)
} else if (validation.args.min !== undefined && validation.args.min > parsedTree.arguments.length) {
errors.add(`'${parsedTree.callee.name}' required minimum ${validation.args.min} arguments`)
errors.add(`'${calleeName}' required minimum ${validation.args.min} arguments`)
} else if (validation.args.max !== undefined && validation.args.max < parsedTree.arguments.length) {
errors.add(`'${parsedTree.callee.name}' required maximum ${validation.args.max} arguments`)
errors.add(`'${calleeName}' required maximum ${validation.args.max} arguments`)
}
}
parsedTree.arguments.map((arg: Record<string, any>) => validateAgainstMeta(arg, errors))
// validate data type
if (parsedTree.callee.type === JSEPNode.IDENTIFIER) {
const expectedType = formulas[parsedTree.callee.name].type
const expectedType = formulas[calleeName.toUpperCase()].type
if (expectedType === formulaTypes.NUMERIC) {
if (parsedTree.callee.name === 'WEEKDAY') {
if (calleeName === 'WEEKDAY') {
// parsedTree.arguments[0] = date
validateAgainstType(
parsedTree.arguments[0],
@ -202,7 +203,7 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
parsedTree.arguments.map((arg: Record<string, any>) => validateAgainstType(arg, expectedType, null, typeErrors))
}
} else if (expectedType === formulaTypes.DATE) {
if (parsedTree.callee.name === 'DATEADD') {
if (calleeName === 'DATEADD') {
// parsedTree.arguments[0] = date
validateAgainstType(
parsedTree.arguments[0],
@ -236,7 +237,7 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
},
typeErrors,
)
} else if (parsedTree.callee.name === 'DATETIME_DIFF') {
} else if (calleeName === 'DATETIME_DIFF') {
// parsedTree.arguments[0] = date
validateAgainstType(
parsedTree.arguments[0],
@ -504,8 +505,9 @@ function validateAgainstType(parsedTree: any, expectedType: string, func: any, t
typeErrors.add(`${formulaTypes.NUMERIC} type is found but ${expectedType} type is expected`)
}
} else if (parsedTree.type === JSEPNode.CALL_EXP) {
if (formulas[parsedTree.callee.name]?.type && expectedType !== formulas[parsedTree.callee.name].type) {
typeErrors.add(`${expectedType} not matched with ${formulas[parsedTree.callee.name].type}`)
const calleeName = parsedTree.callee.name.toUpperCase()
if (formulas[calleeName]?.type && expectedType !== formulas[calleeName].type) {
typeErrors.add(`${expectedType} not matched with ${formulas[calleeName].type}`)
}
}
return typeErrors
@ -514,7 +516,7 @@ function validateAgainstType(parsedTree: any, expectedType: string, func: any, t
function getRootDataType(parsedTree: any): any {
// given a parse tree, return the data type of it
if (parsedTree.type === JSEPNode.CALL_EXP) {
return formulas[parsedTree.callee.name].type
return formulas[parsedTree.callee.name.toUpperCase()].type
} else if (parsedTree.type === JSEPNode.IDENTIFIER) {
const col = supportedColumns.value.find((c) => c.title === parsedTree.name) as Record<string, any>
if (col?.uidt === UITypes.Formula) {

12
packages/nc-gui/composables/useSharedFormViewStore.ts

@ -101,18 +101,10 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
{} as Record<string, FormColumnType>,
)
let order = 1
columns.value = meta?.value?.columns
?.map((c: Record<string, any>) => ({
columns.value = viewMeta.model?.columns?.map((c) => ({
...c,
fk_column_id: c.id,
fk_view_id: viewMeta.id,
...(fieldById[c.id] ? fieldById[c.id] : {}),
order: (fieldById[c.id] && fieldById[c.id].order) || order++,
id: fieldById[c.id] && fieldById[c.id].id,
description: fieldById[c.id].description,
}))
.sort((a: Record<string, any>, b: Record<string, any>) => a.order - b.order) as Record<string, any>[]
const _sharedViewMeta = (viewMeta as any).meta
sharedViewMeta.value = isString(_sharedViewMeta) ? JSON.parse(_sharedViewMeta) : _sharedViewMeta

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

@ -110,7 +110,7 @@
}
},
"../nocodb-sdk": {
"version": "0.107.1",
"version": "0.107.3",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

2
packages/nc-lib-gui/package.json

@ -1,6 +1,6 @@
{
"name": "nc-lib-gui",
"version": "0.107.1",
"version": "0.107.3",
"description": "NocoDB GUI",
"author": {
"name": "NocoDB",

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

@ -1,12 +1,12 @@
{
"name": "nocodb-sdk",
"version": "0.107.1",
"version": "0.107.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb-sdk",
"version": "0.107.1",
"version": "0.107.3",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

2
packages/nocodb-sdk/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb-sdk",
"version": "0.107.1",
"version": "0.107.3",
"description": "NocoDB SDK",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",

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

@ -173,7 +173,7 @@ export function jsepTreeToFormula(node) {
'SWITCH',
'URL',
];
if (!formulas.includes(node.name)) return '{' + node.name + '}';
if (!formulas.includes(node.name.toUpperCase())) return '{' + node.name + '}';
return node.name;
}

1
packages/nocodb/Dockerfile.local

@ -21,6 +21,7 @@ COPY ./public/favicon.ico ./docker/public/
# install production dependencies,
# reduce node_module size with modclean & removing sqlite deps,
# package built code into app.tar.gz & add execute permission to start.sh
RUN npm uninstall --save nocodb-sdk
RUN npm ci --omit=dev --quiet \
&& npx modclean --patterns="default:*" --ignore="nc-lib-gui/**,dayjs/**,express-status-monitor/**,@azure/msal-node/dist/**" --run \
&& rm -rf ./node_modules/sqlite3/deps \

20
packages/nocodb/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb",
"version": "0.107.1",
"version": "0.107.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb",
"version": "0.107.1",
"version": "0.107.3",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@google-cloud/storage": "^5.7.2",
@ -80,7 +80,7 @@
"mysql2": "^3.2.0",
"nanoid": "^3.1.20",
"nc-help": "^0.2.87",
"nc-lib-gui": "0.107.1",
"nc-lib-gui": "0.107.3",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
@ -190,7 +190,7 @@
}
},
"../nocodb-sdk": {
"version": "0.107.1",
"version": "0.107.3",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -13157,9 +13157,9 @@
}
},
"node_modules/nc-lib-gui": {
"version": "0.107.1",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.107.1.tgz",
"integrity": "sha512-mAW85IXG1BkuoSACc7ZKyaozIO+OyiKSZoEUYBZQ0EtqfulRPHzBMoHw5yOUZ+wnsieMRrzCh2o+AMn9XdY0Ug==",
"version": "0.107.3",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.107.3.tgz",
"integrity": "sha512-U/GEGt4AFIIA0W1uD5nzG9drWrwdZjj1V1AFTNXdXECqpW3tshev6IFJhBOPXNl7QbXR3POAfZLW3x/IUjQZ7Q==",
"dependencies": {
"express": "^4.17.1"
}
@ -28442,9 +28442,9 @@
}
},
"nc-lib-gui": {
"version": "0.107.1",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.107.1.tgz",
"integrity": "sha512-mAW85IXG1BkuoSACc7ZKyaozIO+OyiKSZoEUYBZQ0EtqfulRPHzBMoHw5yOUZ+wnsieMRrzCh2o+AMn9XdY0Ug==",
"version": "0.107.3",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.107.3.tgz",
"integrity": "sha512-U/GEGt4AFIIA0W1uD5nzG9drWrwdZjj1V1AFTNXdXECqpW3tshev6IFJhBOPXNl7QbXR3POAfZLW3x/IUjQZ7Q==",
"requires": {
"express": "^4.17.1"
}

4
packages/nocodb/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb",
"version": "0.107.1",
"version": "0.107.3",
"description": "NocoDB Backend",
"main": "dist/bundle.js",
"author": {
@ -113,7 +113,7 @@
"mysql2": "^3.2.0",
"nanoid": "^3.1.20",
"nc-help": "^0.2.87",
"nc-lib-gui": "0.107.1",
"nc-lib-gui": "0.107.3",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",

9
packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts

@ -593,11 +593,11 @@ async function _formulaQueryBuilder(
const colAlias = a ? ` as ${a}` : '';
pt.arguments?.forEach?.((arg) => {
if (arg.fnName) return;
arg.fnName = pt.callee.name;
arg.fnName = pt.callee.name.toUpperCase();
arg.argsCount = pt.arguments?.length;
});
if (pt.type === 'CallExpression') {
switch (pt.callee.name) {
switch (pt.callee.name.toUpperCase()) {
case 'ADD':
case 'SUM':
if (pt.arguments.length > 1) {
@ -676,13 +676,14 @@ async function _formulaQueryBuilder(
break;
}
const calleeName = pt.callee.name.toUpperCase();
return {
builder: knex.raw(
`${pt.callee.name}(${(
`${calleeName}(${(
await Promise.all(
pt.arguments.map(async (arg) => {
let query = (await fn(arg)).builder.toQuery();
if (pt.callee.name === 'CONCAT') {
if (calleeName === 'CONCAT') {
if (knex.clientType() !== 'sqlite3') {
query = await convertDateFormatForConcat(
arg,

2
packages/nocodb/src/db/mapFunctionName.ts

@ -20,7 +20,7 @@ export interface MapFnArgs {
}
const mapFunctionName = async (args: MapFnArgs): Promise<any> => {
const name = args.pt.callee.name;
const name = args.pt.callee.name.toUpperCase();
let val;
switch (args.knex.clientType()) {

1
packages/nocodb/webpack.local.config.js

@ -42,7 +42,6 @@ module.exports = {
globalObject: "typeof self !== 'undefined' ? self : this",
},
node: {
fs: 'empty',
__dirname: false,
},
plugins: [new webpack.EnvironmentPlugin(['EE'])],

35
tests/playwright/pages/SharedForm/index.ts

@ -29,4 +29,39 @@ export class SharedFormPage extends BasePage {
})
).toBeVisible();
}
async clickLinkToChildList() {
await this.get().locator('button[data-testid="nc-child-list-button-link-to"]').click();
}
async verifyChildList(cardTitle?: string[]) {
await this.get().locator('.nc-modal-link-record').waitFor();
const linkRecord = await this.get();
// DOM element validation
// title: Link Record
// button: Add new record
// icon: reload
await expect(this.get().locator(`.ant-modal-title`)).toHaveText(`Link record`);
// add new record option is not available for shared form
await expect(await linkRecord.locator(`button:has-text("Add new record")`).isVisible()).toBeFalsy();
await expect(await linkRecord.locator(`.nc-reload`).isVisible()).toBeTruthy();
// placeholder: Filter query
await expect(await linkRecord.locator(`[placeholder="Filter query"]`).isVisible()).toBeTruthy();
{
const childList = linkRecord.locator(`.ant-card`);
const childCards = await childList.count();
await expect(childCards).toEqual(cardTitle.length);
for (let i = 0; i < cardTitle.length; i++) {
await expect(await childList.nth(i).textContent()).toContain(cardTitle[i]);
}
}
}
async selectChildList(cardTitle: string) {
await this.get().locator(`.ant-card:has-text("${cardTitle}"):visible`).click();
}
}

14
tests/playwright/tests/db/columnFormula.spec.ts

@ -134,6 +134,20 @@ const formulaDataByDbType = (context: NcContext) => [
formula: `IF((SEARCH({Address List}, "Parkway") != 0), "2.0","WRONG")`,
result: ['WRONG', 'WRONG', 'WRONG', '2.0', '2.0'],
},
// additional tests for formula case-insensitivity
{
formula: `weekday("2022-07-19")`,
result: ['1', '1', '1', '1', '1'],
},
{
formula: `Weekday("2022-07-19")`,
result: ['1', '1', '1', '1', '1'],
},
{
formula: `WeekDay("2022-07-19")`,
result: ['1', '1', '1', '1', '1'],
},
];
test.describe('Virtual Columns', () => {

126
tests/playwright/tests/db/viewForm.spec.ts

@ -5,6 +5,8 @@ import { FormPage } from '../../pages/Dashboard/Form';
import { SharedFormPage } from '../../pages/SharedForm';
import { AccountPage } from '../../pages/Account';
import { AccountAppStorePage } from '../../pages/Account/AppStore';
import { Api, UITypes } from 'nocodb-sdk';
let api: Api<any>;
// todo: Move most of the ui actions to page object and await on the api response
test.describe('Form view', () => {
@ -238,3 +240,127 @@ test.describe('Form view', () => {
await sharedForm.verifySuccessMessage();
});
});
test.describe('Form view with LTAR', () => {
let dashboard: DashboardPage;
let form: FormPage;
let context: any;
let cityTable: any, countryTable: any;
test.beforeEach(async ({ page }) => {
context = await setup({ page, isEmptyProject: true });
dashboard = new DashboardPage(page, context.project);
form = dashboard.form;
api = new Api({
baseURL: `http://localhost:8080/`,
headers: {
'xc-auth': context.token,
},
});
const cityColumns = [
{
column_name: 'Id',
title: 'Id',
uidt: UITypes.ID,
},
{
column_name: 'City',
title: 'City',
uidt: UITypes.SingleLineText,
pv: true,
},
];
const countryColumns = [
{
column_name: 'Id',
title: 'Id',
uidt: UITypes.ID,
},
{
column_name: 'Country',
title: 'Country',
uidt: UITypes.SingleLineText,
pv: true,
},
];
try {
const project = await api.project.read(context.project.id);
cityTable = await api.base.tableCreate(context.project.id, project.bases?.[0].id, {
table_name: 'City',
title: 'City',
columns: cityColumns,
});
countryTable = await api.base.tableCreate(context.project.id, project.bases?.[0].id, {
table_name: 'Country',
title: 'Country',
columns: countryColumns,
});
const cityRowAttributes = [{ City: 'Atlanta' }, { City: 'Pune' }, { City: 'London' }, { City: 'Sydney' }];
await api.dbTableRow.bulkCreate('noco', context.project.id, cityTable.id, cityRowAttributes);
const countryRowAttributes = [{ Country: 'India' }, { Country: 'UK' }, { Country: 'Australia' }];
await api.dbTableRow.bulkCreate('noco', context.project.id, countryTable.id, countryRowAttributes);
// create LTAR Country has-many City
await api.dbTableColumn.create(countryTable.id, {
column_name: 'CityList',
title: 'CityList',
uidt: UITypes.LinkToAnotherRecord,
parentId: countryTable.id,
childId: cityTable.id,
type: 'hm',
});
// await api.dbTableRow.nestedAdd('noco', context.project.id, countryTable.id, '1', 'hm', 'CityList', '1');
} catch (e) {
console.log(e);
}
// reload page after api calls
await page.reload();
});
test('Form view with LTAR', async () => {
await dashboard.treeView.openTable({ title: 'Country' });
const url = dashboard.rootPage.url();
await dashboard.viewSidebar.createFormView({ title: 'NewForm' });
await dashboard.form.toolbar.clickShareView();
const formLink = await dashboard.form.toolbar.shareView.getShareLink();
await dashboard.rootPage.goto(formLink);
const sharedForm = new SharedFormPage(dashboard.rootPage);
await sharedForm.cell.fillText({
columnHeader: 'Country',
text: 'USA',
});
await sharedForm.clickLinkToChildList();
await sharedForm.verifyChildList(['Atlanta', 'Pune', 'London', 'Sydney']);
await sharedForm.selectChildList('Atlanta');
await sharedForm.submit();
await sharedForm.verifySuccessMessage();
await dashboard.rootPage.goto(url);
await dashboard.viewSidebar.openView({ title: 'Country' });
await dashboard.grid.cell.verify({
index: 3,
columnHeader: 'Country',
value: 'USA',
});
await dashboard.grid.cell.verifyVirtualCell({
index: 3,
columnHeader: 'CityList',
count: 1,
value: ['Atlanta'],
});
});
});

Loading…
Cancel
Save