diff --git a/lerna.json b/lerna.json index d7175264a2..0f8f5cf16f 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.5", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 20a1b55607..616c89e417 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.5", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -20,7 +20,7 @@ "test:watch": "jest --watchAll" }, "dependencies": { - "@budibase/types": "^1.1.18-alpha.2", + "@budibase/types": "^1.1.18-alpha.5", "@techpass/passport-openidconnect": "0.3.2", "aws-sdk": "2.1030.0", "bcrypt": "5.0.1", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 7beb5f2663..feca151043 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.5", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "^1.2.1", - "@budibase/string-templates": "^1.1.18-alpha.2", + "@budibase/string-templates": "^1.1.18-alpha.5", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", diff --git a/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js b/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js index 32f62efe1f..f436f3ff39 100644 --- a/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js +++ b/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js @@ -17,16 +17,15 @@ filterTests(['all'], () => { it("should add form with multi select picker, containing 5 options", () => { cy.navigateToFrontend() // Add data provider - cy.get(interact.CATEGORY_DATA, { timeout: 500 }).click() - cy.get(interact.COMPONENT_DATA_PROVIDER).click() + cy.searchAndAddComponent("Data Provider") cy.get(interact.DATASOURCE_PROP_CONTROL).click() cy.get(interact.DROPDOWN).contains("Multi Data").click() // Add Form with schema to match table - cy.addComponent("Form", "Form") + cy.searchAndAddComponent("Form") cy.get(interact.DATASOURCE_PROP_CONTROL).click() cy.get(interact.DROPDOWN).contains("Multi Data").click() // Add multi-select picker to form - cy.addComponent("Form", "Multi-select Picker").then(componentId => { + cy.searchAndAddComponent("Multi-select Picker").then(componentId => { cy.get(interact.DATASOURCE_FIELD_CONTROL).type("Test Data").type("{enter}") cy.wait(1000) cy.getComponent(componentId).contains("Choose some options").click() diff --git a/packages/builder/cypress/integration/addRadioButtons.spec.js b/packages/builder/cypress/integration/addRadioButtons.spec.js index 578b519341..91cfd0c7fa 100644 --- a/packages/builder/cypress/integration/addRadioButtons.spec.js +++ b/packages/builder/cypress/integration/addRadioButtons.spec.js @@ -10,15 +10,13 @@ filterTests(['all'], () => { it("should add Radio Buttons options picker on form, add data, and confirm", () => { cy.navigateToFrontend() - cy.wait(500) - cy.addComponent("Form", "Form") - cy.addComponent("Form", "Options Picker").then((componentId) => { - // Provide field setting + cy.searchAndAddComponent("Form") + cy.searchAndAddComponent("Options Picker").then((componentId) => { + // Provide field setting cy.get(interact.DATASOURCE_FIELD_CONTROL).type("1") // Open dropdown and select Radio buttons cy.get(interact.OPTION_TYPE_PROP_CONTROL).click().then(() => { cy.get(interact.SPECTRUM_POPOVER).contains('Radio buttons') - .wait(500) .click() }) const radioButtonsTotal = 3 @@ -32,8 +30,8 @@ filterTests(['all'], () => { const addRadioButtonData = (totalRadioButtons) => { cy.get(interact.OPTION_SOURCE_PROP_CONROL).click().then(() => { cy.get(interact.SPECTRUM_POPOVER).contains('Custom') - .wait(500) .click() + .wait(1000) }) cy.addCustomSourceOptions(totalRadioButtons) } diff --git a/packages/builder/cypress/integration/appOverview.spec.js b/packages/builder/cypress/integration/appOverview.spec.js index dbfce3ce63..feaace6fb6 100644 --- a/packages/builder/cypress/integration/appOverview.spec.js +++ b/packages/builder/cypress/integration/appOverview.spec.js @@ -205,7 +205,7 @@ filterTests(["all"], () => { cy.navigateToFrontend() - cy.addComponent("Elements", "Headline").then(componentId => { + cy.searchAndAddComponent("Headline").then(componentId => { cy.getComponent(componentId).should("exist") }) diff --git a/packages/builder/cypress/integration/autoScreensUI.spec.js b/packages/builder/cypress/integration/autoScreensUI.spec.js index ca997479ae..9431fc1782 100644 --- a/packages/builder/cypress/integration/autoScreensUI.spec.js +++ b/packages/builder/cypress/integration/autoScreensUI.spec.js @@ -14,11 +14,8 @@ filterTests(['smoke', 'all'], () => { cy.closeModal(); cy.contains("Design").click() - cy.get(interact.LABEL_ADD_CIRCLE).click() - cy.get(interact.SPECTRUM_MODAL).within(() => { - cy.get(interact.ITEM_DISABLED).contains("Autogenerated screens") - cy.get(interact.CONFIRM_WRAP_SPE_BUTTON).should('be.disabled') - }) + cy.navigateToAutogeneratedModal() + cy.get(interact.CONFIRM_WRAP_SPE_BUTTON).should('be.disabled') cy.deleteAllApps() }); @@ -45,25 +42,25 @@ filterTests(['smoke', 'all'], () => { // Create Autogenerated screens from the internal table cy.createDatasourceScreen(["Cypress Tests"]) // Confirm screens have been auto generated - cy.get(interact.NAV_ITEMS_CONTAINER).contains("cypress-tests").click({ force: true }) - cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'cypress-tests/:id') + cy.get(interact.BODY).should('contain', "cypress-tests") + .and('contain', 'cypress-tests/:id') .and('contain', 'cypress-tests/new/row') }) it("should generate multiple internal table screens at once", () => { - // Create a second internal table const initialTable = "Cypress Tests" const secondTable = "Table Two" + // Create a second internal table cy.createTable(secondTable) // Create Autogenerated screens from the internal tables cy.createDatasourceScreen([initialTable, secondTable]) // Confirm screens have been auto generated - cy.get(interact.NAV_ITEMS_CONTAINER).contains("cypress-tests").click({ force: true }) // Previously generated tables are suffixed with numbers - as expected - cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'cypress-tests-2/:id') + cy.get(interact.BODY).should('contain', 'cypress-tests-2') + .and('contain', 'cypress-tests-2/:id') .and('contain', 'cypress-tests-2/new/row') - cy.get(interact.NAV_ITEMS_CONTAINER).contains("table-two").click() - cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'table-two/:id') + .and('contain', 'table-two') + .and('contain', 'table-two/:id') .and('contain', 'table-two/new/row') }) @@ -73,17 +70,17 @@ filterTests(['smoke', 'all'], () => { cy.createTable("Table Four") cy.createDatasourceScreen(["Table Three", "Table Four"], "Admin") - cy.get(interact.NAV_ITEMS_CONTAINER).contains("table-three").click() - cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'table-three/:id') + // Filter screens to Admin + cy.filterScreensAccessLevel('Admin') + + cy.get(interact.BODY).should('contain', 'table-three') + .and('contain', 'table-three/:id') .and('contain', 'table-three/new/row') - - cy.get(interact.NAV_ITEMS_CONTAINER).contains("table-four").click() - cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'table-four/:id') + .and('contain', 'table-four') + .and('contain', 'table-four/:id') .and('contain', 'table-four/new/row') - - //The access level should now be set to admin. Previous screens should be filtered. - cy.get(interact.NAV_ITEMS_CONTAINER).contains("table-two").should('not.exist') - cy.get(interact.NAV_ITEMS_CONTAINER).contains("cypress-tests").should('not.exist') + .and('not.contain', 'table-two') + .and('not.contain', 'cypress-tests') }) if (Cypress.env("TEST_ENV")) { @@ -96,8 +93,8 @@ filterTests(['smoke', 'all'], () => { // Create Autogenerated screens from a MySQL table - MySQL contains books table cy.createDatasourceScreen(["books"]) - cy.get(interact.NAV_ITEMS_CONTAINER).contains("books").click() - cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'books/:id') + cy.get(interact.BODY).should('contain', 'books') + .and('contain', 'books/:id') .and('contain', 'books/new/row') }) } diff --git a/packages/builder/cypress/integration/createApp.spec.js b/packages/builder/cypress/integration/createApp.spec.js index 1dd0d2264f..516489b093 100644 --- a/packages/builder/cypress/integration/createApp.spec.js +++ b/packages/builder/cypress/integration/createApp.spec.js @@ -13,7 +13,7 @@ filterTests(['smoke', 'all'], () => { it("should show the new user UI/UX", () => { cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/create`, { timeout: 5000 }) //added /portal/apps/create cy.wait(1000) - cy.get(interact.CREATE_APP_BUTTON).contains('Start from scratch').should("exist") + cy.get(interact.CREATE_APP_BUTTON, { timeout: 10000 }).contains('Start from scratch').should("exist") cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist") cy.get(interact.TEMPLATE_CATEGORY).should("exist") diff --git a/packages/builder/cypress/integration/createBinding.spec.js b/packages/builder/cypress/integration/createBinding.spec.js index 5abe0b27d8..160f23d2d6 100644 --- a/packages/builder/cypress/integration/createBinding.spec.js +++ b/packages/builder/cypress/integration/createBinding.spec.js @@ -9,13 +9,13 @@ filterTests(['smoke', 'all'], () => { }) it("should add a current user binding", () => { - cy.addComponent("Elements", "Paragraph").then(() => { + cy.searchAndAddComponent("Paragraph").then(() => { addSettingBinding("text", "Current User._id") }) }) it("should handle an invalid binding", () => { - cy.addComponent("Elements", "Paragraph").then(componentId => { + cy.searchAndAddComponent("Paragraph").then(componentId => { // Cypress needs to escape curly brackets cy.get("[data-cy=setting-text] input") .type("{{}{{}{{} Current User._id {}}{}}") @@ -27,7 +27,7 @@ filterTests(['smoke', 'all'], () => { xit("should add a URL param binding", () => { const paramName = "foo" cy.createScreen(`/test/:${paramName}`) - cy.addComponent("Elements", "Paragraph").then(componentId => { + cy.searchAndAddComponent("Paragraph").then(componentId => { addSettingBinding("text", `URL.${paramName}`) // The builder preview pages don't have a real URL, so all we can do // is check that we were able to bind to the property, and that the @@ -37,7 +37,7 @@ filterTests(['smoke', 'all'], () => { }) it("should add a binding with a handlebars helper", () => { - cy.addComponent("Elements", "Paragraph").then(componentId => { + cy.searchAndAddComponent("Paragraph").then(componentId => { // Cypress needs to escape curly brackets cy.get("[data-cy=setting-text] input") .type("{{}{{} add 1 2 {}}{}}") diff --git a/packages/builder/cypress/integration/createComponents.spec.js b/packages/builder/cypress/integration/createComponents.spec.js index eff7fc216f..649a77e442 100644 --- a/packages/builder/cypress/integration/createComponents.spec.js +++ b/packages/builder/cypress/integration/createComponents.spec.js @@ -31,13 +31,13 @@ filterTests(["all"], () => { } it("should add a container", () => { - cy.addComponent("Layout", "Container").then(componentId => { + cy.searchAndAddComponent("Container").then(componentId => { cy.getComponent(componentId).should("exist") }) }) it("should add a headline", () => { - cy.addComponent("Elements", "Headline").then(componentId => { + cy.searchAndAddComponent("Headline").then(componentId => { headlineId = componentId cy.getComponent(headlineId).should("exist") }) @@ -63,11 +63,11 @@ filterTests(["all"], () => { }) it("should create a form and reset to match schema", () => { - cy.addComponent("Form", "Form").then(() => { + cy.searchAndAddComponent("Form").then(() => { cy.get("[data-cy=setting-dataSource]").contains("Custom").click() cy.get(interact.DROPDOWN).contains("dog").click() cy.wait(500) - cy.addComponent("Form", "Field Group").then(fieldGroupId => { + cy.searchAndAddComponent("Field Group").then(fieldGroupId => { cy.contains("Update form fields").click() cy.get(".spectrum-Modal") .get(".confirm-wrap .spectrum-Button") @@ -88,7 +88,7 @@ filterTests(["all"], () => { }) it("deletes a component", () => { - cy.addComponent("Elements", "Paragraph").then(componentId => { + cy.searchAndAddComponent("Paragraph").then(componentId => { cy.get("[data-cy=setting-_instanceName] input").type(componentId).blur() cy.get( ".nav-items-container .nav-item.selected .actions > div > .icon" @@ -104,7 +104,7 @@ filterTests(["all"], () => { }) it("should clear the iframe place holder when a form field has been set", () => { - cy.addComponent("Form", "Form").then(formId => { + cy.searchAndAddComponent("Form").then(formId => { //For deletion cy.get("[data-cy=setting-_instanceName] input") .clear() @@ -123,10 +123,7 @@ filterTests(["all"], () => { const testFieldFocusOnCreate = componentLabel => { cy.log("Adding: " + componentLabel) - return cy.addComponent("Form", componentLabel).then(componentId => { - cy.getComponent(componentId) - .find(".component-placeholder") - .should("exist") + return cy.searchAndAddComponent(componentLabel).then(componentId => { cy.get("[data-cy=setting-field] button.spectrum-Picker").click() //Click the first appropriate field. They are filtered by type @@ -157,7 +154,7 @@ filterTests(["all"], () => { }) it("should populate the provider for charts with a data provider in its path", () => { - cy.addComponent("Data", "Data Provider").then(providerId => { + cy.searchAndAddComponent("Data Provider").then(providerId => { //For deletion cy.get("[data-cy=setting-_instanceName] input") .clear() @@ -181,7 +178,7 @@ filterTests(["all"], () => { const testFocusOnCreate = chartLabel => { cy.log("Adding: " + chartLabel) - cy.addComponent("Chart", chartLabel).then(componentId => { + cy.searchAndAddComponent(chartLabel).then(componentId => { cy.get( "[data-cy=dataProvider-prop-control] .spectrum-Picker" ).should("not.have.class", "is-focused") @@ -207,7 +204,7 @@ filterTests(["all"], () => { }) it("should replace the placeholder when a url is set on an image", () => { - cy.addComponent("Elements", "Image").then(imageId => { + cy.searchAndAddComponent("Image").then(imageId => { cy.get("[data-cy=setting-_instanceName] input") .clear() .type(imageId) @@ -229,7 +226,7 @@ filterTests(["all"], () => { }) it("should add a markdown component.", () => { - cy.addComponent("Elements", "Markdown Viewer").then(markdownId => { + cy.searchAndAddComponent("Markdown Viewer").then(markdownId => { cy.get("[data-cy=setting-_instanceName] input") .clear() .type(markdownId) @@ -253,8 +250,7 @@ filterTests(["all"], () => { }) it("should direct the user when adding an Icon component.", () => { - cy.addComponent("Elements", "Icon").then(iconId => { - cy.getComponent(iconId).find(".component-placeholder").should("exist") + cy.searchAndAddComponent("Icon").then(iconId => { cy.get("[data-cy=setting-_instanceName] input") .clear() .type(iconId) diff --git a/packages/builder/cypress/integration/createScreen.spec.js b/packages/builder/cypress/integration/createScreen.spec.js index 94a827f26f..a516e279f4 100644 --- a/packages/builder/cypress/integration/createScreen.spec.js +++ b/packages/builder/cypress/integration/createScreen.spec.js @@ -1,4 +1,5 @@ import filterTests from "../support/filterTests" +const interact = require('../support/interact') filterTests(["smoke", "all"], () => { context("Screen Tests", () => { @@ -10,32 +11,44 @@ filterTests(["smoke", "all"], () => { it("Should successfully create a screen", () => { cy.createScreen("test") - cy.get(".nav-items-container").within(() => { + cy.get(interact.BODY).within(() => { cy.contains("/test").should("exist") }) }) it("Should update the url", () => { cy.createScreen("test with spaces") - cy.get(".nav-items-container").within(() => { + cy.get(interact.BODY).within(() => { cy.contains("/test-with-spaces").should("exist") }) }) - it("Should create a blank screen with the selected access level", () => { - cy.createScreen("admin only", "Admin") + it("should delete all screens then create first screen via button", () => { + cy.deleteAllScreens() + + cy.contains("Create first screen").click() + cy.get(interact.BODY, { timeout: 2000 }).should('contain', '/home') + }) - cy.get(".nav-items-container").within(() => { - cy.contains("/admin-only").should("exist") - }) + it("Should create and filter screens by access level", () => { + const accessLevels = ["Basic", "Admin", "Public", "Power"] - cy.createScreen("open to all", "Public") + for (const access of accessLevels){ + // Create screen with specified access level + cy.createScreen(access, access) + // Filter by access level and confirm screen visible + cy.filterScreensAccessLevel(access) + cy.get(interact.BODY).within(() => { + cy.get(interact.NAV_ITEM).should('contain', access.toLowerCase()) + }) + } - cy.get(".nav-items-container").within(() => { - cy.contains("/open-to-all").should("exist") - //The access level should now be set to admin. Previous screens should be filtered. - cy.get(".nav-item").contains("/test-screen").should("not.exist") - }) + // Filter by All screens - Confirm all screens visible + cy.filterScreensAccessLevel("All screens") + cy.get(interact.BODY).should('contain', accessLevels[0]) + .and('contain', accessLevels[1]) + .and('contain', accessLevels[2]) + .and('contain', accessLevels[3]) }) }) }) diff --git a/packages/builder/cypress/integration/datasources/postgreSql.spec.js b/packages/builder/cypress/integration/datasources/postgreSql.spec.js index ccecfbd5df..0c3f2b9124 100644 --- a/packages/builder/cypress/integration/datasources/postgreSql.spec.js +++ b/packages/builder/cypress/integration/datasources/postgreSql.spec.js @@ -108,7 +108,7 @@ filterTests(["all"], () => { }) it("should delete a relationship", () => { - cy.get(".hierarchy-items-container").contains("PostgreSQL").click() + cy.get(".hierarchy-items-container").contains("PostgreSQL").click({ force: true }) cy.reload() // Delete one relationship cy.get(".spectrum-Table") @@ -156,7 +156,7 @@ filterTests(["all"], () => { it("should switch to schema with no tables", () => { // Switch Schema - To one without any tables - cy.get(".hierarchy-items-container").contains("PostgreSQL").click() + cy.get(".hierarchy-items-container").contains("PostgreSQL").click({ force: true }) switchSchema("randomText") // No tables displayed @@ -219,7 +219,7 @@ filterTests(["all"], () => { // Access query cy.get(".hierarchy-items-container", { timeout: 2000 }) .contains(queryName + " (1)") - .click() + .click({ force: true }) // Rename query cy.wait(1000) diff --git a/packages/builder/cypress/integration/revertApp.spec.js b/packages/builder/cypress/integration/revertApp.spec.js index 4c6f245b76..006b6854ba 100644 --- a/packages/builder/cypress/integration/revertApp.spec.js +++ b/packages/builder/cypress/integration/revertApp.spec.js @@ -30,7 +30,7 @@ filterTests(['smoke', 'all'], () => { cy.navigateToFrontend() // Add initial component - Paragraph - cy.addComponent("Elements", "Paragraph") + cy.searchAndAddComponent("Paragraph") // Publish app cy.get(interact.SPECTRUM_BUTTON).contains("Publish").click({ force: true }) cy.get(interact.SPECTRUM_BUTTON_GROUP).within(() => { @@ -42,7 +42,7 @@ filterTests(['smoke', 'all'], () => { }) // Add second component - Button - cy.addComponent("Elements", "Button") + cy.searchAndAddComponent("Button") // Click Revert cy.get(interact.TOP_RIGHT_NAV).within(() => { cy.get(interact.AREA_LABEL_REVERT).click({ force: true }) diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js index 21df5b8592..cc92ca280a 100644 --- a/packages/builder/cypress/support/commands.js +++ b/packages/builder/cypress/support/commands.js @@ -4,7 +4,7 @@ Cypress.on("uncaught:exception", () => { // ACCOUNTS & USERS Cypress.Commands.add("login", (email, password) => { - cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) cy.wait(2000) cy.url().then(url => { if (url.includes("builder/admin")) { @@ -210,7 +210,7 @@ Cypress.Commands.add("deleteApp", name => { cy.get(".app-overview-actions-icon").within(() => { cy.get(".spectrum-Icon").click({ force: true }) }) - cy.get(".spectrum-Menu").contains("Delete").click() + cy.get(".spectrum-Menu").contains("Delete").click({ force: true }) cy.get(".spectrum-Dialog-grid").within(() => { cy.get("input").type(name) }) @@ -491,26 +491,44 @@ Cypress.Commands.add("selectTable", tableName => { }) Cypress.Commands.add("addCustomSourceOptions", totalOptions => { - cy.get(".spectrum-ActionButton") - .contains("Define Options") - .click() - .then(() => { - for (let i = 0; i < totalOptions; i++) { - // Add radio button options - cy.get(".spectrum-Button") - .contains("Add Option") - .click({ force: true }) - .then(() => { - cy.get("[placeholder='Label']", { timeout: 500 }).eq(i).type(i) - cy.get("[placeholder='Value']").eq(i).type(i) - }) - } - // Save options - cy.get(".spectrum-Button").contains("Save").click({ force: true }) - }) + cy.get('[data-cy="customOptions-prop-control"]').within(() => { + cy.get(".spectrum-ActionButton-label").click({ force: true }) + }) + for (let i = 0; i < totalOptions; i++) { + // Add radio button options + cy.get(".spectrum-Button-label", { timeout: 1000 }) + .contains("Add Option") + .click({ force: true }) + .then(() => { + cy.get("[placeholder='Label']", { timeout: 500 }).eq(i).type(i) + cy.get("[placeholder='Value']").eq(i).type(i) + }) + } + // Save options + cy.get(".spectrum-Button").contains("Save").click({ force: true }) +}) + +// DESIGN SECTION +Cypress.Commands.add("searchAndAddComponent", component => { + // Open component menu + cy.get(".spectrum-Button").contains("Component").click({ force: true }) + + // Search and add component + cy.get(".spectrum-Textfield-input").wait(500).clear().type(component) + cy.get(".body").within(() => { + cy.get(".component") + .contains(new RegExp("^" + component + "$"), { timeout: 3000 }) + .click({ force: true }) + }) + cy.wait(1000) + cy.location().then(loc => { + const params = loc.pathname.split("/") + const componentId = params[params.length - 1] + cy.getComponent(componentId, { timeout: 3000 }).should("exist") + return cy.wrap(componentId) + }) }) -// DESIGN AREA Cypress.Commands.add("addComponent", (category, component) => { if (category) { cy.get(`[data-cy="category-${category}"]`, { timeout: 3000 }).click({ @@ -546,7 +564,7 @@ Cypress.Commands.add("getComponent", componentId => { Cypress.Commands.add("createScreen", (route, accessLevelLabel) => { // Blank Screen cy.contains("Design").click() - cy.get("[aria-label=AddCircle]").click() + cy.get(".header > .add-button").click() cy.get(".spectrum-Modal").within(() => { cy.get("[data-cy='blank-screen']").click() cy.get(".spectrum-Button").contains("Continue").click({ force: true }) @@ -571,7 +589,7 @@ Cypress.Commands.add( "createDatasourceScreen", (datasourceNames, accessLevelLabel) => { cy.contains("Design").click() - cy.get("[aria-label=AddCircle]").click() + cy.get(".header > .add-button").click() cy.get(".spectrum-Modal").within(() => { cy.get(".item").contains("Autogenerated screens").click() cy.get(".spectrum-Button").contains("Continue").click({ force: true }) @@ -626,13 +644,60 @@ Cypress.Commands.add( } ) +Cypress.Commands.add("filterScreensAccessLevel", accessLevel => { + // Filters screens by access level dropdown + cy.get(".body").within(() => { + cy.get(".spectrum-Form-item").eq(1).click() + }) + cy.get(".spectrum-Menu").within(() => { + cy.contains(accessLevel).click() + }) +}) + +Cypress.Commands.add("deleteScreen", screen => { + // Navigates to Design section and deletes specified screen + cy.contains("Design").click() + cy.get(".body").within(() => { + cy.contains(screen) + .siblings(".actions") + .within(() => { + cy.get(".spectrum-Icon").click({ force: true }) + }) + }) + cy.get(".spectrum-Menu > .spectrum-Menu-item > .spectrum-Menu-itemLabel") + .contains("Delete") + .click() + + cy.get( + ".spectrum-Dialog-grid > .spectrum-ButtonGroup > .confirm-wrap > .spectrum-Button" + ).click({ force: true }) + cy.get(".spectrum-Dialog-grid", { timeout: 10000 }).should("not.exist") +}) + +Cypress.Commands.add("deleteAllScreens", () => { + // Deletes all screens + cy.get(".body") + .find(".nav-item") + .its("length") + .then(len => { + for (let i = 0; i < len; i++) { + cy.get(".body > .nav-item") + .eq(0) + .invoke("text") + .then(text => { + cy.deleteScreen(text.trim()) + }) + } + }) +}) + // NAVIGATION Cypress.Commands.add("navigateToFrontend", () => { // Clicks on Design tab and then the Home nav item cy.wait(500) cy.contains("Design").click() cy.get(".spectrum-Search", { timeout: 2000 }).type("/") - cy.get(".nav-item", { timeout: 2000 }).contains("home").click() + cy.get(".nav-item", { timeout: 2000 }).contains("home").click({ force: true }) }) Cypress.Commands.add("navigateToDataSection", () => { @@ -644,9 +709,11 @@ Cypress.Commands.add("navigateToDataSection", () => { Cypress.Commands.add("navigateToAutogeneratedModal", () => { // Screen name must already exist within data source cy.contains("Design").click() - cy.get("[aria-label=AddCircle]").click() + cy.get(".header > .add-button").click() cy.get(".spectrum-Modal").within(() => { - cy.get(".item").contains("Autogenerated screens").click() + cy.get(".item", { timeout: 2000 }) + .contains("Autogenerated screens") + .click({ force: true }) cy.get(".spectrum-Button").contains("Continue").click({ force: true }) cy.wait(500) }) diff --git a/packages/builder/cypress/support/interact.js b/packages/builder/cypress/support/interact.js index 0b31d8a8c5..b96e2692b4 100644 --- a/packages/builder/cypress/support/interact.js +++ b/packages/builder/cypress/support/interact.js @@ -12,7 +12,7 @@ export const APP_NAME_INPUT = "input" // we need to update this with atribute cy export const SPECTRUM_BUTTON_GROUP = ".spectrum-ButtonGroup" export const SPECTRUM_MODAL_INPUT = ".spectrum-Modal input" -//AddMultiOptionDatatype test +//AddMultiOptionDatatype export const CATEGORY_DATA = '[data-cy="category-Data"]' export const COMPONENT_DATA_PROVIDER = '[data-cy="component-Data Provider"]' export const DATASOURCE_PROP_CONTROL = '[data-cy="dataSource-prop-control"]' @@ -51,7 +51,7 @@ export const LABEL_ADD_CIRCLE = "[aria-label=AddCircle]" export const ITEM_DISABLED = ".item.disabled" export const CONFIRM_WRAP_SPE_BUTTON = ".confirm-wrap .spectrum-Button" export const DATA_SOURCE_ENTRY = ".data-source-entry" -export const NAV_ITEMS_CONTAINER = ".nav-items-container" +export const BODY = ".body" //publishWorkFlow export const DEPLOY_APP_MODAL = ".spectrum-Modal [data-cy=deploy-app-modal]" diff --git a/packages/builder/package.json b/packages/builder/package.json index 570e8b1be8..a3fb670911 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.5", "license": "GPL-3.0", "private": true, "scripts": { @@ -69,10 +69,10 @@ } }, "dependencies": { - "@budibase/bbui": "^1.1.18-alpha.2", - "@budibase/client": "^1.1.18-alpha.2", - "@budibase/frontend-core": "^1.1.18-alpha.2", - "@budibase/string-templates": "^1.1.18-alpha.2", + "@budibase/bbui": "^1.1.18-alpha.5", + "@budibase/client": "^1.1.18-alpha.5", + "@budibase/frontend-core": "^1.1.18-alpha.5", + "@budibase/string-templates": "^1.1.18-alpha.5", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 8b34cf8cd2..f77374985d 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -246,6 +246,7 @@ {bindings} allowJS={false} updateOnChange={false} + drawerLeft="260px" /> {/if} {:else if value.customType === "query"} @@ -335,6 +336,7 @@ {bindings} updateOnChange={false} placeholder={value.customType === "queryLimit" ? queryLimit : ""} + drawerLeft="260px" /> {/if} diff --git a/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte b/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte index b162408b26..b8d418c62b 100644 --- a/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte +++ b/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte @@ -18,6 +18,7 @@ export let fillWidth export let allowJS = true export let updateOnChange = true + export let drawerLeft const dispatch = createEventDispatcher() let bindingDrawer @@ -53,7 +54,7 @@ {/if} - + Add the objects on the left to enrich your text. diff --git a/packages/builder/src/components/integration/KeyValueBuilder.svelte b/packages/builder/src/components/integration/KeyValueBuilder.svelte index 9b46bc0364..4ffb380aa4 100644 --- a/packages/builder/src/components/integration/KeyValueBuilder.svelte +++ b/packages/builder/src/components/integration/KeyValueBuilder.svelte @@ -32,6 +32,7 @@ export let menuItems export let showMenu = false export let bindings = [] + export let bindingDrawerLeft let fields = Object.entries(object || {}).map(([name, value]) => ({ name, @@ -119,6 +120,7 @@ value={field.value} allowJS={false} fillWidth={true} + drawerLeft={bindingDrawerLeft} /> {:else} @@ -448,6 +449,7 @@ name="param" headings bindings={mergedBindings} + bindingDrawerLeft="260px" /> @@ -458,6 +460,7 @@ name="header" headings bindings={mergedBindings} + bindingDrawerLeft="260px" /> diff --git a/packages/cli/package.json b/packages/cli/package.json index 887eaf4c76..21cdbff7db 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.5", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,7 +26,7 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "^1.1.18-alpha.2", + "@budibase/backend-core": "^1.1.18-alpha.5", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/manifest.json b/packages/client/manifest.json index fdb5700a5c..8be92e19f6 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -3509,17 +3509,18 @@ }, { "type": "boolean", - "label": "Show button", + "label": "Use button for click action", "key": "showButton" }, { "type": "text", "key": "buttonText", - "label": "Button text" + "label": "Button text", + "dependsOn": "showButton" }, { "type": "event", - "label": "Button action", + "label": "Click action", "key": "buttonOnClick" } ] @@ -3841,18 +3842,19 @@ }, { "type": "boolean", - "label": "Show button", + "label": "Use button for click action", "key": "showCardButton" }, { "type": "text", "key": "cardButtonText", "label": "Button text", - "nested": true + "nested": true, + "dependsOn": "showCardButton" }, { "type": "event", - "label": "Button action", + "label": "Click action", "key": "cardButtonOnClick", "nested": true } diff --git a/packages/client/package.json b/packages/client/package.json index 19a581c6d0..4f14748a9c 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.5", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "^1.1.18-alpha.2", - "@budibase/frontend-core": "^1.1.18-alpha.2", - "@budibase/string-templates": "^1.1.18-alpha.2", + "@budibase/bbui": "^1.1.18-alpha.5", + "@budibase/frontend-core": "^1.1.18-alpha.5", + "@budibase/string-templates": "^1.1.18-alpha.5", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/client/src/components/app/SpectrumCard.svelte b/packages/client/src/components/app/SpectrumCard.svelte index 4b4f9d62f2..3b2fe44f4e 100644 --- a/packages/client/src/components/app/SpectrumCard.svelte +++ b/packages/client/src/components/app/SpectrumCard.svelte @@ -19,9 +19,10 @@ const handleLink = e => { if (!linkURL) { - return + return false } e.preventDefault() + e.stopPropagation() routeStore.actions.navigate(linkURL, linkPeek) } @@ -32,6 +33,8 @@ tabindex="0" role="figure" class:horizontal + class:clickable={buttonOnClick && !showButton} + on:click={showButton ? null : buttonOnClick} > {#if imageURL} {/if} @@ -81,6 +86,11 @@ flex-direction: column; justify-content: flex-start; align-items: stretch; + transition: border-color 130ms ease-out; + } + .spectrum-Card.clickable:hover { + cursor: pointer; + border-color: var(--spectrum-global-color-gray-500) !important; } .spectrum-Card.horizontal { flex-direction: row; @@ -90,7 +100,7 @@ padding: var(--spectrum-global-dimension-size-50) 0; } .spectrum-Card-title.link { - transition: color 130ms ease-in-out; + transition: color 130ms ease-out; } .spectrum-Card-title.link:hover { cursor: pointer; diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index ceacf027c1..6a9ca9dbdb 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.5", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "^1.1.18-alpha.2", + "@budibase/bbui": "^1.1.18-alpha.5", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/frontend-core/src/constants.js b/packages/frontend-core/src/constants.js index 00af4127e4..4ac2a02f6c 100644 --- a/packages/frontend-core/src/constants.js +++ b/packages/frontend-core/src/constants.js @@ -35,7 +35,7 @@ export const OperatorOptions = { label: "Less than", }, Contains: { - value: "equal", + value: "contains", label: "Contains", }, NotContains: { diff --git a/packages/server/package.json b/packages/server/package.json index 22d1ef43a0..8860026d4d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.5", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -77,11 +77,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "^1.1.18-alpha.2", - "@budibase/client": "^1.1.18-alpha.2", - "@budibase/pro": "1.1.18-alpha.2", - "@budibase/string-templates": "^1.1.18-alpha.2", - "@budibase/types": "^1.1.18-alpha.2", + "@budibase/backend-core": "^1.1.18-alpha.5", + "@budibase/client": "^1.1.18-alpha.5", + "@budibase/pro": "1.1.18-alpha.5", + "@budibase/string-templates": "^1.1.18-alpha.5", + "@budibase/types": "^1.1.18-alpha.5", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/server/src/api/controllers/row/internalSearch.js b/packages/server/src/api/controllers/row/internalSearch.js index e6090ad8f0..8a04fc2bd0 100644 --- a/packages/server/src/api/controllers/row/internalSearch.js +++ b/packages/server/src/api/controllers/row/internalSearch.js @@ -19,6 +19,7 @@ class QueryBuilder { empty: {}, notEmpty: {}, oneOf: {}, + contains: {}, ...base, } this.limit = 50 @@ -119,6 +120,11 @@ class QueryBuilder { return this } + addContains(key, value) { + this.query.contains[key] = value + return this + } + /** * Preprocesses a value before going into a lucene search. * Transforms strings to lowercase and wraps strings and bools in quotes. @@ -164,6 +170,31 @@ class QueryBuilder { return `${key}:${builder.preprocess(value, allPreProcessingOpts)}` } + const contains = (key, value) => { + if (!value && value !== 0) { + return null + } + return `${key}:${builder.preprocess(value, { escape: true })}` + } + + const oneOf = (key, value) => { + if (!Array.isArray(value)) { + if (typeof value === "string") { + value = value.split(",") + } else { + return "" + } + } + let orStatement = `${builder.preprocess(value[0], allPreProcessingOpts)}` + for (let i = 1; i < value.length; i++) { + orStatement += ` OR ${builder.preprocess( + value[i], + allPreProcessingOpts + )}` + } + return `${key}:(${orStatement})` + } + function build(structure, queryFn) { for (let [key, value] of Object.entries(structure)) { key = builder.preprocess(key.replace(/ /g, "_"), { @@ -239,26 +270,10 @@ class QueryBuilder { build(this.query.notEmpty, key => `${key}:["" TO *]`) } if (this.query.oneOf) { - build(this.query.oneOf, (key, value) => { - if (!Array.isArray(value)) { - if (typeof value === "string") { - value = value.split(",") - } else { - return "" - } - } - let orStatement = `${builder.preprocess( - value[0], - allPreProcessingOpts - )}` - for (let i = 1; i < value.length; i++) { - orStatement += ` OR ${builder.preprocess( - value[i], - allPreProcessingOpts - )}` - } - return `${key}:(${orStatement})` - }) + build(this.query.oneOf, oneOf) + } + if (this.query.contains) { + build(this.query.contains, contains) } // make sure table ID is always added as an AND if (tableId) { diff --git a/packages/server/src/definitions/datasource.ts b/packages/server/src/definitions/datasource.ts index 90c81abe9f..9752fc947a 100644 --- a/packages/server/src/definitions/datasource.ts +++ b/packages/server/src/definitions/datasource.ts @@ -131,6 +131,9 @@ export interface SearchFilters { oneOf?: { [key: string]: any[] } + contains?: { + [key: string]: any + } } export interface SortJson { diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index fbbc42151a..750564c6ff 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -142,6 +142,21 @@ class InternalBuilder { } } } + + const like = (key: string, value: any) => { + const fnc = allOr ? "orWhere" : "where" + // postgres supports ilike, nothing else does + if (this.client === SqlClients.POSTGRES) { + query = query[fnc](key, "ilike", `%${value}%`) + } else { + const rawFnc = `${fnc}Raw` + // @ts-ignore + query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [ + `%${value}%`, + ]) + } + } + if (!filters) { return query } @@ -168,19 +183,7 @@ class InternalBuilder { }) } if (filters.fuzzy) { - iterate(filters.fuzzy, (key, value) => { - const fnc = allOr ? "orWhere" : "where" - // postgres supports ilike, nothing else does - if (this.client === SqlClients.POSTGRES) { - query = query[fnc](key, "ilike", `%${value}%`) - } else { - const rawFnc = `${fnc}Raw` - // @ts-ignore - query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [ - `%${value}%`, - ]) - } - }) + iterate(filters.fuzzy, like) } if (filters.range) { iterate(filters.range, (key, value) => { @@ -223,6 +226,34 @@ class InternalBuilder { query = query[fnc](key) }) } + if (filters.contains) { + const fnc = allOr ? "orWhere" : "where" + const rawFnc = `${fnc}Raw` + if (this.client === SqlClients.POSTGRES) { + iterate(filters.contains, (key: string, value: any) => { + const fieldNames = key.split(/\./g) + const tableName = fieldNames[0] + const columnName = fieldNames[1] + if (typeof value === "string") { + value = `"${value}"` + } + // @ts-ignore + query = query[rawFnc]( + `"${tableName}"."${columnName}"::jsonb @> '[${value}]'` + ) + }) + } else if (this.client === SqlClients.MY_SQL) { + iterate(filters.contains, (key: string, value: any) => { + if (typeof value === "string") { + value = `"${value}"` + } + // @ts-ignore + query = query[rawFnc](`JSON_CONTAINS(${key}, '${value}')`) + }) + } else { + iterate(filters.contains, like) + } + } return query } diff --git a/packages/server/src/integrations/tests/sql.spec.js b/packages/server/src/integrations/tests/sql.spec.js index c2e65c56b7..55c762573a 100644 --- a/packages/server/src/integrations/tests/sql.spec.js +++ b/packages/server/src/integrations/tests/sql.spec.js @@ -1,4 +1,5 @@ const Sql = require("../base/sql") +const { SqlClients } = require("../utils") const TABLE_NAME = "test" @@ -46,7 +47,7 @@ function generateDeleteJson(table = TABLE_NAME, filters = {}) { describe("SQL query builder", () => { const limit = 500 - const client = "pg" + const client = SqlClients.POSTGRES let sql beforeEach(() => { @@ -173,15 +174,15 @@ describe("SQL query builder", () => { }) it("should work with MS-SQL", () => { - const query = new Sql("mssql", 10)._query(generateReadJson()) + const query = new Sql(SqlClients.MS_SQL, 10)._query(generateReadJson()) expect(query).toEqual({ bindings: [10], sql: `select * from (select top (@p0) * from [${TABLE_NAME}]) as [${TABLE_NAME}]` }) }) - it("should work with mySQL", () => { - const query = new Sql("mysql", 10)._query(generateReadJson()) + it("should work with MySQL", () => { + const query = new Sql(SqlClients.MY_SQL, 10)._query(generateReadJson()) expect(query).toEqual({ bindings: [10], sql: `select * from (select * from \`${TABLE_NAME}\` limit ?) as \`${TABLE_NAME}\`` @@ -238,4 +239,49 @@ describe("SQL query builder", () => { sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" > $1 limit $2) as "${TABLE_NAME}"` }) }) + + it("should use like expression for MS-SQL when filter is contains", () => { + const query = new Sql(SqlClients.MS_SQL, 10)._query(generateReadJson({ + filters: { + contains: { + age: 20, + name: "John" + } + } + })) + expect(query).toEqual({ + bindings: [10, "%20%", "%John%"], + sql: `select * from (select top (@p0) * from [${TABLE_NAME}] where LOWER(${TABLE_NAME}.age) LIKE @p1 and LOWER(${TABLE_NAME}.name) LIKE @p2) as [${TABLE_NAME}]` + }) + }) + + it("should use JSON_CONTAINS expression for MySQL when filter is contains", () => { + const query = new Sql(SqlClients.MY_SQL, 10)._query(generateReadJson({ + filters: { + contains: { + age: 20, + name: "John" + } + } + })) + expect(query).toEqual({ + bindings: [10], + sql: `select * from (select * from \`${TABLE_NAME}\` where JSON_CONTAINS(${TABLE_NAME}.age, '20') and JSON_CONTAINS(${TABLE_NAME}.name, '"John"') limit ?) as \`${TABLE_NAME}\`` + }) + }) + + it("should use jsonb operator expression for PostgreSQL when filter is contains", () => { + const query = new Sql(SqlClients.POSTGRES, 10)._query(generateReadJson({ + filters: { + contains: { + age: 20, + name: "John" + } + } + })) + expect(query).toEqual({ + bindings: [10], + sql: `select * from (select * from \"${TABLE_NAME}\" where \"${TABLE_NAME}\".\"age\"::jsonb @> '[20]' and \"${TABLE_NAME}\".\"name\"::jsonb @> '["John"]' limit $1) as \"${TABLE_NAME}\"` + }) + }) }) diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index c7fca714ca..d55a3e17a7 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.5", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index 3d68984e5b..725f3edd49 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.5", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index 5f5648e8b6..57db310c11 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.5", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -35,10 +35,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "^1.1.18-alpha.2", - "@budibase/pro": "1.1.18-alpha.2", - "@budibase/string-templates": "^1.1.18-alpha.2", - "@budibase/types": "^1.1.18-alpha.2", + "@budibase/backend-core": "^1.1.18-alpha.5", + "@budibase/pro": "1.1.18-alpha.5", + "@budibase/string-templates": "^1.1.18-alpha.5", + "@budibase/types": "^1.1.18-alpha.5", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2",