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

This commit is contained in:
mike12345567 2021-11-24 13:27:58 +00:00
commit de71e83411
46 changed files with 672 additions and 407 deletions

View File

@ -1,5 +1,5 @@
{
"version": "0.9.185-alpha.14",
"version": "0.9.185-alpha.19",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/auth",
"version": "0.9.185-alpha.14",
"version": "0.9.185-alpha.19",
"description": "Authentication middlewares for budibase builder and apps",
"main": "src/index.js",
"author": "Budibase",

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "0.9.185-alpha.14",
"version": "0.9.185-alpha.19",
"license": "AGPL-3.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",

View File

@ -6,6 +6,7 @@
export let value = null
export let label = undefined
export let disabled = false
export let readonly = false
export let labelPosition = "above"
export let error = null
export let placeholder = "Choose an option or type"
@ -33,6 +34,7 @@
{value}
{options}
{placeholder}
{readonly}
{getOptionLabel}
{getOptionValue}
on:change={onChange}

View File

@ -9,6 +9,7 @@
export let id = null
export let placeholder = "Choose an option or type"
export let disabled = false
export let readonly = false
export let error = null
export let options = []
export let getOptionLabel = option => option
@ -73,6 +74,7 @@
value={value || ""}
placeholder={placeholder || ""}
{disabled}
{readonly}
class="spectrum-Textfield-input spectrum-InputGroup-input"
/>
</div>

View File

@ -16,15 +16,15 @@
const dispatch = createEventDispatcher()
let focus = false
const updateValue = value => {
const updateValue = newValue => {
if (readonly) {
return
}
if (type === "number") {
const float = parseFloat(value)
value = isNaN(float) ? null : float
const float = parseFloat(newValue)
newValue = isNaN(float) ? null : float
}
dispatch("change", value)
dispatch("change", newValue)
}
const onFocus = () => {

View File

@ -1,41 +1,44 @@
context("Add Multi-Option Datatype", () => {
before(() => {
cy.login()
cy.createTestApp()
})
before(() => {
cy.login()
cy.createTestApp()
})
it("should create a new table, with data", () => {
cy.createTable("Multi Data")
cy.addColumn("Multi Data", "Test Data", "Multi-select", "1\n2\n3\n4\n5")
cy.addRowMultiValue(["1", "2", "3", "4", "5"])
})
it("should create a new table, with data", () => {
cy.createTable("Multi Data")
cy.addColumn("Multi Data", "Test Data", "Multi-select", "1\n2\n3\n4\n5")
cy.addRowMultiValue(["1", "2", "3", "4", "5"])
})
it ("should add form with multi select picker, containing 5 options", () => {
cy.navigateToFrontend()
cy.wait(500)
// Add data provider
cy.get(`[data-cy="category-Data Provider"]`).click()
cy.get('[data-cy="dataSource-prop-control"]').click()
cy.get(".dropdown").contains("Multi Data").click()
cy.wait(500)
// Add Form with schema to match table
cy.addComponent("Form", "Form")
cy.get('[data-cy="dataSource-prop-control"').click()
cy.get(".dropdown").contains("Multi Data").click()
cy.wait(500)
// Add multi-select picker to form
cy.addComponent("Form", "Multi-select Picker").then((componentId) => {
cy.get('[data-cy="field-prop-control"]').type("Test Data").type('{enter}')
cy.wait(1000)
cy.getComponent(componentId).contains("Choose some options").click()
// Check picker has 5 items
cy.getComponent(componentId).find('li').should('have.length', 5)
// Select all items
for (let i = 1; i < 6; i++) {
cy.getComponent(componentId).find('li').contains(i).click()
}
// Check items have been selected
cy.getComponent(componentId).find('.spectrum-Picker-label').contains("(5)")
})
it("should add form with multi select picker, containing 5 options", () => {
cy.navigateToFrontend()
cy.wait(500)
// Add data provider
cy.get(`[data-cy="category-Data"]`).click()
cy.get(`[data-cy="component-Data Provider"]`).click()
cy.get('[data-cy="dataSource-prop-control"]').click()
cy.get(".dropdown").contains("Multi Data").click()
cy.wait(500)
// Add Form with schema to match table
cy.addComponent("Form", "Form")
cy.get('[data-cy="dataSource-prop-control"').click()
cy.get(".dropdown").contains("Multi Data").click()
cy.wait(500)
// Add multi-select picker to form
cy.addComponent("Form", "Multi-select Picker").then(componentId => {
cy.get('[data-cy="field-prop-control"]').type("Test Data").type("{enter}")
cy.wait(1000)
cy.getComponent(componentId).contains("Choose some options").click()
// Check picker has 5 items
cy.getComponent(componentId).find("li").should("have.length", 5)
// Select all items
for (let i = 1; i < 6; i++) {
cy.getComponent(componentId).find("li").contains(i).click()
}
// Check items have been selected
cy.getComponent(componentId)
.find(".spectrum-Picker-label")
.contains("(5)")
})
})
})

View File

@ -22,9 +22,9 @@ context("Create a automation", () => {
cy.get(".spectrum-Picker-label").click()
cy.wait(500)
cy.contains("dog").click()
cy.wait(2000)
// Create action
cy.contains("Add Action").click()
cy.get(".block > .spectrum-Icon").click()
cy.get(".modal-inner-wrapper").within(() => {
cy.wait(1000)
cy.contains("Create Row").trigger('mouseover').click().click()

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "0.9.185-alpha.14",
"version": "0.9.185-alpha.19",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@ -65,10 +65,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^0.9.185-alpha.14",
"@budibase/client": "^0.9.185-alpha.14",
"@budibase/bbui": "^0.9.185-alpha.19",
"@budibase/client": "^0.9.185-alpha.19",
"@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^0.9.185-alpha.14",
"@budibase/string-templates": "^0.9.185-alpha.19",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",

View File

@ -93,7 +93,9 @@ const automationActions = store => ({
},
select: automation => {
store.update(state => {
let testResults = state.selectedAutomation?.testResults
state.selectedAutomation = new Automation(cloneDeep(automation))
state.selectedAutomation.testResults = testResults
state.selectedBlock = null
return state
})

View File

@ -84,7 +84,6 @@
class="block"
animate:flip={{ duration: 500 }}
in:fly|local={{ x: 500, duration: 1500 }}
out:fly|local={{ x: 500, duration: 800 }}
>
<FlowItem {testDataModal} {testAutomation} {onSelect} {block} />
</div>

View File

@ -25,10 +25,10 @@
let resultsModal
let setupToggled
let blockComplete
$: testResult = $automationStore.selectedAutomation.testResults?.steps.filter(
step => step.stepId === block.stepId
)
$: isTrigger = block.type === "TRIGGER"
$: selected = $automationStore.selectedBlock?.id === block.id
@ -150,15 +150,6 @@
>Finish and test automation</Button
>
{/if}
<Button
disabled={!hasCompletedInputs}
on:click={() => {
setupToggled = false
actionModal.show()
}}
primary={!isTrigger}
cta={isTrigger}>Add Action</Button
>
{/if}
</Layout>
</div>

View File

@ -9,6 +9,7 @@
onMount(() => {
automationStore.actions.fetch()
})
function selectAutomation(automation) {
automationStore.actions.select(automation)
$goto(`./${automation._id}`)

View File

@ -51,31 +51,31 @@
: { schema: {} }
$: schemaFields = table ? Object.values(table.schema) : []
const onChange = debounce(
async function (e, key) {
if (isTestModal) {
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === "WEBHOOK") {
automationStore.actions.addTestDataToAutomation({
body: {
[key]: e.detail,
...$automationStore.selectedAutomation.automation.testData.body,
},
})
}
const onChange = debounce(async function (e, key) {
if (isTestModal) {
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === "WEBHOOK") {
automationStore.actions.addTestDataToAutomation({
[key]: e.detail,
body: {
[key]: e.detail,
...$automationStore.selectedAutomation.automation.testData.body,
},
})
testData[key] = e.detail
} else {
block.inputs[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
}
},
isTestModal ? 0 : 800
)
automationStore.actions.addTestDataToAutomation({
[key]: e.detail,
})
testData[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} else {
block.inputs[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
}
}, 800)
function getAvailableBindings(block, automation) {
if (!block || !automation) {

View File

@ -398,7 +398,7 @@
value={field.formula}
on:change={e => (field.formula = e.detail)}
bindings={getBindings({ table })}
serverSide="true"
allowJS
/>
{:else if field.type === AUTO_TYPE}
<Select

View File

@ -72,15 +72,17 @@
}
</script>
<ActionMenu>
<div slot="control" class="icon">
<Icon s hoverable name="MoreSmallList" />
</div>
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
{#if allowDeletion}
{#if allowDeletion}
<ActionMenu>
<div slot="control" class="icon">
<Icon s hoverable name="MoreSmallList" />
</div>
{#if !external}
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
{/if}
<MenuItem icon="Delete" on:click={showDeleteModal}>Delete</MenuItem>
{/if}
</ActionMenu>
</ActionMenu>
{/if}
<Modal bind:this={editorModal}>
<ModalContent

View File

@ -1,6 +1,4 @@
<script context="module">
import { Label } from "@budibase/bbui"
export const EditorModes = {
JS: {
name: "javascript",
@ -21,6 +19,7 @@
</script>
<script>
import { Label } from "@budibase/bbui"
import CodeMirror from "components/integration/codemirror"
import { themeStore } from "builderStore"
import { createEventDispatcher, onMount } from "svelte"
@ -156,4 +155,9 @@
div :global(.CodeMirror-focused) {
border-color: var(--spectrum-alias-border-color-mouse-focus);
}
/* Ensure hints are always on top */
:global(.CodeMirror-hints) {
z-index: 999999;
}
</style>

View File

@ -22,7 +22,7 @@
const dispatch = createEventDispatcher()
export let bindableProperties
export let bindings
export let value = ""
export let valid
export let allowJS = false
@ -36,17 +36,23 @@
let hbsValue = initialValueJS ? null : value
$: usingJS = mode === "JavaScript"
$: ({ context } = groupBy("type", bindableProperties))
$: searchRgx = new RegExp(search, "ig")
$: filteredBindings = context?.filter(context => {
return context.readableBinding.match(searchRgx)
})
$: categories = Object.entries(groupBy("category", bindings))
$: filteredCategories = categories
.map(([name, categoryBindings]) => ({
name,
bindings: categoryBindings?.filter(binding => {
return binding.readableBinding.match(searchRgx)
}),
}))
.filter(category => category.bindings?.length > 0)
$: filteredHelpers = helpers?.filter(helper => {
return helper.label.match(searchRgx) || helper.description.match(searchRgx)
})
$: codeMirrorHints = bindings?.map(x => `$("${x.readableBinding}")`)
const updateValue = value => {
valid = isValid(readableToRuntimeBinding(bindableProperties, value))
valid = isValid(readableToRuntimeBinding(bindings, value))
if (valid) {
dispatch("change", value)
}
@ -91,7 +97,7 @@
}
onMount(() => {
valid = isValid(readableToRuntimeBinding(bindableProperties, value))
valid = isValid(readableToRuntimeBinding(bindings, value))
})
</script>
@ -102,18 +108,29 @@
<div class="heading">Search</div>
<Search placeholder="Search" bind:value={search} />
</section>
{#if filteredBindings?.length}
<section>
<div class="heading">Bindable Values</div>
<ul>
{#each filteredBindings as binding}
<li on:click={() => addBinding(binding)}>
{binding.readableBinding}
</li>
{/each}
</ul>
</section>
{/if}
{#each filteredCategories as category}
{#if category.bindings?.length}
<section>
<div class="heading">{category.name}</div>
<ul>
{#each category.bindings as binding}
<li on:click={() => addBinding(binding)}>
<span class="binding__label">{binding.readableBinding}</span>
{#if binding.type}
<span class="binding__type">{binding.type}</span>
{/if}
{#if binding.description}
<br />
<div class="binding__description">
{binding.description || ""}
</div>
{/if}
</li>
{/each}
</ul>
</section>
{/if}
{/each}
{#if filteredHelpers?.length && !usingJS}
<section>
<div class="heading">Helpers</div>
@ -162,7 +179,7 @@
height={200}
value={decodeJSBinding(jsValue)}
on:change={onChangeJSValue}
hints={context?.map(x => `$("${x.readableBinding}")`)}
hints={codeMirrorHints}
/>
<Body size="S">
JavaScript expressions are executed as functions, so ensure that
@ -234,6 +251,24 @@
color: var(--spectrum-global-color-gray-900) !important;
}
.binding__label {
font-weight: 600;
text-transform: capitalize;
}
.binding__description {
color: var(--spectrum-global-color-gray-700);
margin: 0.5rem 0 0 0;
white-space: normal;
}
.binding__type {
font-family: monospace;
background-color: var(--spectrum-global-color-gray-200);
border-radius: var(--border-radius-s);
padding: 2px 4px;
margin-left: 2px;
font-weight: 600;
}
.helper {
display: flex;
flex-direction: column;

View File

@ -0,0 +1,30 @@
<script>
import BindingPanel from "./BindingPanel.svelte"
export let bindings = []
export let valid
export let value = ""
export let allowJS = false
$: enrichedBindings = enrichBindings(bindings)
// Ensure bindings have the correct categories
const enrichBindings = bindings => {
if (!bindings?.length) {
return bindings
}
return bindings?.map(binding => ({
...binding,
category: "Bindable Values",
type: null,
}))
}
</script>
<BindingPanel
bind:valid
bindings={enrichedBindings}
{value}
{allowJS}
on:change
/>

View File

@ -4,11 +4,11 @@
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { createEventDispatcher } from "svelte"
import { isJSBinding } from "@budibase/string-templates"
export let panel = BindingPanel
export let panel = ClientBindingPanel
export let value = ""
export let bindings = []
export let title = "Bindings"
@ -55,6 +55,7 @@
<Combobox
{label}
{disabled}
readonly={isJS}
value={isJS ? "(JavaScript function)" : readableValue}
on:type={e => onChange(e.detail, false)}
on:pick={e => onChange(e.detail, true)}
@ -82,7 +83,7 @@
value={readableValue}
close={handleClose}
on:change={event => (tempValue = event.detail)}
bindableProperties={bindings}
{bindings}
{allowJS}
/>
</Drawer>

View File

@ -4,11 +4,11 @@
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { createEventDispatcher } from "svelte"
import { isJSBinding } from "@budibase/string-templates"
export let panel = BindingPanel
export let panel = ClientBindingPanel
export let value = ""
export let bindings = []
export let title = "Bindings"
@ -40,6 +40,7 @@
<Input
{label}
{disabled}
readonly={isJS}
value={isJS ? "(JavaScript function)" : readableValue}
on:change={event => onChange(event.detail)}
{placeholder}
@ -63,7 +64,7 @@
bind:valid
value={readableValue}
on:change={event => (tempValue = event.detail)}
bindableProperties={bindings}
{bindings}
{allowJS}
/>
</Drawer>

View File

@ -6,20 +6,23 @@
} from "builderStore/dataBinding"
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
import { createEventDispatcher } from "svelte"
import { isJSBinding } from "@budibase/string-templates"
export let panel = ServerBindingPanel
export let value = ""
export let bindings = []
export let thin = true
export let title = "Bindings"
export let placeholder
export let label
export let allowJS = false
const dispatch = createEventDispatcher()
let bindingModal
let valid = true
$: readableValue = runtimeToReadableBinding(bindings, value)
$: tempValue = readableValue
$: isJS = isJSBinding(value)
const saveBinding = () => {
onChange(tempValue)
@ -34,8 +37,8 @@
<div class="control">
<Input
{label}
{thin}
value={readableValue}
readonly={isJS}
value={isJS ? "(JavaScript function)" : readableValue}
on:change={event => onChange(event.detail)}
{placeholder}
/>
@ -55,7 +58,8 @@
value={readableValue}
bind:valid
on:change={e => (tempValue = e.detail)}
bindableProperties={bindings}
{bindings}
{allowJS}
/>
</div>
</ModalContent>

View File

@ -1,209 +1,27 @@
<script>
import groupBy from "lodash/fp/groupBy"
import { Search, TextArea, DrawerContent } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { isValid } from "@budibase/string-templates"
import { handlebarsCompletions } from "constants/completions"
import { readableToRuntimeBinding } from "builderStore/dataBinding"
import { addHBSBinding } from "./utils"
import BindingPanel from "./BindingPanel.svelte"
const dispatch = createEventDispatcher()
export let bindableProperties = []
export let valid = true
export let bindings = []
export let valid
export let value = ""
export let allowJS = false
let helpers = handlebarsCompletions()
let getCaretPosition
let search = ""
$: enrichedBindings = enrichBindings(bindings)
$: categories = Object.entries(groupBy("category", bindableProperties))
$: valid = isValid(readableToRuntimeBinding(bindableProperties, value))
$: dispatch("change", value)
$: searchRgx = new RegExp(search, "ig")
$: filteredCategories = categories.map(([categoryName, bindings]) => {
const filteredBindings = bindings.filter(binding => {
return binding.label.match(searchRgx)
})
return [categoryName, filteredBindings]
})
$: filteredHelpers = helpers?.filter(helper => {
return helper.label.match(searchRgx) || helper.description.match(searchRgx)
})
// Ensure bindings have the correct properties
const enrichBindings = bindings => {
return bindings?.map(binding => ({
...binding,
readableBinding: binding.label || binding.readableBinding,
runtimeBinding: binding.path || binding.runtimeBinding,
}))
}
</script>
<DrawerContent>
<svelte:fragment slot="sidebar">
<div class="container">
<section>
<div class="heading">Search</div>
<Search placeholder="Search" bind:value={search} />
</section>
{#each filteredCategories as [categoryName, bindings]}
{#if bindings.length}
<section>
<div class="heading">{categoryName}</div>
<ul>
{#each bindings as binding}
<li
on:click={() => {
value = addHBSBinding(value, getCaretPosition(), binding)
}}
>
<span class="binding__label">{binding.label}</span>
<span class="binding__type">{binding.type}</span>
{#if binding.description}
<br />
<div class="binding__description">
{binding.description || ""}
</div>
{/if}
</li>
{/each}
</ul>
</section>
{/if}
{/each}
{#if filteredHelpers?.length}
<section>
<div class="heading">Helpers</div>
<ul>
{#each filteredHelpers as helper}
<li
on:click={() => {
value = addHBSBinding(value, getCaretPosition(), helper.text)
}}
>
<div class="helper">
<div class="helper__name">{helper.displayText}</div>
<div class="helper__description">
{@html helper.description}
</div>
<pre class="helper__example">{helper.example || ''}</pre>
</div>
</li>
{/each}
</ul>
</section>
{/if}
</div>
</svelte:fragment>
<div class="main">
<TextArea
bind:getCaretPosition
bind:value
placeholder="Add text, or click the objects on the left to add them to the textbox."
/>
{#if !valid}
<p class="syntax-error">
Current Handlebars syntax is invalid, please check the guide
<a href="https://handlebarsjs.com/guide/">here</a>
for more details.
</p>
{/if}
</div>
</DrawerContent>
<style>
.main :global(textarea) {
min-height: 150px !important;
}
.container {
margin: calc(-1 * var(--spacing-xl));
}
.heading {
font-size: var(--font-size-s);
font-weight: 600;
text-transform: uppercase;
color: var(--spectrum-global-color-gray-600);
padding: var(--spacing-xl) 0 var(--spacing-m) 0;
}
section {
padding: 0 var(--spacing-xl) var(--spacing-xl) var(--spacing-xl);
}
section:not(:first-child) {
border-top: var(--border-light);
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
font-size: var(--font-size-s);
padding: var(--spacing-m);
border-radius: 4px;
border: var(--border-light);
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
border-color 130ms ease-in-out;
}
li:not(:last-of-type) {
margin-bottom: var(--spacing-s);
}
li :global(*) {
transition: color 130ms ease-in-out;
}
li:hover {
color: var(--spectrum-global-color-gray-900);
background-color: var(--spectrum-global-color-gray-50);
border-color: var(--spectrum-global-color-gray-500);
cursor: pointer;
}
li:hover :global(*) {
color: var(--spectrum-global-color-gray-900) !important;
}
.helper {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: var(--spacing-xs);
}
.helper__name {
font-weight: bold;
}
.helper__description,
.helper__description :global(*) {
color: var(--spectrum-global-color-gray-700);
}
.helper__example {
white-space: normal;
margin: 0.5rem 0 0 0;
font-weight: 700;
}
.helper__description :global(p) {
margin: 0;
}
.syntax-error {
padding-top: var(--spacing-m);
color: var(--red);
font-size: 12px;
}
.syntax-error a {
color: var(--red);
text-decoration: underline;
}
.binding__label {
font-weight: 600;
text-transform: capitalize;
}
.binding__description {
color: var(--spectrum-global-color-gray-700);
margin: 0.5rem 0 0 0;
white-space: normal;
}
.binding__type {
font-family: monospace;
background-color: var(--spectrum-global-color-gray-200);
border-radius: var(--border-radius-s);
padding: 2px 4px;
margin-left: 2px;
font-weight: 600;
}
</style>
<BindingPanel
bind:valid
bindings={enrichedBindings}
{value}
{allowJS}
on:change
/>

View File

@ -8,12 +8,24 @@
"repeaterblock"
]
},
"section",
"container",
"dataprovider",
"table",
"repeater",
"button",
{
"name": "Layout",
"icon": "ClassicGridView",
"children": [
"container",
"section"
]
},
{
"name": "Data",
"icon": "Data",
"children": [
"dataprovider",
"repeater",
"table",
"dynamicfilter"
]
},
{
"name": "Form",
"icon": "Form",
@ -60,6 +72,7 @@
"children": [
"heading",
"text",
"button",
"divider",
"image",
"backgroundimage",

View File

@ -60,7 +60,7 @@
</div>
<Detail size="S">Autogenerated Screens</Detail>
{#each $tables.list.filter(table => table.type !== "external" && table._id !== "ta_users") as table}
{#each $tables.list.filter(table => table._id !== "ta_users") as table}
<div
class:disabled={blankSelected}
class:selected={selectedScreens.find(x => x.name.includes(table.name))}
@ -90,7 +90,6 @@
.content {
letter-spacing: 0px;
color: #2c2c2c;
}
.item {
cursor: pointer;

View File

@ -11,14 +11,14 @@
Select,
} from "@budibase/bbui"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { generate } from "shortid"
import { getValidOperatorsForType, OperatorOptions } from "constants/lucene"
export let schemaFields
export let filters = []
export let bindings = []
export let panel = BindingPanel
export let panel = ClientBindingPanel
export let allowBindings = true
const BannedTypes = ["link", "attachment", "formula"]

View File

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

View File

@ -247,7 +247,6 @@
"description": "A basic html button that is ready for styling",
"icon": "Button",
"editable": true,
"illegalChildren": ["section"],
"showSettingsBar": true,
"settings": [
{
@ -2647,6 +2646,49 @@
}
]
},
"dynamicfilter": {
"name": "Dynamic Filter",
"icon": "FilterEdit",
"showSettingsBar": true,
"settings": [
{
"type": "dataProvider",
"label": "Provider",
"key": "dataProvider"
},
{
"type": "multifield",
"label": "Allowed filter fields",
"key": "allowedFields",
"placeholder": "All fields"
},
{
"type": "select",
"label": "Button size",
"showInBar": true,
"key": "size",
"options": [
{
"label": "Small",
"value": "S"
},
{
"label": "Medium",
"value": "M"
},
{
"label": "Large",
"value": "L"
},
{
"label": "Extra large",
"value": "XL"
}
],
"defaultValue": "M"
}
]
},
"tableblock": {
"block": true,
"name": "Table block",

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "0.9.185-alpha.14",
"version": "0.9.185-alpha.19",
"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": "^0.9.185-alpha.14",
"@budibase/bbui": "^0.9.185-alpha.19",
"@budibase/standard-components": "^0.9.139",
"@budibase/string-templates": "^0.9.185-alpha.14",
"@budibase/string-templates": "^0.9.185-alpha.19",
"regexparam": "^1.3.0",
"shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5"

View File

@ -121,6 +121,9 @@
-->
<div id="flatpickr-root" />
<!-- Modal container to ensure they sit on top -->
<div class="modal-container" />
<!-- Layers on top of app -->
<NotificationDisplay />
<ConfirmationDisplay />

View File

@ -72,13 +72,18 @@
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
$: inDragPath = inSelectedPath && $builderStore.editMode
// Derive definition properties which can all be optional, so need to be
// coerced to booleans
$: editable = !!definition?.editable
$: hasChildren = !!definition?.hasChildren
$: showEmptyState = definition?.showEmptyState !== false
// Interactive components can be selected, dragged and highlighted inside
// the builder preview
$: interactive =
$builderStore.inBuilder &&
($builderStore.previewType === "layout" || insideScreenslot) &&
!isBlock
$: editable = definition?.editable
$: editing = editable && selected && $builderStore.editMode
$: draggable = !inDragPath && interactive && !isLayout && !isScreen
$: droppable = interactive && !isLayout && !isScreen
@ -86,8 +91,8 @@
// Empty components are those which accept children but do not have any.
// Empty states can be shown for these components, but can be disabled
// in the component manifest.
$: empty = interactive && !children.length && definition?.hasChildren
$: emptyState = empty && definition?.showEmptyState !== false
$: empty = interactive && !children.length && hasChildren
$: emptyState = empty && showEmptyState
// Raw settings are all settings excluding internal props and children
$: rawSettings = getRawSettings(instance)
@ -103,6 +108,9 @@
// Build up the final settings object to be passed to the component
$: cacheSettings(enrichedSettings, nestedSettings, conditionalSettings)
// Render key is used to determine when components need to fully remount
$: renderKey = getRenderKey(id, editing)
// Update component context
$: componentStore.set({
id,
@ -268,35 +276,43 @@
})
}
}
// Generates a key used to determine when components need to fully remount.
// Currently only toggling editing requires remounting.
const getRenderKey = (id, editing) => {
return hashString(`${id}-${editing}`)
}
</script>
{#if constructor && cachedSettings && (visible || inSelectedPath)}
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
<!-- and the performance matters for the selection indicators -->
<div
class={`component ${id}`}
class:draggable
class:droppable
class:empty
class:interactive
class:editing
class:block={isBlock}
data-id={id}
data-name={name}
>
<svelte:component this={constructor} {...cachedSettings}>
{#if children.length}
{#each children as child (child._id)}
<svelte:self instance={child} />
{/each}
{:else if emptyState}
<Placeholder />
{:else if isBlock}
<slot />
{/if}
</svelte:component>
</div>
{/if}
{#key renderKey}
{#if constructor && cachedSettings && (visible || inSelectedPath)}
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
<!-- and the performance matters for the selection indicators -->
<div
class={`component ${id}`}
class:draggable
class:droppable
class:empty
class:interactive
class:editing
class:block={isBlock}
data-id={id}
data-name={name}
>
<svelte:component this={constructor} {...cachedSettings}>
{#if children.length}
{#each children as child (child._id)}
<svelte:self instance={child} />
{/each}
{:else if emptyState}
<Placeholder />
{:else if isBlock}
<slot />
{/if}
</svelte:component>
</div>
{/if}
{/key}
<style>
.component {

View File

@ -12,6 +12,10 @@
export let type = "primary"
export let quiet = false
// For internal use only for now - not defined in the manifest
export let icon = null
export let active = false
let node
$: $component.editing && node?.focus()
@ -25,7 +29,7 @@
}
const updateText = e => {
builderStore.actions.updateProp("text", e.target.textContent)
builderStore.actions.updateProp("text", e.target.textContent.trim())
}
</script>
@ -35,10 +39,22 @@
{disabled}
use:styleable={$component.styles}
on:click={onClick}
contenteditable={$component.editing}
contenteditable={$component.editing && !icon}
on:blur={$component.editing ? updateText : null}
bind:this={node}
class:active
>
{#if icon}
<svg
class:hasText={componentText?.length > 0}
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
focusable="false"
aria-hidden="true"
aria-label={icon}
>
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
{/if}
{componentText}
</button>
@ -56,4 +72,10 @@
.spectrum-Button::after {
display: none;
}
.spectrum-Icon.hasText {
margin-right: var(--spectrum-button-primary-icon-gap);
}
.active {
color: var(--spectrum-global-color-blue-600);
}
</style>

View File

@ -275,11 +275,10 @@
allRows = res.rows
}
const addQueryExtension = (key, operator, field, value) => {
if (!key || !operator || !field) {
const addQueryExtension = (key, extension) => {
if (!key || !extension) {
return
}
const extension = { operator, field, value }
queryExtensions = { ...queryExtensions, [key]: extension }
}
@ -295,11 +294,13 @@
const extendQuery = (defaultQuery, extensions) => {
const extensionValues = Object.values(extensions || {})
let extendedQuery = { ...defaultQuery }
extensionValues.forEach(({ operator, field, value }) => {
extendedQuery[operator] = {
...extendedQuery[operator],
[field]: value,
}
extensionValues.forEach(extension => {
Object.entries(extension || {}).forEach(([operator, fields]) => {
extendedQuery[operator] = {
...extendedQuery[operator],
...fields,
}
})
})
if (JSON.stringify(query) !== JSON.stringify(extendedQuery)) {

View File

@ -13,15 +13,6 @@
const component = getContext("component")
const { styleable, ActionTypes, getAction } = getContext("sdk")
$: addExtension = getAction(
dataProvider?.id,
ActionTypes.AddDataProviderQueryExtension
)
$: removeExtension = getAction(
dataProvider?.id,
ActionTypes.RemoveDataProviderQueryExtension
)
const options = [
"Last 1 day",
"Last 7 days",
@ -32,10 +23,23 @@
]
let value = options.includes(defaultValue) ? defaultValue : "Last 30 days"
$: queryExtension = getQueryExtension(value)
$: addExtension?.($component.id, "range", field, queryExtension)
$: dataProviderId = dataProvider?.id
$: addExtension = getAction(
dataProviderId,
ActionTypes.AddDataProviderQueryExtension
)
$: removeExtension = getAction(
dataProviderId,
ActionTypes.RemoveDataProviderQueryExtension
)
$: queryExtension = getQueryExtension(field, value)
$: addExtension?.($component.id, queryExtension)
const getQueryExtension = (field, value) => {
if (!field || !value) {
return null
}
const getQueryExtension = value => {
let low = dayjs.utc().subtract(1, "year")
let high = dayjs.utc().add(1, "day")
@ -51,7 +55,14 @@
low = dayjs.utc().subtract(6, "months")
}
return { low: low.format(), high: high.format() }
return {
range: {
[field]: {
low: low.format(),
high: high.format(),
},
},
}
}
onDestroy(() => {

View File

@ -47,7 +47,7 @@
// Convert contenteditable HTML to text and save
const updateText = e => {
const sanitized = e.target.innerHTML.replace(/<br>/gi, "\n")
const sanitized = e.target.innerHTML.replace(/<br>/gi, "\n").trim()
builderStore.actions.updateProp("text", sanitized)
}
</script>

View File

@ -47,7 +47,7 @@
}
const updateText = e => {
builderStore.actions.updateProp("text", e.target.textContent)
builderStore.actions.updateProp("text", e.target.textContent.trim())
}
</script>

View File

@ -46,7 +46,7 @@
// Convert contenteditable HTML to text and save
const updateText = e => {
const sanitized = e.target.innerHTML.replace(/<br>/gi, "\n")
const sanitized = e.target.innerHTML.replace(/<br>/gi, "\n").trim()
builderStore.actions.updateProp("text", sanitized)
}
</script>

View File

@ -0,0 +1,85 @@
<script>
import { get } from "svelte/store"
import { getContext, onDestroy } from "svelte"
import { ModalContent, Modal } from "@budibase/bbui"
import FilterModal from "./FilterModal.svelte"
import { buildLuceneQuery } from "builder/src/helpers/lucene"
import Button from "../Button.svelte"
export let dataProvider
export let allowedFields
export let size = "M"
const component = getContext("component")
const { builderStore, ActionTypes, getAction } = getContext("sdk")
let modal
let tmpFilters = []
let filters = []
$: dataProviderId = dataProvider?.id
$: addExtension = getAction(
dataProviderId,
ActionTypes.AddDataProviderQueryExtension
)
$: removeExtension = getAction(
dataProviderId,
ActionTypes.RemoveDataProviderQueryExtension
)
$: schema = dataProvider?.schema
$: schemaFields = getSchemaFields(schema, allowedFields)
// Add query extension to data provider
$: {
if (filters?.length) {
const queryExtension = buildLuceneQuery(filters)
addExtension?.($component.id, queryExtension)
} else {
removeExtension?.($component.id)
}
}
const getSchemaFields = (schema, allowedFields) => {
let clonedSchema = {}
if (!allowedFields?.length) {
clonedSchema = schema
} else {
allowedFields?.forEach(field => {
clonedSchema[field] = schema[field]
})
}
return Object.values(clonedSchema || {})
}
const openEditor = () => {
if (get(builderStore).inBuilder) {
return
}
tmpFilters = [...filters]
modal.show()
}
const updateQuery = () => {
filters = [...tmpFilters]
}
onDestroy(() => {
removeExtension?.($component.id)
})
</script>
<Button
onClick={openEditor}
icon="Properties"
text="Filter"
{size}
type="secondary"
quiet
active={filters?.length > 0}
/>
<Modal bind:this={modal}>
<ModalContent title="Edit filters" size="XL" onConfirm={updateQuery}>
<FilterModal bind:filters={tmpFilters} {schemaFields} />
</ModalContent>
</Modal>

View File

@ -0,0 +1,176 @@
<script>
import {
Body,
Button,
Combobox,
DatePicker,
DrawerContent,
Icon,
Input,
Layout,
Select,
} from "@budibase/bbui"
import { generate } from "shortid"
import {
getValidOperatorsForType,
OperatorOptions,
} from "builder/src/constants/lucene"
export let schemaFields
export let filters = []
const BannedTypes = ["link", "attachment", "formula"]
$: fieldOptions = (schemaFields ?? [])
.filter(field => !BannedTypes.includes(field.type))
.map(field => field.name)
const addFilter = () => {
filters = [
...filters,
{
id: generate(),
field: null,
operator: OperatorOptions.Equals.value,
value: null,
valueType: "Value",
},
]
}
const removeFilter = id => {
filters = filters.filter(field => field.id !== id)
}
const duplicateFilter = id => {
const existingFilter = filters.find(filter => filter.id === id)
const duplicate = { ...existingFilter, id: generate() }
filters = [...filters, duplicate]
}
const onFieldChange = (expression, field) => {
// Update the field type
expression.type = schemaFields.find(x => x.name === field)?.type
// Ensure a valid operator is set
const validOperators = getValidOperatorsForType(expression.type).map(
x => x.value
)
if (!validOperators.includes(expression.operator)) {
expression.operator = validOperators[0] ?? OperatorOptions.Equals.value
onOperatorChange(expression, expression.operator)
}
// if changed to an array, change default value to empty array
const idx = filters.findIndex(x => x.field === field)
if (expression.type === "array") {
filters[idx].value = []
} else {
filters[idx].value = null
}
}
const onOperatorChange = (expression, operator) => {
const noValueOptions = [
OperatorOptions.Empty.value,
OperatorOptions.NotEmpty.value,
]
expression.noValue = noValueOptions.includes(operator)
if (expression.noValue) {
expression.value = null
}
}
const getFieldOptions = field => {
const schema = schemaFields.find(x => x.name === field)
return schema?.constraints?.inclusion || []
}
</script>
<DrawerContent>
<div class="container">
<Layout noPadding>
<Body size="S">
{#if !filters?.length}
Add your first filter expression.
{:else}
Results are filtered to only those which match all of the following
constraints.
{/if}
</Body>
{#if filters?.length}
<div class="fields">
{#each filters as filter, idx}
<Select
bind:value={filter.field}
options={fieldOptions}
on:change={e => onFieldChange(filter, e.detail)}
placeholder="Column"
/>
<Select
disabled={!filter.field}
options={getValidOperatorsForType(filter.type)}
bind:value={filter.operator}
on:change={e => onOperatorChange(filter, e.detail)}
placeholder={null}
/>
{#if ["string", "longform", "number"].includes(filter.type)}
<Input disabled={filter.noValue} bind:value={filter.value} />
{:else if ["options", "array"].includes(filter.type)}
<Combobox
disabled={filter.noValue}
options={getFieldOptions(filter.field)}
bind:value={filter.value}
/>
{:else if filter.type === "boolean"}
<Combobox
disabled={filter.noValue}
options={[
{ label: "True", value: "true" },
{ label: "False", value: "false" },
]}
bind:value={filter.value}
/>
{:else if filter.type === "datetime"}
<DatePicker disabled={filter.noValue} bind:value={filter.value} />
{:else}
<Input disabled />
{/if}
<Icon
name="Duplicate"
hoverable
size="S"
on:click={() => duplicateFilter(filter.id)}
/>
<Icon
name="Close"
hoverable
size="S"
on:click={() => removeFilter(filter.id)}
/>
{/each}
</div>
{/if}
<div>
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
Add filter
</Button>
</div>
</Layout>
</div>
</DrawerContent>
<style>
.container {
width: 100%;
max-width: 1000px;
margin: 0 auto;
}
.fields {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
align-items: center;
grid-template-columns: 1fr 120px 1fr auto auto;
}
</style>

View File

@ -0,0 +1 @@
export { default as dynamicfilter } from "./DynamicFilter.svelte"

View File

@ -49,7 +49,7 @@
$: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}`
const updateLabel = e => {
builderStore.actions.updateProp("label", e.target.textContent)
builderStore.actions.updateProp("label", e.target.textContent.trim())
}
</script>

View File

@ -33,6 +33,7 @@ export * from "./charts"
export * from "./forms"
export * from "./table"
export * from "./blocks"
export * from "./dynamic-filter"
// Deprecated component left for compatibility in old apps
export { default as navigation } from "./deprecated/Navigation.svelte"

View File

@ -10,7 +10,6 @@ const dispatchEvent = (type, data = {}) => {
const createBuilderStore = () => {
const initialState = {
inBuilder: false,
appId: null,
layout: null,
screen: null,
selectedComponentId: null,
@ -94,6 +93,7 @@ const createBuilderStore = () => {
}
return {
...writableStore,
set: state => writableStore.set({ ...initialState, ...state }),
subscribe: derivedStore.subscribe,
actions,
}

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "0.9.185-alpha.14",
"version": "0.9.185-alpha.19",
"description": "Budibase Web Server",
"main": "src/index.js",
"repository": {
@ -68,9 +68,9 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.9.185-alpha.14",
"@budibase/client": "^0.9.185-alpha.14",
"@budibase/string-templates": "^0.9.185-alpha.14",
"@budibase/auth": "^0.9.185-alpha.19",
"@budibase/client": "^0.9.185-alpha.19",
"@budibase/string-templates": "^0.9.185-alpha.19",
"@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0",
@ -155,4 +155,4 @@
"oracledb": "^5.3.0"
},
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
}
}

View File

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

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
"version": "0.9.185-alpha.14",
"version": "0.9.185-alpha.19",
"description": "Budibase background service",
"main": "src/index.js",
"repository": {
@ -29,8 +29,8 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.9.185-alpha.14",
"@budibase/string-templates": "^0.9.185-alpha.14",
"@budibase/auth": "^0.9.185-alpha.19",
"@budibase/string-templates": "^0.9.185-alpha.19",
"@koa/router": "^8.0.0",
"@sentry/node": "^6.0.0",
"@techpass/passport-openidconnect": "^0.3.0",