binding data context in component

This commit is contained in:
Martin McKeaveney 2021-01-08 18:22:03 +00:00
parent 98a7085bbc
commit d710874ef1
16 changed files with 367 additions and 331 deletions

View File

@ -1,43 +0,0 @@
<script>
import {
DropdownMenu,
TextButton as Button,
Icon,
Modal,
ModalContent,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
import EditIntegrationConfig from "../modals/EditIntegrationConfig.svelte"
export let table
$: console.log("The table config is", table)
let modal
// TODO: revisit
async function saveTable() {
const SAVE_TABLE_URL = `/api/tables`
const response = await api.post(SAVE_TABLE_URL, table)
const savedTable = await response.json()
await backendUiStore.actions.tables.fetch()
backendUiStore.actions.tables.select(savedTable)
}
</script>
<div>
<Button text small on:click={modal.show}>
<Icon name="edit" />
Configure Datasource
</Button>
</div>
<Modal bind:this={modal}>
<ModalContent
confirmText="Save"
cancelText="Cancel"
onConfirm={saveTable}
title={'Datasource Configuration'}>
<EditIntegrationConfig onClosed={modal.hide} bind:table />
</ModalContent>
</Modal>

View File

@ -3,15 +3,19 @@
import Portal from "svelte-portal"
export let title
export let onClose
export let onClose = () => {}
</script>
<Portal>
<section class="drawer" transition:slide>
{#if title}
<heading>{title}</heading>
{/if}
<slot />
<header>
{title}
<div class="controls">
<slot name="buttons" />
<i class="ri-close-fill close" on:click={onClose} />
</div>
</header>
<slot name="body" />
</section>
</Portal>
@ -20,10 +24,28 @@
height: 50vh;
position: absolute;
bottom: 0;
width: 97%;
width: 100vw;
background: var(--background);
padding: var(--spacing-xl);
border-top: var(--border-light);
z-index: 2;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
border: var(--border-light);
padding: var(--spacing-s);
}
.controls {
display: grid;
grid-auto-flow: column;
grid-gap: var(--spacing-m);
align-items: center;
}
.close {
font-size: var(--font-size-xl);
}
</style>

View File

@ -44,4 +44,7 @@
background: var(--background);
border-radius: var(--border-radius-m);
}
:global(.CodeMirror) {
border-radius: var(--border-radius-m);
}
</style>

View File

@ -1,5 +1,5 @@
<script>
import { TextArea, Label, Input, Heading } from "@budibase/bbui"
import { Button, TextArea, Label, Input, Heading } from "@budibase/bbui"
import Editor from "./QueryEditor.svelte"
import BindableInput from "components/userInterface/BindableInput.svelte"
@ -17,26 +17,28 @@
}
</script>
<Heading extraSmall black>Parameters</Heading>
<div class="parameters">
<Label extraSmall grey>Parameter Name</Label>
<Label extraSmall grey>Default</Label>
<Label extraSmall grey>Value</Label>
<div />
{#each parameters as parameter, idx}
<Input thin bind:value={parameter.name} />
<Input thin bind:value={parameter.default} />
<BindableInput
type="string"
thin
bind:value={customParams[parameter.name]}
{bindings} />
<i
class="ri-close-circle-line delete"
on:click={() => deleteQueryParameter(idx)} />
{/each}
</div>
<i class="ri-add-circle-line add" on:click={newQueryParameter} />
<section>
<Heading extraSmall black>Parameters</Heading>
<div class="parameters">
<Label extraSmall grey>Parameter Name</Label>
<Label extraSmall grey>Default</Label>
<Label extraSmall grey>Value</Label>
<div />
{#each parameters as parameter, idx}
<Input thin bind:value={parameter.name} />
<Input thin bind:value={parameter.default} />
<BindableInput
type="string"
thin
bind:value={customParams[parameter.name]}
{bindings} />
<i
class="ri-close-circle-line delete"
on:click={() => deleteQueryParameter(idx)} />
{/each}
</div>
<Button thin secondary on:click={newQueryParameter}>New Parameter</Button>
</section>
<style>
.parameters {
@ -44,9 +46,6 @@
grid-template-columns: 1fr 1fr 1fr 5%;
grid-gap: 10px;
align-items: center;
}
.add {
margin-top: var(--spacing-m);
margin-bottom: var(--spacing-xl);
}
</style>

View File

@ -1,5 +1,5 @@
<script>
import { TextArea, Label, Input, Heading } from "@budibase/bbui"
import { TextArea, Label, Input, Heading, Spacer } from "@budibase/bbui"
import Editor from "./QueryEditor.svelte"
import ParameterBuilder from "./QueryParameterBuilder.svelte"
@ -12,5 +12,6 @@
{#if query.queryType === QueryTypes.SQL}
<ParameterBuilder bind:parameters={query.parameters} />
<Spacer large />
<Editor label="Query" bind:value={query.queryString} />
{/if}

View File

@ -1,35 +1,62 @@
<script>
import BottomDrawer from "components/common/BottomDrawer.svelte"
import { store, backendUiStore } from "builderStore"
import { Button } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import { slide } from "svelte/transition"
import QueryInterface from "components/integration/QueryViewer.svelte"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
$: query = $backendUiStore.queries.find(
query => query._id === $backendUiStore.selectedQueryId
)
export let query
export let parameters = {}
$: console.log("CUSTOM PARAMS", parameters)
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
}).map(property => ({
...property,
category: property.type === "instance" ? "Component" : "Table",
label: property.readableBinding,
path: property.runtimeBinding,
}))
function closeDatabindingDrawer() {
store.update(state => {
state.bindingDrawerVisible = false
state.bottomDrawerVisible = false
return state
})
}
function saveComponentQuery() {
// save the parameters to the datasource of the component
}
</script>
{#if query}
<BottomDrawer>
<div class="drawer-contents">
<i class="ri-close-fill close" on:click={closeDatabindingDrawer} />
<QueryInterface {query} />
<BottomDrawer title={'Query'} onClose={closeDatabindingDrawer}>
<div slot="buttons">
<Button blue thin on:click={saveComponentQuery}>Save</Button>
</div>
<div class="drawer-contents" slot="body">
<pre>{query.queryString}</pre>
<ParameterBuilder
bind:customParams={parameters}
parameters={query.parameters}
bindings={bindableProperties} />
</div>
</BottomDrawer>
{/if}
<style>
i {
position: absolute;
top: var(--spacing-xl);
right: var(--spacing-xl);
font-size: var(--font-size-m);
.drawer-contents {
display: grid;
grid-auto-flow: column;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-m);
height: 100%;
padding: var(--spacing-xl);
}
</style>

View File

@ -0,0 +1,200 @@
<script>
import {
Button,
TextButton,
Body,
DropdownMenu,
ModalContent,
} from "@budibase/bbui"
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
import actionTypes from "./actions"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
const eventTypeKey = "##eventHandlerType"
export let event
let addActionButton
let addActionDropdown
let selectedAction
let draftEventHandler = { parameters: [] }
$: actions = event || []
$: selectedActionComponent =
selectedAction &&
actionTypes.find(t => t.name === selectedAction[eventTypeKey]).component
const updateEventHandler = (updatedHandler, index) => {
actions[index] = updatedHandler
}
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 = () => {
dispatch("change", actions)
}
</script>
<div class="actions-container">
<div class="actions-list">
<div>
<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>
{#if actions && actions.length > 0}
{#each actions as action, index}
<div class="action-container">
<div class="action-header" on:click={selectAction(action)}>
<span class:selected={action === selectedAction}>
{index + 1}. {action[eventTypeKey]}
</span>
<!-- <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 class="action-config">
{#if 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}
<Button thin blue on:click={saveEventData}>Save</Button>
</div>
</div>
<a href="https://docs.budibase.com">Learn more about Actions</a>
<style>
.action-header {
display: flex;
flex-direction: row;
align-items: center;
}
.action-header > span {
margin-bottom: var(--spacing-m);
}
.action-header > span:hover,
.selected {
cursor: pointer;
font-weight: 500;
}
.actions-list {
border: var(--border-light);
}
.available-action {
padding: var(--spacing-s);
font-size: var(--font-size-m);
cursor: pointer;
}
.available-action:hover {
background: var(--grey-2);
}
.actions-container {
display: grid;
grid-gap: var(--spacing-m);
grid-template-columns: 15% 1fr;
grid-auto-flow: column;
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);
}
</style>

View File

@ -1,176 +0,0 @@
<script>
import {
Button,
TextButton,
Body,
DropdownMenu,
ModalContent,
} from "@budibase/bbui"
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
import actionTypes from "./actions"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
const eventTypeKey = "##eventHandlerType"
export let event
let addActionButton
let addActionDropdown
let selectedAction
let draftEventHandler = { parameters: [] }
$: actions = event || []
$: selectedActionComponent =
selectedAction &&
actionTypes.find(t => t.name === selectedAction[eventTypeKey]).component
const updateEventHandler = (updatedHandler, index) => {
actions[index] = updatedHandler
}
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 = () => {
dispatch("change", actions)
}
</script>
<div>
<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}
<Button thin blue on:click={saveEventData}>Save</Button>
</div>
<a href="https://docs.budibase.com">Learn more about Actions</a>
<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-s);
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

@ -2,7 +2,7 @@
import { Button, Modal } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store } from "builderStore"
import EventEditorModal from "./EventEditorModal.svelte"
import EventEditor from "./EventEditor.svelte"
import BottomDrawer from "components/common/BottomDrawer.svelte"
const dispatch = createEventDispatcher()
@ -20,7 +20,12 @@
<Button secondary small on:click={showDrawer}>Define Actions</Button>
{#if drawerVisible}
<BottomDrawer>
<EventEditorModal event={value} eventType={name} on:change />
<BottomDrawer title={'Actions'} onClose={() => (drawerVisible = false)}>
<heading slot="buttons">
<Button thin blue>Save</Button>
</heading>
<div slot="body">
<EventEditor event={value} eventType={name} on:change />
</div>
</BottomDrawer>
{/if}

View File

@ -2,10 +2,14 @@
import { Button, Icon, DropdownMenu, Spacer, Heading } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store, backendUiStore, currentAsset } from "builderStore"
// import DataBindingDrawer from "components/userInterface/DataBindingDrawer/index.svelte"
import BottomDrawer from "components/common/BottomDrawer.svelte"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
import fetchBindableProperties from "../../builderStore/fetchBindableProperties"
const dispatch = createEventDispatcher()
let anchorRight, dropdownRight
let bindingDrawerOpen
export let value = {}
@ -15,14 +19,11 @@
}
function openBindingDrawer() {
backendUiStore.update(state => {
state.selectedQueryId = value._id
return state
})
store.update(state => {
state.bottomDrawerVisible = true
return state
})
bindingDrawerOpen = true
}
function closeDatabindingDrawer() {
bindingDrawerOpen = false
}
$: tables = $backendUiStore.tables.map(m => ({
@ -47,6 +48,7 @@
name: query.name,
...query,
schema: query.schema,
parameters: query.parameters,
type: "query",
}))
@ -57,6 +59,15 @@
tables: $backendUiStore.tables,
})
$: queryBindableProperties = bindableProperties.map(property => ({
...property,
category: property.type === "instance" ? "Component" : "Table",
label: property.readableBinding,
path: property.runtimeBinding,
}))
$: console.log("selected", value)
$: links = bindableProperties
.filter(x => x.fieldSchema?.type === "link")
.map(property => {
@ -78,9 +89,6 @@
<span>{value.label ? value.label : 'Table / View / Query'}</span>
<Icon name="arrowdown" />
</div>
{#if value.type === "query"}
<i class="ri-settings-3-line" on:click={openBindingDrawer} />
{/if}
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
<div class="dropdown">
<div class="title">
@ -138,6 +146,25 @@
</div>
</DropdownMenu>
{#if value.type === "query"}
<Button blue on:click={openBindingDrawer}/>
{#if bindingDrawerOpen}
<BottomDrawer title={'Query'} onClose={closeDatabindingDrawer}>
<div slot="buttons">
<Button blue thin on:click={() => handleSelected(value)}>Save</Button>
</div>
<div class="drawer-contents" slot="body">
<pre>{value.queryString}</pre>
<ParameterBuilder
bind:customParams={value.queryParams}
parameters={value.parameters || []}
bindings={queryBindableProperties} />
</div>
</BottomDrawer>
{/if}
{/if}
<style>
.dropdownbutton {
background-color: var(--grey-2);
@ -199,4 +226,8 @@
li:hover {
background-color: var(--grey-4);
}
.drawer-contents {
padding: var(--spacing-xl);
}
</style>

View File

@ -10,7 +10,6 @@
import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte"
import FrontendNavigatePane from "components/userInterface/FrontendNavigatePane.svelte"
import DataBindingDrawer from "components/userInterface/DataBindingDrawer/index.svelte"
$: instance = $store.appInstance
@ -48,10 +47,6 @@
{/if}
</div>
{#if $store.bottomDrawerVisible}
<DataBindingDrawer />
{/if}
{#if $selectedComponent != null}
<div class="components-pane">
<ComponentPropertiesPanel />

View File

@ -1,8 +1,9 @@
import { fetchTableData } from "./tables"
import { fetchViewData } from "./views"
import { fetchRelationshipData } from "./relationships"
import { fetchQueryData } from "./queries"
import { executeQuery } from "./queries"
import { enrichRows } from "./rows"
import { enrichDataBindings } from "../utils/enrichDataBinding"
/**
* Fetches all rows for a particular Budibase data source.
@ -20,8 +21,12 @@ export const fetchDatasource = async (datasource, dataContext) => {
} else if (type === "view") {
rows = await fetchViewData(datasource)
} else if (type === "query") {
// TODO: map to schema
return await fetchQueryData(datasource)
console.log("Query Datasource", datasource)
console.log("Data Context", dataContext)
// TODO: You left here
const parameters = enrichDataBindings(datasource.queryParams, dataContext)
console.log("PARSED PARAMS", parameters)
return await executeQuery({ _id: datasource._id, parameters })
} else if (type === "link") {
const row = dataContext[datasource.providerId]
rows = await fetchRelationshipData({

View File

@ -1,23 +1,13 @@
import API from "./api"
/**
* Fetches all rows from a query.
*/
export const fetchQueryData = async ({ _id }) => {
const response = await API.get({
url: `/api/queries/${_id}`,
})
return response.rows
}
/**
* Executes a query against an external data connector.
*/
export const executeQuery = async ({ queryId, params }) => {
export const executeQuery = async ({ queryId, parameters }) => {
const response = await API.post({
url: `/api/queries/${queryId}`,
body: {
params,
parameters,
},
})
return response.rows

View File

@ -28,10 +28,13 @@ const navigationHandler = action => {
const queryExecutionHandler = async (action, context) => {
const { datasourceId, queryId, queryParams } = action.parameters
// TODO: allow context based bindings for query params
const enrichedQueryParameters = enrichDataBindings(queryParams, context)
await executeQuery({ datasourceId, queryId, params: enrichedQueryParameters })
await executeQuery({
datasourceId,
queryId,
parameters: enrichedQueryParameters,
})
}
const handlerMap = {

View File

@ -81,7 +81,7 @@ exports.execute = async function(ctx) {
const queryTemplate = handlebars.compile(query.queryString)
// TODO: Take the default params into account
const parsedQuery = queryTemplate(ctx.request.body.params)
const parsedQuery = queryTemplate(ctx.request.body.parameters)
const Integration = integrations[datasource.source]
@ -94,31 +94,6 @@ exports.execute = async function(ctx) {
ctx.body = response
}
exports.fetchQuery = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const query = await db.get(ctx.params.queryId)
const datasource = await db.get(query.datasourceId)
const Integration = integrations[datasource.source]
if (!Integration) {
ctx.throw(400, "Integration type does not exist.")
return
}
const rows = await new Integration(
datasource.config,
query.queryString
).query()
ctx.body = {
schema: query.schema,
rows,
}
}
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
await db.destroy(ctx.params.queryId)

View File

@ -8,10 +8,9 @@ const router = Router()
// TODO: sort out auth so apps have the right permissions
router
.get("/api/queries", authorized(BUILDER), queryController.fetch)
.get("/api/queries/:queryId", authorized(BUILDER), queryController.fetchQuery)
.post("/api/queries", authorized(BUILDER), queryController.save)
.post("/api/queries/preview", authorized(BUILDER), queryController.preview)
.post("/api/queries/:queryId", authorized(BUILDER), queryController.execute)
.post("/api/queries/preview", authorized(BUILDER), queryController.preview)
.delete("/api/queries/:queryId", authorized(BUILDER), queryController.destroy)
module.exports = router