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

This commit is contained in:
mike12345567 2022-03-01 18:43:52 +00:00
commit 2436bc2e32
41 changed files with 705 additions and 319 deletions

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "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", "description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js", "main": "src/index.js",
"author": "Budibase", "author": "Budibase",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "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", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
], ],
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1", "@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/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2", "@spectrum-css/avatar": "^3.0.2",

View File

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

View File

@ -54,34 +54,43 @@
<svelte:window on:keydown={handleKey} /> <svelte:window on:keydown={handleKey} />
<!-- These svelte if statements need to be defined like this. --> {#if inline}
<!-- The modal transitions do not work if nested inside more than one "if" --> {#if visible}
{#if visible && inline} <div use:focusFirstInput class="spectrum-Modal inline is-open">
<div use:focusFirstInput class="spectrum-Modal inline is-open"> <slot />
<slot /> </div>
</div> {/if}
{:else if visible} {: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"> <Portal target=".modal-container">
<div {#if visible}
class="spectrum-Underlay is-open" <div
in:fade={{ duration: 200 }} class="spectrum-Underlay is-open"
out:fade|local={{ duration: 200 }} in:fade={{ duration: 200 }}
on:mousedown|self={cancel} 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}> <div class="modal-wrapper" on:mousedown|self={cancel}>
<slot name="outside" /> <div class="modal-inner-wrapper" on:mousedown|self={cancel}>
<div <slot name="outside" />
use:focusFirstInput <div
class="spectrum-Modal is-open" use:focusFirstInput
in:fly={{ y: 30, duration: 200 }} class="spectrum-Modal is-open"
out:fly|local={{ y: 30, duration: 200 }} in:fly={{ y: 30, duration: 200 }}
> out:fly|local={{ y: 30, duration: 200 }}
<slot /> >
<slot />
</div>
</div> </div>
</div> </div>
</div> </div>
</div> {/if}
</Portal> </Portal>
{/if} {/if}

View File

@ -8,9 +8,21 @@
export let allowEditRows = false export let allowEditRows = false
</script> </script>
{#if allowSelectRows} <div>
<Checkbox value={selected} /> {#if allowSelectRows}
{/if} <Checkbox value={selected} />
{#if allowEditRows} {/if}
<ActionButton size="S" on:click={onEdit}>Edit</ActionButton> {#if allowEditRows}
{/if} <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>

View File

@ -5,6 +5,7 @@
import SelectEditRenderer from "./SelectEditRenderer.svelte" import SelectEditRenderer from "./SelectEditRenderer.svelte"
import { cloneDeep, deepGet } from "../helpers" import { cloneDeep, deepGet } from "../helpers"
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte" import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
import Checkbox from "../Form/Checkbox.svelte"
/** /**
* The expected schema is our normal couch schemas for our tables. * The expected schema is our normal couch schemas for our tables.
@ -31,7 +32,6 @@
export let allowEditRows = true export let allowEditRows = true
export let allowEditColumns = true export let allowEditColumns = true
export let selectedRows = [] export let selectedRows = []
export let editColumnTitle = "Edit"
export let customRenderers = [] export let customRenderers = []
export let disableSorting = false export let disableSorting = false
export let autoSortColumns = true export let autoSortColumns = true
@ -50,6 +50,8 @@
// Table state // Table state
let height = 0 let height = 0
let loaded = false let loaded = false
let checkboxStatus = false
$: schema = fixSchema(schema) $: schema = fixSchema(schema)
$: if (!loading) loaded = true $: if (!loading) loaded = true
$: fields = getFields(schema, showAutoColumns, autoSortColumns) $: fields = getFields(schema, showAutoColumns, autoSortColumns)
@ -67,6 +69,16 @@
$: showEditColumn = allowEditRows || allowSelectRows $: showEditColumn = allowEditRows || allowSelectRows
$: cellStyles = computeCellStyles(schema) $: 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 => { const fixSchema = schema => {
let fixedSchema = {} let fixedSchema = {}
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => { Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
@ -197,13 +209,32 @@
if (!allowSelectRows) { if (!allowSelectRows) {
return return
} }
if (selectedRows.includes(row)) { if (selectedRows.some(selectedRow => selectedRow._id === row._id)) {
selectedRows = selectedRows.filter(selectedRow => selectedRow !== row) selectedRows = selectedRows.filter(
selectedRow => selectedRow._id !== row._id
)
} else { } else {
selectedRows = [...selectedRows, row] 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 => { const computeCellStyles = schema => {
let styles = {} let styles = {}
Object.keys(schema || {}).forEach(field => { Object.keys(schema || {}).forEach(field => {
@ -244,7 +275,14 @@
<div <div
class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit" 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> </div>
{/if} {/if}
{#each fields as field} {#each fields as field}
@ -302,11 +340,16 @@
{#if showEditColumn} {#if showEditColumn}
<div <div
class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit" class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
on:click={e => {
toggleSelectRow(row)
e.stopPropagation()
}}
> >
<SelectEditRenderer <SelectEditRenderer
data={row} data={row}
selected={selectedRows.includes(row)} selected={selectedRows.findIndex(
onToggleSelection={() => toggleSelectRow(row)} selectedRow => selectedRow._id === row._id
) !== -1}
onEdit={e => editRow(e, row)} onEdit={e => editRow(e, row)}
{allowSelectRows} {allowSelectRows}
{allowEditRows} {allowEditRows}

View File

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

View File

@ -27,10 +27,13 @@ filterTests(["smoke", "all"], () => {
it("updates a column on the table", () => { it("updates a column on the table", () => {
cy.get(".title").click() cy.get(".title").click()
cy.get(".spectrum-Table-editIcon > use").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 // Unset table display column
cy.get(".spectrum-Switch-input").eq(1).click() cy.get(".spectrum-Switch-input").eq(1).click()
cy.contains("Save Column").click() cy.contains("Save Column").click()
})
cy.contains("nameupdated ").should("contain", "nameupdated") cy.contains("nameupdated ").should("contain", "nameupdated")
}) })

View File

@ -1,43 +1,45 @@
import filterTests from "../../support/filterTests" import filterTests from "../../support/filterTests"
filterTests(['smoke', 'all'], () => { filterTests(["smoke", "all"], () => {
context("REST Datasource Testing", () => { context("REST Datasource Testing", () => {
before(() => { before(() => {
cy.login() cy.login()
cy.createTestApp() 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)
})
}) })
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)
})
})
}) })

View File

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

View File

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

View File

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

View File

@ -32,12 +32,14 @@ export const getBindableProperties = (asset, componentId) => {
const urlBindings = getUrlBindings(asset) const urlBindings = getUrlBindings(asset)
const deviceBindings = getDeviceBindings() const deviceBindings = getDeviceBindings()
const stateBindings = getStateBindings() const stateBindings = getStateBindings()
const selectedRowsBindings = getSelectedRowsBindings(asset)
return [ return [
...contextBindings, ...contextBindings,
...urlBindings, ...urlBindings,
...stateBindings, ...stateBindings,
...userBindings, ...userBindings,
...deviceBindings, ...deviceBindings,
...selectedRowsBindings,
] ]
} }
@ -315,6 +317,40 @@ const getDeviceBindings = () => {
return bindings 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. * Gets all state bindings that are globally available.
*/ */
@ -597,14 +633,9 @@ const buildFormSchema = component => {
* in the app. * in the app.
*/ */
export const getAllStateVariables = () => { 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 // Find all button action settings in all components
let eventSettings = [] let eventSettings = []
allAssets.forEach(asset => { getAllAssets().forEach(asset => {
findAllMatchingComponents(asset.props, component => { findAllMatchingComponents(asset.props, component => {
const settings = getComponentSettings(component._component) const settings = getComponentSettings(component._component)
settings settings
@ -635,6 +666,15 @@ export const getAllStateVariables = () => {
return Array.from(bindingSet) 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. * Recurses the input object to remove any instances of bindings.
*/ */

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@
Button, Button,
StatusLight, StatusLight,
ActionButton, ActionButton,
Select,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
@ -18,7 +19,6 @@
import ActionModal from "./ActionModal.svelte" import ActionModal from "./ActionModal.svelte"
import { externalActions } from "./ExternalActions" import { externalActions } from "./ExternalActions"
export let onSelect
export let block export let block
export let testDataModal export let testDataModal
let selected let selected
@ -28,6 +28,10 @@
let setupToggled let setupToggled
let blockComplete let blockComplete
$: rowControl = $automationStore.selectedAutomation.automation.rowControl
$: showBindingPicker =
block.stepId === "CREATE_ROW" || block.stepId === "UPDATE_ROW"
$: testResult = $automationStore.selectedAutomation.testResults?.steps.filter( $: testResult = $automationStore.selectedAutomation.testResults?.steps.filter(
step => (block.id ? step.id === block.id : step.stepId === block.stepId) step => (block.id ? step.id === block.id : step.stepId === block.stepId)
) )
@ -44,12 +48,6 @@
$automationStore.selectedAutomation?.automation?.definition?.steps.length + $automationStore.selectedAutomation?.automation?.definition?.steps.length +
1 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( $: hasCompletedInputs = Object.keys(
block.schema?.inputs?.properties || {} block.schema?.inputs?.properties || {}
).every(x => block?.inputs[x]) ).every(x => block?.inputs[x])
@ -64,6 +62,26 @@
notifications.error("Error saving notification") 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> </script>
<div <div
@ -126,15 +144,33 @@
<Layout noPadding gap="S"> <Layout noPadding gap="S">
<div class="splitHeader"> <div class="splitHeader">
<ActionButton <ActionButton
on:click={() => (setupToggled = !setupToggled)} on:click={() => {
onSelect(block)
setupToggled = !setupToggled
}}
quiet quiet
icon={setupToggled ? "ChevronDown" : "ChevronRight"} icon={setupToggled ? "ChevronDown" : "ChevronRight"}
> >
<Detail size="S">Setup</Detail> <Detail size="S">Setup</Detail>
</ActionButton> </ActionButton>
{#if !isTrigger} {#if !isTrigger}
<div on:click={() => deleteStep()}> <div class="block-options">
<Icon name="DeleteOutline" /> {#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> </div>
{/if} {/if}
</div> </div>
@ -180,6 +216,13 @@
{/if} {/if}
<style> <style>
.delete-padding {
padding-left: 30px;
}
.block-options {
display: flex;
align-items: center;
}
.center-items { .center-items {
display: flex; display: flex;
align-items: center; align-items: center;

View File

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

View File

@ -1,26 +1,31 @@
<script> <script>
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { import { Select } from "@budibase/bbui"
Select,
Toggle,
DatePicker,
Multiselect,
TextArea,
} from "@budibase/bbui"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import { createEventDispatcher } from "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 { automationStore } from "builderStore"
import RowSelectorTypes from "./RowSelectorTypes.svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value export let value
export let bindings export let bindings
export let block
let table let table
let schemaFields 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) table = $tables.list.find(table => table._id === value?.tableId)
schemaFields = Object.entries(table?.schema ?? {}) schemaFields = Object.entries(table?.schema ?? {})
@ -37,18 +42,48 @@
dispatch("change", value) dispatch("change", value)
} }
const onChange = (e, field) => { const coerce = (value, type) => {
value[field] = e.detail 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) dispatch("change", value)
} }
// Ensure any nullish tableId values get set to empty string so // Ensure any nullish tableId values get set to empty string so
// that the select works // that the select works
$: if (value?.tableId == null) value = { tableId: "" } $: if (value?.tableId == null) value = { tableId: "" }
function schemaHasOptions(schema) {
return !!schema.constraints?.inclusion?.length
}
</script> </script>
<Select <Select
@ -62,55 +97,46 @@
<div class="schema-fields"> <div class="schema-fields">
{#each schemaFields as [field, schema]} {#each schemaFields as [field, schema]}
{#if !schema.autocolumn} {#if !schema.autocolumn}
{#if schemaHasOptions(schema) && schema.type !== "array"} {#if schema.type !== "attachment"}
<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 $automationStore.selectedAutomation.automation.testData} {#if $automationStore.selectedAutomation.automation.testData}
<ModalBindableInput {#if !rowControl}
value={value[field]} <RowSelectorTypes
panel={AutomationBindingPanel} {field}
label={field} {schema}
type={value.customType} {bindings}
on:change={e => onChange(e, field)} {value}
{bindings} {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} {:else}
<DrawerBindableInput <DrawerBindableInput
placeholder={placeholders[schema.type]}
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
value={value[field]} value={Array.isArray(value[field])
on:change={e => onChange(e, field)} ? value[field].join(" ")
: value[field]}
on:change={e => onChange(e, field, schema.type)}
label={field} label={field}
type="string" type="string"
{bindings} {bindings}
fillWidth={true} fillWidth={true}
allowJS={false} allowJS={true}
/> />
{/if} {/if}
{/if} {/if}

View File

@ -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}

View File

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

View File

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

View File

@ -30,8 +30,8 @@ export function breakQueryString(qs) {
const params = qs.split("&") const params = qs.split("&")
let paramObj = {} let paramObj = {}
for (let param of params) { for (let param of params) {
const [key, value] = param.split("=") const split = param.split("=")
paramObj[key] = value paramObj[split[0]] = split.slice(1).join("=")
} }
return paramObj 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) { export function flipHeaderState(headersActivity) {
if (!headersActivity) { if (!headersActivity) {
return {} return {}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.0.76-alpha.5", "version": "1.0.79-alpha.5",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.76-alpha.5", "@budibase/bbui": "^1.0.79-alpha.5",
"@budibase/frontend-core": "^1.0.76-alpha.5", "@budibase/frontend-core": "^1.0.79-alpha.5",
"@budibase/string-templates": "^1.0.76-alpha.5", "@budibase/string-templates": "^1.0.79-alpha.5",
"@spectrum-css/button": "^3.0.3", "@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3", "@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()

View File

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

View File

@ -1,12 +1,12 @@
{ {
"name": "@budibase/frontend-core", "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", "description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.76-alpha.5", "@budibase/bbui": "^1.0.79-alpha.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"svelte": "^3.46.2" "svelte": "^3.46.2"
} }

View File

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

View File

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

View File

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