Merge branch 'form-builder' of github.com:Budibase/budibase into form-builder

This commit is contained in:
mike12345567 2021-02-05 16:45:37 +00:00
commit c7ed77381e
64 changed files with 1252 additions and 1716 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "0.7.1", "version": "0.7.4",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "0.7.1", "version": "0.7.4",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -64,26 +64,25 @@
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.56.0", "@budibase/bbui": "^1.56.0",
"@budibase/client": "^0.7.1", "@budibase/client": "^0.7.4",
"@budibase/colorpicker": "^1.0.1", "@budibase/colorpicker": "1.0.1",
"@budibase/string-templates": "^0.7.1", "@budibase/string-templates": "^0.7.4",
"@budibase/svelte-ag-grid": "^0.0.16", "@budibase/svelte-ag-grid": "^0.0.16",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@svelteschool/svelte-forms": "^0.7.0", "@svelteschool/svelte-forms": "0.7.0",
"britecharts": "^2.16.0",
"codemirror": "^5.59.0", "codemirror": "^5.59.0",
"d3-selection": "^1.4.1", "d3-selection": "^1.4.1",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"downloadjs": "^1.4.7", "downloadjs": "1.4.7",
"fast-sort": "^2.2.0", "fast-sort": "^2.2.0",
"lodash": "^4.17.13", "lodash": "4.17.13",
"posthog-js": "1.4.5", "posthog-js": "1.4.5",
"remixicon": "^2.5.0", "remixicon": "2.5.0",
"shortid": "^2.2.15", "shortid": "2.2.15",
"svelte-loading-spinners": "^0.1.1", "svelte-loading-spinners": "^0.1.1",
"svelte-portal": "^0.1.0", "svelte-portal": "0.1.0",
"uuid": "^8.3.1", "uuid": "8.3.1",
"yup": "^0.29.2" "yup": "0.29.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.5.5", "@babel/core": "^7.5.5",

View File

@ -69,8 +69,9 @@ export const getDatasourceForProvider = component => {
} }
// Extract datasource from component instance // Extract datasource from component instance
const validSettingTypes = ["datasource", "table", "schema"]
const datasourceSetting = def.settings.find(setting => { const datasourceSetting = def.settings.find(setting => {
return setting.type === "datasource" || setting.type === "table" return validSettingTypes.includes(setting.type)
}) })
if (!datasourceSetting) { if (!datasourceSetting) {
return null return null
@ -80,15 +81,14 @@ export const getDatasourceForProvider = component => {
// example an actual datasource object, or a table ID string. // example an actual datasource object, or a table ID string.
// Convert the datasource setting into a proper datasource object so that // Convert the datasource setting into a proper datasource object so that
// we can use it properly // we can use it properly
if (datasourceSetting.type === "datasource") { if (datasourceSetting.type === "table") {
return component[datasourceSetting?.key]
} else if (datasourceSetting.type === "table") {
return { return {
tableId: component[datasourceSetting?.key], tableId: component[datasourceSetting?.key],
type: "table", type: "table",
} }
} else {
return component[datasourceSetting?.key]
} }
return null
} }
/** /**
@ -99,21 +99,37 @@ export const getContextBindings = (rootComponent, componentId) => {
// Extract any components which provide data contexts // Extract any components which provide data contexts
const dataProviders = getDataProviderComponents(rootComponent, componentId) const dataProviders = getDataProviderComponents(rootComponent, componentId)
let contextBindings = [] let contextBindings = []
// Create bindings for each data provider
dataProviders.forEach(component => { dataProviders.forEach(component => {
const isForm = component._component.endsWith("/form")
const datasource = getDatasourceForProvider(component) const datasource = getDatasourceForProvider(component)
if (!datasource) { let tableName, schema
// Forms are an edge case which do not need table schemas
if (isForm) {
schema = buildFormSchema(component)
tableName = "Schema"
} else {
if (!datasource) {
return
}
// Get schema and table for the datasource
const info = getSchemaForDatasource(datasource, isForm)
schema = info.schema
tableName = info.table?.name
// Add _id and _rev fields for certain types
if (datasource.type === "table" || datasource.type === "link") {
schema["_id"] = { type: "string" }
schema["_rev"] = { type: "string" }
}
}
if (!schema || !tableName) {
return return
} }
// Get schema and add _id and _rev fields for certain types
let { schema, table } = getSchemaForDatasource(datasource)
if (!schema || !table) {
return
}
if (datasource.type === "table" || datasource.type === "link") {
schema["_id"] = { type: "string" }
schema["_rev"] = { type: "string " }
}
const keys = Object.keys(schema).sort() const keys = Object.keys(schema).sort()
// Create bindable properties for each schema field // Create bindable properties for each schema field
@ -132,11 +148,13 @@ export const getContextBindings = (rootComponent, componentId) => {
runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe( runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe(
runtimeBoundKey runtimeBoundKey
)}`, )}`,
readableBinding: `${component._instanceName}.${table.name}.${key}`, readableBinding: `${component._instanceName}.${tableName}.${key}`,
// Field schema and provider are required to construct relationship
// datasource options, based on bindable properties
fieldSchema, fieldSchema,
providerId: component._id, providerId: component._id,
tableId: datasource.tableId, // tableId: table._id,
field: key, // field: key,
}) })
}) })
}) })
@ -164,10 +182,12 @@ export const getContextBindings = (rootComponent, componentId) => {
type: "context", type: "context",
runtimeBinding: `user.${runtimeBoundKey}`, runtimeBinding: `user.${runtimeBoundKey}`,
readableBinding: `Current User.${key}`, readableBinding: `Current User.${key}`,
// Field schema and provider are required to construct relationship
// datasource options, based on bindable properties
fieldSchema, fieldSchema,
providerId: "user", providerId: "user",
tableId: TableNames.USERS, // tableId: TableNames.USERS,
field: key, // field: key,
}) })
}) })
@ -177,7 +197,7 @@ export const getContextBindings = (rootComponent, componentId) => {
/** /**
* Gets a schema for a datasource object. * Gets a schema for a datasource object.
*/ */
export const getSchemaForDatasource = datasource => { export const getSchemaForDatasource = (datasource, isForm = false) => {
let schema, table let schema, table
if (datasource) { if (datasource) {
const { type } = datasource const { type } = datasource
@ -191,6 +211,12 @@ export const getSchemaForDatasource = datasource => {
if (table) { if (table) {
if (type === "view") { if (type === "view") {
schema = cloneDeep(table.views?.[datasource.name]?.schema) schema = cloneDeep(table.views?.[datasource.name]?.schema)
} else if (type === "query" && isForm) {
schema = {}
const params = table.parameters || []
params.forEach(param => {
schema[param.name] = { ...param, type: "string" }
})
} else { } else {
schema = cloneDeep(table.schema) schema = cloneDeep(table.schema)
} }
@ -199,6 +225,32 @@ export const getSchemaForDatasource = datasource => {
return { schema, table } return { schema, table }
} }
/**
* Builds a form schema given a form component.
* A form schema is a schema of all the fields nested anywhere within a form.
*/
const buildFormSchema = component => {
let schema = {}
if (!component) {
return schema
}
const def = store.actions.components.getDefinition(component._component)
const fieldSetting = def?.settings?.find(
setting => setting.key === "field" && setting.type.startsWith("field/")
)
if (fieldSetting && component.field) {
const type = fieldSetting.type.split("field/")[1]
if (type) {
schema[component.field] = { name: component.field, type }
}
}
component._children?.forEach(child => {
const childSchema = buildFormSchema(child)
schema = { ...schema, ...childSchema }
})
return schema
}
/** /**
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding. * utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
*/ */

View File

@ -416,7 +416,14 @@ export const getFrontendStore = () => {
if (cut) { if (cut) {
state.componentToPaste = null state.componentToPaste = null
} else { } else {
componentToPaste._id = uuid() const randomizeIds = component => {
if (!component) {
return
}
component._id = uuid()
component._children?.forEach(randomizeIds)
}
randomizeIds(componentToPaste)
} }
if (mode === "inside") { if (mode === "inside") {

View File

@ -35,7 +35,7 @@ const createScreen = table => {
const form = makeMainForm() const form = makeMainForm()
.instanceName("Form") .instanceName("Form")
.customProps({ .customProps({
theme: "spectrum--light", theme: "spectrum--lightest",
size: "spectrum--medium", size: "spectrum--medium",
datasource: { datasource: {
label: table.name, label: table.name,

View File

@ -59,6 +59,7 @@ function generateTitleContainer(table, title, formId) {
onClick: [ onClick: [
{ {
parameters: { parameters: {
providerId: formId,
rowId: `{{ ${makePropSafe(formId)}._id }}`, rowId: `{{ ${makePropSafe(formId)}._id }}`,
revId: `{{ ${makePropSafe(formId)}._rev }}`, revId: `{{ ${makePropSafe(formId)}._rev }}`,
tableId: table._id, tableId: table._id,
@ -90,7 +91,7 @@ const createScreen = table => {
const form = makeMainForm() const form = makeMainForm()
.instanceName("Form") .instanceName("Form")
.customProps({ .customProps({
theme: "spectrum--light", theme: "spectrum--lightest",
size: "spectrum--medium", size: "spectrum--medium",
datasource: { datasource: {
label: table.name, label: table.name,

View File

@ -169,7 +169,11 @@ export function makeTableFormComponents(tableId) {
export function makeQueryFormComponents(queryId) { export function makeQueryFormComponents(queryId) {
const queries = get(backendUiStore).queries const queries = get(backendUiStore).queries
const schema = queries.find(query => query._id === queryId)?.schema ?? [] const params = queries.find(query => query._id === queryId)?.parameters ?? []
let schema = {}
params.forEach(param => {
schema[param.name] = { ...param, type: "string" }
})
return makeSchemaFormComponents(schema) return makeSchemaFormComponents(schema)
} }

View File

@ -64,7 +64,11 @@
{:else if value.customType === 'password'} {:else if value.customType === 'password'}
<Input type="password" extraThin bind:value={block.inputs[key]} /> <Input type="password" extraThin bind:value={block.inputs[key]} />
{:else if value.customType === 'email'} {:else if value.customType === 'email'}
<Input type="email" extraThin bind:value={block.inputs[key]} /> <BindableInput
type={'email'}
extraThin
bind:value={block.inputs[key]}
{bindings} />
{:else if value.customType === 'table'} {:else if value.customType === 'table'}
<TableSelector bind:value={block.inputs[key]} /> <TableSelector bind:value={block.inputs[key]} />
{:else if value.customType === 'row'} {:else if value.customType === 'row'}
@ -75,7 +79,7 @@
<SchemaSetup bind:value={block.inputs[key]} /> <SchemaSetup bind:value={block.inputs[key]} />
{:else if value.type === 'string' || value.type === 'number'} {:else if value.type === 'string' || value.type === 'number'}
<BindableInput <BindableInput
type="string" type={value.customType}
extraThin extraThin
bind:value={block.inputs[key]} bind:value={block.inputs[key]}
{bindings} /> {bindings} />

View File

@ -20,13 +20,12 @@
let exportFormat = FORMATS[0].key let exportFormat = FORMATS[0].key
async function exportView() { async function exportView() {
const response = await api.post( download(
`/api/views/export?format=${exportFormat}`, `/api/views/export?view=${encodeURIComponent(
view view.name
)}&format=${exportFormat}`
) )
const downloadInfo = await response.json()
onClosed() onClosed()
window.location = downloadInfo.url
} }
</script> </script>

View File

@ -19,6 +19,7 @@
let drawer let drawer
export let value = {} export let value = {}
export let otherSources
$: tables = $backendUiStore.tables.map(m => ({ $: tables = $backendUiStore.tables.map(m => ({
label: m.name, label: m.name,
@ -88,7 +89,7 @@
class="dropdownbutton" class="dropdownbutton"
bind:this={anchorRight} bind:this={anchorRight}
on:click={dropdownRight.show}> on:click={dropdownRight.show}>
<span>{value?.label ? value.label : 'Choose option'}</span> <span>{value?.label ?? 'Choose option'}</span>
<Icon name="arrowdown" /> <Icon name="arrowdown" />
</div> </div>
{#if value?.type === 'query'} {#if value?.type === 'query'}
@ -175,6 +176,22 @@
</li> </li>
{/each} {/each}
</ul> </ul>
{#if otherSources?.length}
<hr />
<div class="title">
<Heading extraSmall>Other</Heading>
</div>
<ul>
{#each otherSources as source}
<li
class:selected={value === source}
on:click={() => handleSelected(source)}>
{source.label}
</li>
{/each}
</ul>
{/if}
</div> </div>
</DropdownMenu> </DropdownMenu>

View File

@ -15,8 +15,9 @@
) )
$: { $: {
// Automatically set rev and table ID based on row ID // Automatically set rev and table ID based on row ID
if (parameters.rowId) { if (parameters.providerId) {
parameters.revId = parameters.rowId.replace("_id", "_rev") parameters.rowId = `{{ ${parameters.providerId}._id }}`
parameters.revId = `{{ ${parameters.providerId}._rev }}`
const providerComponent = dataProviderComponents.find( const providerComponent = dataProviderComponents.find(
provider => provider._id === parameters.providerId provider => provider._id === parameters.providerId
) )
@ -37,12 +38,10 @@
</div> </div>
{:else} {:else}
<Label size="m" color="dark">Datasource</Label> <Label size="m" color="dark">Datasource</Label>
<Select secondary bind:value={parameters.rowId}> <Select secondary bind:value={parameters.providerId}>
<option value="" /> <option value="" />
{#each dataProviderComponents as provider} {#each dataProviderComponents as provider}
<option value={`{{ ${provider._id}._id }}`}> <option value={provider._id}>{provider._instanceName}</option>
{provider._instanceName}
</option>
{/each} {/each}
</Select> </Select>
{/if} {/if}

View File

@ -0,0 +1,37 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore"
import { getDataProviderComponents } from "builderStore/dataBinding"
export let parameters
$: dataProviders = getDataProviderComponents(
$currentAsset.props,
$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

@ -4,6 +4,7 @@ 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
@ -35,4 +36,8 @@ export default [
name: "Validate Form", name: "Validate Form",
component: ValidateForm, component: ValidateForm,
}, },
{
name: "Refresh Datasource",
component: RefreshDatasource,
},
] ]

View File

@ -1,5 +1,5 @@
<script> <script>
import OptionSelect from "./OptionSelect.svelte" import { DataList } from "@budibase/bbui"
import { import {
getDatasourceForProvider, getDatasourceForProvider,
getSchemaForDatasource, getSchemaForDatasource,
@ -18,7 +18,7 @@
component => component._component === "@budibase/standard-components/form" component => component._component === "@budibase/standard-components/form"
) )
$: datasource = getDatasourceForProvider(form) $: datasource = getDatasourceForProvider(form)
$: schema = getSchemaForDatasource(datasource).schema $: schema = getSchemaForDatasource(datasource, true).schema
$: options = getOptions(schema, type) $: options = getOptions(schema, type)
const getOptions = (schema, fieldType) => { const getOptions = (schema, fieldType) => {
@ -28,6 +28,32 @@
} }
return entries.map(entry => entry[0]) return entries.map(entry => entry[0])
} }
const handleBlur = () => onChange(value)
</script> </script>
<OptionSelect {value} {onChange} {options} /> <div>
<DataList
editable
secondary
extraThin
on:blur={handleBlur}
on:change
bind:value>
<option value="" />
{#each options as option}
<option value={option}>{option}</option>
{/each}
</DataList>
</div>
<style>
div {
flex: 1 1 auto;
display: flex;
flex-direction: row;
}
div :global(> div) {
flex: 1 1 auto;
}
</style>

View File

@ -0,0 +1,5 @@
<script>
import FieldSelect from "./FieldSelect.svelte"
</script>
<FieldSelect {...$$props} multiselect />

View File

@ -1,5 +0,0 @@
<script>
import TableViewFieldSelect from "./TableViewFieldSelect.svelte"
</script>
<TableViewFieldSelect {...$$props} multiselect />

View File

@ -138,7 +138,7 @@
align-items: center; align-items: center;
display: flex; display: flex;
box-sizing: border-box; box-sizing: border-box;
padding-left: var(--spacing-xs); padding-left: 7px;
border-left: 1px solid var(--grey-4); border-left: 1px solid var(--grey-4);
background-color: var(--grey-2); background-color: var(--grey-2);
border-top-right-radius: var(--border-radius-m); border-top-right-radius: var(--border-radius-m);

View File

@ -25,7 +25,7 @@
<DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin> <DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin>
{#if open} {#if open}
<div> <div>
{#each properties as prop} {#each properties as prop (`${componentInstance._id}-${prop.key}`)}
<PropertyControl <PropertyControl
bindable={false} bindable={false}
label={`${prop.label}${hasPropChanged(style, prop) ? ' *' : ''}`} label={`${prop.label}${hasPropChanged(style, prop) ? ' *' : ''}`}

View File

@ -0,0 +1,7 @@
<script>
import DatasourceSelect from "./DatasourceSelect.svelte"
const otherSources = [{ name: "Custom", label: "Custom" }]
</script>
<DatasourceSelect on:change {...$$props} {otherSources} />

View File

@ -11,11 +11,12 @@
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte" import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
import RoleSelect from "./PropertyControls/RoleSelect.svelte" import RoleSelect from "./PropertyControls/RoleSelect.svelte"
import OptionSelect from "./PropertyControls/OptionSelect.svelte" import OptionSelect from "./PropertyControls/OptionSelect.svelte"
import MultiTableViewFieldSelect from "./PropertyControls/MultiTableViewFieldSelect.svelte"
import Checkbox from "./PropertyControls/Checkbox.svelte" import Checkbox from "./PropertyControls/Checkbox.svelte"
import TableSelect from "./PropertyControls/TableSelect.svelte" import TableSelect from "./PropertyControls/TableSelect.svelte"
import TableViewSelect from "./PropertyControls/TableViewSelect.svelte" import DatasourceSelect from "./PropertyControls/DatasourceSelect.svelte"
import TableViewFieldSelect from "./PropertyControls/TableViewFieldSelect.svelte" import FieldSelect from "./PropertyControls/FieldSelect.svelte"
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
import EventsEditor from "./PropertyControls/EventsEditor" import EventsEditor from "./PropertyControls/EventsEditor"
import ScreenSelect from "./PropertyControls/ScreenSelect.svelte" import ScreenSelect from "./PropertyControls/ScreenSelect.svelte"
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte" import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
@ -60,7 +61,7 @@
const controlMap = { const controlMap = {
text: Input, text: Input,
select: OptionSelect, select: OptionSelect,
datasource: TableViewSelect, datasource: DatasourceSelect,
screen: ScreenSelect, screen: ScreenSelect,
detailScreen: DetailScreenSelect, detailScreen: DetailScreenSelect,
boolean: Checkbox, boolean: Checkbox,
@ -69,8 +70,9 @@
table: TableSelect, table: TableSelect,
color: ColorPicker, color: ColorPicker,
icon: IconSelect, icon: IconSelect,
field: TableViewFieldSelect, field: FieldSelect,
multifield: MultiTableViewFieldSelect, multifield: MultiFieldSelect,
schema: SchemaSelect,
"field/string": StringFieldSelect, "field/string": StringFieldSelect,
"field/number": NumberFieldSelect, "field/number": NumberFieldSelect,
"field/options": OptionsFieldSelect, "field/options": OptionsFieldSelect,
@ -78,7 +80,7 @@
"field/longform": LongFormFieldSelect, "field/longform": LongFormFieldSelect,
"field/datetime": DateTimeFieldSelect, "field/datetime": DateTimeFieldSelect,
"field/attachment": AttachmentFieldSelect, "field/attachment": AttachmentFieldSelect,
"field/relationship": RelationshipFieldSelect, "field/link": RelationshipFieldSelect,
} }
const getControl = type => { const getControl = type => {

View File

@ -1,6 +1,7 @@
<script> <script>
import { Popover } from "@budibase/bbui" import { Popover } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import { onMount } from "svelte"
import FeedbackIframe from "./FeedbackIframe.svelte" import FeedbackIframe from "./FeedbackIframe.svelte"
import analytics from "analytics" import analytics from "analytics"
@ -10,9 +11,15 @@
let iconContainer let iconContainer
let popover let popover
setInterval(() => { onMount(() => {
$store.highlightFeedbackIcon = analytics.highlightFeedbackIcon() const interval = setInterval(() => {
}, FIVE_MINUTES) store.update(state => {
state.highlightFeedbackIcon = analytics.highlightFeedbackIcon()
return state
})
}, FIVE_MINUTES)
return () => clearInterval(interval)
})
</script> </script>
<div class="container" bind:this={iconContainer} on:click={popover.show}> <div class="container" bind:this={iconContainer} on:click={popover.show}>

View File

@ -132,7 +132,8 @@
<header> <header>
<div class="input"> <div class="input">
<Input placeholder="✎ Edit Query Name" bind:value={query.name} /> <div class="label">Enter query name:</div>
<Input outline border bind:value={query.name} />
</div> </div>
{#if config} {#if config}
<div class="props"> <div class="props">
@ -216,7 +217,9 @@
<style> <style>
.input { .input {
width: 300px; width: 500px;
display: flex;
align-items: center;
} }
.select { .select {
@ -288,4 +291,12 @@
margin-top: -28px; margin-top: -28px;
z-index: -2; z-index: -2;
} }
.label {
font-family: var(--font-sans);
color: var(--grey-8);
font-size: var(--font-size-s);
margin-right: 8px;
font-weight: 600;
}
</style> </style>

View File

@ -14,9 +14,12 @@
async function exportApp() { async function exportApp() {
appExportLoading = true appExportLoading = true
try { try {
download(`/api/backups/export?appId=${_id}`) download(
`/api/backups/export?appId=${_id}&appname=${encodeURIComponent(name)}`
)
notifier.success("App Export Complete.") notifier.success("App Export Complete.")
} catch (err) { } catch (err) {
console.error(err)
notifier.danger("App Export Failed.") notifier.danger("App Export Failed.")
} finally { } finally {
appExportLoading = false appExportLoading = false
@ -29,13 +32,13 @@
<Spacer medium /> <Spacer medium />
<div class="card-footer"> <div class="card-footer">
<TextButton text medium blue href="/_builder/{_id}"> <TextButton text medium blue href="/_builder/{_id}">
Open Open {name}
{name}
</TextButton> </TextButton>
{#if appExportLoading} {#if appExportLoading}
<Spinner size="10" /> <Spinner size="10" />
{:else}<i class="ri-folder-download-line" on:click={exportApp} />{/if} {:else}
<i class="ri-folder-download-line" on:click={exportApp} />
{/if}
</div> </div>
</div> </div>

View File

@ -854,7 +854,7 @@
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
turndown "^7.0.0" turndown "^7.0.0"
"@budibase/colorpicker@^1.0.1": "@budibase/colorpicker@1.0.1":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@budibase/colorpicker/-/colorpicker-1.0.1.tgz#940c180e7ebba0cb0756c4c8ef13f5dfab58e810" resolved "https://registry.yarnpkg.com/@budibase/colorpicker/-/colorpicker-1.0.1.tgz#940c180e7ebba0cb0756c4c8ef13f5dfab58e810"
integrity sha512-+DTHYhU0sTi5RfCyd7AAvMsLFwyF/wgs0owf7KyQU+ZILRW+YsWa7OQMz+hKQfgVAmvzwrNz8ATiBlG3Ac6Asg== integrity sha512-+DTHYhU0sTi5RfCyd7AAvMsLFwyF/wgs0owf7KyQU+ZILRW+YsWa7OQMz+hKQfgVAmvzwrNz8ATiBlG3Ac6Asg==
@ -1257,7 +1257,7 @@
node-fetch "^2.6.0" node-fetch "^2.6.0"
utf-8-validate "^5.0.2" utf-8-validate "^5.0.2"
"@svelteschool/svelte-forms@^0.7.0": "@svelteschool/svelte-forms@0.7.0":
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/@svelteschool/svelte-forms/-/svelte-forms-0.7.0.tgz#4ecba15e9a9ab2b04fad3d892931a561118a4cea" resolved "https://registry.yarnpkg.com/@svelteschool/svelte-forms/-/svelte-forms-0.7.0.tgz#4ecba15e9a9ab2b04fad3d892931a561118a4cea"
integrity sha512-TSt8ROqK6wq+Hav7EhZL1I0GtsZhg28aJuuDSviBzG/NG9pC0eprf8roWjl59DKHOVWIUTPTeY+T+lipb9gf8w== integrity sha512-TSt8ROqK6wq+Hav7EhZL1I0GtsZhg28aJuuDSviBzG/NG9pC0eprf8roWjl59DKHOVWIUTPTeY+T+lipb9gf8w==
@ -1775,11 +1775,6 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
base-64@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
integrity sha1-eAqZyE59YAJgNhURxId2E78k9rs=
base@^0.11.1: base@^0.11.1:
version "0.11.2" version "0.11.2"
resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
@ -1870,15 +1865,6 @@ braces@^3.0.1, braces@~3.0.2:
dependencies: dependencies:
fill-range "^7.0.1" fill-range "^7.0.1"
britecharts@^2.16.0:
version "2.17.2"
resolved "https://registry.yarnpkg.com/britecharts/-/britecharts-2.17.2.tgz#78e7743e7c1dcaccd78ab7dacc479d37d509cdf2"
integrity sha512-+wMG/ci+UHPRIySppTs8wQZmmlYFQHn2bCvbNiWUOYd1qAoiEQyKA/dVtgdTyR09qM+h8b9YsFofaWHJRT1mQg==
dependencies:
base-64 "^0.1.0"
d3 "^5.16.0"
lodash.assign "^4.2.0"
brorand@^1.0.1: brorand@^1.0.1:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
@ -2282,16 +2268,16 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies: dependencies:
delayed-stream "~1.0.0" delayed-stream "~1.0.0"
commander@2, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@2.17.x: commander@2.17.x:
version "2.17.1" version "2.17.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^5.0.0, commander@^5.1.0: commander@^5.0.0, commander@^5.1.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
@ -2536,254 +2522,11 @@ cypress@^5.1.0:
url "^0.11.0" url "^0.11.0"
yauzl "^2.10.0" yauzl "^2.10.0"
d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0: d3-selection@^1.4.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
d3-axis@1:
version "1.0.12"
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9"
integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==
d3-brush@1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.1.6.tgz#b0a22c7372cabec128bdddf9bddc058592f89e9b"
integrity sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==
dependencies:
d3-dispatch "1"
d3-drag "1"
d3-interpolate "1"
d3-selection "1"
d3-transition "1"
d3-chord@1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.6.tgz#309157e3f2db2c752f0280fedd35f2067ccbb15f"
integrity sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==
dependencies:
d3-array "1"
d3-path "1"
d3-collection@1:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
d3-color@1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a"
integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==
d3-contour@1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3"
integrity sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==
dependencies:
d3-array "^1.1.1"
d3-dispatch@1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58"
integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==
d3-drag@1:
version "1.2.5"
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.5.tgz#2537f451acd39d31406677b7dc77c82f7d988f70"
integrity sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==
dependencies:
d3-dispatch "1"
d3-selection "1"
d3-dsv@1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.2.0.tgz#9d5f75c3a5f8abd611f74d3f5847b0d4338b885c"
integrity sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==
dependencies:
commander "2"
iconv-lite "0.4"
rw "1"
d3-ease@1:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2"
integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==
d3-fetch@1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.2.0.tgz#15ce2ecfc41b092b1db50abd2c552c2316cf7fc7"
integrity sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==
dependencies:
d3-dsv "1"
d3-force@1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.2.1.tgz#fd29a5d1ff181c9e7f0669e4bd72bdb0e914ec0b"
integrity sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==
dependencies:
d3-collection "1"
d3-dispatch "1"
d3-quadtree "1"
d3-timer "1"
d3-format@1:
version "1.4.5"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4"
integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==
d3-geo@1:
version "1.12.1"
resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.12.1.tgz#7fc2ab7414b72e59fbcbd603e80d9adc029b035f"
integrity sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==
dependencies:
d3-array "1"
d3-hierarchy@1:
version "1.1.9"
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83"
integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==
d3-interpolate@1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987"
integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==
dependencies:
d3-color "1"
d3-path@1:
version "1.0.9"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
d3-polygon@1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.6.tgz#0bf8cb8180a6dc107f518ddf7975e12abbfbd38e"
integrity sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==
d3-quadtree@1:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz#ca8b84df7bb53763fe3c2f24bd435137f4e53135"
integrity sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==
d3-random@1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.2.tgz#2833be7c124360bf9e2d3fd4f33847cfe6cab291"
integrity sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==
d3-scale-chromatic@1:
version "1.5.0"
resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98"
integrity sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==
dependencies:
d3-color "1"
d3-interpolate "1"
d3-scale@2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f"
integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==
dependencies:
d3-array "^1.2.0"
d3-collection "1"
d3-format "1"
d3-interpolate "1"
d3-time "1"
d3-time-format "2"
d3-selection@1, d3-selection@^1.1.0, d3-selection@^1.4.1:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.2.tgz#dcaa49522c0dbf32d6c1858afc26b6094555bc5c" resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.2.tgz#dcaa49522c0dbf32d6c1858afc26b6094555bc5c"
integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg== integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==
d3-shape@1:
version "1.3.7"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==
dependencies:
d3-path "1"
d3-time-format@2:
version "2.3.0"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850"
integrity sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==
dependencies:
d3-time "1"
d3-time@1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1"
integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==
d3-timer@1:
version "1.0.10"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5"
integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==
d3-transition@1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398"
integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==
dependencies:
d3-color "1"
d3-dispatch "1"
d3-ease "1"
d3-interpolate "1"
d3-selection "^1.1.0"
d3-timer "1"
d3-voronoi@1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297"
integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==
d3-zoom@1:
version "1.8.3"
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.8.3.tgz#b6a3dbe738c7763121cd05b8a7795ffe17f4fc0a"
integrity sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==
dependencies:
d3-dispatch "1"
d3-drag "1"
d3-interpolate "1"
d3-selection "1"
d3-transition "1"
d3@^5.16.0:
version "5.16.0"
resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877"
integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==
dependencies:
d3-array "1"
d3-axis "1"
d3-brush "1"
d3-chord "1"
d3-collection "1"
d3-color "1"
d3-contour "1"
d3-dispatch "1"
d3-drag "1"
d3-dsv "1"
d3-ease "1"
d3-fetch "1"
d3-force "1"
d3-format "1"
d3-geo "1"
d3-hierarchy "1"
d3-interpolate "1"
d3-path "1"
d3-polygon "1"
d3-quadtree "1"
d3-random "1"
d3-scale "2"
d3-scale-chromatic "1"
d3-selection "1"
d3-shape "1"
d3-time "1"
d3-time-format "2"
d3-timer "1"
d3-transition "1"
d3-voronoi "1"
d3-zoom "1"
dashdash@^1.12.0: dashdash@^1.12.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@ -2995,7 +2738,7 @@ dotenv@^8.2.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
downloadjs@^1.4.7: downloadjs@1.4.7:
version "1.4.7" version "1.4.7"
resolved "https://registry.yarnpkg.com/downloadjs/-/downloadjs-1.4.7.tgz#f69f96f940e0d0553dac291139865a3cd0101e3c" resolved "https://registry.yarnpkg.com/downloadjs/-/downloadjs-1.4.7.tgz#f69f96f940e0d0553dac291139865a3cd0101e3c"
integrity sha1-9p+W+UDg0FU9rCkROYZaPNAQHjw= integrity sha1-9p+W+UDg0FU9rCkROYZaPNAQHjw=
@ -3842,7 +3585,7 @@ human-signals@^1.1.1:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
iconv-lite@0.4, iconv-lite@0.4.24: iconv-lite@0.4.24:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@ -5047,11 +4790,6 @@ lodash-es@^4.17.11:
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
lodash.assign@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=
lodash.once@^4.1.1: lodash.once@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
@ -5062,7 +4800,12 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19: lodash@4.17.13:
version "4.17.13"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93"
integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA==
lodash@^4.17.15, lodash@^4.17.19:
version "4.17.20" version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
@ -6135,7 +5878,7 @@ relateurl@0.2.x:
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
remixicon@^2.5.0: remixicon@2.5.0:
version "2.5.0" version "2.5.0"
resolved "https://registry.yarnpkg.com/remixicon/-/remixicon-2.5.0.tgz#b5e245894a1550aa23793f95daceadbf96ad1a41" resolved "https://registry.yarnpkg.com/remixicon/-/remixicon-2.5.0.tgz#b5e245894a1550aa23793f95daceadbf96ad1a41"
integrity sha512-q54ra2QutYDZpuSnFjmeagmEiN9IMo56/zz5dDNitzKD23oFRw77cWo4TsrAdmdkPiEn8mxlrTqxnkujDbEGww== integrity sha512-q54ra2QutYDZpuSnFjmeagmEiN9IMo56/zz5dDNitzKD23oFRw77cWo4TsrAdmdkPiEn8mxlrTqxnkujDbEGww==
@ -6436,11 +6179,6 @@ run-parallel@^1.1.9:
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef"
integrity sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw== integrity sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==
rw@1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=
rxjs@^6.3.3, rxjs@^6.5.5: rxjs@^6.3.3, rxjs@^6.5.5:
version "6.6.3" version "6.6.3"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552"
@ -6583,10 +6321,10 @@ shellwords@^0.1.1:
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
shortid@^2.2.15: shortid@2.2.15:
version "2.2.16" version "2.2.15"
resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.16.tgz#b742b8f0cb96406fd391c76bfc18a67a57fe5608" resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.15.tgz#2b902eaa93a69b11120373cd42a1f1fe4437c122"
integrity sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g== integrity sha512-5EaCy2mx2Jgc/Fdn9uuDuNIIfWBpzY4XIlhoqtXF6qsf+/+SGZ+FxDdX/ZsMZiWupIWNqAEmiNY4RC+LSmCeOw==
dependencies: dependencies:
nanoid "^2.1.0" nanoid "^2.1.0"
@ -6996,7 +6734,7 @@ svelte-loading-spinners@^0.1.1:
resolved "https://registry.yarnpkg.com/svelte-loading-spinners/-/svelte-loading-spinners-0.1.1.tgz#a35a811b7db0389ec2a5de6904c718c58c36e1f9" resolved "https://registry.yarnpkg.com/svelte-loading-spinners/-/svelte-loading-spinners-0.1.1.tgz#a35a811b7db0389ec2a5de6904c718c58c36e1f9"
integrity sha512-or4zs10VOdczOJo3u25IINXQOkZbLNAxMrXK0PRbzVoJtPQq/QZPNxI32383bpe+soYcEKmESbmW+JlW3MbUKQ== integrity sha512-or4zs10VOdczOJo3u25IINXQOkZbLNAxMrXK0PRbzVoJtPQq/QZPNxI32383bpe+soYcEKmESbmW+JlW3MbUKQ==
svelte-portal@^0.1.0: svelte-portal@0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-0.1.0.tgz#cc2821cc84b05ed5814e0218dcdfcbebc53c1742" resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-0.1.0.tgz#cc2821cc84b05ed5814e0218dcdfcbebc53c1742"
integrity sha512-kef+ksXVKun224mRxat+DdO4C+cGHla+fEcZfnBAvoZocwiaceOfhf5azHYOPXSSB1igWVFTEOF3CDENPnuWxg== integrity sha512-kef+ksXVKun224mRxat+DdO4C+cGHla+fEcZfnBAvoZocwiaceOfhf5azHYOPXSSB1igWVFTEOF3CDENPnuWxg==
@ -7317,16 +7055,16 @@ util.promisify@^1.0.0:
has-symbols "^1.0.1" has-symbols "^1.0.1"
object.getownpropertydescriptors "^2.1.0" object.getownpropertydescriptors "^2.1.0"
uuid@8.3.1:
version "8.3.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==
uuid@^3.3.2: uuid@^3.3.2:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.3.1:
version "8.3.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==
validate-npm-package-license@^3.0.1: validate-npm-package-license@^3.0.1:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@ -7586,10 +7324,10 @@ yauzl@^2.10.0:
buffer-crc32 "~0.2.3" buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0" fd-slicer "~1.1.0"
yup@^0.29.2: yup@0.29.2:
version "0.29.3" version "0.29.2"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.3.tgz#69a30fd3f1c19f5d9e31b1cf1c2b851ce8045fea" resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.2.tgz#5302abd9024cca335b987793f8df868e410b7b67"
integrity sha512-RNUGiZ/sQ37CkhzKFoedkeMfJM0vNQyaz+wRZJzxdKE7VfDeVKH8bb4rr7XhRLbHJz5hSjoDNwMEIaKhuMZ8gQ== integrity sha512-FbAAeopli+TnpZ8Lzv2M72wltLw58iWBT7wW8FuAPFPb3CelXmSKCXQbV1o4keywpIK1BZ0ULTLv2s3w1CfOwA==
dependencies: dependencies:
"@babel/runtime" "^7.10.5" "@babel/runtime" "^7.10.5"
fn-name "~3.0.0" fn-name "~3.0.0"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "0.7.1", "version": "0.7.4",
"license": "MPL-2.0", "license": "MPL-2.0",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
@ -9,14 +9,14 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/string-templates": "^0.7.1", "@budibase/string-templates": "^0.7.4",
"deep-equal": "^2.0.1", "deep-equal": "^2.0.1",
"regexparam": "^1.3.0", "regexparam": "^1.3.0",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5" "svelte-spa-router": "^3.0.5"
}, },
"devDependencies": { "devDependencies": {
"@budibase/standard-components": "^0.7.1", "@budibase/standard-components": "^0.7.4",
"@rollup/plugin-commonjs": "^16.0.0", "@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-node-resolve": "^10.0.0", "@rollup/plugin-node-resolve": "^10.0.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
@ -30,5 +30,5 @@
"svelte": "^3.30.0", "svelte": "^3.30.0",
"svelte-jester": "^1.0.6" "svelte-jester": "^1.0.6"
}, },
"gitHead": "62ebf3cedcd7e9b2494b4f8cbcfb90927609b491" "gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd"
} }

View File

@ -1,4 +1,4 @@
import { notificationStore } from "../store/notification" import { notificationStore, datasourceStore } from "../store"
import API from "./api" import API from "./api"
import { fetchTableDefinition } from "./tables" import { fetchTableDefinition } from "./tables"
@ -6,6 +6,9 @@ import { fetchTableDefinition } from "./tables"
* Fetches data about a certain row in a table. * Fetches data about a certain row in a table.
*/ */
export const fetchRow = async ({ tableId, rowId }) => { export const fetchRow = async ({ tableId, rowId }) => {
if (!tableId || !rowId) {
return
}
const row = await API.get({ const row = await API.get({
url: `/api/${tableId}/rows/${rowId}`, url: `/api/${tableId}/rows/${rowId}`,
}) })
@ -16,6 +19,9 @@ export const fetchRow = async ({ tableId, rowId }) => {
* Creates a row in a table. * Creates a row in a table.
*/ */
export const saveRow = async row => { export const saveRow = async row => {
if (!row?.tableId) {
return
}
const res = await API.post({ const res = await API.post({
url: `/api/${row.tableId}/rows`, url: `/api/${row.tableId}/rows`,
body: row, body: row,
@ -23,6 +29,10 @@ export const saveRow = async row => {
res.error res.error
? notificationStore.danger("An error has occurred") ? notificationStore.danger("An error has occurred")
: notificationStore.success("Row saved") : notificationStore.success("Row saved")
// Refresh related datasources
datasourceStore.actions.invalidateDatasource(row.tableId)
return res return res
} }
@ -30,6 +40,9 @@ export const saveRow = async row => {
* Updates a row in a table. * Updates a row in a table.
*/ */
export const updateRow = async row => { export const updateRow = async row => {
if (!row?.tableId || !row?._id) {
return
}
const res = await API.patch({ const res = await API.patch({
url: `/api/${row.tableId}/rows/${row._id}`, url: `/api/${row.tableId}/rows/${row._id}`,
body: row, body: row,
@ -37,6 +50,10 @@ export const updateRow = async row => {
res.error res.error
? notificationStore.danger("An error has occurred") ? notificationStore.danger("An error has occurred")
: notificationStore.success("Row updated") : notificationStore.success("Row updated")
// Refresh related datasources
datasourceStore.actions.invalidateDatasource(row.tableId)
return res return res
} }
@ -44,12 +61,19 @@ export const updateRow = async row => {
* Deletes a row from a table. * Deletes a row from a table.
*/ */
export const deleteRow = async ({ tableId, rowId, revId }) => { export const deleteRow = async ({ tableId, rowId, revId }) => {
if (!tableId || !rowId || !revId) {
return
}
const res = await API.del({ const res = await API.del({
url: `/api/${tableId}/rows/${rowId}/${revId}`, url: `/api/${tableId}/rows/${rowId}/${revId}`,
}) })
res.error res.error
? notificationStore.danger("An error has occurred") ? notificationStore.danger("An error has occurred")
: notificationStore.success("Row deleted") : notificationStore.success("Row deleted")
// Refresh related datasources
datasourceStore.actions.invalidateDatasource(tableId)
return res return res
} }
@ -57,6 +81,9 @@ export const deleteRow = async ({ tableId, rowId, revId }) => {
* Deletes many rows from a table. * Deletes many rows from a table.
*/ */
export const deleteRows = async ({ tableId, rows }) => { export const deleteRows = async ({ tableId, rows }) => {
if (!tableId || !rows) {
return
}
const res = await API.post({ const res = await API.post({
url: `/api/${tableId}/rows`, url: `/api/${tableId}/rows`,
body: { body: {
@ -67,6 +94,10 @@ export const deleteRows = async ({ tableId, rows }) => {
res.error res.error
? notificationStore.danger("An error has occurred") ? notificationStore.danger("An error has occurred")
: notificationStore.success(`${rows.length} row(s) deleted`) : notificationStore.success(`${rows.length} row(s) deleted`)
// Refresh related datasources
datasourceStore.actions.invalidateDatasource(tableId)
return res return res
} }

View File

@ -3,6 +3,7 @@
import { setContext, onMount } from "svelte" import { setContext, onMount } from "svelte"
import Component from "./Component.svelte" import Component from "./Component.svelte"
import NotificationDisplay from "./NotificationDisplay.svelte" import NotificationDisplay from "./NotificationDisplay.svelte"
import Provider from "./Provider.svelte"
import SDK from "../sdk" import SDK from "../sdk"
import { import {
createContextStore, createContextStore,
@ -27,6 +28,8 @@
</script> </script>
{#if loaded && $screenStore.activeLayout} {#if loaded && $screenStore.activeLayout}
<Component definition={$screenStore.activeLayout.props} /> <Provider key="user" data={$authStore}>
<NotificationDisplay /> <Component definition={$screenStore.activeLayout.props} />
<NotificationDisplay />
</Provider>
{/if} {/if}

View File

@ -4,7 +4,7 @@
import * as ComponentLibrary from "@budibase/standard-components" import * as ComponentLibrary from "@budibase/standard-components"
import Router from "./Router.svelte" import Router from "./Router.svelte"
import { enrichProps, propsAreSame } from "../utils/componentProps" import { enrichProps, propsAreSame } from "../utils/componentProps"
import { authStore, builderStore } from "../store" import { builderStore } from "../store"
import { hashString } from "../utils/hash" import { hashString } from "../utils/hash"
export let definition = {} export let definition = {}
@ -32,7 +32,7 @@
$: constructor = getComponentConstructor(definition._component) $: constructor = getComponentConstructor(definition._component)
$: children = definition._children || [] $: children = definition._children || []
$: id = definition._id $: id = definition._id
$: updateComponentProps(definition, $context, $authStore) $: updateComponentProps(definition, $context)
$: styles = definition._styles $: styles = definition._styles
// Update component context // Update component context
@ -53,13 +53,13 @@
} }
// Enriches any string component props using handlebars // Enriches any string component props using handlebars
const updateComponentProps = async (definition, context, user) => { const updateComponentProps = async (definition, context) => {
// Record the timestamp so we can reference it after enrichment // Record the timestamp so we can reference it after enrichment
latestUpdateTime = Date.now() latestUpdateTime = Date.now()
const enrichmentTime = latestUpdateTime const enrichmentTime = latestUpdateTime
// Enrich props with context // Enrich props with context
const enrichedProps = await enrichProps(definition, context, user) const enrichedProps = await enrichProps(definition, context)
// Abandon this update if a newer update has started // Abandon this update if a newer update has started
if (enrichmentTime !== latestUpdateTime) { if (enrichmentTime !== latestUpdateTime) {

View File

@ -1,29 +1,54 @@
<script> <script>
import { getContext, setContext } from "svelte" import { getContext, setContext, onMount } from "svelte"
import { createContextStore } from "../store" import { datasourceStore, createContextStore } from "../store"
import { ActionTypes } from "../constants"
import { generate } from "shortid"
export let data export let data
export let actions export let actions
export let key
// Clone and create new data context for this component tree // Clone and create new data context for this component tree
const context = getContext("context") const context = getContext("context")
const component = getContext("component") const component = getContext("component")
const newContext = createContextStore($context) const newContext = createContextStore($context)
setContext("context", newContext) setContext("context", newContext)
$: providerKey = key || $component.id
// Instance ID is unique to each instance of a provider
let instanceId
// Add data context // Add data context
$: { $: data !== undefined && newContext.actions.provideData(providerKey, data)
if (data !== undefined) {
newContext.actions.provideData($component.id, data)
}
}
// Add actions context // Add actions context
$: { $: {
actions?.forEach(({ type, callback }) => { if (instanceId) {
newContext.actions.provideAction($component.id, type, callback) actions?.forEach(({ type, callback, metadata }) => {
}) newContext.actions.provideAction(providerKey, type, callback)
// Register any "refresh datasource" actions with a singleton store
// so we can easily refresh data at all levels for any datasource
if (type === ActionTypes.RefreshDatasource) {
const { datasource } = metadata || {}
datasourceStore.actions.registerDatasource(
datasource,
instanceId,
callback
)
}
})
}
} }
onMount(() => {
// Generate a permanent unique ID for this component and use it to register
// any datasource actions
instanceId = generate()
// Unregister all datasource instances when unmounting this provider
return () => datasourceStore.actions.unregisterInstance(instanceId)
})
</script> </script>
<slot /> <slot />

View File

@ -4,4 +4,5 @@ export const TableNames = {
export const ActionTypes = { export const ActionTypes = {
ValidateForm: "ValidateForm", ValidateForm: "ValidateForm",
RefreshDatasource: "RefreshDatasource",
} }

View File

@ -4,26 +4,28 @@ export const createContextStore = existingContext => {
const store = writable({ ...existingContext }) const store = writable({ ...existingContext })
// Adds a data context layer to the tree // Adds a data context layer to the tree
const provideData = (componentId, data) => { const provideData = (providerId, data) => {
if (!providerId) {
return
}
store.update(state => { store.update(state => {
if (componentId) { state[providerId] = data
state[componentId] = data
// Keep track of the closest component ID so we can later hydrate a "data" prop. // Keep track of the closest component ID so we can later hydrate a "data" prop.
// This is only required for legacy bindings that used "data" rather than a // This is only required for legacy bindings that used "data" rather than a
// component ID. // component ID.
state.closestComponentId = componentId state.closestComponentId = providerId
}
return state return state
}) })
} }
// Adds an action context layer to the tree // Adds an action context layer to the tree
const provideAction = (componentId, actionType, callback) => { const provideAction = (providerId, actionType, callback) => {
if (!providerId || !actionType) {
return
}
store.update(state => { store.update(state => {
if (actionType && componentId) { state[`${providerId}_${actionType}`] = callback
state[`${componentId}_${actionType}`] = callback
}
return state return state
}) })
} }

View File

@ -0,0 +1,80 @@
import { writable, get } from "svelte/store"
export const createDatasourceStore = () => {
const store = writable([])
// Registers a new datasource instance
const registerDatasource = (datasource, instanceId, refresh) => {
if (!datasource || !instanceId || !refresh) {
return
}
// Create a list of all relevant datasource IDs which would require that
// this datasource is refreshed
let datasourceIds = []
// Extract table ID
if (datasource.type === "table") {
if (datasource.tableId) {
datasourceIds.push(datasource.tableId)
}
}
// Extract both table IDs from both sides of the relationship
else if (datasource.type === "link") {
if (datasource.rowTableId) {
datasourceIds.push(datasource.rowTableId)
}
if (datasource.tableId) {
datasourceIds.push(datasource.tableId)
}
}
// Extract the datasource ID (not the query ID) for queries
else if (datasource.type === "query") {
if (datasource.datasourceId) {
datasourceIds.push(datasource.datasourceId)
}
}
// Store configs for each relevant datasource ID
if (datasourceIds.length) {
store.update(state => {
datasourceIds.forEach(id => {
state.push({
datasourceId: id,
instanceId,
refresh,
})
})
return state
})
}
}
// Removes all registered datasource instances belonging to a particular
// instance ID
const unregisterInstance = instanceId => {
store.update(state => {
return state.filter(instance => instance.instanceId !== instanceId)
})
}
// Invalidates a specific datasource ID by refreshing all instances
// which depend on data from that datasource
const invalidateDatasource = datasourceId => {
const relatedInstances = get(store).filter(instance => {
return instance.datasourceId === datasourceId
})
relatedInstances?.forEach(instance => {
instance.refresh()
})
}
return {
subscribe: store.subscribe,
actions: { registerDatasource, unregisterInstance, invalidateDatasource },
}
}
export const datasourceStore = createDatasourceStore()

View File

@ -3,6 +3,7 @@ export { notificationStore } from "./notification"
export { routeStore } from "./routes" export { routeStore } from "./routes"
export { screenStore } from "./screens" export { screenStore } from "./screens"
export { builderStore } from "./builder" export { builderStore } from "./builder"
export { datasourceStore } from "./datasource"
// Context stores are layered and duplicated, so it is not a singleton // Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context" export { createContextStore } from "./context"

View File

@ -1,5 +1,4 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import { enrichDataBinding, enrichDataBindings } from "./enrichDataBinding"
import { routeStore, builderStore } from "../store" import { routeStore, builderStore } from "../store"
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api" import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
import { ActionTypes } from "../constants" import { ActionTypes } from "../constants"
@ -10,35 +9,26 @@ const saveRowHandler = async (action, context) => {
let draft = context[providerId] let draft = context[providerId]
if (fields) { if (fields) {
for (let [key, entry] of Object.entries(fields)) { for (let [key, entry] of Object.entries(fields)) {
draft[key] = await enrichDataBinding(entry.value, context) draft[key] = entry.value
} }
} }
await saveRow(draft) await saveRow(draft)
} }
} }
const deleteRowHandler = async (action, context) => { const deleteRowHandler = async action => {
const { tableId, revId, rowId } = action.parameters const { tableId, revId, rowId } = action.parameters
if (tableId && revId && rowId) { if (tableId && revId && rowId) {
const [enrichTable, enrichRow, enrichRev] = await Promise.all([ await deleteRow({ tableId, rowId, revId })
enrichDataBinding(tableId, context),
enrichDataBinding(rowId, context),
enrichDataBinding(revId, context),
])
await deleteRow({
tableId: enrichTable,
rowId: enrichRow,
revId: enrichRev,
})
} }
} }
const triggerAutomationHandler = async (action, context) => { const triggerAutomationHandler = async action => {
const { fields } = action.parameters() const { fields } = action.parameters
if (fields) { if (fields) {
const params = {} const params = {}
for (let field in fields) { for (let field in fields) {
params[field] = await enrichDataBinding(fields[field].value, context) params[field] = fields[field].value
} }
await triggerAutomation(action.parameters.automationId, params) await triggerAutomation(action.parameters.automationId, params)
} }
@ -60,14 +50,29 @@ const queryExecutionHandler = async action => {
}) })
} }
const validateFormHandler = async (action, context) => { const executeActionHandler = async (context, componentId, actionType) => {
const { componentId } = action.parameters const fn = context[`${componentId}_${actionType}`]
const fn = context[`${componentId}_${ActionTypes.ValidateForm}`]
if (fn) { if (fn) {
return await fn() return await fn()
} }
} }
const validateFormHandler = async (action, context) => {
return await executeActionHandler(
context,
action.parameters.componentId,
ActionTypes.ValidateForm
)
}
const refreshDatasourceHandler = async (action, context) => {
return await executeActionHandler(
context,
action.parameters.componentId,
ActionTypes.RefreshDatasource
)
}
const handlerMap = { const handlerMap = {
["Save Row"]: saveRowHandler, ["Save Row"]: saveRowHandler,
["Delete Row"]: deleteRowHandler, ["Delete Row"]: deleteRowHandler,
@ -75,6 +80,7 @@ const handlerMap = {
["Execute Query"]: queryExecutionHandler, ["Execute Query"]: queryExecutionHandler,
["Trigger Automation"]: triggerAutomationHandler, ["Trigger Automation"]: triggerAutomationHandler,
["Validate Form"]: validateFormHandler, ["Validate Form"]: validateFormHandler,
["Refresh Datasource"]: refreshDatasourceHandler,
} }
/** /**

View File

@ -21,7 +21,7 @@ export const propsAreSame = (a, b) => {
* Enriches component props. * Enriches component props.
* Data bindings are enriched, and button actions are enriched. * Data bindings are enriched, and button actions are enriched.
*/ */
export const enrichProps = async (props, context, user) => { export const enrichProps = async (props, context) => {
// Exclude all private props that start with an underscore // Exclude all private props that start with an underscore
let validProps = {} let validProps = {}
Object.entries(props) Object.entries(props)
@ -34,7 +34,6 @@ export const enrichProps = async (props, context, user) => {
// Duplicate the closest context as "data" which the builder requires // Duplicate the closest context as "data" which the builder requires
const totalContext = { const totalContext = {
...context, ...context,
user,
// This is only required for legacy bindings that used "data" rather than a // This is only required for legacy bindings that used "data" rather than a
// component ID. // component ID.

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "0.7.1", "version": "0.7.4",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/electron.js", "main": "src/electron.js",
"repository": { "repository": {
@ -50,58 +50,58 @@
"author": "Budibase", "author": "Budibase",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@budibase/client": "^0.7.1", "@budibase/client": "^0.7.4",
"@budibase/string-templates": "^0.7.1", "@budibase/string-templates": "^0.7.4",
"@elastic/elasticsearch": "^7.10.0", "@elastic/elasticsearch": "7.10.0",
"@koa/router": "^8.0.0", "@koa/router": "8.0.0",
"@sendgrid/mail": "^7.1.1", "@sendgrid/mail": "7.1.1",
"@sentry/node": "^5.19.2", "@sentry/node": "5.19.2",
"airtable": "^0.10.1", "airtable": "0.10.1",
"arangojs": "^7.2.0", "arangojs": "7.2.0",
"aws-sdk": "^2.767.0", "aws-sdk": "^2.767.0",
"bcryptjs": "^2.4.3", "bcryptjs": "2.4.3",
"chmodr": "^1.2.0", "chmodr": "1.2.0",
"csvtojson": "^2.0.10", "csvtojson": "2.0.10",
"dotenv": "^8.2.0", "dotenv": "8.2.0",
"download": "^8.0.0", "download": "8.0.0",
"electron-is-dev": "^1.2.0", "electron-is-dev": "1.2.0",
"electron-unhandled": "^3.0.2", "electron-unhandled": "3.0.2",
"electron-updater": "^4.3.1", "electron-updater": "4.3.1",
"electron-util": "^0.14.2", "electron-util": "0.14.2",
"fix-path": "^3.0.0", "fix-path": "3.0.0",
"fs-extra": "^8.1.0", "fs-extra": "8.1.0",
"jimp": "^0.16.1", "jimp": "0.16.1",
"joi": "^17.2.1", "joi": "17.2.1",
"jsonschema": "^1.4.0", "jsonschema": "1.4.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "8.5.1",
"koa": "^2.7.0", "koa": "2.7.0",
"koa-body": "^4.2.0", "koa-body": "4.2.0",
"koa-compress": "^4.0.1", "koa-compress": "4.0.1",
"koa-pino-logger": "^3.0.0", "koa-pino-logger": "3.0.0",
"koa-send": "^5.0.0", "koa-send": "5.0.0",
"koa-session": "^5.12.0", "koa-session": "5.12.0",
"koa-static": "^5.0.0", "koa-static": "5.0.0",
"lodash": "^4.17.13", "lodash": "4.17.13",
"mongodb": "^3.6.3", "mongodb": "3.6.3",
"mssql": "^6.2.3", "mssql": "6.2.3",
"mysql": "^2.18.1", "mysql": "2.18.1",
"node-fetch": "^2.6.0", "node-fetch": "2.6.0",
"open": "^7.3.0", "open": "7.3.0",
"pg": "^8.5.1", "pg": "8.5.1",
"pino-pretty": "^4.0.0", "pino-pretty": "4.0.0",
"pouchdb": "^7.2.1", "pouchdb": "7.2.1",
"pouchdb-all-dbs": "^1.0.2", "pouchdb-all-dbs": "1.0.2",
"pouchdb-replication-stream": "^1.2.9", "pouchdb-replication-stream": "1.2.9",
"sanitize-s3-objectkey": "^0.0.1", "sanitize-s3-objectkey": "0.0.1",
"server-destroy": "^1.0.1", "server-destroy": "1.0.1",
"svelte": "^3.30.0", "svelte": "3.30.0",
"tar-fs": "^2.1.0", "tar-fs": "2.1.0",
"to-json-schema": "^0.2.5", "to-json-schema": "0.2.5",
"uuid": "^3.3.2", "uuid": "3.3.2",
"validate.js": "^0.13.1", "validate.js": "0.13.1",
"worker-farm": "^1.7.0", "worker-farm": "1.7.0",
"yargs": "^13.2.4", "yargs": "13.2.4",
"zlib": "^1.0.5" "zlib": "1.0.5"
}, },
"devDependencies": { "devDependencies": {
"@jest/test-sequencer": "^24.8.0", "@jest/test-sequencer": "^24.8.0",
@ -120,5 +120,5 @@
"./scripts/jestSetup.js" "./scripts/jestSetup.js"
] ]
}, },
"gitHead": "62ebf3cedcd7e9b2494b4f8cbcfb90927609b491" "gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd"
} }

View File

@ -6,10 +6,12 @@ const fs = require("fs-extra")
exports.exportAppDump = async function(ctx) { exports.exportAppDump = async function(ctx) {
const { appId } = ctx.query const { appId } = ctx.query
const appname = decodeURI(ctx.query.appname)
const backupsDir = path.join(os.homedir(), ".budibase", "backups") const backupsDir = path.join(os.homedir(), ".budibase", "backups")
fs.ensureDirSync(backupsDir) fs.ensureDirSync(backupsDir)
const backupIdentifier = `${appId} Backup: ${new Date()}.txt` const backupIdentifier = `${appname}Backup${new Date().getTime()}.txt`
await performDump({ await performDump({
dir: backupsDir, dir: backupsDir,
@ -23,19 +25,4 @@ exports.exportAppDump = async function(ctx) {
ctx.attachment(backupIdentifier) ctx.attachment(backupIdentifier)
ctx.body = fs.createReadStream(backupFile) ctx.body = fs.createReadStream(backupFile)
// ctx.body = {
// url: `/api/backups/download/${backupIdentifier}`,
// }
} }
// exports.downloadAppDump = async function(ctx) {
// const fileName = ctx.params.fileName
// const backupsDir = path.join(os.homedir(), ".budibase", "backups")
// fs.ensureDirSync(backupsDir)
// const backupFile = path.join(backupsDir, fileName)
// ctx.attachment(fileName)
// ctx.body = fs.createReadStream(backupFile)
// }

View File

@ -83,23 +83,42 @@ const controller = {
ctx.message = `View ${ctx.params.viewName} saved successfully.` ctx.message = `View ${ctx.params.viewName} saved successfully.`
}, },
exportView: async ctx => { exportView: async ctx => {
const view = ctx.query.view const db = new CouchDB(ctx.user.appId)
const designDoc = await db.get("_design/database")
const viewName = decodeURI(ctx.query.view)
const view = designDoc.views[viewName]
const format = ctx.query.format const format = ctx.query.format
// Fetch view rows if (view) {
ctx.params.viewName = view.name ctx.params.viewName = viewName
ctx.query.group = view.groupBy // Fetch view rows
if (view.field) { ctx.query = {
ctx.query.stats = true group: view.meta.groupBy,
ctx.query.field = view.field calculation: view.meta.calculation,
stats: !!view.meta.field,
field: view.meta.field,
}
} else {
// table all_ view
ctx.params.viewName = viewName
} }
await fetchView(ctx) await fetchView(ctx)
let schema = view && view.meta && view.meta.schema
if (!schema) {
const tableId = ctx.params.tableId || view.meta.tableId
const table = await db.get(tableId)
schema = table.schema
}
// Export part // Export part
let headers = Object.keys(view.schema) let headers = Object.keys(schema)
const exporter = exporters[format] const exporter = exporters[format]
const exportedFile = exporter(headers, ctx.body) const exportedFile = exporter(headers, ctx.body)
const filename = `${view.name}.${format}` const filename = `${viewName}.${format}`
fs.writeFileSync(join(os.tmpdir(), filename), exportedFile) fs.writeFileSync(join(os.tmpdir(), filename), exportedFile)
ctx.attachment(filename) ctx.attachment(filename)

View File

@ -6,10 +6,5 @@ const { BUILDER } = require("../../utilities/security/permissions")
const router = Router() const router = Router()
router.get("/api/backups/export", authorized(BUILDER), controller.exportAppDump) router.get("/api/backups/export", authorized(BUILDER), controller.exportAppDump)
// .get(
// "/api/backups/download/:fileName",
// authorized(BUILDER),
// controller.downloadAppDump
// )
module.exports = router module.exports = router

View File

@ -12,6 +12,7 @@ const usage = require("../../middleware/usageQuota")
const router = Router() const router = Router()
router router
.get("/api/views/export", authorized(BUILDER), viewController.exportView)
.get( .get(
"/api/views/:viewName", "/api/views/:viewName",
authorized(PermissionTypes.VIEW, PermissionLevels.READ), authorized(PermissionTypes.VIEW, PermissionLevels.READ),
@ -25,6 +26,5 @@ router
viewController.destroy viewController.destroy
) )
.post("/api/views", authorized(BUILDER), usage, viewController.save) .post("/api/views", authorized(BUILDER), usage, viewController.save)
.post("/api/views/export", authorized(BUILDER), viewController.exportView)
module.exports = router module.exports = router

File diff suppressed because it is too large Load Diff

View File

@ -106,6 +106,7 @@
"styleable": true, "styleable": true,
"hasChildren": true, "hasChildren": true,
"dataProvider": true, "dataProvider": true,
"actions": ["RefreshDatasource"],
"settings": [ "settings": [
{ {
"type": "datasource", "type": "datasource",
@ -1045,12 +1046,11 @@
"styleable": true, "styleable": true,
"hasChildren": true, "hasChildren": true,
"dataProvider": true, "dataProvider": true,
"datasourceSetting": "datasource",
"actions": ["ValidateForm"], "actions": ["ValidateForm"],
"settings": [ "settings": [
{ {
"type": "datasource", "type": "schema",
"label": "Data", "label": "Schema",
"key": "datasource" "key": "datasource"
}, },
{ {
@ -1059,6 +1059,10 @@
"key": "theme", "key": "theme",
"defaultValue": "spectrum--light", "defaultValue": "spectrum--light",
"options": [ "options": [
{
"label": "Lightest",
"value": "spectrum--lightest"
},
{ {
"label": "Light", "label": "Light",
"value": "spectrum--light" "value": "spectrum--light"
@ -1282,7 +1286,7 @@
"styleable": true, "styleable": true,
"settings": [ "settings": [
{ {
"type": "field/relationship", "type": "field/link",
"label": "Field", "label": "Field",
"key": "field" "key": "field"
}, },

View File

@ -35,9 +35,9 @@
"keywords": [ "keywords": [
"svelte" "svelte"
], ],
"version": "0.7.1", "version": "0.7.4",
"license": "MIT", "license": "MIT",
"gitHead": "62ebf3cedcd7e9b2494b4f8cbcfb90927609b491", "gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd",
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.1.0", "@adobe/spectrum-css-workflow-icons": "^1.1.0",
"@budibase/bbui": "^1.55.1", "@budibase/bbui": "^1.55.1",

View File

@ -2,15 +2,23 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import { isEmpty } from "lodash/fp" import { isEmpty } from "lodash/fp"
const { API, styleable, Provider, builderStore } = getContext("sdk")
const component = getContext("component")
export let datasource = [] export let datasource = []
const { API, styleable, Provider, builderStore, ActionTypes } = getContext(
"sdk"
)
const component = getContext("component")
let rows = [] let rows = []
let loaded = false let loaded = false
$: fetchData(datasource) $: fetchData(datasource)
$: actions = [
{
type: ActionTypes.RefreshDatasource,
callback: () => fetchData(datasource),
metadata: { datasource },
},
]
async function fetchData(datasource) { async function fetchData(datasource) {
if (!isEmpty(datasource)) { if (!isEmpty(datasource)) {
@ -20,23 +28,25 @@
} }
</script> </script>
{#if rows.length > 0} <Provider {actions}>
<div use:styleable={$component.styles}> {#if rows.length > 0}
{#if $component.children === 0 && $builderStore.inBuilder} <div use:styleable={$component.styles}>
<p>Add some components too</p> {#if $component.children === 0 && $builderStore.inBuilder}
{:else} <p>Add some components too</p>
{#each rows as row} {:else}
<Provider data={row}> {#each rows as row}
<slot /> <Provider data={row}>
</Provider> <slot />
{/each} </Provider>
{/if} {/each}
</div> {/if}
{:else if loaded && $builderStore.inBuilder} </div>
<div use:styleable={$component.styles}> {:else if loaded && $builderStore.inBuilder}
<p>Feed me some data</p> <div use:styleable={$component.styles}>
</div> <p>Feed me some data</p>
{/if} </div>
{/if}
</Provider>
<style> <style>
p { p {

View File

@ -63,13 +63,18 @@
.nav__menu { .nav__menu {
display: flex; display: flex;
margin-top: 40px; margin-top: 40px;
gap: 16px;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
} }
.nav__menu > a {
.nav__menu > * {
margin-right: 16px;
}
:global(.nav__menu > a) {
font-size: 1.5em; font-size: 1.5em;
text-decoration: none; text-decoration: none;
margin-right: 16px;
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<script> <script>
import SpectrumField from "./SpectrumField.svelte" import Field from "./Field.svelte"
import Dropzone from "../attachments/Dropzone.svelte" import Dropzone from "../attachments/Dropzone.svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
@ -21,8 +21,14 @@
}) })
</script> </script>
<SpectrumField {label} {field} bind:fieldState bind:fieldApi defaultValue={[]}> <Field
{label}
{field}
type="attachment"
bind:fieldState
bind:fieldApi
defaultValue={[]}>
{#if mounted} {#if mounted}
<Dropzone bind:files={value} /> <Dropzone bind:files={value} />
{/if} {/if}
</SpectrumField> </Field>

View File

@ -1,7 +1,6 @@
<script> <script>
import { onMount } from "svelte"
import "@spectrum-css/checkbox/dist/index-vars.css" import "@spectrum-css/checkbox/dist/index-vars.css"
import SpectrumField from "./SpectrumField.svelte" import Field from "./Field.svelte"
export let field export let field
export let label export let label
@ -15,9 +14,10 @@
} }
</script> </script>
<SpectrumField <Field
{label} {label}
{field} {field}
type="boolean"
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
defaultValue={false}> defaultValue={false}>
@ -48,4 +48,4 @@
</label> </label>
</div> </div>
{/if} {/if}
</SpectrumField> </Field>

View File

@ -1,6 +1,6 @@
<script> <script>
import Flatpickr from "svelte-flatpickr" import Flatpickr from "svelte-flatpickr"
import SpectrumField from "./SpectrumField.svelte" import Field from "./Field.svelte"
import "flatpickr/dist/flatpickr.css" import "flatpickr/dist/flatpickr.css"
import "@spectrum-css/inputgroup/dist/index-vars.css" import "@spectrum-css/inputgroup/dist/index-vars.css"
@ -53,7 +53,7 @@
} }
</script> </script>
<SpectrumField {label} {field} bind:fieldState bind:fieldApi> <Field {label} {field} type="datetime" bind:fieldState bind:fieldApi>
{#if fieldState} {#if fieldState}
<Flatpickr <Flatpickr
bind:flatpickr bind:flatpickr
@ -114,7 +114,7 @@
<div class="overlay" on:mousedown|self={flatpickr?.close} /> <div class="overlay" on:mousedown|self={flatpickr?.close} />
{/if} {/if}
{/if} {/if}
</SpectrumField> </Field>
<style> <style>
.spectrum-Textfield-input { .spectrum-Textfield-input {

View File

@ -9,6 +9,7 @@
export let fieldApi export let fieldApi
export let fieldSchema export let fieldSchema
export let defaultValue export let defaultValue
export let type
// Get contexts // Get contexts
const formContext = getContext("form") const formContext = getContext("form")
@ -45,6 +46,10 @@
<Placeholder> <Placeholder>
Add the Field setting to start using your component Add the Field setting to start using your component
</Placeholder> </Placeholder>
{:else if fieldSchema?.type && fieldSchema?.type !== type}
<Placeholder>
This Field setting is the wrong data type for this component
</Placeholder>
{:else} {:else}
<slot /> <slot />
{#if $fieldState.error} {#if $fieldState.error}

View File

@ -133,7 +133,15 @@
} else { } else {
table = await API.fetchTableDefinition(datasource?.tableId) table = await API.fetchTableDefinition(datasource?.tableId)
if (table) { if (table) {
schema = table.schema || {} if (datasource?.type === "query") {
schema = {}
const params = table.parameters || []
params.forEach(param => {
schema[param.name] = { ...param, type: "string" }
})
} else {
schema = table.schema || {}
}
} }
} }
loaded = true loaded = true

View File

@ -1,7 +1,7 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { RichText } from "@budibase/bbui" import { RichText } from "@budibase/bbui"
import SpectrumField from "./SpectrumField.svelte" import Field from "./Field.svelte"
export let field export let field
export let label export let label
@ -38,13 +38,19 @@
} }
</script> </script>
<SpectrumField {label} {field} bind:fieldState bind:fieldApi defaultValue=""> <Field
{label}
{field}
type="longform"
bind:fieldState
bind:fieldApi
defaultValue="">
{#if mounted} {#if mounted}
<div> <div>
<RichText bind:value {options} /> <RichText bind:value {options} />
</div> </div>
{/if} {/if}
</SpectrumField> </Field>
<style> <style>
div { div {

View File

@ -1,5 +1,5 @@
<script> <script>
import SpectrumField from "./SpectrumField.svelte" import Field from "./Field.svelte"
import Picker from "./Picker.svelte" import Picker from "./Picker.svelte"
export let field export let field
@ -23,7 +23,13 @@
} }
</script> </script>
<SpectrumField {field} {label} bind:fieldState bind:fieldApi bind:fieldSchema> <Field
{field}
{label}
type="options"
bind:fieldState
bind:fieldApi
bind:fieldSchema>
{#if fieldState} {#if fieldState}
<Picker <Picker
bind:open bind:open
@ -35,4 +41,4 @@
isOptionSelected={option => option === $fieldState.value} isOptionSelected={option => option === $fieldState.value}
onSelectOption={selectOption} /> onSelectOption={selectOption} />
{/if} {/if}
</SpectrumField> </Field>

View File

@ -1,6 +1,6 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import SpectrumField from "./SpectrumField.svelte" import Field from "./Field.svelte"
import Picker from "./Picker.svelte" import Picker from "./Picker.svelte"
const { API } = getContext("sdk") const { API } = getContext("sdk")
@ -62,9 +62,10 @@
} }
</script> </script>
<SpectrumField <Field
{label} {label}
{field} {field}
type="link"
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
bind:fieldSchema bind:fieldSchema
@ -77,4 +78,4 @@
onSelectOption={toggleOption} onSelectOption={toggleOption}
getOptionLabel={getDisplayName} getOptionLabel={getDisplayName}
getOptionValue={option => option._id} /> getOptionValue={option => option._id} />
</SpectrumField> </Field>

View File

@ -1,7 +1,6 @@
<script> <script>
import { onMount } from "svelte"
import "@spectrum-css/textfield/dist/index-vars.css" import "@spectrum-css/textfield/dist/index-vars.css"
import SpectrumField from "./SpectrumField.svelte" import Field from "./Field.svelte"
export let field export let field
export let label export let label
@ -31,7 +30,12 @@
} }
</script> </script>
<SpectrumField {label} {field} bind:fieldState bind:fieldApi> <Field
{label}
{field}
type={type === 'number' ? 'number' : 'string'}
bind:fieldState
bind:fieldApi>
{#if fieldState} {#if fieldState}
<div class="spectrum-Textfield" class:is-invalid={!$fieldState.valid}> <div class="spectrum-Textfield" class:is-invalid={!$fieldState.valid}>
{#if !$fieldState.valid} {#if !$fieldState.valid}
@ -53,4 +57,4 @@
class="spectrum-Textfield-input" /> class="spectrum-Textfield-input" />
</div> </div>
{/if} {/if}
</SpectrumField> </Field>

View File

@ -1,5 +1,4 @@
import flatpickr from "flatpickr" import flatpickr from "flatpickr"
import { isEmpty } from "lodash/fp"
export const createValidatorFromConstraints = (constraints, field, table) => { export const createValidatorFromConstraints = (constraints, field, table) => {
let checks = [] let checks = []
@ -14,33 +13,33 @@ export const createValidatorFromConstraints = (constraints, field, table) => {
} }
// String length constraint // String length constraint
if (constraints.length?.maximum) { if (exists(constraints.length?.maximum)) {
const length = constraints.length.maximum const length = constraints.length.maximum
checks.push(lengthConstraint(length)) checks.push(lengthConstraint(length))
} }
// Min / max number constraint // Min / max number constraint
if (!isEmpty(constraints.numericality?.greaterThanOrEqualTo)) { if (exists(constraints.numericality?.greaterThanOrEqualTo)) {
const min = constraints.numericality.greaterThanOrEqualTo const min = constraints.numericality.greaterThanOrEqualTo
checks.push(numericalConstraint(x => x >= min, `Minimum value is ${min}`)) checks.push(numericalConstraint(x => x >= min, `Minimum value is ${min}`))
} }
if (!isEmpty(constraints.numericality?.lessThanOrEqualTo)) { if (exists(constraints.numericality?.lessThanOrEqualTo)) {
const max = constraints.numericality.lessThanOrEqualTo const max = constraints.numericality.lessThanOrEqualTo
checks.push(numericalConstraint(x => x <= max, `Maximum value is ${max}`)) checks.push(numericalConstraint(x => x <= max, `Maximum value is ${max}`))
} }
// Inclusion constraint // Inclusion constraint
if (!isEmpty(constraints.inclusion)) { if (exists(constraints.inclusion)) {
const options = constraints.inclusion const options = constraints.inclusion
checks.push(inclusionConstraint(options)) checks.push(inclusionConstraint(options))
} }
// Date constraint // Date constraint
if (!isEmpty(constraints.datetime?.earliest)) { if (exists(constraints.datetime?.earliest)) {
const limit = constraints.datetime.earliest const limit = constraints.datetime.earliest
checks.push(dateConstraint(limit, true)) checks.push(dateConstraint(limit, true))
} }
if (!isEmpty(constraints.datetime?.latest)) { if (exists(constraints.datetime?.latest)) {
const limit = constraints.datetime.latest const limit = constraints.datetime.latest
checks.push(dateConstraint(limit, false)) checks.push(dateConstraint(limit, false))
} }
@ -58,6 +57,8 @@ export const createValidatorFromConstraints = (constraints, field, table) => {
} }
} }
const exists = value => value != null && value !== ""
const presenceConstraint = value => { const presenceConstraint = value => {
let invalid let invalid
if (Array.isArray(value)) { if (Array.isArray(value)) {

View File

@ -2,6 +2,7 @@ import "@budibase/bbui/dist/bbui.css"
import "@spectrum-css/vars/dist/spectrum-global.css" import "@spectrum-css/vars/dist/spectrum-global.css"
import "@spectrum-css/vars/dist/spectrum-medium.css" import "@spectrum-css/vars/dist/spectrum-medium.css"
import "@spectrum-css/vars/dist/spectrum-large.css" import "@spectrum-css/vars/dist/spectrum-large.css"
import "@spectrum-css/vars/dist/spectrum-lightest.css"
import "@spectrum-css/vars/dist/spectrum-light.css" import "@spectrum-css/vars/dist/spectrum-light.css"
import "@spectrum-css/vars/dist/spectrum-dark.css" import "@spectrum-css/vars/dist/spectrum-dark.css"
import "@spectrum-css/vars/dist/spectrum-darkest.css" import "@spectrum-css/vars/dist/spectrum-darkest.css"

View File

@ -1097,8 +1097,17 @@
"format" "format"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{date now \"DD-MM-YYYY\"}}", "example": "{{date now \"DD-MM-YYYY\"}} -> 21-01-2021",
"description": "<p>Format a date using moment.js date formatting.</p>\n" "description": "<p>Format a date using moment.js date formatting.</p>\n"
},
"duration": {
"args": [
"time",
"durationType"
],
"numArgs": 2,
"example": "{{duration timeLeft \"seconds\"}} -> a few seconds",
"description": "<p>Produce a humanized duration left/until given an amount of time and the type of time measurement.</p>\n"
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "0.7.1", "version": "0.7.4",
"description": "Handlebars wrapper for Budibase templating.", "description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.js", "main": "src/index.js",
"module": "src/index.js", "module": "src/index.js",
@ -32,5 +32,6 @@
"rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"typescript": "^4.1.3" "typescript": "^4.1.3"
} },
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd"
} }

View File

@ -5,18 +5,32 @@ const fs = require("fs")
const doctrine = require("doctrine") const doctrine = require("doctrine")
const marked = require("marked") const marked = require("marked")
const DIRECTORY = fs.existsSync("node_modules") ? "." : ".."
const FILENAME = `${DIRECTORY}/manifest.json`
/** /**
* full list of supported helpers can be found here: * full list of supported helpers can be found here:
* https://github.com/helpers/handlebars-helpers * https://github.com/budibase/handlebars-helpers
*/ */
const DIRECTORY = fs.existsSync("node_modules") ? "." : ".."
const COLLECTIONS = ["math", "array", "number", "url", "string", "comparison"] const COLLECTIONS = ["math", "array", "number", "url", "string", "comparison"]
const FILENAME = `${DIRECTORY}/manifest.json`
const outputJSON = {} const outputJSON = {}
const ADDED_HELPERS = {
date: {
date: {
args: ["datetime", "format"],
numArgs: 2,
example: '{{date now "DD-MM-YYYY"}} -> 21-01-2021',
description: "Format a date using moment.js date formatting.",
},
duration: {
args: ["time", "durationType"],
numArgs: 2,
example: '{{duration timeLeft "seconds"}} -> a few seconds',
description:
"Produce a humanized duration left/until given an amount of time and the type of time measurement.",
},
},
}
function fixSpecialCases(name, obj) { function fixSpecialCases(name, obj) {
const args = obj.args const args = obj.args
@ -134,15 +148,15 @@ function run() {
} }
outputJSON[collection] = collectionInfo outputJSON[collection] = collectionInfo
} }
// add the date helper // add extra helpers
outputJSON["date"] = { for (let [collectionName, collection] of Object.entries(ADDED_HELPERS)) {
date: { let input = collection
args: ["datetime", "format"], if (outputJSON[collectionName]) {
numArgs: 2, input = Object.assign(outputJSON[collectionName], collection)
example: '{{date now "DD-MM-YYYY"}}', }
description: "Format a date using moment.js date formatting.", outputJSON[collectionName] = input
},
} }
// convert all markdown to HTML // convert all markdown to HTML
for (let collection of Object.values(outputJSON)) { for (let collection of Object.values(outputJSON)) {
for (let helper of Object.values(collection)) { for (let helper of Object.values(collection)) {

View File

@ -1,4 +1,7 @@
const dayjs = require("dayjs") const dayjs = require("dayjs")
dayjs.extend(require("dayjs/plugin/duration"))
dayjs.extend(require("dayjs/plugin/advancedFormat"))
dayjs.extend(require("dayjs/plugin/relativeTime"))
/** /**
* This file was largely taken from the helper-date package - we did this for two reasons: * This file was largely taken from the helper-date package - we did this for two reasons:
@ -50,7 +53,7 @@ function getContext(thisArg, locals, options) {
return context return context
} }
module.exports = function dateHelper(str, pattern, options) { function initialConfig(str, pattern, options) {
if (isOptions(pattern)) { if (isOptions(pattern)) {
options = pattern options = pattern
pattern = null pattern = null
@ -61,18 +64,42 @@ module.exports = function dateHelper(str, pattern, options) {
pattern = null pattern = null
str = null str = null
} }
return { str, pattern, options }
}
function setLocale(str, pattern, options) {
// if options is null then it'll get updated here
const config = initialConfig(str, pattern, options)
const defaults = { lang: "en", date: new Date(config.str) }
const opts = getContext(this, defaults, config.options)
// set the language to use
dayjs.locale(opts.lang || opts.language)
}
module.exports.date = (str, pattern, options) => {
const config = initialConfig(str, pattern, options)
// if no args are passed, return a formatted date // if no args are passed, return a formatted date
if (str == null && pattern == null) { if (config.str == null && config.pattern == null) {
dayjs.locale("en") dayjs.locale("en")
return dayjs().format("MMMM DD, YYYY") return dayjs().format("MMMM DD, YYYY")
} }
const defaults = { lang: "en", date: new Date(str) } setLocale(config.str, config.pattern, config.options)
const opts = getContext(this, defaults, options)
// set the language to use return dayjs(new Date(config.str)).format(config.pattern)
dayjs.locale(opts.lang || opts.language) }
return dayjs(new Date(str)).format(pattern) module.exports.duration = (str, pattern, format) => {
const config = initialConfig(str, pattern)
setLocale(config.str, config.pattern)
const duration = dayjs.duration(config.str, config.pattern)
if (!isOptions(format)) {
return duration.format(format)
} else {
return duration.humanize()
}
} }

View File

@ -1,5 +1,5 @@
const helpers = require("@budibase/handlebars-helpers") const helpers = require("@budibase/handlebars-helpers")
const dateHelper = require("./date") const { date, duration } = require("./date")
const { HelperFunctionBuiltin } = require("./constants") const { HelperFunctionBuiltin } = require("./constants")
/** /**
@ -18,10 +18,15 @@ const EXTERNAL_FUNCTION_COLLECTIONS = [
"regex", "regex",
] ]
const DATE_NAME = "date" const ADDED_HELPERS = {
date: date,
duration: duration,
}
exports.registerAll = handlebars => { exports.registerAll = handlebars => {
handlebars.registerHelper(DATE_NAME, dateHelper) for (let [name, helper] of Object.entries(ADDED_HELPERS)) {
handlebars.registerHelper(name, helper)
}
let externalNames = [] let externalNames = []
for (let collection of EXTERNAL_FUNCTION_COLLECTIONS) { for (let collection of EXTERNAL_FUNCTION_COLLECTIONS) {
// collect information about helper // collect information about helper
@ -43,12 +48,13 @@ exports.registerAll = handlebars => {
}) })
} }
// add date external functionality // add date external functionality
externalNames.push(DATE_NAME) exports.externalHelperNames = externalNames.concat(Object.keys(ADDED_HELPERS))
exports.externalHelperNames = externalNames
} }
exports.unregisterAll = handlebars => { exports.unregisterAll = handlebars => {
handlebars.unregisterHelper(DATE_NAME) for (let name of Object.keys(ADDED_HELPERS)) {
handlebars.unregisterHelper(name)
}
for (let name of module.exports.externalHelperNames) { for (let name of module.exports.externalHelperNames) {
handlebars.unregisterHelper(name) handlebars.unregisterHelper(name)
} }

View File

@ -318,6 +318,17 @@ describe("Cover a few complex use cases", () => {
expect(validity).toBe(true) expect(validity).toBe(true)
}) })
it("test a very complex duration output", async () => {
const currentTime = new Date(1612432082000).toISOString(),
eventTime = new Date(1612432071000).toISOString()
const input = `{{duration ( subtract (date currentTime "X")(date eventTime "X")) "seconds"}}`
const output = await processString(input, {
currentTime,
eventTime,
})
expect(output).toBe("a few seconds")
})
it("should confirm a bunch of invalid strings", () => { it("should confirm a bunch of invalid strings", () => {
const invalids = ["{{ awd )", "{{ awdd () ", "{{ awdwad ", "{{ awddawd }"] const invalids = ["{{ awd )", "{{ awdd () ", "{{ awdwad ", "{{ awddawd }"]
for (let invalid of invalids) { for (let invalid of invalids) {

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/deployment", "name": "@budibase/deployment",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "0.7.1", "version": "0.7.4",
"description": "Budibase Deployment Server", "description": "Budibase Deployment Server",
"main": "src/index.js", "main": "src/index.js",
"repository": { "repository": {
@ -34,5 +34,5 @@
"pouchdb-all-dbs": "^1.0.2", "pouchdb-all-dbs": "^1.0.2",
"server-destroy": "^1.0.1" "server-destroy": "^1.0.1"
}, },
"gitHead": "62ebf3cedcd7e9b2494b4f8cbcfb90927609b491" "gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd"
} }