Merge branch 'develop' of github.com:Budibase/budibase into feature/budibase-api
This commit is contained in:
commit
2436bc2e32
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.0.76-alpha.5",
|
||||
"version": "1.0.79-alpha.5",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -41,6 +41,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
intelligentLoading: false,
|
||||
deviceAwareness: false,
|
||||
state: false,
|
||||
rowSelection: false,
|
||||
customThemes: false,
|
||||
devicePreview: false,
|
||||
messagePassing: false,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -227,6 +227,7 @@
|
|||
/>
|
||||
{:else if value.customType === "row"}
|
||||
<RowSelector
|
||||
{block}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
|
@ -57,7 +57,8 @@
|
|||
{data}
|
||||
{loading}
|
||||
{type}
|
||||
allowEditing={!view?.calculation}
|
||||
allowEditing={false}
|
||||
rowCount={10}
|
||||
bind:hideAutocolumns
|
||||
>
|
||||
<ViewFilterButton {view} />
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -60,7 +60,7 @@ export function createQueriesStore() {
|
|||
})
|
||||
return savedQuery
|
||||
},
|
||||
import: async (data, datasourceId) => {
|
||||
import: async ({ data, datasourceId }) => {
|
||||
return await API.importQueries({
|
||||
datasourceId,
|
||||
data,
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<script>
|
||||
import Provider from "./Provider.svelte"
|
||||
import { rowSelectionStore } from "stores"
|
||||
</script>
|
||||
|
||||
<Provider key="rowSelection" data={$rowSelectionStore}>
|
||||
<slot />
|
||||
</Provider>
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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…
Reference in New Issue