1
0
Fork 0
mirror of synced 2024-10-03 19:43:32 +13:00

Merge pull request #1132 from Budibase/repeater-filtering

Repeater Filtering + Drawer Fixes + Button Action Fixes
This commit is contained in:
Andrew Kingston 2021-02-22 07:04:08 -08:00 committed by GitHub
commit 4968357788
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 451 additions and 1396 deletions

View file

@ -63,7 +63,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.58.12", "@budibase/bbui": "^1.58.13",
"@budibase/client": "^0.7.8", "@budibase/client": "^0.7.8",
"@budibase/colorpicker": "1.0.1", "@budibase/colorpicker": "1.0.1",
"@budibase/string-templates": "^0.7.8", "@budibase/string-templates": "^0.7.8",

View file

@ -231,6 +231,15 @@ export const getSchemaForDatasource = (datasource, isForm = false) => {
if (table) { if (table) {
if (type === "view") { if (type === "view") {
schema = cloneDeep(table.views?.[datasource.name]?.schema) schema = cloneDeep(table.views?.[datasource.name]?.schema)
// Some calc views don't include a "name" property inside the schema
if (schema) {
Object.keys(schema).forEach(field => {
if (!schema[field].name) {
schema[field].name = field
}
})
}
} else if (type === "query" && isForm) { } else if (type === "query" && isForm) {
schema = {} schema = {}
const params = table.parameters || [] const params = table.parameters || []

View file

@ -10,10 +10,13 @@
export let value = "" export let value = ""
export let bindings = [] export let bindings = []
export let thin = true
export let title = "Bindings"
export let placeholder
let bindingDrawer let bindingDrawer
let tempValue = value
$: tempValue = value
$: readableValue = runtimeToReadableBinding(bindings, value) $: readableValue = runtimeToReadableBinding(bindings, value)
const handleClose = () => { const handleClose = () => {
@ -28,15 +31,15 @@
<div class="control"> <div class="control">
<Input <Input
thin {thin}
value={readableValue} value={readableValue}
on:change={event => onChange(event.target.value)} on:change={event => onChange(event.target.value)}
placeholder="/screen" /> {placeholder} />
<div class="icon" on:click={bindingDrawer.show}> <div class="icon" on:click={bindingDrawer.show}>
<Icon name="lightning" /> <Icon name="lightning" />
</div> </div>
</div> </div>
<Drawer bind:this={bindingDrawer} title="Bindings"> <Drawer bind:this={bindingDrawer} {title}>
<div slot="description"> <div slot="description">
<Body extraSmall grey> <Body extraSmall grey>
Add the objects on the left to enrich your text. Add the objects on the left to enrich your text.
@ -57,7 +60,6 @@
<style> <style>
.control { .control {
flex: 1; flex: 1;
margin-left: var(--spacing-l);
position: relative; position: relative;
} }

View file

@ -114,8 +114,7 @@
bind:getCaretPosition bind:getCaretPosition
thin thin
bind:value bind:value
placeholder="Add text, or click the objects on the left to add them to placeholder="Add text, or click the objects on the left to add them to the textbox." />
the textbox." />
{#if !valid} {#if !valid}
<p class="syntax-error"> <p class="syntax-error">
Current Handlebars syntax is invalid, please check the guide Current Handlebars syntax is invalid, please check the guide
@ -144,9 +143,12 @@
} }
.text { .text {
padding: var(--spacing-xl); padding: var(--spacing-l);
font-family: var(--font-sans); font-family: var(--font-sans);
} }
.text :global(textarea) {
min-height: 100px;
}
.text :global(p) { .text :global(p) {
margin: 0; margin: 0;
} }

View file

@ -77,7 +77,7 @@
dropdownRight.hide() dropdownRight.hide()
} }
function fetchDatasourceSchema(query) { function fetchQueryDefinition(query) {
const source = $backendUiStore.datasources.find( const source = $backendUiStore.datasources.find(
ds => ds._id === query.datasourceId ds => ds._id === query.datasourceId
).source ).source
@ -85,6 +85,7 @@
} }
</script> </script>
<div class="container">
<div <div
class="dropdownbutton" class="dropdownbutton"
bind:this={anchorRight} bind:this={anchorRight}
@ -94,7 +95,7 @@
</div> </div>
{#if value?.type === 'query'} {#if value?.type === 'query'}
<i class="ri-settings-5-line" on:click={drawer.show} /> <i class="ri-settings-5-line" on:click={drawer.show} />
<Drawer title={'Query'} bind:this={drawer}> <Drawer title={'Query Parameters'} bind:this={drawer}>
<div slot="buttons"> <div slot="buttons">
<Button <Button
blue blue
@ -108,21 +109,24 @@
</Button> </Button>
</div> </div>
<div class="drawer-contents" slot="body"> <div class="drawer-contents" slot="body">
<IntegrationQueryEditor
datasource={$backendUiStore.datasources.find(ds => ds._id === value.datasourceId)}
query={value}
schema={fetchDatasourceSchema(value)}
editable={false} />
<Spacer large />
{#if value.parameters.length > 0} {#if value.parameters.length > 0}
<ParameterBuilder <ParameterBuilder
bind:customParams={value.queryParams} bind:customParams={value.queryParams}
parameters={queries.find(query => query._id === value._id).parameters} parameters={queries.find(query => query._id === value._id).parameters}
bindings={queryBindableProperties} /> bindings={queryBindableProperties} />
{/if} {/if}
<!-- <Spacer large />-->
<IntegrationQueryEditor
height={200}
query={value}
schema={fetchQueryDefinition(value)}
datasource={$backendUiStore.datasources.find(ds => ds._id === value.datasourceId)}
editable={false} />
<Spacer large />
</div> </div>
</Drawer> </Drawer>
{/if} {/if}
</div>
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}> <DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
<div class="dropdown"> <div class="dropdown">
<div class="title"> <div class="title">
@ -197,6 +201,13 @@
</DropdownMenu> </DropdownMenu>
<style> <style>
.container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.dropdownbutton { .dropdownbutton {
background-color: var(--grey-2); background-color: var(--grey-2);
border: var(--border-transparent); border: var(--border-transparent);
@ -259,8 +270,8 @@
} }
.drawer-contents { .drawer-contents {
padding: var(--spacing-xl); padding: var(--spacing-l);
height: 40vh; height: calc(40vh - 2 * var(--spacing-l));
overflow-y: auto; overflow-y: auto;
} }

View file

@ -8,12 +8,19 @@
let addActionButton let addActionButton
let addActionDropdown let addActionDropdown
let selectedAction let selectedAction = actions?.length ? actions[0] : null
$: selectedActionComponent = $: selectedActionComponent =
selectedAction && selectedAction &&
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY]).component actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY]).component
// Select the first action if we delete an action
$: {
if (selectedAction && !actions?.includes(selectedAction)) {
selectedAction = actions?.[0]
}
}
const deleteAction = index => { const deleteAction = index => {
actions.splice(index, 1) actions.splice(index, 1)
actions = actions actions = actions
@ -42,11 +49,10 @@
<div class="actions-list"> <div class="actions-list">
<div> <div>
<div bind:this={addActionButton}> <div bind:this={addActionButton}>
<Spacer small />
<Button wide secondary on:click={addActionDropdown.show}> <Button wide secondary on:click={addActionDropdown.show}>
Add Action Add Action
</Button> </Button>
<Spacer medium /> <Spacer small />
</div> </div>
<DropdownMenu <DropdownMenu
bind:this={addActionDropdown} bind:this={addActionDropdown}
@ -65,11 +71,12 @@
{#if actions && actions.length > 0} {#if actions && actions.length > 0}
{#each actions as action, index} {#each actions as action, index}
<div class="action-container"> <div class="action-container">
<div class="action-header" on:click={selectAction(action)}> <div
<span class:selected={action === selectedAction}> class="action-header"
class:selected={action === selectedAction}
on:click={selectAction(action)}>
{index + 1}. {index + 1}.
{action[EVENT_TYPE_KEY]} {action[EVENT_TYPE_KEY]}
</span>
</div> </div>
<i <i
class="ri-close-fill" class="ri-close-fill"
@ -98,20 +105,22 @@
margin-top: var(--spacing-m); margin-top: var(--spacing-m);
} }
.action-header > span { .action-header {
margin-bottom: var(--spacing-m); margin-bottom: var(--spacing-m);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--grey-7);
font-weight: 500;
} }
.action-header > span:hover, .action-header:hover,
.selected { .action-header.selected {
cursor: pointer; cursor: pointer;
font-weight: 500; color: var(--ink);
} }
.actions-list { .actions-list {
border-right: var(--border-light); border-right: var(--border-light);
padding: var(--spacing-s); padding: var(--spacing-l);
} }
.available-action { .available-action {
@ -127,7 +136,6 @@
.actions-container { .actions-container {
height: 40vh; height: 40vh;
display: grid; display: grid;
grid-gap: var(--spacing-m);
grid-template-columns: 260px 1fr; grid-template-columns: 260px 1fr;
grid-auto-flow: column; grid-auto-flow: column;
min-height: 0; min-height: 0;
@ -136,13 +144,16 @@
} }
.action-container { .action-container {
border-top: var(--border-light); border-bottom: 1px solid var(--grey-1);
display: flex; display: flex;
align-items: center; align-items: center;
} }
.action-container:last-child {
border-bottom: none;
}
.selected-action-container { .selected-action-container {
padding: var(--spacing-xl); padding: var(--spacing-l);
} }
a { a {

View file

@ -1,207 +0,0 @@
<script>
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
import actionTypes from "./actions"
import { createEventDispatcher } from "svelte"
import { automationStore } from "builderStore"
const dispatch = createEventDispatcher()
const eventTypeKey = "##eventHandlerType"
export let event
let addActionButton
let addActionDropdown
let selectedAction
$: actions = event || []
$: selectedActionComponent =
selectedAction &&
actionTypes.find(t => t.name === selectedAction[eventTypeKey]).component
const deleteAction = index => {
actions.splice(index, 1)
actions = actions
}
const addAction = actionType => () => {
const newAction = {
parameters: {},
[eventTypeKey]: actionType.name,
}
actions.push(newAction)
selectedAction = newAction
actions = actions
addActionDropdown.hide()
}
const selectAction = action => () => {
selectedAction = action
}
const saveEventData = async () => {
// e.g. The Trigger Automation action exposes beforeSave, so it can
// create any automations it needs to
for (let action of actions) {
if (action[eventTypeKey] === "Trigger Automation") {
await createAutomation(action.parameters)
}
}
dispatch("change", actions)
}
// called by the parent modal when actions are saved
const createAutomation = async parameters => {
if (parameters.automationId || !parameters.newAutomationName) return
await automationStore.actions.create({ name: parameters.newAutomationName })
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER",
"APP",
appActionDefinition
)
newBlock.inputs = {
fields: Object.entries(parameters.fields).reduce(
(fields, [key, value]) => {
fields[key] = value.type
return fields
},
{}
),
}
automationStore.actions.addBlockToAutomation(newBlock)
await automationStore.actions.save($automationStore.selectedAutomation)
parameters.automationId = $automationStore.selectedAutomation.automation._id
delete parameters.newAutomationName
}
</script>
<ModalContent title="Actions" confirmText="Save" onConfirm={saveEventData}>
<div slot="header">
<div bind:this={addActionButton}>
<TextButton text small blue on:click={addActionDropdown.show}>
<div style="height: 20px; width: 20px;">
<AddIcon />
</div>
Add Action
</TextButton>
</div>
<DropdownMenu
bind:this={addActionDropdown}
anchor={addActionButton}
align="right">
<div class="available-actions-container">
{#each actionTypes as actionType}
<div class="available-action" on:click={addAction(actionType)}>
<span>{actionType.name}</span>
</div>
{/each}
</div>
</DropdownMenu>
</div>
<div class="actions-container">
{#if actions && actions.length > 0}
{#each actions as action, index}
<div class="action-container">
<div class="action-header" on:click={selectAction(action)}>
<Body small lh>{index + 1}. {action[eventTypeKey]}</Body>
<div class="row-expander" class:rotate={action !== selectedAction}>
<ArrowDownIcon />
</div>
</div>
{#if action === selectedAction}
<div class="selected-action-container">
<svelte:component
this={selectedActionComponent}
parameters={selectedAction.parameters} />
<div class="delete-action-button">
<TextButton text medium on:click={() => deleteAction(index)}>
Delete
</TextButton>
</div>
</div>
{/if}
</div>
{/each}
{/if}
</div>
<div slot="footer">
<a href="https://docs.budibase.com">Learn more about Actions</a>
</div>
</ModalContent>
<style>
.action-header {
display: flex;
flex-direction: row;
align-items: center;
}
.action-header > p {
flex: 1;
}
.row-expander {
height: 30px;
width: 30px;
}
.available-action {
padding: var(--spacing-m);
font-size: var(--font-size-m);
cursor: pointer;
}
.available-action:hover {
background: var(--grey-2);
}
.actions-container {
flex: 1;
min-height: 0;
padding-top: 0;
border: var(--border-light);
border-width: 0 0 1px 0;
overflow-y: auto;
}
.action-container {
border: var(--border-light);
border-width: 1px 0 0 0;
}
.selected-action-container {
padding-bottom: var(--spacing-s);
padding-top: var(--spacing-s);
}
.delete-action-button {
padding-top: var(--spacing-l);
display: flex;
justify-content: flex-end;
flex-direction: row;
}
a {
flex: 1;
color: var(--grey-5);
font-size: var(--font-size-s);
text-decoration: none;
}
a:hover {
color: var(--blue);
}
.rotate :global(svg) {
transform: rotate(90deg);
}
</style>

View file

@ -17,7 +17,9 @@
const automationsToCreate = value.filter( const automationsToCreate = value.filter(
action => action["##eventHandlerType"] === "Trigger Automation" action => action["##eventHandlerType"] === "Trigger Automation"
) )
automationsToCreate.forEach(action => createAutomation(action.parameters)) for (let action of automationsToCreate) {
await createAutomation(action.parameters)
}
dispatch("change", value) dispatch("change", value)
notifier.success("Component actions saved.") notifier.success("Component actions saved.")
@ -27,11 +29,8 @@
// called by the parent modal when actions are saved // called by the parent modal when actions are saved
const createAutomation = async parameters => { const createAutomation = async parameters => {
if (parameters.automationId || !parameters.newAutomationName) return if (parameters.automationId || !parameters.newAutomationName) return
await automationStore.actions.create({ name: parameters.newAutomationName }) await automationStore.actions.create({ name: parameters.newAutomationName })
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
const newBlock = $automationStore.selectedAutomation.constructBlock( const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER", "TRIGGER",
"APP", "APP",
@ -39,19 +38,14 @@
) )
newBlock.inputs = { newBlock.inputs = {
fields: Object.entries(parameters.fields).reduce( fields: Object.keys(parameters.fields).reduce((fields, key) => {
(fields, [key, value]) => { fields[key] = "string"
fields[key] = value.type
return fields return fields
}, }, {}),
{}
),
} }
automationStore.actions.addBlockToAutomation(newBlock) automationStore.actions.addBlockToAutomation(newBlock)
await automationStore.actions.save($automationStore.selectedAutomation) await automationStore.actions.save($automationStore.selectedAutomation)
parameters.automationId = $automationStore.selectedAutomation.automation._id parameters.automationId = $automationStore.selectedAutomation.automation._id
delete parameters.newAutomationName delete parameters.newAutomationName
} }

View file

@ -37,8 +37,8 @@
a List a List
</div> </div>
{:else} {:else}
<Label size="m" color="dark">Datasource</Label> <Label small>Datasource</Label>
<Select secondary bind:value={parameters.providerId}> <Select thin secondary bind:value={parameters.providerId}>
<option value="" /> <option value="" />
{#each dataProviderComponents as provider} {#each dataProviderComponents as provider}
<option value={provider._id}>{provider._instanceName}</option> <option value={provider._id}>{provider._instanceName}</option>
@ -50,22 +50,15 @@
<style> <style>
.root { .root {
display: grid; display: grid;
column-gap: var(--spacing-s); column-gap: var(--spacing-l);
row-gap: var(--spacing-s); row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto; grid-template-columns: auto 1fr;
align-items: baseline; align-items: baseline;
} }
.root :global(> div:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}
.cannot-use { .cannot-use {
color: var(--red); color: var(--red);
font-size: var(--font-size-s); font-size: var(--font-size-s);
text-align: center;
width: 70%;
margin: auto; margin: auto;
} }
</style> </style>

View file

@ -3,29 +3,28 @@
import { store, backendUiStore, currentAsset } from "builderStore" import { store, backendUiStore, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding" import { getBindableProperties } from "builderStore/dataBinding"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte" import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte"
export let parameters export let parameters
$: query = $backendUiStore.queries.find(q => q._id === parameters.queryId)
$: datasource = $backendUiStore.datasources.find( $: datasource = $backendUiStore.datasources.find(
ds => ds._id === parameters.datasourceId ds => ds._id === parameters.datasourceId
) )
$: bindableProperties = getBindableProperties( $: bindableProperties = getBindableProperties(
$currentAsset, $currentAsset,
$store.selectedComponentId $store.selectedComponentId
).map(property => ({ )
...property,
category: property.type === "instance" ? "Component" : "Table",
label: property.readableBinding,
path: property.runtimeBinding,
}))
$: query = function fetchQueryDefinition(query) {
parameters.queryId && const source = $backendUiStore.datasources.find(
$backendUiStore.queries.find(query => query._id === parameters.queryId) ds => ds._id === query.datasourceId
).source
return $backendUiStore.integrations[source].query[query.queryVerb]
}
</script> </script>
<div class="root"> <Label small>Datasource</Label>
<Label size="m" color="dark">Datasource</Label>
<Select thin secondary bind:value={parameters.datasourceId}> <Select thin secondary bind:value={parameters.datasourceId}>
<option value="" /> <option value="" />
{#each $backendUiStore.datasources as datasource} {#each $backendUiStore.datasources as datasource}
@ -36,7 +35,7 @@
<Spacer medium /> <Spacer medium />
{#if parameters.datasourceId} {#if parameters.datasourceId}
<Label size="m" color="dark">Query</Label> <Label small>Query</Label>
<Select thin secondary bind:value={parameters.queryId}> <Select thin secondary bind:value={parameters.queryId}>
<option value="" /> <option value="" />
{#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query} {#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query}
@ -52,14 +51,9 @@
bind:customParams={parameters.queryParams} bind:customParams={parameters.queryParams}
parameters={query.parameters} parameters={query.parameters}
bindings={bindableProperties} /> bindings={bindableProperties} />
{#if query.fields.sql} <IntegrationQueryEditor
<pre>{query.fields.queryString}</pre> height={200}
{query}
schema={fetchQueryDefinition(query)}
editable={false} />
{/if} {/if}
{/if}
</div>
<style>
.root {
padding: var(--spacing-m);
}
</style>

View file

@ -13,8 +13,10 @@
</script> </script>
<div class="root"> <div class="root">
<Label size="m" color="dark">Screen</Label> <Label small>Screen</Label>
<DrawerBindableInput <DrawerBindableInput
title="Destination URL"
placeholder="/screen"
value={parameters.url} value={parameters.url}
on:change={value => (parameters.url = value.detail)} on:change={value => (parameters.url = value.detail)}
{bindings} /> {bindings} />

View file

@ -1,37 +0,0 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore"
import { getDataProviderComponents } from "builderStore/dataBinding"
export let parameters
$: dataProviders = getDataProviderComponents(
$currentAsset,
$store.selectedComponentId
)
</script>
<div class="root">
<Label size="m" color="dark">Form</Label>
<Select secondary bind:value={parameters.componentId}>
<option value="" />
{#if dataProviders}
{#each dataProviders as component}
<option value={component._id}>{component._instanceName}</option>
{/each}
{/if}
</Select>
</div>
<style>
.root {
display: flex;
flex-direction: row;
align-items: baseline;
}
.root :global(> div) {
flex: 1;
margin-left: var(--spacing-l);
}
</style>

View file

@ -1,115 +1,89 @@
<script> <script>
import { import { Label, TextButton, Spacer, Select, Input } from "@budibase/bbui"
DataList,
Label,
TextButton,
Spacer,
Select,
Input,
} from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { import { getBindableProperties } from "builderStore/dataBinding"
getBindableProperties,
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import { CloseCircleIcon, AddIcon } from "components/common/Icons" import { CloseCircleIcon, AddIcon } from "components/common/Icons"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let parameterFields export let parameterFields
export let schemaFields export let schemaFields
export let fieldLabel = "Column" export let fieldLabel = "Column"
export let valueLabel = "Value"
const emptyField = () => ({ name: "", value: "" }) let fields = Object.entries(parameterFields || {})
$: onChange(fields)
$: bindableProperties = getBindableProperties( $: bindableProperties = getBindableProperties(
$currentAsset, $currentAsset,
$store.selectedComponentId $store.selectedComponentId
) )
// this statement initialises fields from parameters.fields
$: fields =
fields ||
Object.keys(parameterFields || {}).map(name => ({
name,
value:
(parameterFields &&
runtimeToReadableBinding(
bindableProperties,
parameterFields[name].value
)) ||
"",
}))
const addField = () => { const addField = () => {
const newFields = fields.filter(f => f.name) fields = [...fields.filter(field => field[0]), ["", ""]]
newFields.push(emptyField())
fields = newFields
rebuildParameters()
} }
const removeField = field => () => { const removeField = name => {
fields = fields.filter(f => f !== field) fields = fields.filter(field => field[0] !== name)
rebuildParameters()
} }
const rebuildParameters = () => { const updateFieldValue = (idx, value) => {
// rebuilds paramters.fields every time a field name or value is added fields[idx][1] = value
// as UI below is bound to "fields" array, but we need to output a { key: value } fields = fields
const newParameterFields = {}
for (let field of fields) {
if (field.name) {
// value and type is needed by the client, so it can parse
// a string into a correct type
newParameterFields[field.name] = {
type: schemaFields
? schemaFields.find(f => f.name === field.name).type
: "string",
value: readableToRuntimeBinding(bindableProperties, field.value),
}
}
}
dispatch("fieldschanged", newParameterFields)
} }
// just wraps binding in {{ ... }} const updateFieldName = (idx, name) => {
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}` fields[idx][0] = name
fields = fields
}
const onChange = fields => {
const newParamFields = {}
fields
.filter(field => field[0])
.forEach(([field, value]) => {
newParamFields[field] = value
})
dispatch("change", newParamFields)
}
</script> </script>
{#if fields} {#if fields}
{#each fields as field} {#each fields as field, idx}
<Label size="m" color="dark">{fieldLabel}</Label> <Label small>{fieldLabel}</Label>
{#if schemaFields} {#if schemaFields}
<Select secondary bind:value={field.name} on:blur={rebuildParameters}> <Select
thin
secondary
value={field[0]}
on:change={event => updateFieldName(idx, event.target.value)}>
<option value="" /> <option value="" />
{#each schemaFields as schemaField} {#each schemaFields as schemaField}
<option value={schemaField.name}>{schemaField.name}</option> <option value={schemaField.name}>{schemaField.name}</option>
{/each} {/each}
</Select> </Select>
{:else} {:else}
<Input secondary bind:value={field.name} on:blur={rebuildParameters} /> <Input
thin
secondary
value={field[0]}
on:change={event => updateFieldName(idx, event.target.value)} />
{/if} {/if}
<Label size="m" color="dark">Value</Label> <Label small>{valueLabel}</Label>
<DataList secondary bind:value={field.value} on:blur={rebuildParameters}> <DrawerBindableInput
<option value="" /> title={`Value for "${field[0]}"`}
{#each bindableProperties as bindableProp} value={field[1]}
<option value={toBindingExpression(bindableProp.readableBinding)}> bindings={bindableProperties}
{bindableProp.readableBinding} on:change={event => updateFieldValue(idx, event.detail)} />
</option>
{/each}
</DataList>
<div class="remove-field-container"> <div class="remove-field-container">
<TextButton text small on:click={removeField(field)}> <TextButton text small on:click={() => removeField(field[0])}>
<CloseCircleIcon /> <CloseCircleIcon />
</TextButton> </TextButton>
</div> </div>
{/each} {/each}
<div> <div>
<Spacer small /> <Spacer small />
<TextButton text small blue on:click={addField}> <TextButton text small blue on:click={addField}>
Add Add
{fieldLabel} {fieldLabel}

View file

@ -37,8 +37,8 @@
Repeater Repeater
</div> </div>
{:else} {:else}
<Label size="m" color="dark">Datasource</Label> <Label small>Datasource</Label>
<Select secondary bind:value={parameters.providerId}> <Select thin secondary bind:value={parameters.providerId}>
<option value="" /> <option value="" />
{#each dataProviderComponents as provider} {#each dataProviderComponents as provider}
<option value={provider._id}>{provider._instanceName}</option> <option value={provider._id}>{provider._instanceName}</option>
@ -49,7 +49,7 @@
<SaveFields <SaveFields
parameterFields={parameters.fields} parameterFields={parameters.fields}
{schemaFields} {schemaFields}
on:fieldschanged={onFieldsChanged} /> on:change={onFieldsChanged} />
{/if} {/if}
{/if} {/if}
</div> </div>
@ -57,7 +57,7 @@
<style> <style>
.root { .root {
display: grid; display: grid;
column-gap: var(--spacing-s); column-gap: var(--spacing-l);
row-gap: var(--spacing-s); row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto; grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline; align-items: baseline;
@ -71,8 +71,6 @@
.cannot-use { .cannot-use {
color: var(--red); color: var(--red);
font-size: var(--font-size-s); font-size: var(--font-size-s);
text-align: center;
width: 70%;
margin: auto; margin: auto;
} }
</style> </style>

View file

@ -27,13 +27,11 @@
schema, schema,
} }
}) })
$: hasAutomations = automations && automations.length > 0 $: hasAutomations = automations && automations.length > 0
$: selectedAutomation = automations?.find(
$: selectedAutomation = a => a._id === parameters?.automationId
parameters && )
parameters.automationId && $: selectedSchema = selectedAutomation?.schema
automations.find(a => a._id === parameters.automationId)
const onFieldsChanged = e => { const onFieldsChanged = e => {
parameters.fields = e.detail parameters.fields = e.detail
@ -42,95 +40,98 @@
const setNew = () => { const setNew = () => {
automationStatus = AUTOMATION_STATUS.NEW automationStatus = AUTOMATION_STATUS.NEW
parameters.automationId = undefined parameters.automationId = undefined
parameters.fields = {}
} }
const setExisting = () => { const setExisting = () => {
automationStatus = AUTOMATION_STATUS.EXISTING automationStatus = AUTOMATION_STATUS.EXISTING
parameters.newAutomationName = "" parameters.newAutomationName = ""
parameters.fields = {}
parameters.automationId = automations[0]?._id
} }
</script> </script>
<div class="root"> <div class="root">
<div class="radios">
<div class="radio-container" on:click={setNew}> <div class="radio-container" on:click={setNew}>
<input <input
type="radio" type="radio"
value={AUTOMATION_STATUS.NEW} value={AUTOMATION_STATUS.NEW}
bind:group={automationStatus} bind:group={automationStatus} />
disabled={!hasAutomations} /> <Label small>Create a new automation</Label>
<Label disabled={!hasAutomations}>Create a new automation</Label>
</div> </div>
<div class="radio-container" on:click={hasAutomations ? setExisting : null}>
<div class="radio-container" on:click={setExisting}>
<input <input
type="radio" type="radio"
value={AUTOMATION_STATUS.EXISTING} value={AUTOMATION_STATUS.EXISTING}
bind:group={automationStatus} bind:group={automationStatus}
disabled={!hasAutomations} /> disabled={!hasAutomations} />
<Label small grey={!hasAutomations}>Use an existing automation</Label>
<Label disabled={!hasAutomations}>Use an existing automation</Label> </div>
</div> </div>
<Label size="m" color="dark">Automation</Label> <div class="fields">
<Label small>Automation</Label>
{#if automationStatus === AUTOMATION_STATUS.EXISTING} {#if automationStatus === AUTOMATION_STATUS.EXISTING}
<Select <Select
thin
secondary secondary
bind:value={parameters.automationId} bind:value={parameters.automationId}
placeholder="Choose automation"> placeholder="Choose automation">
<option value="" />
{#each automations as automation} {#each automations as automation}
<option value={automation._id}>{automation.name}</option> <option value={automation._id}>{automation.name}</option>
{/each} {/each}
</Select> </Select>
{:else} {:else}
<Input <Input
secondary thin
bind:value={parameters.newAutomationName} bind:value={parameters.newAutomationName}
placeholder="Enter automation name" /> placeholder="Enter automation name" />
{/if} {/if}
{#key parameters.automationId}
<SaveFields <SaveFields
schemaFields={automationStatus === AUTOMATION_STATUS.EXISTING && selectedAutomation && selectedAutomation.schema} schemaFields={selectedSchema}
parameterFields={parameters.fields}
fieldLabel="Field" fieldLabel="Field"
on:fieldschanged={onFieldsChanged} /> on:change={onFieldsChanged} />
{/key}
</div>
</div> </div>
<style> <style>
.root { .fields {
display: grid; display: grid;
column-gap: var(--spacing-s); column-gap: var(--spacing-l);
row-gap: var(--spacing-s); row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto; grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline; align-items: baseline;
} }
.root :global(> div:nth-child(4)) { .fields :global(> div:nth-child(2)) {
grid-column: 2 / span 4; grid-column: 2 / span 4;
} }
.radios,
.radio-container { .radio-container {
display: grid; display: flex;
grid-template-columns: auto 1fr; flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.radios {
gap: var(--spacing-m);
margin-bottom: var(--spacing-l);
}
.radio-container {
gap: var(--spacing-m);
}
.radio-container :global(label) {
margin: 0;
} }
.radio-container:nth-child(1) { input[type="radio"]:checked {
grid-column: 1 / span 2; background: var(--blue);
}
.radio-container:nth-child(2) {
grid-column: 3 / span 3;
}
.radio-container :global(> label) {
margin-left: var(--spacing-m);
}
.radio-container > input {
margin-bottom: var(--spacing-s);
}
.radio-container > input:focus {
outline: none;
} }
</style> </style>

View file

@ -13,8 +13,8 @@
</script> </script>
<div class="root"> <div class="root">
<Label size="m" color="dark">Form</Label> <Label small>Form</Label>
<Select secondary bind:value={parameters.componentId}> <Select thin secondary bind:value={parameters.componentId}>
<option value="" /> <option value="" />
{#if actionProviders} {#if actionProviders}
{#each actionProviders as component} {#each actionProviders as component}

View file

@ -4,7 +4,6 @@ import DeleteRow from "./DeleteRow.svelte"
import ExecuteQuery from "./ExecuteQuery.svelte" import ExecuteQuery from "./ExecuteQuery.svelte"
import TriggerAutomation from "./TriggerAutomation.svelte" import TriggerAutomation from "./TriggerAutomation.svelte"
import ValidateForm from "./ValidateForm.svelte" import ValidateForm from "./ValidateForm.svelte"
import RefreshDatasource from "./RefreshDatasource.svelte"
// defines what actions are available, when adding a new one // defines what actions are available, when adding a new one
// the component is the setup panel for the action // the component is the setup panel for the action
@ -36,8 +35,4 @@ export default [
name: "Validate Form", name: "Validate Form",
component: ValidateForm, component: ValidateForm,
}, },
{
name: "Refresh Datasource",
component: RefreshDatasource,
},
] ]

View file

@ -0,0 +1,77 @@
<script>
import { Button, Drawer, Spacer, Body } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { notifier } from "builderStore/store/notifications"
import {
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
import SaveFields from "./EventsEditor/actions/SaveFields.svelte"
const dispatch = createEventDispatcher()
export let value = {}
export let componentInstance
let drawer
let tempValue = value
$: schemaFields = getSchemaFields(componentInstance)
const getSchemaFields = component => {
const datasource = getDatasourceForProvider(component)
const { schema } = getSchemaForDatasource(datasource)
return Object.values(schema || {})
}
const saveFilter = async () => {
dispatch("change", tempValue)
notifier.success("Filters saved.")
drawer.hide()
}
const onFieldsChanged = event => {
tempValue = event.detail
}
</script>
<Button secondary wide on:click={drawer.show}>Define Filters</Button>
<Drawer bind:this={drawer} title={'Filtering'}>
<heading slot="buttons">
<Button thin blue on:click={saveFilter}>Save</Button>
</heading>
<div slot="body">
<div class="root">
<Body small grey>
{#if !Object.keys(tempValue || {}).length}
Add your first filter column.
{:else}
Results are filtered to only those which match all of the following
constaints.
{/if}
</Body>
<Spacer medium />
<div class="fields">
<SaveFields
parameterFields={value}
{schemaFields}
valueLabel="Equals"
on:change={onFieldsChanged} />
</div>
</div>
</div>
</Drawer>
<style>
.root {
padding: var(--spacing-l);
min-height: calc(40vh - 2 * var(--spacing-l));
}
.fields {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
</style>

View file

@ -7,6 +7,7 @@
runtimeToReadableBinding, runtimeToReadableBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte" import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
import { capitalise } from "../../../../helpers"
export let label = "" export let label = ""
export let bindable = true export let bindable = true
@ -88,9 +89,7 @@
on:click={bindingDrawer.show}> on:click={bindingDrawer.show}>
<Icon name="lightning" /> <Icon name="lightning" />
</div> </div>
{/if} <Drawer bind:this={bindingDrawer} title={capitalise(key)}>
</div>
<Drawer bind:this={bindingDrawer} title="Bindings">
<div slot="description"> <div slot="description">
<Body extraSmall grey> <Body extraSmall grey>
Add the objects on the left to enrich your text. Add the objects on the left to enrich your text.
@ -108,6 +107,8 @@
{bindableProperties} /> {bindableProperties} />
</div> </div>
</Drawer> </Drawer>
{/if}
</div>
<style> <style>
.property-control { .property-control {

View file

@ -18,6 +18,7 @@
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte" import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte" import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
import EventsEditor from "./PropertyControls/EventsEditor" import EventsEditor from "./PropertyControls/EventsEditor"
import FilterEditor from "./PropertyControls/FilterEditor.svelte"
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte" import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
import { IconSelect } from "./PropertyControls/IconSelect" import { IconSelect } from "./PropertyControls/IconSelect"
import ColorPicker from "./PropertyControls/ColorPicker.svelte" import ColorPicker from "./PropertyControls/ColorPicker.svelte"
@ -71,6 +72,7 @@
field: FieldSelect, field: FieldSelect,
multifield: MultiFieldSelect, multifield: MultiFieldSelect,
schema: SchemaSelect, schema: SchemaSelect,
filter: FilterEditor,
"field/string": StringFieldSelect, "field/string": StringFieldSelect,
"field/number": NumberFieldSelect, "field/number": NumberFieldSelect,
"field/options": OptionsFieldSelect, "field/options": OptionsFieldSelect,

View file

@ -18,6 +18,7 @@
export let lineNumbers = true export let lineNumbers = true
export let tab = true export let tab = true
export let mode export let mode
export let editorHeight = 500
// export let parameters = [] // export let parameters = []
let completions = handlebarsCompletions() let completions = handlebarsCompletions()
@ -171,17 +172,21 @@
} }
</script> </script>
{#if label}
<Label small>{label}</Label> <Label small>{label}</Label>
<Spacer medium /> <Spacer medium />
{/if}
<div style={`--code-mirror-height: ${editorHeight}px`}>
<textarea tabindex="0" bind:this={refs.editor} readonly {value} /> <textarea tabindex="0" bind:this={refs.editor} readonly {value} />
</div>
<style> <style>
textarea { textarea {
visibility: hidden; visibility: hidden;
} }
:global(.CodeMirror) { div :global(.CodeMirror) {
height: 500px !important; height: var(--code-mirror-height) !important;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
font-family: monospace !important; font-family: monospace !important;
line-height: 1.3; line-height: 1.3;

View file

@ -1,13 +1,5 @@
<script> <script>
import { import { Label, Spacer, Input } from "@budibase/bbui"
Button,
TextArea,
Label,
Input,
Heading,
Select,
Spacer,
} from "@budibase/bbui"
import Editor from "./QueryEditor.svelte" import Editor from "./QueryEditor.svelte"
import KeyValueBuilder from "./KeyValueBuilder.svelte" import KeyValueBuilder from "./KeyValueBuilder.svelte"

View file

@ -1,10 +1,10 @@
<script> <script>
import { Body, Button, Input, Heading, Spacer } from "@budibase/bbui" import { Body, Button, Input, Heading, Spacer } from "@budibase/bbui"
import BindableInput from "components/common/BindableInput.svelte"
import { import {
readableToRuntimeBinding, readableToRuntimeBinding,
runtimeToReadableBinding, runtimeToReadableBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
export let bindable = true export let bindable = true
export let parameters = [] export let parameters = []
@ -37,8 +37,13 @@
{/if} {/if}
</div> </div>
<Body small grey> <Body small grey>
{#if !bindable}
Parameters come in two parts: the parameter name, and a default/fallback Parameters come in two parts: the parameter name, and a default/fallback
value. value.
{:else}
Enter a value for each parameter. The default values will be used for any
values left blank.
{/if}
</Body> </Body>
<Spacer large /> <Spacer large />
<div class="parameters" class:bindable> <div class="parameters" class:bindable>
@ -54,9 +59,9 @@
disabled={bindable} disabled={bindable}
bind:value={parameter.default} /> bind:value={parameter.default} />
{#if bindable} {#if bindable}
<BindableInput <DrawerBindableInput
title={`Query parameter "${parameter.name}"`}
placeholder="Value" placeholder="Value"
type="string"
thin thin
on:change={evt => onBindingChange(parameter.name, evt.detail)} on:change={evt => onBindingChange(parameter.name, evt.detail)}
value={runtimeToReadableBinding(bindings, customParams?.[parameter.name])} value={runtimeToReadableBinding(bindings, customParams?.[parameter.name])}

View file

@ -1,5 +1,4 @@
<script> <script>
import { onMount } from "svelte"
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { import {
Select, Select,
@ -7,7 +6,6 @@
Body, Body,
Label, Label,
Input, Input,
TextArea,
Heading, Heading,
Spacer, Spacer,
Switcher, Switcher,
@ -208,7 +206,9 @@
{#if tab === 'JSON'} {#if tab === 'JSON'}
<pre class="preview"> <pre class="preview">
{#if !data[0]} {#if !data[0]}
Please run your query to fetch some data. Please run your query to fetch some data.
{:else} {:else}
{JSON.stringify(data[0], undefined, 2)} {JSON.stringify(data[0], undefined, 2)}
{/if} {/if}

View file

@ -1,9 +1,7 @@
<script> <script>
import { onMount } from "svelte"
import { TextArea, Label, Input, Heading, Spacer } from "@budibase/bbui"
import Editor from "./QueryEditor.svelte" import Editor from "./QueryEditor.svelte"
import ParameterBuilder from "./QueryParameterBuilder.svelte"
import FieldsBuilder from "./QueryFieldsBuilder.svelte" import FieldsBuilder from "./QueryFieldsBuilder.svelte"
import { Label, Input } from "@budibase/bbui"
const QueryTypes = { const QueryTypes = {
SQL: "sql", SQL: "sql",
@ -15,6 +13,7 @@
export let datasource export let datasource
export let schema export let schema
export let editable = true export let editable = true
export let height = 500
$: urlDisplay = $: urlDisplay =
schema.urlDisplay && schema.urlDisplay &&
@ -29,6 +28,7 @@
{#key query._id} {#key query._id}
{#if schema.type === QueryTypes.SQL} {#if schema.type === QueryTypes.SQL}
<Editor <Editor
editorHeight={height}
label="Query" label="Query"
mode="sql" mode="sql"
on:change={updateQuery} on:change={updateQuery}
@ -37,6 +37,7 @@
parameters={query.parameters} /> parameters={query.parameters} />
{:else if schema.type === QueryTypes.JSON} {:else if schema.type === QueryTypes.JSON}
<Editor <Editor
editorHeight={height}
label="Query" label="Query"
mode="json" mode="json"
on:change={updateQuery} on:change={updateQuery}

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,7 @@ export const createDatasourceStore = () => {
let datasourceIds = [] let datasourceIds = []
// Extract table ID // Extract table ID
if (datasource.type === "table") { if (datasource.type === "table" || datasource.type === "view") {
if (datasource.tableId) { if (datasource.tableId) {
datasourceIds.push(datasource.tableId) datasourceIds.push(datasource.tableId)
} }

View file

@ -8,8 +8,8 @@ const saveRowHandler = async (action, context) => {
if (providerId) { if (providerId) {
let draft = context[providerId] let draft = context[providerId]
if (fields) { if (fields) {
for (let [key, entry] of Object.entries(fields)) { for (let [field, value] of Object.entries(fields)) {
draft[key] = entry.value draft[field] = value
} }
} }
await saveRow(draft) await saveRow(draft)
@ -26,11 +26,7 @@ const deleteRowHandler = async action => {
const triggerAutomationHandler = async action => { const triggerAutomationHandler = async action => {
const { fields } = action.parameters const { fields } = action.parameters
if (fields) { if (fields) {
const params = {} await triggerAutomation(action.parameters.automationId, fields)
for (let field in fields) {
params[field] = fields[field].value
}
await triggerAutomation(action.parameters.automationId, params)
} }
} }

View file

@ -118,6 +118,11 @@
"label": "Empty Text", "label": "Empty Text",
"key": "noRowsMessage", "key": "noRowsMessage",
"defaultValue": "No rows found." "defaultValue": "No rows found."
},
{
"type": "filter",
"label": "Filtering",
"key": "filter"
} }
] ]
}, },

View file

@ -40,7 +40,7 @@
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd", "gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd",
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.1.0", "@adobe/spectrum-css-workflow-icons": "^1.1.0",
"@budibase/bbui": "^1.58.5", "@budibase/bbui": "^1.58.13",
"@budibase/svelte-ag-grid": "^0.0.16", "@budibase/svelte-ag-grid": "^0.0.16",
"@spectrum-css/actionbutton": "^1.0.0-beta.1", "@spectrum-css/actionbutton": "^1.0.0-beta.1",
"@spectrum-css/button": "^3.0.0-beta.6", "@spectrum-css/button": "^3.0.0-beta.6",

View file

@ -4,6 +4,7 @@
export let datasource export let datasource
export let noRowsMessage export let noRowsMessage
export let filter
const { API, styleable, Provider, builderStore, ActionTypes } = getContext( const { API, styleable, Provider, builderStore, ActionTypes } = getContext(
"sdk" "sdk"
@ -13,6 +14,7 @@
let loaded = false let loaded = false
$: fetchData(datasource) $: fetchData(datasource)
$: filteredRows = filterRows(rows, filter)
$: actions = [ $: actions = [
{ {
type: ActionTypes.RefreshDatasource, type: ActionTypes.RefreshDatasource,
@ -21,21 +23,36 @@
}, },
] ]
async function fetchData(datasource) { const fetchData = async datasource => {
if (!isEmpty(datasource)) { if (!isEmpty(datasource)) {
rows = await API.fetchDatasource(datasource) rows = await API.fetchDatasource(datasource)
} }
loaded = true loaded = true
} }
const filterRows = (rows, filter) => {
if (!Object.keys(filter || {}).length) {
return rows
}
let filteredData = [...rows]
Object.entries(filter).forEach(([field, value]) => {
if (value != null && value !== "") {
filteredData = filteredData.filter(row => {
return row[field] === value
})
}
})
return filteredData
}
</script> </script>
<Provider {actions}> <Provider {actions}>
<div use:styleable={$component.styles}> <div use:styleable={$component.styles}>
{#if rows.length > 0} {#if filteredRows.length > 0}
{#if $component.children === 0 && $builderStore.inBuilder} {#if $component.children === 0 && $builderStore.inBuilder}
<p><i class="ri-image-line" />Add some components to display.</p> <p><i class="ri-image-line" />Add some components to display.</p>
{:else} {:else}
{#each rows as row} {#each filteredRows as row}
<Provider data={row}> <Provider data={row}>
<slot /> <slot />
</Provider> </Provider>

View file

@ -44,10 +44,10 @@
lodash "^4.17.19" lodash "^4.17.19"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@budibase/bbui@^1.58.5": "@budibase/bbui@^1.58.13":
version "1.58.5" version "1.58.13"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.5.tgz#c9ce712941760825c7774a1de77594e989db4561" resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.13.tgz#59df9c73def2d81c75dcbd2266c52c19db88dbd7"
integrity sha512-0j1I7BetJ2GzB1BXKyvvlkuFphLmADJh2U/Ihubwxx5qUDY8REoVzLgAB4c24zt0CGVTF9VMmOoMLd0zD0QwdQ== integrity sha512-Zk6CKXdBfKsTVzA1Xs5++shdSSZLfphVpZuKVbjfzkgtuhyH7ruucexuSHEpFsxjW5rEKgKIBoRFzCK5vPvN0w==
dependencies: dependencies:
markdown-it "^12.0.2" markdown-it "^12.0.2"
quill "^1.3.7" quill "^1.3.7"