Merge branch 'develop' of github.com:Budibase/budibase into default-field-values

This commit is contained in:
Andrew Kingston 2021-08-04 17:21:47 +01:00
commit c3dde3e5f4
66 changed files with 526 additions and 246 deletions

View File

@ -0,0 +1,14 @@
---
name: Feature Request
about: Request a new budibase feature or enhancement
title: ''
labels: enhancement
assignees: ''
---
**Describe the feature request**
A clear and concise description of what the feature request.
**Screenshots**
If applicable, add screenshots to help explain your problem.

View File

@ -57,7 +57,7 @@
- **Open source and extensible.** Budibase is open-source - licensed as GPL v3. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience.
- **Load data or start from scratch.** Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, mySQL, Airtable, S3, DyanmoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
- **Load data or start from scratch.** Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
- **Design and build apps with powerful pre-made components.** Budibase comes out of the box with beautifully designed, powerful components which you can use like building blocks to build your UI. We also expose a lot of your favourite CSS styling options so you can go that extra creative mile. [Request new component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).

View File

@ -1,5 +1,5 @@
{
"version": "0.9.87-alpha.7",
"version": "0.9.96",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

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

View File

@ -66,6 +66,7 @@ module.exports = (noAuthPatterns = [], opts) => {
}
}
if (error) {
console.error("Auth Error", error)
// remove the cookie as the user does not exist anymore
clearCookie(ctx, Cookies.Auth)
} else {

View File

@ -6,7 +6,6 @@ const { authError } = require("./utils")
const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions")
const { getGlobalUserByEmail } = require("../../utils")
const fetch = require("node-fetch")
/**
* Common authentication logic for third parties. e.g. OAuth, OIDC.

View File

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

View File

@ -70,6 +70,7 @@
>
<div class="modal-wrapper" on:mousedown|self={cancel}>
<div class="modal-inner-wrapper" on:mousedown|self={cancel}>
<slot name="outside" />
<div
use:focusFirstInput
class="spectrum-Modal is-open"
@ -93,6 +94,7 @@
z-index: 999;
overflow: auto;
overflow-x: hidden;
background: rgba(0, 0, 0, 0.75);
}
.modal-wrapper {
@ -112,6 +114,7 @@
justify-content: center;
align-items: flex-start;
width: 0;
position: relative;
}
.spectrum-Modal {
@ -122,6 +125,7 @@
--spectrum-dialog-confirm-border-radius: var(
--spectrum-global-dimension-size-100
);
max-width: 100%;
}
:global(.spectrum--lightest .spectrum-Modal.inline) {
border: var(--border-light);

View File

@ -15,6 +15,7 @@
export let showCloseIcon = true
export let onConfirm = undefined
export let disabled = false
export let showDivider = true
const { hide, cancel } = getContext(Context.Modal)
let loading = false
@ -41,11 +42,17 @@
aria-modal="true"
>
<div class="spectrum-Dialog-grid">
<h1 class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader">
{title}
</h1>
<Divider size="M" />
{#if title}
<h1
class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"
class:noDivider={!showDivider}
>
{title}
</h1>
{#if showDivider}
<Divider size="M" />
{/if}
{/if}
<!-- TODO: Remove content-grid class once Layout components are in bbui -->
<section class="spectrum-Dialog-content content-grid">
<slot />
@ -72,8 +79,8 @@
</div>
{/if}
{#if showCloseIcon}
<div class="close-icon" on:click={hide}>
<Icon hoverable name="Close" />
<div class="close-icon">
<Icon hoverable name="Close" on:click={cancel} />
</div>
{/if}
</div>
@ -96,6 +103,9 @@
.spectrum-Dialog-heading {
font-family: var(--font-sans);
}
.spectrum-Dialog-heading.noDivider {
margin-bottom: 12px;
}
.spectrum-Dialog-buttonGroup {
gap: var(--spectrum-global-dimension-static-size-200);

View File

@ -0,0 +1,20 @@
<script>
export let type = "info"
export let icon = "Info"
export let message = ""
</script>
<div class="spectrum-Toast spectrum-Toast--{type}">
{#if icon}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Toast-typeIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
{/if}
<div class="spectrum-Toast-body">
<div class="spectrum-Toast-content">{message || ""}</div>
</div>
</div>

View File

@ -2,30 +2,16 @@
import "@spectrum-css/toast/dist/index-vars.css"
import Portal from "svelte-portal"
import { flip } from "svelte/animate"
import { fly } from "svelte/transition"
import { notifications } from "../Stores/notifications"
import Notification from "./Notification.svelte"
import { fly } from "svelte/transition"
</script>
<Portal target=".modal-container">
<div class="notifications">
{#each $notifications as { type, icon, message, id } (id)}
<div
animate:flip
transition:fly={{ y: -30 }}
class="spectrum-Toast spectrum-Toast--{type} notification-offset"
>
{#if icon}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Toast-typeIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
{/if}
<div class="spectrum-Toast-body">
<div class="spectrum-Toast-content">{message}</div>
</div>
<div animate:flip transition:fly={{ y: -30 }}>
<Notification {type} {icon} {message} />
</div>
{/each}
</div>
@ -34,7 +20,7 @@
<style>
.notifications {
position: fixed;
top: 10px;
top: 20px;
left: 0;
right: 0;
margin: 0 auto;
@ -45,8 +31,6 @@
justify-content: flex-start;
align-items: center;
pointer-events: none;
}
.notification-offset {
margin-bottom: 10px;
gap: 10px;
}
</style>

View File

@ -38,6 +38,7 @@ export { default as MenuItem } from "./Menu/Item.svelte"
export { default as Modal } from "./Modal/Modal.svelte"
export { default as ModalContent } from "./Modal/ModalContent.svelte"
export { default as NotificationDisplay } from "./Notification/NotificationDisplay.svelte"
export { default as Notification } from "./Notification/Notification.svelte"
export { default as SideNavigation } from "./SideNavigation/Navigation.svelte"
export { default as SideNavigationItem } from "./SideNavigation/Item.svelte"
export { default as DatePicker } from "./Form/DatePicker.svelte"

View File

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

View File

@ -70,7 +70,7 @@ export const getFrontendStore = () => {
url: application.url,
layouts,
screens,
theme: application.theme,
theme: application.theme || "spectrum--light",
hasAppPackage: true,
appInstance: application.instance,
clientLibPath,

View File

@ -18,8 +18,8 @@
const dispatch = createEventDispatcher()
let bindingDrawer
$: tempValue = Array.isArray(value) ? value : []
$: readableValue = runtimeToReadableBinding(bindings, value)
$: tempValue = readableValue
const handleClose = () => {
onChange(tempValue)
@ -56,7 +56,7 @@
slot="body"
value={readableValue}
close={handleClose}
on:update={event => (tempValue = event.detail)}
on:change={event => (tempValue = event.detail)}
bindableProperties={bindings}
/>
</Drawer>

View File

@ -24,7 +24,7 @@
<div>
<Select
value={$store.theme || "spectrum--light"}
value={$store.theme}
options={themeOptions}
placeholder={null}
on:change={e => store.actions.theme.save(e.detail)}

View File

@ -47,6 +47,18 @@ export default `
return
}
// Parse received message
// If parsing fails, just ignore and wait for the next message
let parsed
try {
parsed = JSON.parse(event.data)
} catch (error) {
// Ignore
}
if (!parsed) {
return
}
// Extract data from message
const {
selectedComponentId,
@ -55,7 +67,7 @@ export default `
previewType,
appId,
theme
} = JSON.parse(event.data)
} = parsed
// Set some flags so the app knows we're in the builder
window["##BUDIBASE_IN_BUILDER##"] = true

View File

@ -11,6 +11,7 @@
export let componentDefinition
export let componentInstance
export let assetInstance
export let bindings
const layoutDefinition = []
const screenDefinition = [
@ -65,6 +66,7 @@
options: setting.options,
placeholder: setting.placeholder,
}}
{bindings}
/>
{/if}
{/each}

View File

@ -4,6 +4,7 @@
import ConditionalUIDrawer from "./PropertyControls/ConditionalUIDrawer.svelte"
export let componentInstance
export let bindings
let tempValue
let drawer
@ -32,5 +33,5 @@
Show, hide and update components in response to conditions being met.
</svelte:fragment>
<Button cta slot="buttons" on:click={() => save()}>Save</Button>
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} />
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} {bindings} />
</Drawer>

View File

@ -4,6 +4,7 @@
export let componentDefinition
export let componentInstance
export let bindings
const getStyles = def => {
if (!def?.styles?.length) {
@ -29,6 +30,7 @@
columns={style.columns}
properties={style.settings}
{componentInstance}
{bindings}
/>
{/each}
{/if}

View File

@ -1,27 +1,45 @@
<script>
import { store, selectedComponent } from "builderStore"
import { store, selectedComponent, currentAsset } from "builderStore"
import { Tabs, Tab } from "@budibase/bbui"
import ScreenSettingsSection from "./ScreenSettingsSection.svelte"
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
import DesignSection from "./DesignSection.svelte"
import CustomStylesSection from "./CustomStylesSection.svelte"
import ConditionalUISection from "./ConditionalUISection.svelte"
import { getBindableProperties } from "builderStore/dataBinding"
$: componentInstance = $selectedComponent
$: componentDefinition = store.actions.components.getDefinition(
$selectedComponent?._component
)
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
</script>
<Tabs selected="Settings" noPadding>
<Tab title="Settings">
<div class="container">
{#key componentInstance?._id}
<ScreenSettingsSection {componentInstance} {componentDefinition} />
<ComponentSettingsSection {componentInstance} {componentDefinition} />
<DesignSection {componentInstance} {componentDefinition} />
<CustomStylesSection {componentInstance} {componentDefinition} />
<ConditionalUISection {componentInstance} {componentDefinition} />
<ScreenSettingsSection
{componentInstance}
{componentDefinition}
{bindings}
/>
<ComponentSettingsSection
{componentInstance}
{componentDefinition}
{bindings}
/>
<DesignSection {componentInstance} {componentDefinition} {bindings} />
<CustomStylesSection
{componentInstance}
{componentDefinition}
{bindings}
/>
<ConditionalUISection
{componentInstance}
{componentDefinition}
{bindings}
/>
{/key}
</div>
</Tab>

View File

@ -13,12 +13,12 @@
import { generate } from "shortid"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { OperatorOptions, getValidOperatorsForType } from "helpers/lucene"
import { getBindableProperties } from "builderStore/dataBinding"
import { currentAsset, selectedComponent, store } from "builderStore"
import { selectedComponent, store } from "builderStore"
import { getComponentForSettingType } from "./componentSettings"
import PropertyControl from "./PropertyControl.svelte"
export let conditions = []
export let bindings = []
const flipDurationMs = 150
const actionOptions = [
@ -64,10 +64,6 @@
value: setting.key,
}
})
$: bindableProperties = getBindableProperties(
$currentAsset,
$store.selectedComponentId
)
$: conditions.forEach(link => {
if (!link.id) {
link.id = generate()
@ -194,6 +190,7 @@
placeholder: getSettingDefinition(condition.setting)
.placeholder,
}}
{bindings}
/>
{:else}
<Select disabled placeholder=" " />
@ -201,7 +198,7 @@
{/if}
<div>IF</div>
<DrawerBindableInput
bindings={bindableProperties}
{bindings}
placeholder="Value"
value={condition.newValue}
on:change={e => (condition.newValue = e.detail)}
@ -222,7 +219,7 @@
{#if ["string", "number"].includes(condition.valueType)}
<DrawerBindableInput
disabled={condition.noValue}
bindings={bindableProperties}
{bindings}
placeholder="Value"
value={condition.referenceValue}
on:change={e => (condition.referenceValue = e.detail)}

View File

@ -1,8 +1,5 @@
<script>
import {
getBindableProperties,
getDataProviderComponents,
} from "builderStore/dataBinding"
import { getDataProviderComponents } from "builderStore/dataBinding"
import {
Button,
Popover,
@ -31,6 +28,7 @@
export let value = {}
export let otherSources
export let showAllQueries
export let bindings = []
$: text = value?.label ?? "Choose an option"
$: tables = $tablesStore.list.map(m => ({
@ -60,10 +58,6 @@
parameters: query.parameters,
type: "query",
}))
$: bindableProperties = getBindableProperties(
$currentAsset,
$store.selectedComponentId
)
$: dataProviders = getDataProviderComponents(
$currentAsset,
$store.selectedComponentId
@ -75,13 +69,13 @@
type: "provider",
schema: provider.schema,
}))
$: queryBindableProperties = bindableProperties.map(property => ({
$: queryBindableProperties = bindings.map(property => ({
...property,
category: property.type === "instance" ? "Component" : "Table",
label: property.readableBinding,
path: property.readableBinding,
}))
$: links = bindableProperties
$: links = bindings
.filter(x => x.fieldSchema?.type === "link")
.map(property => {
return {
@ -138,7 +132,7 @@
bind:customParams={value.queryParams}
parameters={queries.find(query => query._id === value._id)
.parameters}
bindings={queryBindableProperties}
{bindings}
/>
{/if}
<IntegrationQueryEditor

View File

@ -13,10 +13,10 @@
import { generate } from "shortid"
const flipDurationMs = 150
const EVENT_TYPE_KEY = "##eventHandlerType"
export let actions
export let bindings = []
// dndzone needs an id on the array items, so this adds some temporary ones.
$: {
@ -121,6 +121,7 @@
<svelte:component
this={selectedActionComponent}
parameters={selectedAction.parameters}
{bindings}
/>
</div>
{/if}

View File

@ -9,6 +9,7 @@
export let value = []
export let name
export let bindings
let drawer
@ -57,5 +58,5 @@
Define what actions to run.
</svelte:fragment>
<Button cta slot="buttons" on:click={saveEventData}>Save</Button>
<EventEditor slot="body" bind:actions={value} eventType={name} />
<EventEditor slot="body" bind:actions={value} eventType={name} {bindings} />
</Drawer>

View File

@ -0,0 +1,17 @@
<script>
import { Body } from "@budibase/bbui"
</script>
<div class="root">
<Body size="S">This action doesn't require any additional settings.</Body>
<Body size="S">
This action won't do anything if there isn't a screen modal open.
</Body>
</div>
<style>
.root {
max-width: 800px;
margin: 0 auto;
}
</style>

View File

@ -1,14 +1,12 @@
<script>
import { Select, Label, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import { tables } from "stores/backend"
import { getBindableProperties } from "builderStore/dataBinding"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let parameters
export let bindings = []
$: tableOptions = $tables.list || []
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
</script>
<div class="root">

View File

@ -1,21 +1,16 @@
<script>
import { Select, Layout, Input, Checkbox } from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import { datasources, integrations, queries } from "stores/backend"
import { getBindableProperties } from "builderStore/dataBinding"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte"
export let parameters
export let bindings = []
$: query = $queries.list.find(q => q._id === parameters.queryId)
$: datasource = $datasources.list.find(
ds => ds._id === parameters.datasourceId
)
$: bindableProperties = getBindableProperties(
$currentAsset,
$store.selectedComponentId
)
function fetchQueryDefinition(query) {
const source = $datasources.list.find(
@ -61,7 +56,7 @@
<ParameterBuilder
bind:customParams={parameters.queryParams}
parameters={query.parameters}
bindings={bindableProperties}
{bindings}
/>
<IntegrationQueryEditor
height={200}

View File

@ -1,12 +1,9 @@
<script>
import { Label } from "@budibase/bbui"
import { getBindableProperties } from "builderStore/dataBinding"
import { currentAsset, store } from "builderStore"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let parameters
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
export let bindings = []
</script>
<div class="root">
@ -18,19 +15,17 @@
on:change={value => (parameters.url = value.detail)}
{bindings}
/>
<div />
<Checkbox text="Open screen in modal" bind:value={parameters.peek} />
</div>
<style>
.root {
display: flex;
flex-direction: row;
align-items: baseline;
display: grid;
align-items: center;
gap: var(--spacing-m);
grid-template-columns: auto 1fr;
max-width: 800px;
margin: 0 auto;
}
.root :global(> div) {
flex: 1;
margin-left: var(--spacing-l);
}
</style>

View File

@ -1,7 +1,5 @@
<script>
import { Label, ActionButton, Button, Select, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
import { createEventDispatcher } from "svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
@ -11,13 +9,10 @@
export let schemaFields
export let fieldLabel = "Column"
export let valueLabel = "Value"
export let bindings = []
let fields = Object.entries(parameterFields || {})
$: onChange(fields)
$: bindableProperties = getBindableProperties(
$currentAsset,
$store.selectedComponentId
)
const addField = () => {
fields = [...fields.filter(field => field[0]), ["", ""]]
@ -69,7 +64,7 @@
<DrawerBindableInput
title={`Value for "${field[0]}"`}
value={field[1]}
bindings={bindableProperties}
{bindings}
on:change={event => updateFieldValue(idx, event.detail)}
/>
<ActionButton

View File

@ -9,6 +9,7 @@
import SaveFields from "./SaveFields.svelte"
export let parameters
export let bindings = []
$: dataProviderComponents = getDataProviderComponents(
$currentAsset,
@ -70,6 +71,7 @@
parameterFields={parameters.fields}
{schemaFields}
on:change={onFieldsChanged}
{bindings}
/>
</div>
{/if}

View File

@ -3,13 +3,14 @@
import { automationStore } from "builderStore"
import SaveFields from "./SaveFields.svelte"
export let parameters = {}
export let bindings = []
const AUTOMATION_STATUS = {
NEW: "new",
EXISTING: "existing",
}
export let parameters = {}
let automationStatus = parameters.automationId
? AUTOMATION_STATUS.EXISTING
: AUTOMATION_STATUS.NEW
@ -109,6 +110,7 @@
parameterFields={parameters.fields}
fieldLabel="Field"
on:change={onFieldsChanged}
{bindings}
/>
{/key}
</div>

View File

@ -6,6 +6,7 @@ import TriggerAutomation from "./TriggerAutomation.svelte"
import ValidateForm from "./ValidateForm.svelte"
import LogOut from "./LogOut.svelte"
import ClearForm from "./ClearForm.svelte"
import CloseScreenModal from "./CloseScreenModal.svelte"
// Defines which actions are available to configure in the front end.
// Unfortunately the "name" property is used as the identifier so please don't
@ -47,4 +48,8 @@ export default [
name: "Clear Form",
component: ClearForm,
},
{
name: "Close Screen Modal",
component: CloseScreenModal,
},
]

View File

@ -20,6 +20,8 @@
export let value = []
export let componentInstance
export let bindings = []
let drawer
let tempValue = value || []
@ -51,7 +53,7 @@
constraints.
{/if}
</Body>
<LuceneFilterBuilder bind:value={tempValue} {schemaFields} />
<LuceneFilterBuilder bind:value={tempValue} {schemaFields} {bindings} />
</Layout>
</DrawerContent>
</Drawer>

View File

@ -7,20 +7,15 @@
Combobox,
Input,
} from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { generate } from "shortid"
import { OperatorOptions, getValidOperatorsForType } from "helpers/lucene"
export let schemaFields
export let value
export let bindings = []
const BannedTypes = ["link", "attachment"]
$: bindableProperties = getBindableProperties(
$currentAsset,
$store.selectedComponentId
)
$: fieldOptions = (schemaFields ?? [])
.filter(field => !BannedTypes.includes(field.type))
.map(field => field.name)
@ -101,7 +96,7 @@
title={`Value for "${expression.field}"`}
value={expression.value}
placeholder="Value"
bindings={bindableProperties}
{bindings}
on:change={event => (expression.value = event.detail)}
/>
{:else if ["string", "longform", "number"].includes(expression.type)}

View File

@ -1,8 +1,6 @@
<script>
import { Button, Icon, Drawer, Label } from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import {
getBindableProperties,
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
@ -18,18 +16,15 @@
export let value = null
export let props = {}
export let onChange = () => {}
export let bindings = []
let bindingDrawer
let anchor
let valid
$: bindableProperties = getBindableProperties(
$currentAsset,
$store.selectedComponentId
)
$: safeValue = getSafeValue(value, props.defaultValue, bindableProperties)
$: safeValue = getSafeValue(value, props.defaultValue, bindings)
$: tempValue = safeValue
$: replaceBindings = val => readableToRuntimeBinding(bindableProperties, val)
$: replaceBindings = val => readableToRuntimeBinding(bindings, val)
const handleClose = () => {
handleChange(tempValue)
@ -61,8 +56,8 @@
// The "safe" value is the value with any bindings made readable
// If there is no value set, any default value is used
const getSafeValue = (value, defaultValue, bindableProperties) => {
const enriched = runtimeToReadableBinding(bindableProperties, value)
const getSafeValue = (value, defaultValue, bindings) => {
const enriched = runtimeToReadableBinding(bindings, value)
return enriched == null && defaultValue !== undefined
? defaultValue
: enriched
@ -83,6 +78,7 @@
updateOnChange={false}
on:change={handleChange}
onChange={handleChange}
{bindings}
name={key}
text={label}
{type}
@ -108,7 +104,7 @@
bind:valid
value={safeValue}
on:change={e => (tempValue = e.detail)}
{bindableProperties}
bindableProperties={bindings}
/>
</Drawer>
{/if}

View File

@ -3,10 +3,11 @@
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
export let value
export let bindings
$: urlOptions = $store.screens
.map(screen => screen.routing?.route)
.filter(x => x != null)
</script>
<DrawerBindableCombobox {value} on:change options={urlOptions} />
<DrawerBindableCombobox {value} {bindings} on:change options={urlOptions} />

View File

@ -9,6 +9,7 @@
import { FrontendTypes } from "constants"
export let componentInstance
export let bindings
function setAssetProps(name, value) {
const selectedAsset = get(currentAsset)
@ -44,6 +45,7 @@
key={def.key}
value={deepGet($currentAsset, def.key)}
onChange={val => setAssetProps(def.key, val)}
{bindings}
/>
{/each}
</DetailSummary>

View File

@ -7,6 +7,7 @@
export let columns
export let properties
export let componentInstance
export let bindings = []
$: style = componentInstance._styles.normal || {}
$: changed = properties?.some(prop => hasPropChanged(style, prop)) ?? false
@ -36,6 +37,7 @@
value={style[prop.key]}
onChange={val => store.actions.components.updateStyle(prop.key, val)}
props={getControlProps(prop)}
{bindings}
/>
</div>
{/each}

View File

@ -158,10 +158,16 @@
fieldName: fromTable.primary[0],
}
} else {
// the relateFrom.fieldName should remain the same, as it is the foreignKey in the other
// table, this is due to the way that budibase represents relationships, the fieldName in a
// link column schema is the column linked to (FK in this case). The foreignKey column is
// essentially what is linked to in the from table, this is unique to SQL as this isn't a feature
// of Budibase internal tables.
// Essentially this means the fieldName is what we are linking to in the other table, and the
// foreignKey is what is linking out of the current table.
relateFrom = {
...relateFrom,
foreignKey: relateFrom.fieldName,
fieldName: fromTable.primary[0],
foreignKey: fromTable.primary[0],
}
relateTo = {
...relateTo,

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "0.9.87-alpha.7",
"version": "0.9.96",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -18,9 +18,9 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "^0.9.87-alpha.7",
"@budibase/standard-components": "^0.9.87-alpha.7",
"@budibase/string-templates": "^0.9.87-alpha.7",
"@budibase/bbui": "^0.9.96",
"@budibase/standard-components": "^0.9.96",
"@budibase/string-templates": "^0.9.96",
"regexparam": "^1.3.0",
"shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5"

View File

@ -38,15 +38,15 @@ const makeApiCall = async ({ method, url, body, json = true }) => {
case 200:
return response.json()
case 401:
notificationStore.danger("Invalid credentials")
notificationStore.actions.error("Invalid credentials")
return handleError(`Invalid credentials`)
case 404:
notificationStore.danger("Not found")
notificationStore.actions.warning("Not found")
return handleError(`${url}: Not Found`)
case 400:
return handleError(`${url}: Bad Request`)
case 403:
notificationStore.danger(
notificationStore.actions.error(
"Your session has expired, or you don't have permission to access that data"
)
return handleError(`${url}: Forbidden`)

View File

@ -9,7 +9,7 @@ export const triggerAutomation = async (automationId, fields) => {
body: { fields },
})
res.error
? notificationStore.danger("An error has occurred")
: notificationStore.success("Automation triggered")
? notificationStore.actions.error("An error has occurred")
: notificationStore.actions.success("Automation triggered")
return res
}

View File

@ -7,7 +7,7 @@ import API from "./api"
export const executeQuery = async ({ queryId, parameters }) => {
const query = await API.get({ url: `/api/queries/${queryId}` })
if (query?.datasourceId == null) {
notificationStore.danger("That query couldn't be found")
notificationStore.actions.error("That query couldn't be found")
return
}
const res = await API.post({
@ -17,9 +17,9 @@ export const executeQuery = async ({ queryId, parameters }) => {
},
})
if (res.error) {
notificationStore.danger("An error has occurred")
notificationStore.actions.error("An error has occurred")
} else if (!query.readable) {
notificationStore.success("Query executed successfully")
notificationStore.actions.success("Query executed successfully")
dataSourceStore.actions.invalidateDataSource(query.datasourceId)
}
return res

View File

@ -27,8 +27,8 @@ export const saveRow = async row => {
body: row,
})
res.error
? notificationStore.danger("An error has occurred")
: notificationStore.success("Row saved")
? notificationStore.actions.error("An error has occurred")
: notificationStore.actions.success("Row saved")
// Refresh related datasources
dataSourceStore.actions.invalidateDataSource(row.tableId)
@ -48,8 +48,8 @@ export const updateRow = async row => {
body: row,
})
res.error
? notificationStore.danger("An error has occurred")
: notificationStore.success("Row updated")
? notificationStore.actions.error("An error has occurred")
: notificationStore.actions.success("Row updated")
// Refresh related datasources
dataSourceStore.actions.invalidateDataSource(row.tableId)
@ -72,8 +72,8 @@ export const deleteRow = async ({ tableId, rowId, revId }) => {
},
})
res.error
? notificationStore.danger("An error has occurred")
: notificationStore.success("Row deleted")
? notificationStore.actions.error("An error has occurred")
: notificationStore.actions.success("Row deleted")
// Refresh related datasources
dataSourceStore.actions.invalidateDataSource(tableId)
@ -95,8 +95,8 @@ export const deleteRows = async ({ tableId, rows }) => {
},
})
res.error
? notificationStore.danger("An error has occurred")
: notificationStore.success(`${rows.length} row(s) deleted`)
? notificationStore.actions.error("An error has occurred")
: notificationStore.actions.success(`${rows.length} row(s) deleted`)
// Refresh related datasources
dataSourceStore.actions.invalidateDataSource(tableId)

View File

@ -4,6 +4,7 @@
import Component from "./Component.svelte"
import NotificationDisplay from "./NotificationDisplay.svelte"
import ConfirmationDisplay from "./ConfirmationDisplay.svelte"
import PeekScreenDisplay from "./PeekScreenDisplay.svelte"
import Provider from "./Provider.svelte"
import SDK from "../sdk"
import {
@ -93,13 +94,14 @@
</div>
{:else if $screenStore.activeLayout}
<Provider key="user" data={$authStore} {actions}>
<div id="app-root">
<div id="app-root" class:preview={$builderStore.inBuilder}>
{#key $screenStore.activeLayout._id}
<Component instance={$screenStore.activeLayout.props} />
{/key}
</div>
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
<!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder}
@ -131,6 +133,9 @@
#app-root {
position: relative;
}
#app-root.preview {
border: 1px solid var(--spectrum-global-color-gray-300);
}
/* Custom scrollbars */
:global(::-webkit-scrollbar) {

View File

@ -1,36 +1,34 @@
<script>
import { flip } from "svelte/animate"
import { notificationStore } from "../store"
import { Notification } from "@budibase/bbui"
import { fly } from "svelte/transition"
import { getContext } from "svelte"
const { notifications } = getContext("sdk")
export let themes = {
danger: "#E26D69",
success: "#84C991",
warning: "#f0ad4e",
info: "#5bc0de",
default: "#aaaaaa",
}
</script>
<div class="notifications">
{#each $notifications as notification (notification.id)}
<div
animate:flip
class="toast"
style="background: {themes[notification.type]};"
transition:fly={{ y: -30 }}
>
<div class="content">{notification.message}</div>
{#if notification.icon}<i class={notification.icon} />{/if}
</div>
{/each}
{#if $notificationStore}
{#key $notificationStore.id}
<div
in:fly={{
duration: 300,
y: -20,
delay: $notificationStore.delay ? 300 : 0,
}}
out:fly={{ y: -20, duration: 150 }}
>
<Notification
type={$notificationStore.type}
message={$notificationStore.message}
icon={$notificationStore.icon}
/>
</div>
{/key}
{/if}
</div>
<style>
.notifications {
position: fixed;
top: 10px;
top: 20px;
left: 0;
right: 0;
margin: 0 auto;
@ -42,19 +40,4 @@
align-items: center;
pointer-events: none;
}
.toast {
flex: 0 0 auto;
margin-bottom: 10px;
border-radius: var(--border-radius-s);
/* The toasts now support being auto sized, so this static width could be removed */
width: 40vw;
}
.content {
padding: 10px;
display: block;
color: white;
font-weight: 600;
}
</style>

View File

@ -0,0 +1,120 @@
<script>
import {
peekStore,
dataSourceStore,
notificationStore,
routeStore,
} from "../store"
import { Modal, ModalContent, ActionButton } from "@budibase/bbui"
import { onDestroy } from "svelte"
let iframe
let listenersAttached = false
const invalidateDataSource = event => {
const { dataSourceId } = event.detail
dataSourceStore.actions.invalidateDataSource(dataSourceId)
}
const proxyNotification = event => {
const { message, type, icon } = event.detail
notificationStore.actions.send(message, type, icon)
}
const attachListeners = () => {
// Mirror datasource invalidation to keep the parent window up to date
iframe.contentWindow.addEventListener(
"invalidate-datasource",
invalidateDataSource
)
// Listen for a close event to close the screen peek
iframe.contentWindow.addEventListener(
"close-screen-modal",
peekStore.actions.hidePeek
)
// Proxy notifications back to the parent window instead of iframe
iframe.contentWindow.addEventListener("notification", proxyNotification)
}
const handleCancel = () => {
peekStore.actions.hidePeek()
iframe.contentWindow.removeEventListener(
"invalidate-datasource",
invalidateDataSource
)
iframe.contentWindow.removeEventListener(
"close-screen-modal",
peekStore.actions.hidePeek
)
iframe.contentWindow.removeEventListener("notification", proxyNotification)
}
const handleFullscreen = () => {
if ($peekStore.external) {
window.location = $peekStore.href
} else {
routeStore.actions.navigate($peekStore.url)
handleCancel()
}
}
$: {
if (iframe && !listenersAttached) {
attachListeners()
listenersAttached = true
} else if (!iframe) {
listenersAttached = false
}
}
onDestroy(() => {
if (iframe) {
handleCancel()
}
})
</script>
{#if $peekStore.showPeek}
<Modal fixed on:cancel={handleCancel}>
<div class="actions spectrum--darkest" slot="outside">
<ActionButton size="S" quiet icon="OpenIn" on:click={handleFullscreen}>
Full screen
</ActionButton>
<ActionButton size="S" quiet icon="Close" on:click={handleCancel}>
Close
</ActionButton>
</div>
<ModalContent
showCancelButton={false}
showConfirmButton={false}
size="L"
showDivider={false}
showCloseIcon={false}
>
<iframe title="Peek" bind:this={iframe} src={$peekStore.href} />
</ModalContent>
</Modal>
{/if}
<style>
iframe {
margin: -40px;
border: none;
width: calc(100% + 80px);
height: 640px;
max-height: calc(100vh - 120px);
transition: width 1s ease, height 1s ease, top 1s ease, left 1s ease;
border-radius: var(--spectrum-global-dimension-size-100);
}
.actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
position: absolute;
top: 0;
width: 640px;
max-width: 100%;
}
</style>

View File

@ -1,6 +1,6 @@
<script>
import { setContext, getContext } from "svelte"
import Router from "svelte-spa-router"
import Router, { querystring } from "svelte-spa-router"
import { routeStore } from "../store"
import Screen from "./Screen.svelte"
@ -16,6 +16,18 @@
id: $routeStore.routeSessionId,
}
// Keep query params up to date
$: {
let queryParams = {}
if ($querystring) {
const urlSearchParams = new URLSearchParams($querystring)
for (const [key, value] of urlSearchParams) {
queryParams[key] = value
}
}
routeStore.actions.setQueryParams(queryParams)
}
const getRouterConfig = routes => {
let config = {}
routes.forEach(route => {

View File

@ -15,7 +15,7 @@ import { ActionTypes } from "./constants"
export default {
API,
authStore,
notifications: notificationStore,
notificationStore,
routeStore,
screenStore,
builderStore,

View File

@ -1,5 +1,4 @@
import { writable, get } from "svelte/store"
import { notificationStore } from "./notification"
export const createDataSourceStore = () => {
const store = writable([])
@ -67,12 +66,17 @@ export const createDataSourceStore = () => {
const relatedInstances = get(store).filter(instance => {
return instance.dataSourceId === dataSourceId
})
if (relatedInstances?.length) {
notificationStore.blockNotifications(1000)
}
relatedInstances?.forEach(instance => {
instance.refresh()
})
// Emit this as a window event, so parent screens which are iframing us in
// can also invalidate the same datasource
window.dispatchEvent(
new CustomEvent("invalidate-datasource", {
detail: { dataSourceId },
})
)
}
return {

View File

@ -6,6 +6,7 @@ export { screenStore } from "./screens"
export { builderStore } from "./builder"
export { dataSourceStore } from "./dataSource"
export { confirmationStore } from "./confirmation"
export { peekStore } from "./peek"
// Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context"

View File

@ -1,56 +1,63 @@
import { writable } from "svelte/store"
import { writable, get } from "svelte/store"
import { generate } from "shortid"
import { routeStore } from "./routes"
const NOTIFICATION_TIMEOUT = 3000
const createNotificationStore = () => {
const timeoutIds = new Set()
const _notifications = writable([], () => {
let timeout
let block = false
const store = writable(null, () => {
return () => {
// clear all the timers
timeoutIds.forEach(timeoutId => {
clearTimeout(timeoutId)
})
_notifications.set([])
clearTimeout(timeout)
}
})
let block = false
const blockNotifications = (timeout = 1000) => {
block = true
setTimeout(() => (block = false), timeout)
}
const send = (message, type = "default") => {
const send = (message, type = "info", icon) => {
if (block) {
return
}
let _id = id()
_notifications.update(state => {
return [...state, { id: _id, type, message }]
})
const timeoutId = setTimeout(() => {
_notifications.update(state => {
return state.filter(({ id }) => id !== _id)
})
}, NOTIFICATION_TIMEOUT)
timeoutIds.add(timeoutId)
}
const { subscribe } = _notifications
// If peeking, pass notifications back to parent window
if (get(routeStore).queryParams?.peek) {
window.dispatchEvent(
new CustomEvent("notification", {
detail: { message, type, icon },
})
)
return
}
store.set({
id: generate(),
type,
message,
icon,
delay: get(store) != null,
})
clearTimeout(timeout)
timeout = setTimeout(() => {
store.set(null)
}, NOTIFICATION_TIMEOUT)
}
return {
subscribe,
send,
danger: msg => send(msg, "danger"),
warning: msg => send(msg, "warning"),
info: msg => send(msg, "info"),
success: msg => send(msg, "success"),
blockNotifications,
subscribe: store.subscribe,
actions: {
send,
info: msg => send(msg, "info", "Info"),
success: msg => send(msg, "success", "CheckmarkCircle"),
warning: msg => send(msg, "warning", "Alert"),
error: msg => send(msg, "error", "Alert"),
blockNotifications,
},
}
}
function id() {
return "_" + Math.random().toString(36).substr(2, 9)
}
export const notificationStore = createNotificationStore()

View File

@ -0,0 +1,36 @@
import { writable } from "svelte/store"
const initialState = {
showPeek: false,
url: null,
href: null,
external: false,
}
const createPeekStore = () => {
const store = writable(initialState)
const showPeek = url => {
let href = url
let external = !url.startsWith("/")
if (!external) {
href = `${window.location.href.split("#")[0]}#${url}?peek=true`
}
store.set({
showPeek: true,
url,
href,
external,
})
}
const hidePeek = () => {
store.set(initialState)
}
return {
subscribe: store.subscribe,
actions: { showPeek, hidePeek },
}
}
export const peekStore = createPeekStore()

View File

@ -9,6 +9,7 @@ const createRouteStore = () => {
activeRoute: null,
routeSessionId: Math.random(),
routerLoaded: false,
queryParams: {},
}
const store = writable(initialState)
@ -41,6 +42,17 @@ const createRouteStore = () => {
return state
})
}
const setQueryParams = queryParams => {
store.update(state => {
state.queryParams = {
...queryParams,
// Never unset the peek param - screen peek modals should always be
// in a peek state, even if they navigate to a different page
peek: queryParams.peek || state.queryParams?.peek,
}
return state
})
}
const setActiveRoute = route => {
store.update(state => {
state.activeRoute = state.routes.find(x => x.path === route)
@ -58,6 +70,7 @@ const createRouteStore = () => {
fetchRoutes,
navigate,
setRouteParams,
setQueryParams,
setActiveRoute,
setRouterLoaded,
},

View File

@ -4,6 +4,7 @@ import {
builderStore,
confirmationStore,
authStore,
peekStore,
} from "../store"
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
import { ActionTypes } from "../constants"
@ -39,13 +40,17 @@ const triggerAutomationHandler = async action => {
}
const navigationHandler = action => {
const { url } = action.parameters
const { url, peek } = action.parameters
if (url) {
const external = !url.startsWith("/")
if (external) {
window.location.href = url
if (peek) {
peekStore.actions.showPeek(url)
} else {
routeStore.actions.navigate(action.parameters.url)
const external = !url.startsWith("/")
if (external) {
window.location.href = url
} else {
routeStore.actions.navigate(action.parameters.url)
}
}
}
}
@ -94,6 +99,12 @@ const clearFormHandler = async (action, context) => {
)
}
const closeScreenModalHandler = () => {
// Emit this as a window event, so parent screens which are iframing us in
// can close the modal
window.dispatchEvent(new Event("close-screen-modal"))
}
const handlerMap = {
["Save Row"]: saveRowHandler,
["Delete Row"]: deleteRowHandler,
@ -104,6 +115,7 @@ const handlerMap = {
["Refresh Datasource"]: refreshDatasourceHandler,
["Log Out"]: logoutHandler,
["Clear Form"]: clearFormHandler,
["Close Screen Modal"]: closeScreenModalHandler,
}
const confirmTextMap = {

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "0.9.87-alpha.7",
"version": "0.9.96",
"description": "Budibase Web Server",
"main": "src/index.js",
"repository": {
@ -60,9 +60,9 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.9.87-alpha.7",
"@budibase/client": "^0.9.87-alpha.7",
"@budibase/string-templates": "^0.9.87-alpha.7",
"@budibase/auth": "^0.9.96",
"@budibase/client": "^0.9.96",
"@budibase/string-templates": "^0.9.96",
"@elastic/elasticsearch": "7.10.0",
"@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1",
@ -115,7 +115,7 @@
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/preset-env": "^7.14.4",
"@budibase/standard-components": "^0.9.87-alpha.7",
"@budibase/standard-components": "^0.9.96",
"@jest/test-sequencer": "^24.8.0",
"@types/bull": "^3.15.1",
"@types/jest": "^26.0.23",

View File

@ -165,6 +165,10 @@ module External {
if (!row[key] || newRow[key] || field.autocolumn) {
continue
}
// parse floats/numbers
if (field.type === FieldTypes.NUMBER && !isNaN(parseFloat(row[key]))) {
newRow[key] = parseFloat(row[key])
}
// if its not a link then just copy it over
if (field.type !== FieldTypes.LINK) {
newRow[key] = row[key]

View File

@ -19,8 +19,6 @@ function parseBody(body: any) {
}
if (isIsoDateString(value)) {
body[key] = new Date(value)
} else if (!isNaN(parseFloat(value))) {
body[key] = parseFloat(value)
}
}
return body

View File

@ -29,13 +29,12 @@
"keywords": [
"svelte"
],
"version": "0.9.87-alpha.7",
"version": "0.9.96",
"license": "MIT",
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc",
"dependencies": {
"@budibase/bbui": "^0.9.87-alpha.7",
"@budibase/bbui": "^0.9.96",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",
"@spectrum-css/link": "^3.1.3",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/typography": "^3.0.2",

View File

@ -1,6 +1,7 @@
<script>
import { getContext } from "svelte"
import { Heading, Icon } from "@budibase/bbui"
import { routeStore } from "../../client/src/store"
const { styleable, linkable, builderStore } = getContext("sdk")
const component = getContext("component")
@ -26,6 +27,14 @@
Small: "s",
}
// Permanently go into peek mode if we ever get the peek flag
let isPeeking = false
$: {
if ($routeStore.queryParams?.peek) {
isPeeking = true
}
}
$: validLinks = links?.filter(link => link.text && link.url) || []
$: typeClass = navigationClasses[navigation] || "none"
$: widthClass = widthClasses[width] || "l"
@ -51,7 +60,7 @@
<div class="layout layout--{typeClass}" use:styleable={$component.styles}>
{#if typeClass !== "none"}
<div class="nav-wrapper" class:sticky>
<div class="nav-wrapper" class:sticky class:hidden={isPeeking}>
<div class="nav nav--{typeClass} size--{widthClass}">
<div class="nav-header">
{#if validLinks?.length}
@ -139,6 +148,9 @@
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.05);
}
.nav-wrapper.hidden {
display: none;
}
.layout--top .nav-wrapper.sticky {
position: sticky;
top: 0;

View File

@ -14,7 +14,7 @@
export let underline
export let size
$: external = url && !url.startsWith("/")
$: external = url && typeof url === "string" && !url.startsWith("/")
$: target = openInNewTab ? "_blank" : "_self"
$: placeholder = $builderStore.inBuilder && !text
$: componentText = $builderStore.inBuilder

View File

@ -10,12 +10,12 @@
let fieldState
let fieldApi
const { API, notifications } = getContext("sdk")
const { API, notificationStore } = getContext("sdk")
const formContext = getContext("form")
const BYTES_IN_MB = 1000000
const handleFileTooLarge = fileSizeLimit => {
notifications.warning(
notificationStore.actions.warning(
`Files cannot exceed ${
fileSizeLimit / BYTES_IN_MB
} MB. Please try again with smaller files.`

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "0.9.87-alpha.7",
"version": "0.9.96",
"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.87-alpha.7",
"version": "0.9.96",
"description": "Budibase background service",
"main": "src/index.js",
"repository": {
@ -21,8 +21,8 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.9.87-alpha.7",
"@budibase/string-templates": "^0.9.87-alpha.7",
"@budibase/auth": "^0.9.96",
"@budibase/string-templates": "^0.9.96",
"@koa/router": "^8.0.0",
"@techpass/passport-openidconnect": "^0.3.0",
"aws-sdk": "^2.811.0",