Browse Source

Merge branch 'develop' of github.com:Budibase/budibase into feature/budibase-api

pull/4732/head
mike12345567 4 years ago
parent
commit
2436bc2e32
  1. 2
      lerna.json
  2. 2
      packages/backend-core/package.json
  3. 4
      packages/bbui/package.json
  4. 4
      packages/bbui/src/Form/Core/Checkbox.svelte
  5. 57
      packages/bbui/src/Modal/Modal.svelte
  6. 24
      packages/bbui/src/Table/SelectEditRenderer.svelte
  7. 55
      packages/bbui/src/Table/Table.svelte
  8. 2
      packages/builder/cypress/integration/createAutomation.spec.js
  9. 5
      packages/builder/cypress/integration/createTable.spec.js
  10. 80
      packages/builder/cypress/integration/datasources/rest.spec.js
  11. 154
      packages/builder/cypress/integration/queryLevelTransformers.spec.js
  12. 34
      packages/builder/cypress/support/commands.js
  13. 12
      packages/builder/package.json
  14. 52
      packages/builder/src/builderStore/dataBinding.js
  15. 7
      packages/builder/src/builderStore/store/automation/index.js
  16. 1
      packages/builder/src/builderStore/store/frontend.js
  17. 8
      packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte
  18. 6
      packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte
  19. 63
      packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte
  20. 1
      packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
  21. 138
      packages/builder/src/components/automation/SetupPanel/RowSelector.svelte
  22. 64
      packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte
  23. 3
      packages/builder/src/components/backend/DataTable/ViewDataTable.svelte
  24. 8
      packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte
  25. 34
      packages/builder/src/helpers/data/utils.js
  26. 2
      packages/builder/src/stores/backend/queries.js
  27. 2
      packages/cli/package.json
  28. 15
      packages/client/manifest.json
  29. 8
      packages/client/package.json
  30. 85
      packages/client/src/components/ClientApp.svelte
  31. 3
      packages/client/src/components/app/blocks/TableBlock.svelte
  32. 29
      packages/client/src/components/app/table/Table.svelte
  33. 8
      packages/client/src/components/context/RowSelectionProvider.svelte
  34. 2
      packages/client/src/sdk.js
  35. 2
      packages/client/src/stores/index.js
  36. 22
      packages/client/src/stores/rowSelection.js
  37. 6
      packages/client/src/utils/buttonActions.js
  38. 4
      packages/frontend-core/package.json
  39. 8
      packages/server/package.json
  40. 2
      packages/string-templates/package.json
  41. 6
      packages/worker/package.json

2
lerna.json

@ -1,5 +1,5 @@
{
"version": "1.0.76-alpha.5",
"version": "1.0.79-alpha.5",
"npmClient": "yarn",
"packages": [
"packages/*"

2
packages/backend-core/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "1.0.76-alpha.5",
"version": "1.0.79-alpha.5",
"description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js",
"author": "Budibase",

4
packages/bbui/package.json

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "1.0.76-alpha.5",
"version": "1.0.79-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.0.72-alpha.0",
"@budibase/string-templates": "^1.0.79-alpha.5",
"@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2",

4
packages/bbui/src/Form/Core/Checkbox.svelte

@ -47,7 +47,9 @@
<use xlink:href="#spectrum-css-icon-Dash100" />
</svg>
</span>
<span class="spectrum-Checkbox-label">{text || ""}</span>
{#if text}
<span class="spectrum-Checkbox-label">{text}</span>
{/if}
</label>
<style>

57
packages/bbui/src/Modal/Modal.svelte

@ -54,34 +54,43 @@
<svelte:window on:keydown={handleKey} />
<!-- These svelte if statements need to be defined like this. -->
<!-- The modal transitions do not work if nested inside more than one "if" -->
{#if visible && inline}
<div use:focusFirstInput class="spectrum-Modal inline is-open">
<slot />
</div>
{:else if visible}
{#if inline}
{#if visible}
<div use:focusFirstInput class="spectrum-Modal inline is-open">
<slot />
</div>
{/if}
{:else}
<!--
We cannot conditionally render the portal as this leads to a missing
insertion point when using nested modals. Therefore we just conditionally
render the content of the portal.
It still breaks the modal animation, but its better than soft bricking the
screen.
-->
<Portal target=".modal-container">
<div
class="spectrum-Underlay is-open"
in:fade={{ duration: 200 }}
out:fade|local={{ duration: 200 }}
on:mousedown|self={cancel}
>
<div class="modal-wrapper" on:mousedown|self={cancel}>
<div class="modal-inner-wrapper" on:mousedown|self={cancel}>
<slot name="outside" />
<div
use:focusFirstInput
class="spectrum-Modal is-open"
in:fly={{ y: 30, duration: 200 }}
out:fly|local={{ y: 30, duration: 200 }}
>
<slot />
{#if visible}
<div
class="spectrum-Underlay is-open"
in:fade={{ duration: 200 }}
out:fade|local={{ duration: 200 }}
on:mousedown|self={cancel}
>
<div class="modal-wrapper" on:mousedown|self={cancel}>
<div class="modal-inner-wrapper" on:mousedown|self={cancel}>
<slot name="outside" />
<div
use:focusFirstInput
class="spectrum-Modal is-open"
in:fly={{ y: 30, duration: 200 }}
out:fly|local={{ y: 30, duration: 200 }}
>
<slot />
</div>
</div>
</div>
</div>
</div>
{/if}
</Portal>
{/if}

24
packages/bbui/src/Table/SelectEditRenderer.svelte

@ -8,9 +8,21 @@
export let allowEditRows = false
</script>
{#if allowSelectRows}
<Checkbox value={selected} />
{/if}
{#if allowEditRows}
<ActionButton size="S" on:click={onEdit}>Edit</ActionButton>
{/if}
<div>
{#if allowSelectRows}
<Checkbox value={selected} />
{/if}
{#if allowEditRows}
<ActionButton size="S" on:click={onEdit}>Edit</ActionButton>
{/if}
</div>
<style>
div {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-m);
}
</style>

55
packages/bbui/src/Table/Table.svelte

@ -5,6 +5,7 @@
import SelectEditRenderer from "./SelectEditRenderer.svelte"
import { cloneDeep, deepGet } from "../helpers"
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
import Checkbox from "../Form/Checkbox.svelte"
/**
* The expected schema is our normal couch schemas for our tables.
@ -31,7 +32,6 @@
export let allowEditRows = true
export let allowEditColumns = true
export let selectedRows = []
export let editColumnTitle = "Edit"
export let customRenderers = []
export let disableSorting = false
export let autoSortColumns = true
@ -50,6 +50,8 @@
// Table state
let height = 0
let loaded = false
let checkboxStatus = false
$: schema = fixSchema(schema)
$: if (!loading) loaded = true
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
@ -67,6 +69,16 @@
$: showEditColumn = allowEditRows || allowSelectRows
$: cellStyles = computeCellStyles(schema)
// Deselect the "select all" checkbox when the user navigates to a new page
$: {
let checkRowCount = rows.filter(o1 =>
selectedRows.some(o2 => o1._id === o2._id)
)
if (checkRowCount.length === 0) {
checkboxStatus = false
}
}
const fixSchema = schema => {
let fixedSchema = {}
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
@ -197,13 +209,32 @@
if (!allowSelectRows) {
return
}
if (selectedRows.includes(row)) {
selectedRows = selectedRows.filter(selectedRow => selectedRow !== row)
if (selectedRows.some(selectedRow => selectedRow._id === row._id)) {
selectedRows = selectedRows.filter(
selectedRow => selectedRow._id !== row._id
)
} else {
selectedRows = [...selectedRows, row]
}
}
const toggleSelectAll = e => {
const select = !!e.detail
if (select) {
// Add any rows which are not already in selected rows
rows.forEach(row => {
if (selectedRows.findIndex(x => x._id === row._id) === -1) {
selectedRows.push(row)
}
})
} else {
// Remove any rows from selected rows that are in the current data set
selectedRows = selectedRows.filter(el =>
rows.every(f => f._id !== el._id)
)
}
}
const computeCellStyles = schema => {
let styles = {}
Object.keys(schema || {}).forEach(field => {
@ -244,7 +275,14 @@
<div
class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit"
>
{editColumnTitle || ""}
{#if allowSelectRows}
<Checkbox
bind:value={checkboxStatus}
on:change={toggleSelectAll}
/>
{:else}
Edit
{/if}
</div>
{/if}
{#each fields as field}
@ -302,11 +340,16 @@
{#if showEditColumn}
<div
class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
on:click={e => {
toggleSelectRow(row)
e.stopPropagation()
}}
>
<SelectEditRenderer
data={row}
selected={selectedRows.includes(row)}
onToggleSelection={() => toggleSelectRow(row)}
selected={selectedRows.findIndex(
selectedRow => selectedRow._id === row._id
) !== -1}
onEdit={e => editRow(e, row)}
{allowSelectRows}
{allowEditRows}

2
packages/builder/cypress/integration/createAutomation.spec.js

@ -33,7 +33,7 @@ filterTests(['smoke', 'all'], () => {
cy.get(".spectrum-Button--cta").click()
})
cy.contains("Setup").click()
cy.get(".spectrum-Picker-label").click()
cy.get(".spectrum-Picker-label").eq(1).click()
cy.contains("dog").click()
cy.get(".spectrum-Textfield-input")
.first()

5
packages/builder/cypress/integration/createTable.spec.js

@ -27,10 +27,13 @@ filterTests(["smoke", "all"], () => {
it("updates a column on the table", () => {
cy.get(".title").click()
cy.get(".spectrum-Table-editIcon > use").click()
cy.get("input").eq(1).type("updated", { force: true })
cy.get(".modal-inner-wrapper").within(() => {
cy.get("input").eq(0).type("updated", { force: true })
// Unset table display column
cy.get(".spectrum-Switch-input").eq(1).click()
cy.contains("Save Column").click()
})
cy.contains("nameupdated ").should("contain", "nameupdated")
})

80
packages/builder/cypress/integration/datasources/rest.spec.js

@ -1,43 +1,45 @@
import filterTests from "../../support/filterTests"
filterTests(['smoke', 'all'], () => {
context("REST Datasource Testing", () => {
before(() => {
cy.login()
cy.createTestApp()
})
const datasource = "REST"
const restUrl = "https://api.openbrewerydb.org/breweries"
it("Should add REST data source with incorrect API", () => {
// Select REST data source
cy.selectExternalDatasource(datasource)
// Enter incorrect api & attempt to send query
cy.wait(500)
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
cy.intercept('**/preview').as('queryError')
cy.get("input").clear().type("random text")
cy.get(".spectrum-Button").contains("Send").click({ force: true })
// Intercept Request after button click & apply assertions
cy.wait("@queryError")
cy.get("@queryError").its('response.body')
.should('have.property', 'message', 'Invalid URL: http://random text?')
cy.get("@queryError").its('response.body')
.should('have.property', 'status', 400)
})
it("should add and configure a REST datasource", () => {
// Select REST datasource and create query
cy.selectExternalDatasource(datasource)
cy.wait(500)
// createRestQuery confirms query creation
cy.createRestQuery("GET", restUrl)
// Confirm status code response within REST datasource
cy.get(".spectrum-FieldLabel")
.contains("Status")
.children()
.should('contain', 200)
})
filterTests(["smoke", "all"], () => {
context("REST Datasource Testing", () => {
before(() => {
cy.login()
cy.createTestApp()
})
const datasource = "REST"
const restUrl = "https://api.openbrewerydb.org/breweries"
it("Should add REST data source with incorrect API", () => {
// Select REST data source
cy.selectExternalDatasource(datasource)
// Enter incorrect api & attempt to send query
cy.wait(500)
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
cy.intercept("**/preview").as("queryError")
cy.get("input").clear().type("random text")
cy.get(".spectrum-Button").contains("Send").click({ force: true })
// Intercept Request after button click & apply assertions
cy.wait("@queryError")
cy.get("@queryError")
.its("response.body")
.should("have.property", "message", "Invalid URL: http://random text?")
cy.get("@queryError")
.its("response.body")
.should("have.property", "status", 400)
})
it("should add and configure a REST datasource", () => {
// Select REST datasource and create query
cy.selectExternalDatasource(datasource)
cy.wait(500)
// createRestQuery confirms query creation
cy.createRestQuery("GET", restUrl, "/breweries")
// Confirm status code response within REST datasource
cy.get(".spectrum-FieldLabel")
.contains("Status")
.children()
.should("contain", 200)
})
})
})

154
packages/builder/cypress/integration/queryLevelTransformers.spec.js

@ -1,115 +1,139 @@
import filterTests from "../support/filterTests"
filterTests(['smoke', 'all'], () => {
filterTests(["smoke", "all"], () => {
context("Query Level Transformers", () => {
before(() => {
cy.login()
cy.deleteApp("Cypress Tests")
cy.createApp("Cypress Tests")
})
it("should write a transformer function", () => {
// Add REST datasource - contains API for breweries
const datasource = "REST"
const restUrl = "https://api.openbrewerydb.org/breweries"
cy.selectExternalDatasource(datasource)
cy.createRestQuery("GET", restUrl)
cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
// Get Transformer Function from file
cy.readFile("cypress/support/queryLevelTransformerFunction.js").then((transformerFunction) => {
// Add REST datasource - contains API for breweries
const datasource = "REST"
const restUrl = "https://api.openbrewerydb.org/breweries"
cy.selectExternalDatasource(datasource)
cy.createRestQuery("GET", restUrl, "/breweries")
cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
// Get Transformer Function from file
cy.readFile("cypress/support/queryLevelTransformerFunction.js").then(
transformerFunction => {
cy.get(".CodeMirror textarea")
// Highlight current text and overwrite with file contents
.type(Cypress.platform === 'darwin' ? '{cmd}a' : '{ctrl}a', { force: true })
.type(transformerFunction, { parseSpecialCharSequences: false })
})
// Send Query
cy.intercept('**/queries/preview').as('query')
cy.get(".spectrum-Button").contains("Send").click({ force: true })
cy.wait("@query")
// Assert against Status Code, body, & body rows
cy.get("@query").its('response.statusCode')
.should('eq', 200)
cy.get("@query").its('response.body').should('not.be.empty')
cy.get("@query").its('response.body.rows').should('not.be.empty')
})
// Highlight current text and overwrite with file contents
.type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", {
force: true,
})
.type(transformerFunction, { parseSpecialCharSequences: false })
}
)
// Send Query
cy.intercept("**/queries/preview").as("query")
cy.get(".spectrum-Button").contains("Send").click({ force: true })
cy.wait("@query")
// Assert against Status Code, body, & body rows
cy.get("@query").its("response.statusCode").should("eq", 200)
cy.get("@query").its("response.body").should("not.be.empty")
cy.get("@query").its("response.body.rows").should("not.be.empty")
})
it("should add data to the previous query", () => {
// Add REST datasource - contains API for breweries
const datasource = "REST"
const restUrl = "https://api.openbrewerydb.org/breweries"
cy.selectExternalDatasource(datasource)
cy.createRestQuery("GET", restUrl)
cy.createRestQuery("GET", restUrl, "/breweries")
cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
// Get Transformer Function with Data from file
cy.readFile("cypress/support/queryLevelTransformerFunctionWithData.js").then((transformerFunction) => {
cy.readFile(
"cypress/support/queryLevelTransformerFunctionWithData.js"
).then(transformerFunction => {
//console.log(transformerFunction[1])
cy.get(".CodeMirror textarea")
// Highlight current text and overwrite with file contents
.type(Cypress.platform === 'darwin' ? '{cmd}a' : '{ctrl}a', { force: true })
.type(transformerFunction, { parseSpecialCharSequences: false })
// Highlight current text and overwrite with file contents
.type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", {
force: true,
})
.type(transformerFunction, { parseSpecialCharSequences: false })
})
// Send Query
cy.intercept('**/queries/preview').as('query')
cy.intercept("**/queries/preview").as("query")
cy.get(".spectrum-Button").contains("Send").click({ force: true })
cy.wait("@query")
// Assert against Status Code, body, & body rows
cy.get("@query").its('response.statusCode')
.should('eq', 200)
cy.get("@query").its('response.body').should('not.be.empty')
cy.get("@query").its('response.body.rows').should('not.be.empty')
cy.get("@query").its("response.statusCode").should("eq", 200)
cy.get("@query").its("response.body").should("not.be.empty")
cy.get("@query").its("response.body.rows").should("not.be.empty")
})
it("should run an invalid query within the transformer section", () => {
// Add REST datasource - contains API for breweries
const datasource = "REST"
const restUrl = "https://api.openbrewerydb.org/breweries"
cy.selectExternalDatasource(datasource)
cy.createRestQuery("GET", restUrl)
cy.createRestQuery("GET", restUrl, "/breweries")
cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
// Clear the code box and add "test"
cy.get(".CodeMirror textarea")
.type(Cypress.platform === 'darwin' ? '{cmd}a' : '{ctrl}a', { force: true })
.type("test")
.type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", {
force: true,
})
.type("test")
// Run Query and intercept
cy.intercept('**/preview').as('queryError')
cy.intercept("**/preview").as("queryError")
cy.get(".spectrum-Button").contains("Send").click({ force: true })
cy.wait("@queryError")
cy.wait(500)
// Assert against message and status for the query error
cy.get("@queryError").its('response.body').should('have.property', 'message', "test is not defined")
cy.get("@queryError").its('response.body').should('have.property', 'status', 400)
cy.get("@queryError")
.its("response.body")
.should("have.property", "message", "test is not defined")
cy.get("@queryError")
.its("response.body")
.should("have.property", "status", 400)
})
xit("should run an invalid query via POST request", () => {
// POST request with transformer as null
cy.request({method: 'POST',
url: `${Cypress.config().baseUrl}/api/queries/`,
body: {fields : {"headers":{},"queryString":null,"path":null},
parameters : [],
schema : {},
name : "test",
queryVerb : "read",
transformer : null,
datasourceId: "test"},
// Expected 400 error - Transformer must be a string
failOnStatusCode: false}).then((response) => {
cy.request({
method: "POST",
url: `${Cypress.config().baseUrl}/api/queries/`,
body: {
fields: { headers: {}, queryString: null, path: null },
parameters: [],
schema: {},
name: "test",
queryVerb: "read",
transformer: null,
datasourceId: "test",
},
// Expected 400 error - Transformer must be a string
failOnStatusCode: false,
}).then(response => {
expect(response.status).to.equal(400)
expect(response.body.message).to.include('Invalid body - "transformer" must be a string')
expect(response.body.message).to.include(
'Invalid body - "transformer" must be a string'
)
})
})
xit("should run an empty query", () => {
// POST request with Transformer as an empty string
cy.request({method: 'POST',
url: `${Cypress.config().baseUrl}/api/queries/preview`,
body: {fields : {"headers":{},"queryString":null,"path":null},
queryVerb : "read",
transformer : "",
datasourceId: "test"},
// Expected 400 error - Transformer is not allowed to be empty
failOnStatusCode: false}).then((response) => {
cy.request({
method: "POST",
url: `${Cypress.config().baseUrl}/api/queries/preview`,
body: {
fields: { headers: {}, queryString: null, path: null },
queryVerb: "read",
transformer: "",
datasourceId: "test",
},
// Expected 400 error - Transformer is not allowed to be empty
failOnStatusCode: false,
}).then(response => {
expect(response.status).to.equal(400)
expect(response.body.message).to.include('Invalid body - "transformer" is not allowed to be empty')
expect(response.body.message).to.include(
'Invalid body - "transformer" is not allowed to be empty'
)
})
})
})

34
packages/builder/cypress/support/commands.js

@ -39,7 +39,7 @@ Cypress.Commands.add("createApp", name => {
cy.get(".spectrum-Modal").within(() => {
cy.get("input").eq(0).type(name).should("have.value", name).blur()
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
cy.wait(5000)
cy.wait(10000)
})
cy.createTable("Cypress Tests", true)
})
@ -116,10 +116,10 @@ Cypress.Commands.add("createTestTableWithData", () => {
Cypress.Commands.add("createTable", (tableName, initialTable) => {
if (!initialTable) {
cy.navigateToDataSection()
cy.get(".add-button").click()
cy.get(`[data-cy="new-table"]`).click()
}
cy.wait(7000)
cy.get(".spectrum-Modal")
cy.wait(5000)
cy.get(".spectrum-Dialog-grid")
.contains("Budibase DB")
.click({ force: true })
.then(() => {
@ -172,17 +172,19 @@ Cypress.Commands.add("addRow", values => {
Cypress.Commands.add("addRowMultiValue", values => {
cy.contains("Create row").click()
cy.get(".spectrum-Form-itemField")
.click()
.then(() => {
cy.get(".spectrum-Popover").within(() => {
for (let i = 0; i < values.length; i++) {
cy.get(".spectrum-Menu-item").eq(i).click()
}
cy.get(".spectrum-Modal").within(() => {
cy.get(".spectrum-Form-itemField")
.click()
.then(() => {
cy.get(".spectrum-Popover").within(() => {
for (let i = 0; i < values.length; i++) {
cy.get(".spectrum-Menu-item").eq(i).click()
}
})
cy.get(".spectrum-Dialog-grid").click("top")
cy.get(".spectrum-ButtonGroup").contains("Create").click()
})
cy.get(".spectrum-Dialog-grid").click("top")
cy.get(".spectrum-ButtonGroup").contains("Create").click()
})
})
})
Cypress.Commands.add("createUser", email => {
@ -435,7 +437,7 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => {
}
})
Cypress.Commands.add("createRestQuery", (method, restUrl) => {
Cypress.Commands.add("createRestQuery", (method, restUrl, queryPrettyName) => {
// addExternalDatasource should be called prior to this
// Configures REST datasource & sends query
cy.wait(1000)
@ -450,5 +452,5 @@ Cypress.Commands.add("createRestQuery", (method, restUrl) => {
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.get(".hierarchy-items-container")
.should("contain", method)
.and("contain", restUrl)
.and("contain", queryPrettyName)
})

12
packages/builder/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "1.0.76-alpha.5",
"version": "1.0.79-alpha.5",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -13,7 +13,7 @@
"cy:setup:ci": "node ./cypress/setup.js",
"cy:open": "cypress open",
"cy:run": "cypress run",
"cy:run:ci": "xvfb-run cypress run --headed --browser chrome --record",
"cy:run:ci": "xvfb-run cypress run --headed --browser chrome",
"cy:test": "start-server-and-test cy:setup http://localhost:4100/builder cy:run",
"cy:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci",
"cy:debug": "start-server-and-test cy:setup http://localhost:4100/builder cy:open",
@ -65,10 +65,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.0.76-alpha.5",
"@budibase/client": "^1.0.76-alpha.5",
"@budibase/frontend-core": "^1.0.76-alpha.5",
"@budibase/string-templates": "^1.0.76-alpha.5",
"@budibase/bbui": "^1.0.79-alpha.5",
"@budibase/client": "^1.0.79-alpha.5",
"@budibase/frontend-core": "^1.0.79-alpha.5",
"@budibase/string-templates": "^1.0.79-alpha.5",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",

52
packages/builder/src/builderStore/dataBinding.js

@ -32,12 +32,14 @@ export const getBindableProperties = (asset, componentId) => {
const urlBindings = getUrlBindings(asset)
const deviceBindings = getDeviceBindings()
const stateBindings = getStateBindings()
const selectedRowsBindings = getSelectedRowsBindings(asset)
return [
...contextBindings,
...urlBindings,
...stateBindings,
...userBindings,
...deviceBindings,
...selectedRowsBindings,
]
}
@ -315,6 +317,40 @@ const getDeviceBindings = () => {
return bindings
}
/**
* Gets all selected rows bindings for tables in the current asset.
*/
const getSelectedRowsBindings = asset => {
let bindings = []
if (get(store).clientFeatures?.rowSelection) {
// Add bindings for table components
let tables = findAllMatchingComponents(asset?.props, component =>
component._component.endsWith("table")
)
const safeState = makePropSafe("rowSelection")
bindings = bindings.concat(
tables.map(table => ({
type: "context",
runtimeBinding: `${safeState}.${makePropSafe(table._id)}`,
readableBinding: `${table._instanceName}.Selected rows`,
}))
)
// Add bindings for table blocks
let tableBlocks = findAllMatchingComponents(asset?.props, component =>
component._component.endsWith("tableblock")
)
bindings = bindings.concat(
tableBlocks.map(block => ({
type: "context",
runtimeBinding: `${safeState}.${makePropSafe(block._id + "-table")}`,
readableBinding: `${block._instanceName}.Selected rows`,
}))
)
}
return bindings
}
/**
* Gets all state bindings that are globally available.
*/
@ -597,14 +633,9 @@ const buildFormSchema = component => {
* in the app.
*/
export const getAllStateVariables = () => {
// Get all component containing assets
let allAssets = []
allAssets = allAssets.concat(get(store).layouts || [])
allAssets = allAssets.concat(get(store).screens || [])
// Find all button action settings in all components
let eventSettings = []
allAssets.forEach(asset => {
getAllAssets().forEach(asset => {
findAllMatchingComponents(asset.props, component => {
const settings = getComponentSettings(component._component)
settings
@ -635,6 +666,15 @@ export const getAllStateVariables = () => {
return Array.from(bindingSet)
}
export const getAllAssets = () => {
// Get all component containing assets
let allAssets = []
allAssets = allAssets.concat(get(store).layouts || [])
allAssets = allAssets.concat(get(store).screens || [])
return allAssets
}
/**
* Recurses the input object to remove any instances of bindings.
*/

7
packages/builder/src/builderStore/store/automation/index.js

@ -57,6 +57,7 @@ const automationActions = store => ({
return state
})
},
save: async automation => {
const response = await API.updateAutomation(automation)
store.update(state => {
@ -130,6 +131,12 @@ const automationActions = store => ({
name: block.name,
})
},
toggleFieldControl: value => {
store.update(state => {
state.selectedBlock.rowControl = value
return state
})
},
deleteAutomationBlock: block => {
store.update(state => {
const idx =

1
packages/builder/src/builderStore/store/frontend.js

@ -41,6 +41,7 @@ const INITIAL_FRONTEND_STATE = {
intelligentLoading: false,
deviceAwareness: false,
state: false,
rowSelection: false,
customThemes: false,
devicePreview: false,
messagePassing: false,

8
packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte

@ -3,14 +3,8 @@
import Flowchart from "./FlowChart/FlowChart.svelte"
$: automation = $automationStore.selectedAutomation?.automation
function onSelect(block) {
automationStore.update(state => {
state.selectedBlock = block
return state
})
}
</script>
{#if automation}
<Flowchart {automation} {onSelect} />
<Flowchart {automation} />
{/if}

6
packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte

@ -14,7 +14,7 @@
} from "@budibase/bbui"
export let automation
export let onSelect
let testDataModal
let blocks
let confirmDeleteDialog
@ -45,7 +45,7 @@
<div class="title">
<div class="subtitle">
<Heading size="S">{automation.name}</Heading>
<div style="display:flex;">
<div style="display:flex; align-items: center;">
<div class="iconPadding">
<div class="icon">
<Icon
@ -72,7 +72,7 @@
animate:flip={{ duration: 500 }}
in:fly|local={{ x: 500, duration: 1500 }}
>
<FlowItem {testDataModal} {onSelect} {block} />
<FlowItem {testDataModal} {block} />
</div>
{/each}
</div>

63
packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte

@ -10,6 +10,7 @@
Button,
StatusLight,
ActionButton,
Select,
notifications,
} from "@budibase/bbui"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
@ -18,7 +19,6 @@
import ActionModal from "./ActionModal.svelte"
import { externalActions } from "./ExternalActions"
export let onSelect
export let block
export let testDataModal
let selected
@ -28,6 +28,10 @@
let setupToggled
let blockComplete
$: rowControl = $automationStore.selectedAutomation.automation.rowControl
$: showBindingPicker =
block.stepId === "CREATE_ROW" || block.stepId === "UPDATE_ROW"
$: testResult = $automationStore.selectedAutomation.testResults?.steps.filter(
step => (block.id ? step.id === block.id : step.stepId === block.stepId)
)
@ -44,12 +48,6 @@
$automationStore.selectedAutomation?.automation?.definition?.steps.length +
1
// Logic for hiding / showing the add button.first we check if it has a child
// then we check to see whether its inputs have been commpleted
$: disableAddButton = isTrigger
? $automationStore.selectedAutomation?.automation?.definition?.steps
.length > 0
: !isTrigger && steps.length - blockIdx > 1
$: hasCompletedInputs = Object.keys(
block.schema?.inputs?.properties || {}
).every(x => block?.inputs[x])
@ -64,6 +62,26 @@
notifications.error("Error saving notification")
}
}
function toggleFieldControl(evt) {
onSelect(block)
let rowControl
if (evt.detail === "Use values") {
rowControl = false
} else {
rowControl = true
}
automationStore.actions.toggleFieldControl(rowControl)
automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
}
async function onSelect(block) {
await automationStore.update(state => {
state.selectedBlock = block
return state
})
}
</script>
<div
@ -126,15 +144,33 @@
<Layout noPadding gap="S">
<div class="splitHeader">
<ActionButton
on:click={() => (setupToggled = !setupToggled)}
on:click={() => {
onSelect(block)
setupToggled = !setupToggled
}}
quiet
icon={setupToggled ? "ChevronDown" : "ChevronRight"}
>
<Detail size="S">Setup</Detail>
</ActionButton>
{#if !isTrigger}
<div on:click={() => deleteStep()}>
<Icon name="DeleteOutline" />
<div class="block-options">
{#if showBindingPicker}
<div>
<Select
on:change={toggleFieldControl}
quiet
defaultValue="Use values"
autoWidth
value={rowControl ? "Use bindings" : "Use values"}
options={["Use values", "Use bindings"]}
placeholder={null}
/>
</div>
{/if}
<div class="delete-padding" on:click={() => deleteStep()}>
<Icon name="DeleteOutline" />
</div>
</div>
{/if}
</div>
@ -180,6 +216,13 @@
{/if}
<style>
.delete-padding {
padding-left: 30px;
}
.block-options {
display: flex;
align-items: center;
}
.center-items {
display: flex;
align-items: center;

1
packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte

@ -227,6 +227,7 @@
/>
{:else if value.customType === "row"}
<RowSelector
{block}
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}

138
packages/builder/src/components/automation/SetupPanel/RowSelector.svelte

@ -1,26 +1,31 @@
<script>
import { tables } from "stores/backend"
import {
Select,
Toggle,
DatePicker,
Multiselect,
TextArea,
} from "@budibase/bbui"
import { Select } from "@budibase/bbui"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import { createEventDispatcher } from "svelte"
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
import { automationStore } from "builderStore"
import RowSelectorTypes from "./RowSelectorTypes.svelte"
const dispatch = createEventDispatcher()
export let value
export let bindings
export let block
let table
let schemaFields
let placeholders = {
number: 10,
boolean: "true",
datetime: "2022-02-16T12:00:00.000Z ",
options: "1",
array: "1 2 3 4",
link: "ro_ta_123_456",
longform: "long form text",
}
$: rowControl = block.rowControl
$: {
table = $tables.list.find(table => table._id === value?.tableId)
schemaFields = Object.entries(table?.schema ?? {})
@ -37,18 +42,48 @@
dispatch("change", value)
}
const onChange = (e, field) => {
value[field] = e.detail
const coerce = (value, type) => {
if (type === "boolean") {
if (typeof value === "boolean") {
return value
}
return value === "true"
}
if (type === "number") {
if (typeof value === "number") {
return value
}
return Number(value)
}
if (type === "options") {
return [value]
}
if (type === "array") {
if (Array.isArray(value)) {
return value
}
return value.split(",").map(x => x.trim())
}
if (type === "link") {
if (Array.isArray(value)) {
return value
}
return [value]
}
return value
}
const onChange = (e, field, type) => {
value[field] = coerce(e.detail, type)
dispatch("change", value)
}
// Ensure any nullish tableId values get set to empty string so
// that the select works
$: if (value?.tableId == null) value = { tableId: "" }
function schemaHasOptions(schema) {
return !!schema.constraints?.inclusion?.length
}
</script>
<Select
@ -62,55 +97,46 @@
<div class="schema-fields">
{#each schemaFields as [field, schema]}
{#if !schema.autocolumn}
{#if schemaHasOptions(schema) && schema.type !== "array"}
<Select
on:change={e => onChange(e, field)}
label={field}
value={value[field]}
options={schema.constraints.inclusion}
/>
{:else if schema.type === "datetime"}
<DatePicker
label={field}
value={value[field]}
on:change={e => onChange(e, field)}
/>
{:else if schema.type === "boolean"}
<Toggle
text={field}
value={value[field]}
on:change={e => onChange(e, field)}
/>
{:else if schema.type === "array"}
<Multiselect
bind:value={value[field]}
label={field}
options={schema.constraints.inclusion}
/>
{:else if schema.type === "longform"}
<TextArea label={field} bind:value={value[field]} />
{:else if schema.type === "link"}
<LinkedRowSelector bind:linkedRows={value[field]} {schema} />
{:else if schema.type === "string" || schema.type === "number"}
{#if schema.type !== "attachment"}
{#if $automationStore.selectedAutomation.automation.testData}
<ModalBindableInput
value={value[field]}
panel={AutomationBindingPanel}
label={field}
type={value.customType}
on:change={e => onChange(e, field)}
{bindings}
/>
{#if !rowControl}
<RowSelectorTypes
{field}
{schema}
{bindings}
{value}
{onChange}
/>
{:else}
<DrawerBindableInput
placeholder={placeholders[schema.type]}
panel={AutomationBindingPanel}
value={Array.isArray(value[field])
? value[field].join(" ")
: value[field]}
on:change={e => onChange(e, field, schema.type)}
label={field}
type="string"
{bindings}
fillWidth={true}
allowJS={true}
/>
{/if}
{:else if !rowControl}
<RowSelectorTypes {field} {schema} {bindings} {value} {onChange} />
{:else}
<DrawerBindableInput
placeholder={placeholders[schema.type]}
panel={AutomationBindingPanel}
value={value[field]}
on:change={e => onChange(e, field)}
value={Array.isArray(value[field])
? value[field].join(" ")
: value[field]}
on:change={e => onChange(e, field, schema.type)}
label={field}
type="string"
{bindings}
fillWidth={true}
allowJS={false}
allowJS={true}
/>
{/if}
{/if}

64
packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte

@ -0,0 +1,64 @@
<script>
import {
Select,
Toggle,
DatePicker,
Multiselect,
TextArea,
} from "@budibase/bbui"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
export let onChange
export let field
export let schema
export let value
export let bindings
function schemaHasOptions(schema) {
return !!schema.constraints?.inclusion?.length
}
</script>
{#if schemaHasOptions(schema) && schema.type !== "array"}
<Select
on:change={e => onChange(e, field)}
label={field}
value={value[field]}
options={schema.constraints.inclusion}
/>
{:else if schema.type === "datetime"}
<DatePicker
label={field}
value={value[field]}
on:change={e => onChange(e, field)}
/>
{:else if schema.type === "boolean"}
<Toggle
text={field}
value={value[field]}
on:change={e => onChange(e, field)}
/>
{:else if schema.type === "array"}
<Multiselect
bind:value={value[field]}
label={field}
options={schema.constraints.inclusion}
/>
{:else if schema.type === "longform"}
<TextArea label={field} bind:value={value[field]} />
{:else if schema.type === "link"}
<LinkedRowSelector bind:linkedRows={value[field]} {schema} />
{:else if schema.type === "string" || schema.type === "number"}
<DrawerBindableInput
panel={AutomationBindingPanel}
value={value[field]}
on:change={e => onChange(e, field)}
label={field}
type="string"
{bindings}
fillWidth={true}
allowJS={true}
/>
{/if}

3
packages/builder/src/components/backend/DataTable/ViewDataTable.svelte

@ -57,7 +57,8 @@
{data}
{loading}
{type}
allowEditing={!view?.calculation}
allowEditing={false}
rowCount={10}
bind:hideAutocolumns
>
<ViewFilterButton {view} />

8
packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte

@ -8,7 +8,11 @@
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
import NavItem from "components/common/NavItem.svelte"
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
import { customQueryIconText, customQueryIconColor } from "helpers/data/utils"
import {
customQueryIconText,
customQueryIconColor,
customQueryText,
} from "helpers/data/utils"
import ICONS from "./icons"
import { notifications } from "@budibase/bbui"
@ -137,7 +141,7 @@
icon="SQLQuery"
iconText={customQueryIconText(datasource, query)}
iconColor={customQueryIconColor(datasource, query)}
text={query.name}
text={customQueryText(datasource, query)}
opened={$queries.selected === query._id}
selected={$queries.selected === query._id}
on:click={() => onClickQuery(query)}

34
packages/builder/src/helpers/data/utils.js

@ -30,8 +30,8 @@ export function breakQueryString(qs) {
const params = qs.split("&")
let paramObj = {}
for (let param of params) {
const [key, value] = param.split("=")
paramObj[key] = value
const split = param.split("=")
paramObj[split[0]] = split.slice(1).join("=")
}
return paramObj
}
@ -109,6 +109,36 @@ export function customQueryIconColor(datasource, query) {
}
}
export function customQueryText(datasource, query) {
if (!query.name || datasource.source !== IntegrationTypes.REST) {
return query.name
}
// Remove protocol
let name = query.name
if (name.includes("//")) {
name = name.split("//")[1]
}
// If no path, return the full name
if (!name.includes("/")) {
return name
}
// Remove trailing slash
if (name.endsWith("/")) {
name = name.slice(0, -1)
}
// Only use path
const split = name.split("/")
if (split[1]) {
return `/${split.slice(1).join("/")}`
} else {
return split[0]
}
}
export function flipHeaderState(headersActivity) {
if (!headersActivity) {
return {}

2
packages/builder/src/stores/backend/queries.js

@ -60,7 +60,7 @@ export function createQueriesStore() {
})
return savedQuery
},
import: async (data, datasourceId) => {
import: async ({ data, datasourceId }) => {
return await API.importQueries({
datasourceId,
data,

2
packages/cli/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "1.0.76-alpha.5",
"version": "1.0.79-alpha.5",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js",
"bin": {

15
packages/client/manifest.json

@ -6,7 +6,8 @@
"state": true,
"customThemes": true,
"devicePreview": true,
"messagePassing": true
"messagePassing": true,
"rowSelection": true
},
"layout": {
"name": "Layout",
@ -2714,6 +2715,13 @@
"key": "showAutoColumns",
"defaultValue": false
},
{
"type": "boolean",
"label": "Allow row selection",
"key": "allowSelectRows",
"defaultValue": false
},
{
"type": "boolean",
"label": "Link table rows",
@ -2973,6 +2981,11 @@
"label": "Show auto columns",
"key": "showAutoColumns"
},
{
"type": "boolean",
"label": "Allow row selection",
"key": "allowSelectRows"
},
{
"type": "boolean",
"label": "Link table rows",

8
packages/client/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "1.0.76-alpha.5",
"version": "1.0.79-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.0.76-alpha.5",
"@budibase/frontend-core": "^1.0.76-alpha.5",
"@budibase/string-templates": "^1.0.76-alpha.5",
"@budibase/bbui": "^1.0.79-alpha.5",
"@budibase/frontend-core": "^1.0.79-alpha.5",
"@budibase/string-templates": "^1.0.79-alpha.5",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",

85
packages/client/src/components/ClientApp.svelte

@ -21,6 +21,7 @@
import UserBindingsProvider from "components/context/UserBindingsProvider.svelte"
import DeviceBindingsProvider from "components/context/DeviceBindingsProvider.svelte"
import StateBindingsProvider from "components/context/StateBindingsProvider.svelte"
import RowSelectionProvider from "components/context/RowSelectionProvider.svelte"
import SettingsBar from "components/preview/SettingsBar.svelte"
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
import HoverIndicator from "components/preview/HoverIndicator.svelte"
@ -90,59 +91,61 @@
<UserBindingsProvider>
<DeviceBindingsProvider>
<StateBindingsProvider>
<!-- Settings bar can be rendered outside of device preview -->
<!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder}
<SettingsBar />
{/if}
{/key}
<RowSelectionProvider>
<!-- Settings bar can be rendered outside of device preview -->
<!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder}
<SettingsBar />
{/if}
{/key}
<!-- Clip boundary for selection indicators -->
<div
id="clip-root"
class:preview={$builderStore.inBuilder}
class:tablet-preview={$builderStore.previewDevice === "tablet"}
class:mobile-preview={$builderStore.previewDevice === "mobile"}
>
<!-- Actual app -->
<div id="app-root">
<CustomThemeWrapper>
{#key `${$screenStore.activeLayout._id}-${$builderStore.previewType}`}
<Component
isLayout
instance={$screenStore.activeLayout.props}
/>
{/key}
<!-- Clip boundary for selection indicators -->
<div
id="clip-root"
class:preview={$builderStore.inBuilder}
class:tablet-preview={$builderStore.previewDevice === "tablet"}
class:mobile-preview={$builderStore.previewDevice === "mobile"}
>
<!-- Actual app -->
<div id="app-root">
<CustomThemeWrapper>
{#key `${$screenStore.activeLayout._id}-${$builderStore.previewType}`}
<Component
isLayout
instance={$screenStore.activeLayout.props}
/>
{/key}
<!--
<!--
Flatpickr needs to be inside the theme wrapper.
It also needs its own container because otherwise it hijacks
key events on the whole page. It is painful to work with.
-->
<div id="flatpickr-root" />
<div id="flatpickr-root" />
<!-- Modal container to ensure they sit on top -->
<div class="modal-container" />
<!-- Modal container to ensure they sit on top -->
<div class="modal-container" />
<!-- Layers on top of app -->
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
</CustomThemeWrapper>
</div>
<!-- Layers on top of app -->
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
</CustomThemeWrapper>
</div>
<!-- Selection indicators should be bounded by device -->
<!--
<!-- Selection indicators should be bounded by device -->
<!--
We don't want to key these by componentID as they control their own
re-mounting to avoid flashes.
-->
{#if $builderStore.inBuilder}
<SelectionIndicator />
<HoverIndicator />
<DNDHandler />
{/if}
</div>
{#if $builderStore.inBuilder}
<SelectionIndicator />
<HoverIndicator />
<DNDHandler />
{/if}
</div>
</RowSelectionProvider>
</StateBindingsProvider>
</DeviceBindingsProvider>
</UserBindingsProvider>

3
packages/client/src/components/app/blocks/TableBlock.svelte

@ -18,6 +18,7 @@
export let quiet
export let compact
export let size
export let allowSelectRows
export let linkRows
export let linkURL
export let linkColumn
@ -157,6 +158,7 @@
>
<BlockComponent
type="table"
context="table"
props={{
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
columns: tableColumns,
@ -164,6 +166,7 @@
rowCount,
quiet,
compact,
allowSelectRows,
size,
linkRows,
linkURL,

29
packages/client/src/components/app/table/Table.svelte

@ -3,6 +3,7 @@
import { Table } from "@budibase/bbui"
import SlotRenderer from "./SlotRenderer.svelte"
import { UnsortableTypes } from "../../../constants"
import { onDestroy } from "svelte"
export let dataProvider
export let columns
@ -14,10 +15,12 @@
export let linkURL
export let linkColumn
export let linkPeek
export let allowSelectRows
export let compact
const component = getContext("component")
const { styleable, getAction, ActionTypes, routeStore } = getContext("sdk")
const { styleable, getAction, ActionTypes, routeStore, rowSelectionStore } =
getContext("sdk")
const customColumnKey = `custom-${Math.random()}`
const customRenderers = [
{
@ -25,7 +28,7 @@
component: SlotRenderer,
},
]
let selectedRows = []
$: hasChildren = $component.children
$: loading = dataProvider?.loading ?? false
$: data = dataProvider?.rows || []
@ -36,6 +39,12 @@
dataProvider?.id,
ActionTypes.SetDataProviderSorting
)
$: {
rowSelectionStore.actions.updateSelection(
$component.id,
selectedRows.map(row => row._id)
)
}
const getFields = (schema, customColumns, showAutoColumns) => {
// Check for an invalid column selection
@ -117,6 +126,10 @@
const split = linkURL.split("/:")
routeStore.actions.navigate(`${split[0]}/${id}`, linkPeek)
}
onDestroy(() => {
rowSelectionStore.actions.updateSelection($component.id, [])
})
</script>
<div use:styleable={$component.styles} class={size}>
@ -128,7 +141,8 @@
{quiet}
{compact}
{customRenderers}
allowSelectRows={false}
allowSelectRows={!!allowSelectRows}
bind:selectedRows
allowEditRows={false}
allowEditColumns={false}
showAutoColumns={true}
@ -139,10 +153,19 @@
>
<slot />
</Table>
{#if allowSelectRows && selectedRows.length}
<div class="row-count">
{selectedRows.length} row{selectedRows.length === 1 ? "" : "s"} selected
</div>
{/if}
</div>
<style>
div {
background-color: var(--spectrum-alias-background-color-secondary);
}
.row-count {
margin-top: var(--spacing-l);
}
</style>

8
packages/client/src/components/context/RowSelectionProvider.svelte

@ -0,0 +1,8 @@
<script>
import Provider from "./Provider.svelte"
import { rowSelectionStore } from "stores"
</script>
<Provider key="rowSelection" data={$rowSelectionStore}>
<slot />
</Provider>

2
packages/client/src/sdk.js

@ -6,6 +6,7 @@ import {
screenStore,
builderStore,
uploadStore,
rowSelectionStore,
} from "stores"
import { styleable } from "utils/styleable"
import { linkable } from "utils/linkable"
@ -19,6 +20,7 @@ export default {
authStore,
notificationStore,
routeStore,
rowSelectionStore,
screenStore,
builderStore,
uploadStore,

2
packages/client/src/stores/index.js

@ -10,7 +10,7 @@ export { peekStore } from "./peek"
export { stateStore } from "./state"
export { themeStore } from "./theme"
export { uploadStore } from "./uploads.js"
export { rowSelectionStore } from "./rowSelection.js"
// Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context"

22
packages/client/src/stores/rowSelection.js

@ -0,0 +1,22 @@
import { writable } from "svelte/store"
const createRowSelectionStore = () => {
const store = writable({})
function updateSelection(componentId, selectedRows) {
store.update(state => {
state[componentId] = [...selectedRows]
return state
})
}
return {
subscribe: store.subscribe,
set: store.set,
actions: {
updateSelection,
},
}
}
export const rowSelectionStore = createRowSelectionStore()

6
packages/client/src/utils/buttonActions.js

@ -127,12 +127,16 @@ const queryExecutionHandler = async action => {
// Trigger a notification and invalidate the datasource as long as this
// was not a readable query
if (!query.readable) {
API.notifications.error.success("Query executed successfully")
notificationStore.actions.success("Query executed successfully")
await dataSourceStore.actions.invalidateDataSource(query.datasourceId)
}
return { result }
} catch (error) {
notificationStore.actions.error(
"An error occurred while executing the query"
)
// Abort next actions
return false
}

4
packages/frontend-core/package.json

@ -1,12 +1,12 @@
{
"name": "@budibase/frontend-core",
"version": "1.0.76-alpha.5",
"version": "1.0.79-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.0.76-alpha.5",
"@budibase/bbui": "^1.0.79-alpha.5",
"lodash": "^4.17.21",
"svelte": "^3.46.2"
}

8
packages/server/package.json

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "1.0.76-alpha.3",
"version": "1.0.79-alpha.5",
"description": "Budibase Web Server",
"main": "src/index.ts",
"repository": {
@ -74,9 +74,9 @@
"license": "GPL-3.0",
"dependencies": {
"@apidevtools/swagger-parser": "^10.0.3",
"@budibase/backend-core": "^1.0.76-alpha.5",
"@budibase/client": "^1.0.76-alpha.5",
"@budibase/string-templates": "^1.0.76-alpha.5",
"@budibase/backend-core": "^1.0.79-alpha.5",
"@budibase/client": "^1.0.79-alpha.5",
"@budibase/string-templates": "^1.0.79-alpha.5",
"@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0",

2
packages/string-templates/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "1.0.76-alpha.5",
"version": "1.0.79-alpha.5",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs",
"module": "dist/bundle.mjs",

6
packages/worker/package.json

@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
"version": "1.0.76-alpha.5",
"version": "1.0.79-alpha.5",
"description": "Budibase background service",
"main": "src/index.ts",
"repository": {
@ -34,8 +34,8 @@
"author": "Budibase",
"license": "GPL-3.0",
"dependencies": {
"@budibase/backend-core": "^1.0.76-alpha.5",
"@budibase/string-templates": "^1.0.76-alpha.5",
"@budibase/backend-core": "^1.0.79-alpha.5",
"@budibase/string-templates": "^1.0.79-alpha.5",
"@koa/router": "^8.0.0",
"@sentry/node": "^6.0.0",
"@techpass/passport-openidconnect": "^0.3.0",

Loading…
Cancel
Save