Merge branch 'develop' of github.com:Budibase/budibase into bug/fix-binding-issue
This commit is contained in:
commit
2151709a9b
|
@ -63,7 +63,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.58.12",
|
"@budibase/bbui": "^1.58.13",
|
||||||
"@budibase/client": "^0.7.8",
|
"@budibase/client": "^0.7.8",
|
||||||
"@budibase/colorpicker": "1.0.1",
|
"@budibase/colorpicker": "1.0.1",
|
||||||
"@budibase/string-templates": "^0.7.8",
|
"@budibase/string-templates": "^0.7.8",
|
||||||
|
|
|
@ -232,6 +232,15 @@ export const getSchemaForDatasource = (datasource, isForm = false) => {
|
||||||
if (table) {
|
if (table) {
|
||||||
if (type === "view") {
|
if (type === "view") {
|
||||||
schema = cloneDeep(table.views?.[datasource.name]?.schema)
|
schema = cloneDeep(table.views?.[datasource.name]?.schema)
|
||||||
|
|
||||||
|
// Some calc views don't include a "name" property inside the schema
|
||||||
|
if (schema) {
|
||||||
|
Object.keys(schema).forEach(field => {
|
||||||
|
if (!schema[field].name) {
|
||||||
|
schema[field].name = field
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
} else if (type === "query" && isForm) {
|
} else if (type === "query" && isForm) {
|
||||||
schema = {}
|
schema = {}
|
||||||
const params = table.parameters || []
|
const params = table.parameters || []
|
||||||
|
|
|
@ -11,8 +11,9 @@
|
||||||
import { capitalise } from "../../../helpers"
|
import { capitalise } from "../../../helpers"
|
||||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||||
|
|
||||||
|
export let defaultValue
|
||||||
export let meta
|
export let meta
|
||||||
export let value = meta.type === "boolean" ? false : ""
|
export let value = defaultValue || (meta.type === "boolean" ? false : "")
|
||||||
export let readonly
|
export let readonly
|
||||||
|
|
||||||
$: type = meta.type
|
$: type = meta.type
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
Select,
|
Select,
|
||||||
Toggle,
|
Toggle,
|
||||||
Radio,
|
Radio,
|
||||||
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
|
@ -38,12 +37,14 @@
|
||||||
$backendUiStore.selectedTable.primaryDisplay === field.name
|
$backendUiStore.selectedTable.primaryDisplay === field.name
|
||||||
|
|
||||||
let relationshipTypes = [
|
let relationshipTypes = [
|
||||||
{text: 'Many to many (N:N)', value: 'many-to-many',},
|
{ text: "Many to many (N:N)", value: "many-to-many" },
|
||||||
{text: 'One to many (1:N)', value: 'one-to-many',}
|
{ text: "One to many (1:N)", value: "one-to-many" },
|
||||||
]
|
]
|
||||||
let types = ['Many to many (N:N)', 'One to many (1:N)']
|
let types = ["Many to many (N:N)", "One to many (1:N)"]
|
||||||
|
|
||||||
let selectedRelationshipType = relationshipTypes.find(type => type.value === field.relationshipType)?.text || 'Many to many (N:N)'
|
let selectedRelationshipType =
|
||||||
|
relationshipTypes.find(type => type.value === field.relationshipType)
|
||||||
|
?.text || "Many to many (N:N)"
|
||||||
|
|
||||||
let indexes = [...($backendUiStore.selectedTable.indexes || [])]
|
let indexes = [...($backendUiStore.selectedTable.indexes || [])]
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
|
@ -68,10 +69,12 @@
|
||||||
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_COL
|
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_COL
|
||||||
|
|
||||||
async function saveColumn() {
|
async function saveColumn() {
|
||||||
// Set relationship type if it's
|
// Set relationship type if it's
|
||||||
if (field.type === 'link') {
|
if (field.type === "link") {
|
||||||
field.relationshipType = relationshipTypes.find(type => type.text === selectedRelationshipType).value
|
field.relationshipType = relationshipTypes.find(
|
||||||
}
|
type => type.text === selectedRelationshipType
|
||||||
|
).value
|
||||||
|
}
|
||||||
|
|
||||||
if (field.type === AUTO_COL) {
|
if (field.type === AUTO_COL) {
|
||||||
field = buildAutoColumn(
|
field = buildAutoColumn(
|
||||||
|
@ -228,11 +231,15 @@
|
||||||
label="Max Value"
|
label="Max Value"
|
||||||
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
|
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
|
||||||
{:else if field.type === 'link'}
|
{:else if field.type === 'link'}
|
||||||
<div>
|
<div>
|
||||||
<Label grey extraSmall>Select relationship type</Label>
|
<Label grey extraSmall>Select relationship type</Label>
|
||||||
<div class="radio-buttons">
|
<div class="radio-buttons">
|
||||||
{#each types as type}
|
{#each types as type}
|
||||||
<Radio disabled={originalName} name="Relationship type" value={type} bind:group={selectedRelationshipType}>
|
<Radio
|
||||||
|
disabled={originalName}
|
||||||
|
name="Relationship type"
|
||||||
|
value={type}
|
||||||
|
bind:group={selectedRelationshipType}>
|
||||||
<label for={type}>{type}</label>
|
<label for={type}>{type}</label>
|
||||||
</Radio>
|
</Radio>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -282,7 +289,7 @@
|
||||||
.radio-buttons {
|
.radio-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
font-size: var(--font-size-xs)
|
font-size: var(--font-size-xs);
|
||||||
}
|
}
|
||||||
.actions {
|
.actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
let customSchema = { ...schema }
|
let customSchema = { ...schema }
|
||||||
delete customSchema["email"]
|
delete customSchema["email"]
|
||||||
delete customSchema["roleId"]
|
delete customSchema["roleId"]
|
||||||
|
delete customSchema["status"]
|
||||||
return Object.entries(customSchema)
|
return Object.entries(customSchema)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +80,13 @@
|
||||||
<option value={role._id}>{role.name}</option>
|
<option value={role._id}>{role.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
|
<RowFieldControl
|
||||||
|
meta={{ name: 'status', type: 'options', constraints: { inclusion: ['active', 'inactive'] } }}
|
||||||
|
bind:value={row.status}
|
||||||
|
defaultValue={'active'} />
|
||||||
{#each customSchemaKeys as [key, meta]}
|
{#each customSchemaKeys as [key, meta]}
|
||||||
<RowFieldControl {meta} bind:value={row[key]} {creating} />
|
{#if !meta.autocolumn}
|
||||||
|
<RowFieldControl {meta} bind:value={row[key]} {creating} />
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -10,10 +10,13 @@
|
||||||
|
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
export let thin = true
|
||||||
|
export let title = "Bindings"
|
||||||
|
export let placeholder
|
||||||
|
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
let tempValue = value
|
|
||||||
|
|
||||||
|
$: tempValue = value
|
||||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
@ -28,15 +31,15 @@
|
||||||
|
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<Input
|
<Input
|
||||||
thin
|
{thin}
|
||||||
value={readableValue}
|
value={readableValue}
|
||||||
on:change={event => onChange(event.target.value)}
|
on:change={event => onChange(event.target.value)}
|
||||||
placeholder="/screen" />
|
{placeholder} />
|
||||||
<div class="icon" on:click={bindingDrawer.show}>
|
<div class="icon" on:click={bindingDrawer.show}>
|
||||||
<Icon name="lightning" />
|
<Icon name="lightning" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Drawer bind:this={bindingDrawer} title="Bindings">
|
<Drawer bind:this={bindingDrawer} {title}>
|
||||||
<div slot="description">
|
<div slot="description">
|
||||||
<Body extraSmall grey>
|
<Body extraSmall grey>
|
||||||
Add the objects on the left to enrich your text.
|
Add the objects on the left to enrich your text.
|
||||||
|
@ -57,7 +60,6 @@
|
||||||
<style>
|
<style>
|
||||||
.control {
|
.control {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-left: var(--spacing-l);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,10 +42,17 @@
|
||||||
</Label>
|
</Label>
|
||||||
{:else}
|
{:else}
|
||||||
{#if schema.relationshipType === 'one-to-many'}
|
{#if schema.relationshipType === 'one-to-many'}
|
||||||
<Select thin secondary on:change={e => linkedRows = [e.target.value]} name={label} {label}>
|
<Select
|
||||||
|
thin
|
||||||
|
secondary
|
||||||
|
on:change={e => (linkedRows = [e.target.value])}
|
||||||
|
name={label}
|
||||||
|
{label}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each rows as row}
|
{#each rows as row}
|
||||||
<option selected={row._id === linkedRows[0]} value={row._id}>{getPrettyName(row)}</option>
|
<option selected={row._id === linkedRows[0]} value={row._id}>
|
||||||
|
{getPrettyName(row)}
|
||||||
|
</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -114,8 +114,7 @@
|
||||||
bind:getCaretPosition
|
bind:getCaretPosition
|
||||||
thin
|
thin
|
||||||
bind:value
|
bind:value
|
||||||
placeholder="Add text, or click the objects on the left to add them to
|
placeholder="Add text, or click the objects on the left to add them to the textbox." />
|
||||||
the textbox." />
|
|
||||||
{#if !valid}
|
{#if !valid}
|
||||||
<p class="syntax-error">
|
<p class="syntax-error">
|
||||||
Current Handlebars syntax is invalid, please check the guide
|
Current Handlebars syntax is invalid, please check the guide
|
||||||
|
@ -144,9 +143,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-l);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
}
|
}
|
||||||
|
.text :global(textarea) {
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
.text :global(p) {
|
.text :global(p) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
dropdownRight.hide()
|
dropdownRight.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchDatasourceSchema(query) {
|
function fetchQueryDefinition(query) {
|
||||||
const source = $backendUiStore.datasources.find(
|
const source = $backendUiStore.datasources.find(
|
||||||
ds => ds._id === query.datasourceId
|
ds => ds._id === query.datasourceId
|
||||||
).source
|
).source
|
||||||
|
@ -85,44 +85,48 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class="container">
|
||||||
class="dropdownbutton"
|
<div
|
||||||
bind:this={anchorRight}
|
class="dropdownbutton"
|
||||||
on:click={dropdownRight.show}>
|
bind:this={anchorRight}
|
||||||
<span>{value?.label ?? 'Choose option'}</span>
|
on:click={dropdownRight.show}>
|
||||||
<Icon name="arrowdown" />
|
<span>{value?.label ?? 'Choose option'}</span>
|
||||||
|
<Icon name="arrowdown" />
|
||||||
|
</div>
|
||||||
|
{#if value?.type === 'query'}
|
||||||
|
<i class="ri-settings-5-line" on:click={drawer.show} />
|
||||||
|
<Drawer title={'Query Parameters'} bind:this={drawer}>
|
||||||
|
<div slot="buttons">
|
||||||
|
<Button
|
||||||
|
blue
|
||||||
|
thin
|
||||||
|
on:click={() => {
|
||||||
|
notifier.success('Query parameters saved.')
|
||||||
|
handleSelected(value)
|
||||||
|
drawer.hide()
|
||||||
|
}}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="drawer-contents" slot="body">
|
||||||
|
{#if value.parameters.length > 0}
|
||||||
|
<ParameterBuilder
|
||||||
|
bind:customParams={value.queryParams}
|
||||||
|
parameters={queries.find(query => query._id === value._id).parameters}
|
||||||
|
bindings={queryBindableProperties} />
|
||||||
|
{/if}
|
||||||
|
<!-- <Spacer large />-->
|
||||||
|
<IntegrationQueryEditor
|
||||||
|
height={200}
|
||||||
|
query={value}
|
||||||
|
schema={fetchQueryDefinition(value)}
|
||||||
|
datasource={$backendUiStore.datasources.find(ds => ds._id === value.datasourceId)}
|
||||||
|
editable={false} />
|
||||||
|
<Spacer large />
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if value?.type === 'query'}
|
|
||||||
<i class="ri-settings-5-line" on:click={drawer.show} />
|
|
||||||
<Drawer title={'Query'} bind:this={drawer}>
|
|
||||||
<div slot="buttons">
|
|
||||||
<Button
|
|
||||||
blue
|
|
||||||
thin
|
|
||||||
on:click={() => {
|
|
||||||
notifier.success('Query parameters saved.')
|
|
||||||
handleSelected(value)
|
|
||||||
drawer.hide()
|
|
||||||
}}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="drawer-contents" slot="body">
|
|
||||||
<IntegrationQueryEditor
|
|
||||||
datasource={$backendUiStore.datasources.find(ds => ds._id === value.datasourceId)}
|
|
||||||
query={value}
|
|
||||||
schema={fetchDatasourceSchema(value)}
|
|
||||||
editable={false} />
|
|
||||||
<Spacer large />
|
|
||||||
{#if value.parameters.length > 0}
|
|
||||||
<ParameterBuilder
|
|
||||||
bind:customParams={value.queryParams}
|
|
||||||
parameters={queries.find(query => query._id === value._id).parameters}
|
|
||||||
bindings={queryBindableProperties} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Drawer>
|
|
||||||
{/if}
|
|
||||||
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
|
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
@ -197,6 +201,13 @@
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdownbutton {
|
.dropdownbutton {
|
||||||
background-color: var(--grey-2);
|
background-color: var(--grey-2);
|
||||||
border: var(--border-transparent);
|
border: var(--border-transparent);
|
||||||
|
@ -259,8 +270,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-contents {
|
.drawer-contents {
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-l);
|
||||||
height: 40vh;
|
height: calc(40vh - 2 * var(--spacing-l));
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,19 @@
|
||||||
|
|
||||||
let addActionButton
|
let addActionButton
|
||||||
let addActionDropdown
|
let addActionDropdown
|
||||||
let selectedAction
|
let selectedAction = actions?.length ? actions[0] : null
|
||||||
|
|
||||||
$: selectedActionComponent =
|
$: selectedActionComponent =
|
||||||
selectedAction &&
|
selectedAction &&
|
||||||
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY]).component
|
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY]).component
|
||||||
|
|
||||||
|
// Select the first action if we delete an action
|
||||||
|
$: {
|
||||||
|
if (selectedAction && !actions?.includes(selectedAction)) {
|
||||||
|
selectedAction = actions?.[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const deleteAction = index => {
|
const deleteAction = index => {
|
||||||
actions.splice(index, 1)
|
actions.splice(index, 1)
|
||||||
actions = actions
|
actions = actions
|
||||||
|
@ -42,11 +49,10 @@
|
||||||
<div class="actions-list">
|
<div class="actions-list">
|
||||||
<div>
|
<div>
|
||||||
<div bind:this={addActionButton}>
|
<div bind:this={addActionButton}>
|
||||||
<Spacer small />
|
|
||||||
<Button wide secondary on:click={addActionDropdown.show}>
|
<Button wide secondary on:click={addActionDropdown.show}>
|
||||||
Add Action
|
Add Action
|
||||||
</Button>
|
</Button>
|
||||||
<Spacer medium />
|
<Spacer small />
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
bind:this={addActionDropdown}
|
bind:this={addActionDropdown}
|
||||||
|
@ -65,11 +71,12 @@
|
||||||
{#if actions && actions.length > 0}
|
{#if actions && actions.length > 0}
|
||||||
{#each actions as action, index}
|
{#each actions as action, index}
|
||||||
<div class="action-container">
|
<div class="action-container">
|
||||||
<div class="action-header" on:click={selectAction(action)}>
|
<div
|
||||||
<span class:selected={action === selectedAction}>
|
class="action-header"
|
||||||
{index + 1}.
|
class:selected={action === selectedAction}
|
||||||
{action[EVENT_TYPE_KEY]}
|
on:click={selectAction(action)}>
|
||||||
</span>
|
{index + 1}.
|
||||||
|
{action[EVENT_TYPE_KEY]}
|
||||||
</div>
|
</div>
|
||||||
<i
|
<i
|
||||||
class="ri-close-fill"
|
class="ri-close-fill"
|
||||||
|
@ -98,20 +105,22 @@
|
||||||
margin-top: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-header > span {
|
.action-header {
|
||||||
margin-bottom: var(--spacing-m);
|
margin-bottom: var(--spacing-m);
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--grey-7);
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-header > span:hover,
|
.action-header:hover,
|
||||||
.selected {
|
.action-header.selected {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions-list {
|
.actions-list {
|
||||||
border-right: var(--border-light);
|
border-right: var(--border-light);
|
||||||
padding: var(--spacing-s);
|
padding: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
.available-action {
|
.available-action {
|
||||||
|
@ -127,7 +136,6 @@
|
||||||
.actions-container {
|
.actions-container {
|
||||||
height: 40vh;
|
height: 40vh;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: var(--spacing-m);
|
|
||||||
grid-template-columns: 260px 1fr;
|
grid-template-columns: 260px 1fr;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
@ -136,13 +144,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-container {
|
.action-container {
|
||||||
border-top: var(--border-light);
|
border-bottom: 1px solid var(--grey-1);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.action-container:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
.selected-action-container {
|
.selected-action-container {
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -1,207 +0,0 @@
|
||||||
<script>
|
|
||||||
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
|
|
||||||
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
|
||||||
import actionTypes from "./actions"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
import { automationStore } from "builderStore"
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
const eventTypeKey = "##eventHandlerType"
|
|
||||||
|
|
||||||
export let event
|
|
||||||
|
|
||||||
let addActionButton
|
|
||||||
let addActionDropdown
|
|
||||||
let selectedAction
|
|
||||||
|
|
||||||
$: actions = event || []
|
|
||||||
$: selectedActionComponent =
|
|
||||||
selectedAction &&
|
|
||||||
actionTypes.find(t => t.name === selectedAction[eventTypeKey]).component
|
|
||||||
|
|
||||||
const deleteAction = index => {
|
|
||||||
actions.splice(index, 1)
|
|
||||||
actions = actions
|
|
||||||
}
|
|
||||||
|
|
||||||
const addAction = actionType => () => {
|
|
||||||
const newAction = {
|
|
||||||
parameters: {},
|
|
||||||
[eventTypeKey]: actionType.name,
|
|
||||||
}
|
|
||||||
actions.push(newAction)
|
|
||||||
selectedAction = newAction
|
|
||||||
actions = actions
|
|
||||||
addActionDropdown.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectAction = action => () => {
|
|
||||||
selectedAction = action
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveEventData = async () => {
|
|
||||||
// e.g. The Trigger Automation action exposes beforeSave, so it can
|
|
||||||
// create any automations it needs to
|
|
||||||
for (let action of actions) {
|
|
||||||
if (action[eventTypeKey] === "Trigger Automation") {
|
|
||||||
await createAutomation(action.parameters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dispatch("change", actions)
|
|
||||||
}
|
|
||||||
|
|
||||||
// called by the parent modal when actions are saved
|
|
||||||
const createAutomation = async parameters => {
|
|
||||||
if (parameters.automationId || !parameters.newAutomationName) return
|
|
||||||
|
|
||||||
await automationStore.actions.create({ name: parameters.newAutomationName })
|
|
||||||
|
|
||||||
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
|
|
||||||
|
|
||||||
const newBlock = $automationStore.selectedAutomation.constructBlock(
|
|
||||||
"TRIGGER",
|
|
||||||
"APP",
|
|
||||||
appActionDefinition
|
|
||||||
)
|
|
||||||
|
|
||||||
newBlock.inputs = {
|
|
||||||
fields: Object.entries(parameters.fields).reduce(
|
|
||||||
(fields, [key, value]) => {
|
|
||||||
fields[key] = value.type
|
|
||||||
return fields
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
automationStore.actions.addBlockToAutomation(newBlock)
|
|
||||||
|
|
||||||
await automationStore.actions.save($automationStore.selectedAutomation)
|
|
||||||
|
|
||||||
parameters.automationId = $automationStore.selectedAutomation.automation._id
|
|
||||||
delete parameters.newAutomationName
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ModalContent title="Actions" confirmText="Save" onConfirm={saveEventData}>
|
|
||||||
<div slot="header">
|
|
||||||
<div bind:this={addActionButton}>
|
|
||||||
<TextButton text small blue on:click={addActionDropdown.show}>
|
|
||||||
<div style="height: 20px; width: 20px;">
|
|
||||||
<AddIcon />
|
|
||||||
</div>
|
|
||||||
Add Action
|
|
||||||
</TextButton>
|
|
||||||
</div>
|
|
||||||
<DropdownMenu
|
|
||||||
bind:this={addActionDropdown}
|
|
||||||
anchor={addActionButton}
|
|
||||||
align="right">
|
|
||||||
<div class="available-actions-container">
|
|
||||||
{#each actionTypes as actionType}
|
|
||||||
<div class="available-action" on:click={addAction(actionType)}>
|
|
||||||
<span>{actionType.name}</span>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions-container">
|
|
||||||
{#if actions && actions.length > 0}
|
|
||||||
{#each actions as action, index}
|
|
||||||
<div class="action-container">
|
|
||||||
<div class="action-header" on:click={selectAction(action)}>
|
|
||||||
<Body small lh>{index + 1}. {action[eventTypeKey]}</Body>
|
|
||||||
<div class="row-expander" class:rotate={action !== selectedAction}>
|
|
||||||
<ArrowDownIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if action === selectedAction}
|
|
||||||
<div class="selected-action-container">
|
|
||||||
<svelte:component
|
|
||||||
this={selectedActionComponent}
|
|
||||||
parameters={selectedAction.parameters} />
|
|
||||||
<div class="delete-action-button">
|
|
||||||
<TextButton text medium on:click={() => deleteAction(index)}>
|
|
||||||
Delete
|
|
||||||
</TextButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div slot="footer">
|
|
||||||
<a href="https://docs.budibase.com">Learn more about Actions</a>
|
|
||||||
</div>
|
|
||||||
</ModalContent>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.action-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-header > p {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-expander {
|
|
||||||
height: 30px;
|
|
||||||
width: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.available-action {
|
|
||||||
padding: var(--spacing-m);
|
|
||||||
font-size: var(--font-size-m);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.available-action:hover {
|
|
||||||
background: var(--grey-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-container {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
border: var(--border-light);
|
|
||||||
border-width: 0 0 1px 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-container {
|
|
||||||
border: var(--border-light);
|
|
||||||
border-width: 1px 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-action-container {
|
|
||||||
padding-bottom: var(--spacing-s);
|
|
||||||
padding-top: var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-action-button {
|
|
||||||
padding-top: var(--spacing-l);
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
flex: 1;
|
|
||||||
color: var(--grey-5);
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: var(--blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rotate :global(svg) {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -17,7 +17,9 @@
|
||||||
const automationsToCreate = value.filter(
|
const automationsToCreate = value.filter(
|
||||||
action => action["##eventHandlerType"] === "Trigger Automation"
|
action => action["##eventHandlerType"] === "Trigger Automation"
|
||||||
)
|
)
|
||||||
automationsToCreate.forEach(action => createAutomation(action.parameters))
|
for (let action of automationsToCreate) {
|
||||||
|
await createAutomation(action.parameters)
|
||||||
|
}
|
||||||
|
|
||||||
dispatch("change", value)
|
dispatch("change", value)
|
||||||
notifier.success("Component actions saved.")
|
notifier.success("Component actions saved.")
|
||||||
|
@ -27,11 +29,8 @@
|
||||||
// called by the parent modal when actions are saved
|
// called by the parent modal when actions are saved
|
||||||
const createAutomation = async parameters => {
|
const createAutomation = async parameters => {
|
||||||
if (parameters.automationId || !parameters.newAutomationName) return
|
if (parameters.automationId || !parameters.newAutomationName) return
|
||||||
|
|
||||||
await automationStore.actions.create({ name: parameters.newAutomationName })
|
await automationStore.actions.create({ name: parameters.newAutomationName })
|
||||||
|
|
||||||
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
|
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
|
||||||
|
|
||||||
const newBlock = $automationStore.selectedAutomation.constructBlock(
|
const newBlock = $automationStore.selectedAutomation.constructBlock(
|
||||||
"TRIGGER",
|
"TRIGGER",
|
||||||
"APP",
|
"APP",
|
||||||
|
@ -39,19 +38,14 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
newBlock.inputs = {
|
newBlock.inputs = {
|
||||||
fields: Object.entries(parameters.fields).reduce(
|
fields: Object.keys(parameters.fields).reduce((fields, key) => {
|
||||||
(fields, [key, value]) => {
|
fields[key] = "string"
|
||||||
fields[key] = value.type
|
return fields
|
||||||
return fields
|
}, {}),
|
||||||
},
|
|
||||||
{}
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
automationStore.actions.addBlockToAutomation(newBlock)
|
automationStore.actions.addBlockToAutomation(newBlock)
|
||||||
|
|
||||||
await automationStore.actions.save($automationStore.selectedAutomation)
|
await automationStore.actions.save($automationStore.selectedAutomation)
|
||||||
|
|
||||||
parameters.automationId = $automationStore.selectedAutomation.automation._id
|
parameters.automationId = $automationStore.selectedAutomation.automation._id
|
||||||
delete parameters.newAutomationName
|
delete parameters.newAutomationName
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,8 +37,8 @@
|
||||||
a List
|
a List
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Label size="m" color="dark">Datasource</Label>
|
<Label small>Datasource</Label>
|
||||||
<Select secondary bind:value={parameters.providerId}>
|
<Select thin secondary bind:value={parameters.providerId}>
|
||||||
<option value="" />
|
<option value="" />
|
||||||
{#each dataProviderComponents as provider}
|
{#each dataProviderComponents as provider}
|
||||||
<option value={provider._id}>{provider._instanceName}</option>
|
<option value={provider._id}>{provider._instanceName}</option>
|
||||||
|
@ -50,22 +50,15 @@
|
||||||
<style>
|
<style>
|
||||||
.root {
|
.root {
|
||||||
display: grid;
|
display: grid;
|
||||||
column-gap: var(--spacing-s);
|
column-gap: var(--spacing-l);
|
||||||
row-gap: var(--spacing-s);
|
row-gap: var(--spacing-s);
|
||||||
grid-template-columns: auto 1fr auto 1fr auto;
|
grid-template-columns: auto 1fr;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(> div:nth-child(2)) {
|
|
||||||
grid-column-start: 2;
|
|
||||||
grid-column-end: 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cannot-use {
|
.cannot-use {
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
text-align: center;
|
|
||||||
width: 70%;
|
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,63 +3,57 @@
|
||||||
import { store, backendUiStore, currentAsset } from "builderStore"
|
import { store, backendUiStore, currentAsset } from "builderStore"
|
||||||
import { getBindableProperties } from "builderStore/dataBinding"
|
import { getBindableProperties } from "builderStore/dataBinding"
|
||||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
||||||
|
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
|
||||||
|
$: query = $backendUiStore.queries.find(q => q._id === parameters.queryId)
|
||||||
$: datasource = $backendUiStore.datasources.find(
|
$: datasource = $backendUiStore.datasources.find(
|
||||||
ds => ds._id === parameters.datasourceId
|
ds => ds._id === parameters.datasourceId
|
||||||
)
|
)
|
||||||
$: bindableProperties = getBindableProperties(
|
$: bindableProperties = getBindableProperties(
|
||||||
$currentAsset,
|
$currentAsset,
|
||||||
$store.selectedComponentId
|
$store.selectedComponentId
|
||||||
).map(property => ({
|
)
|
||||||
...property,
|
|
||||||
category: property.type === "instance" ? "Component" : "Table",
|
|
||||||
label: property.readableBinding,
|
|
||||||
path: property.runtimeBinding,
|
|
||||||
}))
|
|
||||||
|
|
||||||
$: query =
|
function fetchQueryDefinition(query) {
|
||||||
parameters.queryId &&
|
const source = $backendUiStore.datasources.find(
|
||||||
$backendUiStore.queries.find(query => query._id === parameters.queryId)
|
ds => ds._id === query.datasourceId
|
||||||
|
).source
|
||||||
|
return $backendUiStore.integrations[source].query[query.queryVerb]
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<Label small>Datasource</Label>
|
||||||
<Label size="m" color="dark">Datasource</Label>
|
<Select thin secondary bind:value={parameters.datasourceId}>
|
||||||
<Select thin secondary bind:value={parameters.datasourceId}>
|
<option value="" />
|
||||||
|
{#each $backendUiStore.datasources as datasource}
|
||||||
|
<option value={datasource._id}>{datasource.name}</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Spacer medium />
|
||||||
|
|
||||||
|
{#if parameters.datasourceId}
|
||||||
|
<Label small>Query</Label>
|
||||||
|
<Select thin secondary bind:value={parameters.queryId}>
|
||||||
<option value="" />
|
<option value="" />
|
||||||
{#each $backendUiStore.datasources as datasource}
|
{#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query}
|
||||||
<option value={datasource._id}>{datasource.name}</option>
|
<option value={query._id}>{query.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Spacer medium />
|
<Spacer medium />
|
||||||
|
|
||||||
{#if parameters.datasourceId}
|
{#if query?.parameters?.length > 0}
|
||||||
<Label size="m" color="dark">Query</Label>
|
<ParameterBuilder
|
||||||
<Select thin secondary bind:value={parameters.queryId}>
|
bind:customParams={parameters.queryParams}
|
||||||
<option value="" />
|
parameters={query.parameters}
|
||||||
{#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query}
|
bindings={bindableProperties} />
|
||||||
<option value={query._id}>{query.name}</option>
|
<IntegrationQueryEditor
|
||||||
{/each}
|
height={200}
|
||||||
</Select>
|
{query}
|
||||||
{/if}
|
schema={fetchQueryDefinition(query)}
|
||||||
|
editable={false} />
|
||||||
<Spacer medium />
|
{/if}
|
||||||
|
|
||||||
{#if query?.parameters?.length > 0}
|
|
||||||
<ParameterBuilder
|
|
||||||
bind:customParams={parameters.queryParams}
|
|
||||||
parameters={query.parameters}
|
|
||||||
bindings={bindableProperties} />
|
|
||||||
{#if query.fields.sql}
|
|
||||||
<pre>{query.fields.queryString}</pre>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.root {
|
|
||||||
padding: var(--spacing-m);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -13,8 +13,10 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<Label size="m" color="dark">Screen</Label>
|
<Label small>Screen</Label>
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
|
title="Destination URL"
|
||||||
|
placeholder="/screen"
|
||||||
value={parameters.url}
|
value={parameters.url}
|
||||||
on:change={value => (parameters.url = value.detail)}
|
on:change={value => (parameters.url = value.detail)}
|
||||||
{bindings} />
|
{bindings} />
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Select, Label } from "@budibase/bbui"
|
|
||||||
import { currentAsset, store } from "builderStore"
|
|
||||||
import { getDataProviderComponents } from "builderStore/dataBinding"
|
|
||||||
|
|
||||||
export let parameters
|
|
||||||
|
|
||||||
$: dataProviders = getDataProviderComponents(
|
|
||||||
$currentAsset,
|
|
||||||
$store.selectedComponentId
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="root">
|
|
||||||
<Label size="m" color="dark">Form</Label>
|
|
||||||
<Select secondary bind:value={parameters.componentId}>
|
|
||||||
<option value="" />
|
|
||||||
{#if dataProviders}
|
|
||||||
{#each dataProviders as component}
|
|
||||||
<option value={component._id}>{component._instanceName}</option>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.root {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(> div) {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: var(--spacing-l);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,115 +1,89 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Label, TextButton, Spacer, Select, Input } from "@budibase/bbui"
|
||||||
DataList,
|
|
||||||
Label,
|
|
||||||
TextButton,
|
|
||||||
Spacer,
|
|
||||||
Select,
|
|
||||||
Input,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { store, currentAsset } from "builderStore"
|
import { store, currentAsset } from "builderStore"
|
||||||
import {
|
import { getBindableProperties } from "builderStore/dataBinding"
|
||||||
getBindableProperties,
|
|
||||||
readableToRuntimeBinding,
|
|
||||||
runtimeToReadableBinding,
|
|
||||||
} from "builderStore/dataBinding"
|
|
||||||
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
|
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let parameterFields
|
export let parameterFields
|
||||||
export let schemaFields
|
export let schemaFields
|
||||||
export let fieldLabel = "Column"
|
export let fieldLabel = "Column"
|
||||||
|
export let valueLabel = "Value"
|
||||||
|
|
||||||
const emptyField = () => ({ name: "", value: "" })
|
let fields = Object.entries(parameterFields || {})
|
||||||
|
$: onChange(fields)
|
||||||
$: bindableProperties = getBindableProperties(
|
$: bindableProperties = getBindableProperties(
|
||||||
$currentAsset,
|
$currentAsset,
|
||||||
$store.selectedComponentId
|
$store.selectedComponentId
|
||||||
)
|
)
|
||||||
|
|
||||||
// this statement initialises fields from parameters.fields
|
|
||||||
$: fields =
|
|
||||||
fields ||
|
|
||||||
Object.keys(parameterFields || {}).map(name => ({
|
|
||||||
name,
|
|
||||||
value:
|
|
||||||
(parameterFields &&
|
|
||||||
runtimeToReadableBinding(
|
|
||||||
bindableProperties,
|
|
||||||
parameterFields[name].value
|
|
||||||
)) ||
|
|
||||||
"",
|
|
||||||
}))
|
|
||||||
|
|
||||||
const addField = () => {
|
const addField = () => {
|
||||||
const newFields = fields.filter(f => f.name)
|
fields = [...fields.filter(field => field[0]), ["", ""]]
|
||||||
newFields.push(emptyField())
|
|
||||||
fields = newFields
|
|
||||||
rebuildParameters()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeField = field => () => {
|
const removeField = name => {
|
||||||
fields = fields.filter(f => f !== field)
|
fields = fields.filter(field => field[0] !== name)
|
||||||
rebuildParameters()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rebuildParameters = () => {
|
const updateFieldValue = (idx, value) => {
|
||||||
// rebuilds paramters.fields every time a field name or value is added
|
fields[idx][1] = value
|
||||||
// as UI below is bound to "fields" array, but we need to output a { key: value }
|
fields = fields
|
||||||
const newParameterFields = {}
|
|
||||||
for (let field of fields) {
|
|
||||||
if (field.name) {
|
|
||||||
// value and type is needed by the client, so it can parse
|
|
||||||
// a string into a correct type
|
|
||||||
newParameterFields[field.name] = {
|
|
||||||
type: schemaFields
|
|
||||||
? schemaFields.find(f => f.name === field.name).type
|
|
||||||
: "string",
|
|
||||||
value: readableToRuntimeBinding(bindableProperties, field.value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dispatch("fieldschanged", newParameterFields)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// just wraps binding in {{ ... }}
|
const updateFieldName = (idx, name) => {
|
||||||
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
|
fields[idx][0] = name
|
||||||
|
fields = fields
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = fields => {
|
||||||
|
const newParamFields = {}
|
||||||
|
fields
|
||||||
|
.filter(field => field[0])
|
||||||
|
.forEach(([field, value]) => {
|
||||||
|
newParamFields[field] = value
|
||||||
|
})
|
||||||
|
dispatch("change", newParamFields)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if fields}
|
{#if fields}
|
||||||
{#each fields as field}
|
{#each fields as field, idx}
|
||||||
<Label size="m" color="dark">{fieldLabel}</Label>
|
<Label small>{fieldLabel}</Label>
|
||||||
{#if schemaFields}
|
{#if schemaFields}
|
||||||
<Select secondary bind:value={field.name} on:blur={rebuildParameters}>
|
<Select
|
||||||
|
thin
|
||||||
|
secondary
|
||||||
|
value={field[0]}
|
||||||
|
on:change={event => updateFieldName(idx, event.target.value)}>
|
||||||
<option value="" />
|
<option value="" />
|
||||||
{#each schemaFields as schemaField}
|
{#each schemaFields as schemaField}
|
||||||
<option value={schemaField.name}>{schemaField.name}</option>
|
<option value={schemaField.name}>{schemaField.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{:else}
|
{:else}
|
||||||
<Input secondary bind:value={field.name} on:blur={rebuildParameters} />
|
<Input
|
||||||
|
thin
|
||||||
|
secondary
|
||||||
|
value={field[0]}
|
||||||
|
on:change={event => updateFieldName(idx, event.target.value)} />
|
||||||
{/if}
|
{/if}
|
||||||
<Label size="m" color="dark">Value</Label>
|
<Label small>{valueLabel}</Label>
|
||||||
<DataList secondary bind:value={field.value} on:blur={rebuildParameters}>
|
<DrawerBindableInput
|
||||||
<option value="" />
|
title={`Value for "${field[0]}"`}
|
||||||
{#each bindableProperties as bindableProp}
|
value={field[1]}
|
||||||
<option value={toBindingExpression(bindableProp.readableBinding)}>
|
bindings={bindableProperties}
|
||||||
{bindableProp.readableBinding}
|
on:change={event => updateFieldValue(idx, event.detail)} />
|
||||||
</option>
|
|
||||||
{/each}
|
|
||||||
</DataList>
|
|
||||||
<div class="remove-field-container">
|
<div class="remove-field-container">
|
||||||
<TextButton text small on:click={removeField(field)}>
|
<TextButton text small on:click={() => removeField(field[0])}>
|
||||||
<CloseCircleIcon />
|
<CloseCircleIcon />
|
||||||
</TextButton>
|
</TextButton>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Spacer small />
|
<Spacer small />
|
||||||
|
|
||||||
<TextButton text small blue on:click={addField}>
|
<TextButton text small blue on:click={addField}>
|
||||||
Add
|
Add
|
||||||
{fieldLabel}
|
{fieldLabel}
|
||||||
|
|
|
@ -37,8 +37,8 @@
|
||||||
Repeater
|
Repeater
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Label size="m" color="dark">Datasource</Label>
|
<Label small>Datasource</Label>
|
||||||
<Select secondary bind:value={parameters.providerId}>
|
<Select thin secondary bind:value={parameters.providerId}>
|
||||||
<option value="" />
|
<option value="" />
|
||||||
{#each dataProviderComponents as provider}
|
{#each dataProviderComponents as provider}
|
||||||
<option value={provider._id}>{provider._instanceName}</option>
|
<option value={provider._id}>{provider._instanceName}</option>
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
<SaveFields
|
<SaveFields
|
||||||
parameterFields={parameters.fields}
|
parameterFields={parameters.fields}
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
on:fieldschanged={onFieldsChanged} />
|
on:change={onFieldsChanged} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
<style>
|
<style>
|
||||||
.root {
|
.root {
|
||||||
display: grid;
|
display: grid;
|
||||||
column-gap: var(--spacing-s);
|
column-gap: var(--spacing-l);
|
||||||
row-gap: var(--spacing-s);
|
row-gap: var(--spacing-s);
|
||||||
grid-template-columns: auto 1fr auto 1fr auto;
|
grid-template-columns: auto 1fr auto 1fr auto;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
@ -71,8 +71,6 @@
|
||||||
.cannot-use {
|
.cannot-use {
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
text-align: center;
|
|
||||||
width: 70%;
|
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -27,13 +27,11 @@
|
||||||
schema,
|
schema,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$: hasAutomations = automations && automations.length > 0
|
$: hasAutomations = automations && automations.length > 0
|
||||||
|
$: selectedAutomation = automations?.find(
|
||||||
$: selectedAutomation =
|
a => a._id === parameters?.automationId
|
||||||
parameters &&
|
)
|
||||||
parameters.automationId &&
|
$: selectedSchema = selectedAutomation?.schema
|
||||||
automations.find(a => a._id === parameters.automationId)
|
|
||||||
|
|
||||||
const onFieldsChanged = e => {
|
const onFieldsChanged = e => {
|
||||||
parameters.fields = e.detail
|
parameters.fields = e.detail
|
||||||
|
@ -42,95 +40,98 @@
|
||||||
const setNew = () => {
|
const setNew = () => {
|
||||||
automationStatus = AUTOMATION_STATUS.NEW
|
automationStatus = AUTOMATION_STATUS.NEW
|
||||||
parameters.automationId = undefined
|
parameters.automationId = undefined
|
||||||
|
parameters.fields = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setExisting = () => {
|
const setExisting = () => {
|
||||||
automationStatus = AUTOMATION_STATUS.EXISTING
|
automationStatus = AUTOMATION_STATUS.EXISTING
|
||||||
parameters.newAutomationName = ""
|
parameters.newAutomationName = ""
|
||||||
|
parameters.fields = {}
|
||||||
|
parameters.automationId = automations[0]?._id
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="radio-container" on:click={setNew}>
|
<div class="radios">
|
||||||
<input
|
<div class="radio-container" on:click={setNew}>
|
||||||
type="radio"
|
<input
|
||||||
value={AUTOMATION_STATUS.NEW}
|
type="radio"
|
||||||
bind:group={automationStatus}
|
value={AUTOMATION_STATUS.NEW}
|
||||||
disabled={!hasAutomations} />
|
bind:group={automationStatus} />
|
||||||
|
<Label small>Create a new automation</Label>
|
||||||
<Label disabled={!hasAutomations}>Create a new automation</Label>
|
</div>
|
||||||
|
<div class="radio-container" on:click={hasAutomations ? setExisting : null}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value={AUTOMATION_STATUS.EXISTING}
|
||||||
|
bind:group={automationStatus}
|
||||||
|
disabled={!hasAutomations} />
|
||||||
|
<Label small grey={!hasAutomations}>Use an existing automation</Label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="radio-container" on:click={setExisting}>
|
<div class="fields">
|
||||||
<input
|
<Label small>Automation</Label>
|
||||||
type="radio"
|
|
||||||
value={AUTOMATION_STATUS.EXISTING}
|
|
||||||
bind:group={automationStatus}
|
|
||||||
disabled={!hasAutomations} />
|
|
||||||
|
|
||||||
<Label disabled={!hasAutomations}>Use an existing automation</Label>
|
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
|
||||||
|
<Select
|
||||||
|
thin
|
||||||
|
secondary
|
||||||
|
bind:value={parameters.automationId}
|
||||||
|
placeholder="Choose automation">
|
||||||
|
{#each automations as automation}
|
||||||
|
<option value={automation._id}>{automation.name}</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
{:else}
|
||||||
|
<Input
|
||||||
|
thin
|
||||||
|
bind:value={parameters.newAutomationName}
|
||||||
|
placeholder="Enter automation name" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#key parameters.automationId}
|
||||||
|
<SaveFields
|
||||||
|
schemaFields={selectedSchema}
|
||||||
|
parameterFields={parameters.fields}
|
||||||
|
fieldLabel="Field"
|
||||||
|
on:change={onFieldsChanged} />
|
||||||
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Label size="m" color="dark">Automation</Label>
|
|
||||||
|
|
||||||
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
|
|
||||||
<Select
|
|
||||||
secondary
|
|
||||||
bind:value={parameters.automationId}
|
|
||||||
placeholder="Choose automation">
|
|
||||||
<option value="" />
|
|
||||||
{#each automations as automation}
|
|
||||||
<option value={automation._id}>{automation.name}</option>
|
|
||||||
{/each}
|
|
||||||
</Select>
|
|
||||||
{:else}
|
|
||||||
<Input
|
|
||||||
secondary
|
|
||||||
bind:value={parameters.newAutomationName}
|
|
||||||
placeholder="Enter automation name" />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<SaveFields
|
|
||||||
schemaFields={automationStatus === AUTOMATION_STATUS.EXISTING && selectedAutomation && selectedAutomation.schema}
|
|
||||||
fieldLabel="Field"
|
|
||||||
on:fieldschanged={onFieldsChanged} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.root {
|
.fields {
|
||||||
display: grid;
|
display: grid;
|
||||||
column-gap: var(--spacing-s);
|
column-gap: var(--spacing-l);
|
||||||
row-gap: var(--spacing-s);
|
row-gap: var(--spacing-s);
|
||||||
grid-template-columns: auto 1fr auto 1fr auto;
|
grid-template-columns: auto 1fr auto 1fr auto;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(> div:nth-child(4)) {
|
.fields :global(> div:nth-child(2)) {
|
||||||
grid-column: 2 / span 4;
|
grid-column: 2 / span 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.radios,
|
||||||
.radio-container {
|
.radio-container {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: auto 1fr;
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.radios {
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
margin-bottom: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.radio-container {
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.radio-container :global(label) {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-container:nth-child(1) {
|
input[type="radio"]:checked {
|
||||||
grid-column: 1 / span 2;
|
background: var(--blue);
|
||||||
}
|
|
||||||
|
|
||||||
.radio-container:nth-child(2) {
|
|
||||||
grid-column: 3 / span 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-container :global(> label) {
|
|
||||||
margin-left: var(--spacing-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-container > input {
|
|
||||||
margin-bottom: var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-container > input:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -13,8 +13,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<Label size="m" color="dark">Form</Label>
|
<Label small>Form</Label>
|
||||||
<Select secondary bind:value={parameters.componentId}>
|
<Select thin secondary bind:value={parameters.componentId}>
|
||||||
<option value="" />
|
<option value="" />
|
||||||
{#if actionProviders}
|
{#if actionProviders}
|
||||||
{#each actionProviders as component}
|
{#each actionProviders as component}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import DeleteRow from "./DeleteRow.svelte"
|
||||||
import ExecuteQuery from "./ExecuteQuery.svelte"
|
import ExecuteQuery from "./ExecuteQuery.svelte"
|
||||||
import TriggerAutomation from "./TriggerAutomation.svelte"
|
import TriggerAutomation from "./TriggerAutomation.svelte"
|
||||||
import ValidateForm from "./ValidateForm.svelte"
|
import ValidateForm from "./ValidateForm.svelte"
|
||||||
import RefreshDatasource from "./RefreshDatasource.svelte"
|
|
||||||
|
|
||||||
// defines what actions are available, when adding a new one
|
// defines what actions are available, when adding a new one
|
||||||
// the component is the setup panel for the action
|
// the component is the setup panel for the action
|
||||||
|
@ -36,8 +35,4 @@ export default [
|
||||||
name: "Validate Form",
|
name: "Validate Form",
|
||||||
component: ValidateForm,
|
component: ValidateForm,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "Refresh Datasource",
|
|
||||||
component: RefreshDatasource,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
<script>
|
||||||
|
import { Button, Drawer, Spacer, Body } from "@budibase/bbui"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { notifier } from "builderStore/store/notifications"
|
||||||
|
import {
|
||||||
|
getDatasourceForProvider,
|
||||||
|
getSchemaForDatasource,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
import SaveFields from "./EventsEditor/actions/SaveFields.svelte"
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
export let value = {}
|
||||||
|
export let componentInstance
|
||||||
|
let drawer
|
||||||
|
let tempValue = value
|
||||||
|
|
||||||
|
$: schemaFields = getSchemaFields(componentInstance)
|
||||||
|
|
||||||
|
const getSchemaFields = component => {
|
||||||
|
const datasource = getDatasourceForProvider(component)
|
||||||
|
const { schema } = getSchemaForDatasource(datasource)
|
||||||
|
return Object.values(schema || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveFilter = async () => {
|
||||||
|
dispatch("change", tempValue)
|
||||||
|
notifier.success("Filters saved.")
|
||||||
|
drawer.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFieldsChanged = event => {
|
||||||
|
tempValue = event.detail
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button secondary wide on:click={drawer.show}>Define Filters</Button>
|
||||||
|
<Drawer bind:this={drawer} title={'Filtering'}>
|
||||||
|
<heading slot="buttons">
|
||||||
|
<Button thin blue on:click={saveFilter}>Save</Button>
|
||||||
|
</heading>
|
||||||
|
<div slot="body">
|
||||||
|
<div class="root">
|
||||||
|
<Body small grey>
|
||||||
|
{#if !Object.keys(tempValue || {}).length}
|
||||||
|
Add your first filter column.
|
||||||
|
{:else}
|
||||||
|
Results are filtered to only those which match all of the following
|
||||||
|
constaints.
|
||||||
|
{/if}
|
||||||
|
</Body>
|
||||||
|
<Spacer medium />
|
||||||
|
<div class="fields">
|
||||||
|
<SaveFields
|
||||||
|
parameterFields={value}
|
||||||
|
{schemaFields}
|
||||||
|
valueLabel="Equals"
|
||||||
|
on:change={onFieldsChanged} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
min-height: calc(40vh - 2 * var(--spacing-l));
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields {
|
||||||
|
display: grid;
|
||||||
|
column-gap: var(--spacing-l);
|
||||||
|
row-gap: var(--spacing-s);
|
||||||
|
grid-template-columns: auto 1fr auto 1fr auto;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -7,6 +7,7 @@
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
|
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
|
||||||
|
import { capitalise } from "../../../../helpers"
|
||||||
|
|
||||||
export let label = ""
|
export let label = ""
|
||||||
export let bindable = true
|
export let bindable = true
|
||||||
|
@ -88,26 +89,26 @@
|
||||||
on:click={bindingDrawer.show}>
|
on:click={bindingDrawer.show}>
|
||||||
<Icon name="lightning" />
|
<Icon name="lightning" />
|
||||||
</div>
|
</div>
|
||||||
|
<Drawer bind:this={bindingDrawer} title={capitalise(key)}>
|
||||||
|
<div slot="description">
|
||||||
|
<Body extraSmall grey>
|
||||||
|
Add the objects on the left to enrich your text.
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<heading slot="buttons">
|
||||||
|
<Button thin blue disabled={!valid} on:click={handleClose}>Save</Button>
|
||||||
|
</heading>
|
||||||
|
<div slot="body">
|
||||||
|
<BindingPanel
|
||||||
|
bind:valid
|
||||||
|
value={safeValue}
|
||||||
|
close={handleClose}
|
||||||
|
on:update={e => (temporaryBindableValue = e.detail)}
|
||||||
|
{bindableProperties} />
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Drawer bind:this={bindingDrawer} title="Bindings">
|
|
||||||
<div slot="description">
|
|
||||||
<Body extraSmall grey>
|
|
||||||
Add the objects on the left to enrich your text.
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
<heading slot="buttons">
|
|
||||||
<Button thin blue disabled={!valid} on:click={handleClose}>Save</Button>
|
|
||||||
</heading>
|
|
||||||
<div slot="body">
|
|
||||||
<BindingPanel
|
|
||||||
bind:valid
|
|
||||||
value={safeValue}
|
|
||||||
close={handleClose}
|
|
||||||
on:update={e => (temporaryBindableValue = e.detail)}
|
|
||||||
{bindableProperties} />
|
|
||||||
</div>
|
|
||||||
</Drawer>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.property-control {
|
.property-control {
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
|
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
|
||||||
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
|
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
|
||||||
import EventsEditor from "./PropertyControls/EventsEditor"
|
import EventsEditor from "./PropertyControls/EventsEditor"
|
||||||
|
import FilterEditor from "./PropertyControls/FilterEditor.svelte"
|
||||||
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
|
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
|
||||||
import { IconSelect } from "./PropertyControls/IconSelect"
|
import { IconSelect } from "./PropertyControls/IconSelect"
|
||||||
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
|
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
|
||||||
|
@ -71,6 +72,7 @@
|
||||||
field: FieldSelect,
|
field: FieldSelect,
|
||||||
multifield: MultiFieldSelect,
|
multifield: MultiFieldSelect,
|
||||||
schema: SchemaSelect,
|
schema: SchemaSelect,
|
||||||
|
filter: FilterEditor,
|
||||||
"field/string": StringFieldSelect,
|
"field/string": StringFieldSelect,
|
||||||
"field/number": NumberFieldSelect,
|
"field/number": NumberFieldSelect,
|
||||||
"field/options": OptionsFieldSelect,
|
"field/options": OptionsFieldSelect,
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
export let lineNumbers = true
|
export let lineNumbers = true
|
||||||
export let tab = true
|
export let tab = true
|
||||||
export let mode
|
export let mode
|
||||||
|
export let editorHeight = 500
|
||||||
// export let parameters = []
|
// export let parameters = []
|
||||||
|
|
||||||
let completions = handlebarsCompletions()
|
let completions = handlebarsCompletions()
|
||||||
|
@ -171,17 +172,21 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Label small>{label}</Label>
|
{#if label}
|
||||||
<Spacer medium />
|
<Label small>{label}</Label>
|
||||||
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
|
<Spacer medium />
|
||||||
|
{/if}
|
||||||
|
<div style={`--code-mirror-height: ${editorHeight}px`}>
|
||||||
|
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
textarea {
|
textarea {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.CodeMirror) {
|
div :global(.CodeMirror) {
|
||||||
height: 500px !important;
|
height: var(--code-mirror-height) !important;
|
||||||
border-radius: var(--border-radius-s);
|
border-radius: var(--border-radius-s);
|
||||||
font-family: monospace !important;
|
font-family: monospace !important;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
|
|
|
@ -1,13 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Label, Spacer, Input } from "@budibase/bbui"
|
||||||
Button,
|
|
||||||
TextArea,
|
|
||||||
Label,
|
|
||||||
Input,
|
|
||||||
Heading,
|
|
||||||
Select,
|
|
||||||
Spacer,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import Editor from "./QueryEditor.svelte"
|
import Editor from "./QueryEditor.svelte"
|
||||||
import KeyValueBuilder from "./KeyValueBuilder.svelte"
|
import KeyValueBuilder from "./KeyValueBuilder.svelte"
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { Body, Button, Input, Heading, Spacer } from "@budibase/bbui"
|
import { Body, Button, Input, Heading, Spacer } from "@budibase/bbui"
|
||||||
import BindableInput from "components/common/BindableInput.svelte"
|
|
||||||
import {
|
import {
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
|
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
export let bindable = true
|
export let bindable = true
|
||||||
export let parameters = []
|
export let parameters = []
|
||||||
|
@ -37,8 +37,13 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Body small grey>
|
<Body small grey>
|
||||||
Parameters come in two parts: the parameter name, and a default/fallback
|
{#if !bindable}
|
||||||
value.
|
Parameters come in two parts: the parameter name, and a default/fallback
|
||||||
|
value.
|
||||||
|
{:else}
|
||||||
|
Enter a value for each parameter. The default values will be used for any
|
||||||
|
values left blank.
|
||||||
|
{/if}
|
||||||
</Body>
|
</Body>
|
||||||
<Spacer large />
|
<Spacer large />
|
||||||
<div class="parameters" class:bindable>
|
<div class="parameters" class:bindable>
|
||||||
|
@ -54,9 +59,9 @@
|
||||||
disabled={bindable}
|
disabled={bindable}
|
||||||
bind:value={parameter.default} />
|
bind:value={parameter.default} />
|
||||||
{#if bindable}
|
{#if bindable}
|
||||||
<BindableInput
|
<DrawerBindableInput
|
||||||
|
title={`Query parameter "${parameter.name}"`}
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
type="string"
|
|
||||||
thin
|
thin
|
||||||
on:change={evt => onBindingChange(parameter.name, evt.detail)}
|
on:change={evt => onBindingChange(parameter.name, evt.detail)}
|
||||||
value={runtimeToReadableBinding(bindings, customParams?.[parameter.name])}
|
value={runtimeToReadableBinding(bindings, customParams?.[parameter.name])}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto } from "@sveltech/routify"
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
@ -7,7 +6,6 @@
|
||||||
Body,
|
Body,
|
||||||
Label,
|
Label,
|
||||||
Input,
|
Input,
|
||||||
TextArea,
|
|
||||||
Heading,
|
Heading,
|
||||||
Spacer,
|
Spacer,
|
||||||
Switcher,
|
Switcher,
|
||||||
|
@ -133,14 +131,15 @@
|
||||||
<section class="config">
|
<section class="config">
|
||||||
<Heading medium lh>Query {integrationInfo?.friendlyName}</Heading>
|
<Heading medium lh>Query {integrationInfo?.friendlyName}</Heading>
|
||||||
<hr />
|
<hr />
|
||||||
|
<Spacer extraLarge />
|
||||||
<Heading small lh>Config</Heading>
|
<Heading small lh>Config</Heading>
|
||||||
<Body small grey>Provide a name for your query and select its function.</Body>
|
<Body small grey>Provide a name for your query and select its function.</Body>
|
||||||
<Spacer medium />
|
<Spacer large />
|
||||||
<div class="config-field">
|
<div class="config-field">
|
||||||
<Label small>Query Name</Label>
|
<Label small>Query Name</Label>
|
||||||
<Input thin outline bind:value={query.name} />
|
<Input thin outline bind:value={query.name} />
|
||||||
</div>
|
</div>
|
||||||
<Spacer medium />
|
<Spacer extraLarge />
|
||||||
{#if queryConfig}
|
{#if queryConfig}
|
||||||
<div class="config-field">
|
<div class="config-field">
|
||||||
<Label small>Function</Label>
|
<Label small>Function</Label>
|
||||||
|
@ -152,7 +151,10 @@
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
<Spacer extraLarge />
|
||||||
<hr />
|
<hr />
|
||||||
|
<Spacer extraLarge />
|
||||||
|
<Spacer small />
|
||||||
<ParameterBuilder bind:parameters={query.parameters} bindable={false} />
|
<ParameterBuilder bind:parameters={query.parameters} bindable={false} />
|
||||||
<hr />
|
<hr />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -160,20 +162,24 @@
|
||||||
|
|
||||||
{#if shouldShowQueryConfig}
|
{#if shouldShowQueryConfig}
|
||||||
<section>
|
<section>
|
||||||
|
<Spacer extraLarge />
|
||||||
|
<Spacer small />
|
||||||
<div class="config">
|
<div class="config">
|
||||||
<Heading small lh>Fields</Heading>
|
<Heading small lh>Fields</Heading>
|
||||||
<Body small grey>Fill in the fields specific to this query.</Body>
|
<Body small grey>Fill in the fields specific to this query.</Body>
|
||||||
<Spacer medium />
|
<Spacer medium />
|
||||||
|
<Spacer extraLarge />
|
||||||
<IntegrationQueryEditor
|
<IntegrationQueryEditor
|
||||||
{datasource}
|
{datasource}
|
||||||
{query}
|
{query}
|
||||||
schema={queryConfig[query.queryVerb]}
|
schema={queryConfig[query.queryVerb]}
|
||||||
bind:parameters />
|
bind:parameters />
|
||||||
|
<Spacer extraLarge />
|
||||||
<hr />
|
<hr />
|
||||||
|
<Spacer extraLarge />
|
||||||
|
<Spacer medium />
|
||||||
<div class="viewer-controls">
|
<div class="viewer-controls">
|
||||||
<Heading small lh>Query Results</Heading>
|
<Heading small lh>Results</Heading>
|
||||||
<div class="button-container">
|
<div class="button-container">
|
||||||
<Button
|
<Button
|
||||||
secondary
|
secondary
|
||||||
|
@ -191,20 +197,17 @@
|
||||||
schema.
|
schema.
|
||||||
</Body>
|
</Body>
|
||||||
|
|
||||||
<Spacer large />
|
<Spacer extraLarge />
|
||||||
|
<Spacer medium />
|
||||||
|
|
||||||
<section class="viewer">
|
<section class="viewer">
|
||||||
{#if data}
|
{#if data}
|
||||||
<Switcher headings={PREVIEW_HEADINGS} bind:value={tab}>
|
<Switcher headings={PREVIEW_HEADINGS} bind:value={tab}>
|
||||||
{#if tab === 'JSON'}
|
{#if tab === 'JSON'}
|
||||||
<pre class="preview">
|
<pre class="preview">
|
||||||
{#if !data[0]}
|
{#if !data[0]}
|
||||||
|
|
||||||
|
Please run your query to fetch some data.
|
||||||
|
|
||||||
Please run your query to fetch some data.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
{JSON.stringify(data[0], undefined, 2)}
|
{JSON.stringify(data[0], undefined, 2)}
|
||||||
|
@ -242,6 +245,8 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
<Spacer extraLarge />
|
||||||
|
<Spacer extraLarge />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.config-field {
|
.config-field {
|
||||||
|
@ -263,7 +268,7 @@
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin-top: var(--layout-m);
|
margin-top: var(--layout-m);
|
||||||
margin-bottom: var(--layout-m);
|
border: 1px solid var(--grey-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.config {
|
.config {
|
||||||
|
@ -275,12 +280,20 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.viewer {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
.preview {
|
.preview {
|
||||||
width: 800px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 120px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
background-color: var(--grey-1);
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--grey-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewer-controls {
|
.viewer-controls {
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { TextArea, Label, Input, Heading, Spacer } from "@budibase/bbui"
|
|
||||||
import Editor from "./QueryEditor.svelte"
|
import Editor from "./QueryEditor.svelte"
|
||||||
import ParameterBuilder from "./QueryParameterBuilder.svelte"
|
|
||||||
import FieldsBuilder from "./QueryFieldsBuilder.svelte"
|
import FieldsBuilder from "./QueryFieldsBuilder.svelte"
|
||||||
|
import { Label, Input } from "@budibase/bbui"
|
||||||
|
|
||||||
const QueryTypes = {
|
const QueryTypes = {
|
||||||
SQL: "sql",
|
SQL: "sql",
|
||||||
|
@ -15,6 +13,7 @@
|
||||||
export let datasource
|
export let datasource
|
||||||
export let schema
|
export let schema
|
||||||
export let editable = true
|
export let editable = true
|
||||||
|
export let height = 500
|
||||||
|
|
||||||
$: urlDisplay =
|
$: urlDisplay =
|
||||||
schema.urlDisplay &&
|
schema.urlDisplay &&
|
||||||
|
@ -29,6 +28,7 @@
|
||||||
{#key query._id}
|
{#key query._id}
|
||||||
{#if schema.type === QueryTypes.SQL}
|
{#if schema.type === QueryTypes.SQL}
|
||||||
<Editor
|
<Editor
|
||||||
|
editorHeight={height}
|
||||||
label="Query"
|
label="Query"
|
||||||
mode="sql"
|
mode="sql"
|
||||||
on:change={updateQuery}
|
on:change={updateQuery}
|
||||||
|
@ -37,6 +37,7 @@
|
||||||
parameters={query.parameters} />
|
parameters={query.parameters} />
|
||||||
{:else if schema.type === QueryTypes.JSON}
|
{:else if schema.type === QueryTypes.JSON}
|
||||||
<Editor
|
<Editor
|
||||||
|
editorHeight={height}
|
||||||
label="Query"
|
label="Query"
|
||||||
mode="json"
|
mode="json"
|
||||||
on:change={updateQuery}
|
on:change={updateQuery}
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
|
border-right: 1px solid var(--grey-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|
|
@ -80,6 +80,7 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
border-right: 1px solid var(--grey-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
|
|
|
@ -28,19 +28,24 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
{#if $backendUiStore.selectedDatabase._id && selectedQuery}
|
<div class="inner">
|
||||||
<QueryInterface query={selectedQuery} />
|
{#if $backendUiStore.selectedDatabase._id && selectedQuery}
|
||||||
{/if}
|
<QueryInterface query={selectedQuery} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
section {
|
section {
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 0px;
|
width: 0px;
|
||||||
background: transparent; /* make scrollbar transparent */
|
background: transparent; /* make scrollbar transparent */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
width: 640px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -45,20 +45,22 @@
|
||||||
|
|
||||||
{#if datasource}
|
{#if datasource}
|
||||||
<section>
|
<section>
|
||||||
<Spacer medium />
|
<Spacer extraLarge />
|
||||||
<header>
|
<header>
|
||||||
<div class="datasource-icon">
|
<div class="datasource-icon">
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={ICONS[datasource.source]}
|
this={ICONS[datasource.source]}
|
||||||
height="30"
|
height="26"
|
||||||
width="30" />
|
width="26" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="section-title">{datasource.name}</h3>
|
<h3 class="section-title">{datasource.name}</h3>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Body small grey lh>{integration.description}</Body>
|
<Body small grey lh>{integration.description}</Body>
|
||||||
|
<Spacer extraLarge />
|
||||||
<hr />
|
<hr />
|
||||||
|
<Spacer large />
|
||||||
|
<Spacer extraLarge />
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="config-header">
|
<div class="config-header">
|
||||||
|
@ -70,14 +72,14 @@
|
||||||
Connect your database to Budibase using the config below.
|
Connect your database to Budibase using the config below.
|
||||||
</Body>
|
</Body>
|
||||||
|
|
||||||
<Spacer medium />
|
<Spacer extraLarge />
|
||||||
<IntegrationConfigForm
|
<IntegrationConfigForm
|
||||||
integration={datasource.config}
|
integration={datasource.config}
|
||||||
on:change={setUnsaved} />
|
on:change={setUnsaved} />
|
||||||
<Spacer medium />
|
<Spacer extraLarge />
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
<Spacer large />
|
||||||
|
<Spacer extraLarge />
|
||||||
<div class="query-header">
|
<div class="query-header">
|
||||||
<Heading small>Queries</Heading>
|
<Heading small>Queries</Heading>
|
||||||
<Button secondary on:click={() => $goto('../new')}>Add Query</Button>
|
<Button secondary on:click={() => $goto('../new')}>Add Query</Button>
|
||||||
|
@ -99,15 +101,16 @@
|
||||||
<style>
|
<style>
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 800px;
|
width: 640px;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin-bottom: var(--layout-m);
|
border: 1px solid var(--grey-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* API cache for cached request responses.
|
* API cache for cached request responses.
|
||||||
*/
|
*/
|
||||||
import { notificationStore } from "../store/notification"
|
import { notificationStore } from "../store"
|
||||||
let cache = {}
|
let cache = {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,6 +34,9 @@ const makeApiCall = async ({ method, url, body, json = true }) => {
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 200:
|
case 200:
|
||||||
return response.json()
|
return response.json()
|
||||||
|
case 401:
|
||||||
|
notificationStore.danger("Invalid credentials")
|
||||||
|
return handleError(`Invalid credentials`)
|
||||||
case 404:
|
case 404:
|
||||||
notificationStore.danger("Not found")
|
notificationStore.danger("Not found")
|
||||||
return handleError(`${url}: Not Found`)
|
return handleError(`${url}: Not Found`)
|
||||||
|
|
|
@ -15,7 +15,7 @@ export const createDatasourceStore = () => {
|
||||||
let datasourceIds = []
|
let datasourceIds = []
|
||||||
|
|
||||||
// Extract table ID
|
// Extract table ID
|
||||||
if (datasource.type === "table") {
|
if (datasource.type === "table" || datasource.type === "view") {
|
||||||
if (datasource.tableId) {
|
if (datasource.tableId) {
|
||||||
datasourceIds.push(datasource.tableId)
|
datasourceIds.push(datasource.tableId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ const saveRowHandler = async (action, context) => {
|
||||||
if (providerId) {
|
if (providerId) {
|
||||||
let draft = context[providerId]
|
let draft = context[providerId]
|
||||||
if (fields) {
|
if (fields) {
|
||||||
for (let [key, entry] of Object.entries(fields)) {
|
for (let [field, value] of Object.entries(fields)) {
|
||||||
draft[key] = entry.value
|
draft[field] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await saveRow(draft)
|
await saveRow(draft)
|
||||||
|
@ -26,11 +26,7 @@ const deleteRowHandler = async action => {
|
||||||
const triggerAutomationHandler = async action => {
|
const triggerAutomationHandler = async action => {
|
||||||
const { fields } = action.parameters
|
const { fields } = action.parameters
|
||||||
if (fields) {
|
if (fields) {
|
||||||
const params = {}
|
await triggerAutomation(action.parameters.automationId, fields)
|
||||||
for (let field in fields) {
|
|
||||||
params[field] = fields[field].value
|
|
||||||
}
|
|
||||||
await triggerAutomation(action.parameters.automationId, params)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,9 @@ const { generateUserID } = require("../../db/utils")
|
||||||
const { setCookie } = require("../../utilities")
|
const { setCookie } = require("../../utilities")
|
||||||
const { outputProcessing } = require("../../utilities/rowProcessor")
|
const { outputProcessing } = require("../../utilities/rowProcessor")
|
||||||
const { ViewNames } = require("../../db/utils")
|
const { ViewNames } = require("../../db/utils")
|
||||||
|
const { UserStatus } = require("../../constants")
|
||||||
|
|
||||||
|
const INVALID_ERR = "Invalid Credentials"
|
||||||
|
|
||||||
exports.authenticate = async ctx => {
|
exports.authenticate = async ctx => {
|
||||||
const appId = ctx.appId
|
const appId = ctx.appId
|
||||||
|
@ -27,7 +30,12 @@ exports.authenticate = async ctx => {
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// do not want to throw a 404 - as this could be
|
// do not want to throw a 404 - as this could be
|
||||||
// used to determine valid emails
|
// used to determine valid emails
|
||||||
ctx.throw(401, "Invalid Credentials")
|
ctx.throw(401, INVALID_ERR)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that the user is currently inactive, if this is the case throw invalid
|
||||||
|
if (dbUser.status === UserStatus.INACTIVE) {
|
||||||
|
ctx.throw(401, INVALID_ERR)
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticate
|
// authenticate
|
||||||
|
@ -56,7 +64,7 @@ exports.authenticate = async ctx => {
|
||||||
appId,
|
appId,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(401, "Invalid credentials.")
|
ctx.throw(401, INVALID_ERR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,285 +0,0 @@
|
||||||
const CouchDB = require("../../db")
|
|
||||||
const linkRows = require("../../db/linkedRows")
|
|
||||||
const csvParser = require("../../utilities/csvParser")
|
|
||||||
const {
|
|
||||||
getRowParams,
|
|
||||||
getTableParams,
|
|
||||||
generateTableID,
|
|
||||||
generateRowID,
|
|
||||||
} = require("../../db/utils")
|
|
||||||
const { isEqual } = require("lodash/fp")
|
|
||||||
const { FieldTypes, AutoFieldSubTypes } = require("../../constants")
|
|
||||||
const { inputProcessing } = require("../../utilities/rowProcessor")
|
|
||||||
|
|
||||||
async function checkForColumnUpdates(db, oldTable, updatedTable) {
|
|
||||||
let updatedRows
|
|
||||||
const rename = updatedTable._rename
|
|
||||||
let deletedColumns = []
|
|
||||||
if (oldTable && oldTable.schema && updatedTable.schema) {
|
|
||||||
deletedColumns = Object.keys(oldTable.schema).filter(
|
|
||||||
colName => updatedTable.schema[colName] == null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// check for renaming of columns or deleted columns
|
|
||||||
if (rename || deletedColumns.length !== 0) {
|
|
||||||
const rows = await db.allDocs(
|
|
||||||
getRowParams(updatedTable._id, null, {
|
|
||||||
include_docs: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
updatedRows = rows.rows.map(({ doc }) => {
|
|
||||||
if (rename) {
|
|
||||||
doc[rename.updated] = doc[rename.old]
|
|
||||||
delete doc[rename.old]
|
|
||||||
} else if (deletedColumns.length !== 0) {
|
|
||||||
deletedColumns.forEach(colName => delete doc[colName])
|
|
||||||
}
|
|
||||||
return doc
|
|
||||||
})
|
|
||||||
delete updatedTable._rename
|
|
||||||
}
|
|
||||||
return updatedRows
|
|
||||||
}
|
|
||||||
|
|
||||||
// makes sure the passed in table isn't going to reset the auto ID
|
|
||||||
function makeSureTableUpToDate(table, tableToSave) {
|
|
||||||
if (!table) {
|
|
||||||
return tableToSave
|
|
||||||
}
|
|
||||||
// sure sure rev is up to date
|
|
||||||
tableToSave._rev = table._rev
|
|
||||||
// make sure auto IDs are always updated - these are internal
|
|
||||||
// so the client may not know they have changed
|
|
||||||
for (let [field, column] of Object.entries(table.schema)) {
|
|
||||||
if (
|
|
||||||
column.autocolumn &&
|
|
||||||
column.subtype === AutoFieldSubTypes.AUTO_ID &&
|
|
||||||
tableToSave.schema[field]
|
|
||||||
) {
|
|
||||||
tableToSave.schema[field].lastID = column.lastID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tableToSave
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDataImport(user, table, dataImport) {
|
|
||||||
const db = new CouchDB(user.appId)
|
|
||||||
if (dataImport && dataImport.csvString) {
|
|
||||||
// Populate the table with rows imported from CSV in a bulk update
|
|
||||||
const data = await csvParser.transform(dataImport)
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
let row = data[i]
|
|
||||||
row._id = generateRowID(table._id)
|
|
||||||
row.tableId = table._id
|
|
||||||
const processed = inputProcessing(user, table, row)
|
|
||||||
row = processed.row
|
|
||||||
// these auto-fields will never actually link anywhere (always builder)
|
|
||||||
for (let [fieldName, schema] of Object.entries(table.schema)) {
|
|
||||||
if (
|
|
||||||
schema.autocolumn &&
|
|
||||||
(schema.subtype === AutoFieldSubTypes.CREATED_BY ||
|
|
||||||
schema.subtype === AutoFieldSubTypes.UPDATED_BY)
|
|
||||||
) {
|
|
||||||
delete row[fieldName]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
table = processed.table
|
|
||||||
data[i] = row
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.bulkDocs(data)
|
|
||||||
let response = await db.put(table)
|
|
||||||
table._rev = response._rev
|
|
||||||
}
|
|
||||||
return table
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSearchIndexes(db, table) {
|
|
||||||
// create relevant search indexes
|
|
||||||
if (table.indexes && table.indexes.length > 0) {
|
|
||||||
const currentIndexes = await db.getIndexes()
|
|
||||||
const indexName = `search:${table._id}`
|
|
||||||
|
|
||||||
const existingIndex = currentIndexes.indexes.find(
|
|
||||||
existing => existing.name === indexName
|
|
||||||
)
|
|
||||||
|
|
||||||
if (existingIndex) {
|
|
||||||
const currentFields = existingIndex.def.fields.map(
|
|
||||||
field => Object.keys(field)[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
// if index fields have changed, delete the original index
|
|
||||||
if (!isEqual(currentFields, table.indexes)) {
|
|
||||||
await db.deleteIndex(existingIndex)
|
|
||||||
// create/recreate the index with fields
|
|
||||||
await db.createIndex({
|
|
||||||
index: {
|
|
||||||
fields: table.indexes,
|
|
||||||
name: indexName,
|
|
||||||
ddoc: "search_ddoc",
|
|
||||||
type: "json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// create/recreate the index with fields
|
|
||||||
await db.createIndex({
|
|
||||||
index: {
|
|
||||||
fields: table.indexes,
|
|
||||||
name: indexName,
|
|
||||||
ddoc: "search_ddoc",
|
|
||||||
type: "json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return table
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
|
||||||
const db = new CouchDB(ctx.user.appId)
|
|
||||||
const body = await db.allDocs(
|
|
||||||
getTableParams(null, {
|
|
||||||
include_docs: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
ctx.body = body.rows.map(row => row.doc)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.find = async function(ctx) {
|
|
||||||
const db = new CouchDB(ctx.user.appId)
|
|
||||||
ctx.body = await db.get(ctx.params.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.save = async function(ctx) {
|
|
||||||
const appId = ctx.user.appId
|
|
||||||
const db = new CouchDB(appId)
|
|
||||||
const { dataImport, ...rest } = ctx.request.body
|
|
||||||
let tableToSave = {
|
|
||||||
type: "table",
|
|
||||||
_id: generateTableID(),
|
|
||||||
views: {},
|
|
||||||
...rest,
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the table obj had an _id then it will have been retrieved
|
|
||||||
let oldTable
|
|
||||||
if (ctx.request.body && ctx.request.body._id) {
|
|
||||||
oldTable = await db.get(ctx.request.body._id)
|
|
||||||
tableToSave = makeSureTableUpToDate(oldTable, tableToSave)
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure that types don't change of a column, have to remove
|
|
||||||
// the column if you want to change the type
|
|
||||||
if (oldTable && oldTable.schema) {
|
|
||||||
for (let propKey of Object.keys(tableToSave.schema)) {
|
|
||||||
let column = tableToSave.schema[propKey]
|
|
||||||
let oldColumn = oldTable.schema[propKey]
|
|
||||||
if (oldColumn && oldColumn.type !== column.type) {
|
|
||||||
ctx.throw(400, "Cannot change the type of a column")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't rename if the name is the same
|
|
||||||
let { _rename } = tableToSave
|
|
||||||
if (_rename && _rename.old === _rename.updated) {
|
|
||||||
_rename = null
|
|
||||||
delete tableToSave._rename
|
|
||||||
}
|
|
||||||
|
|
||||||
// rename row fields when table column is renamed
|
|
||||||
if (_rename && tableToSave.schema[_rename.updated].type === FieldTypes.LINK) {
|
|
||||||
ctx.throw(400, "Cannot rename a linked column.")
|
|
||||||
} else if (_rename && tableToSave.primaryDisplay === _rename.old) {
|
|
||||||
ctx.throw(400, "Cannot rename the display column.")
|
|
||||||
}
|
|
||||||
|
|
||||||
let updatedRows = await checkForColumnUpdates(db, oldTable, tableToSave)
|
|
||||||
|
|
||||||
// update schema of non-statistics views when new columns are added
|
|
||||||
for (let view in tableToSave.views) {
|
|
||||||
const tableView = tableToSave.views[view]
|
|
||||||
if (!tableView) continue
|
|
||||||
|
|
||||||
if (tableView.schema.group || tableView.schema.field) continue
|
|
||||||
tableView.schema = tableToSave.schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// update linked rows
|
|
||||||
const linkResp = await linkRows.updateLinks({
|
|
||||||
appId,
|
|
||||||
eventType: oldTable
|
|
||||||
? linkRows.EventType.TABLE_UPDATED
|
|
||||||
: linkRows.EventType.TABLE_SAVE,
|
|
||||||
table: tableToSave,
|
|
||||||
oldTable: oldTable,
|
|
||||||
})
|
|
||||||
if (linkResp != null && linkResp._rev) {
|
|
||||||
tableToSave._rev = linkResp._rev
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't perform any updates until relationships have been
|
|
||||||
// checked by the updateLinks function
|
|
||||||
if (updatedRows && updatedRows.length !== 0) {
|
|
||||||
await db.bulkDocs(updatedRows)
|
|
||||||
}
|
|
||||||
const result = await db.post(tableToSave)
|
|
||||||
tableToSave._rev = result.rev
|
|
||||||
|
|
||||||
tableToSave = await handleSearchIndexes(db, tableToSave)
|
|
||||||
tableToSave = await handleDataImport(ctx.user, tableToSave, dataImport)
|
|
||||||
|
|
||||||
ctx.eventEmitter &&
|
|
||||||
ctx.eventEmitter.emitTable(`table:save`, appId, tableToSave)
|
|
||||||
|
|
||||||
ctx.status = 200
|
|
||||||
ctx.message = `Table ${ctx.request.body.name} saved successfully.`
|
|
||||||
ctx.body = tableToSave
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.destroy = async function(ctx) {
|
|
||||||
const appId = ctx.user.appId
|
|
||||||
const db = new CouchDB(appId)
|
|
||||||
const tableToDelete = await db.get(ctx.params.tableId)
|
|
||||||
|
|
||||||
// Delete all rows for that table
|
|
||||||
const rows = await db.allDocs(
|
|
||||||
getRowParams(ctx.params.tableId, null, {
|
|
||||||
include_docs: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
await db.bulkDocs(rows.rows.map(row => ({ ...row.doc, _deleted: true })))
|
|
||||||
|
|
||||||
// update linked rows
|
|
||||||
await linkRows.updateLinks({
|
|
||||||
appId,
|
|
||||||
eventType: linkRows.EventType.TABLE_DELETE,
|
|
||||||
table: tableToDelete,
|
|
||||||
})
|
|
||||||
|
|
||||||
// don't remove the table itself until very end
|
|
||||||
await db.remove(tableToDelete)
|
|
||||||
|
|
||||||
// remove table search index
|
|
||||||
const currentIndexes = await db.getIndexes()
|
|
||||||
const existingIndex = currentIndexes.indexes.find(
|
|
||||||
existing => existing.name === `search:${ctx.params.tableId}`
|
|
||||||
)
|
|
||||||
if (existingIndex) {
|
|
||||||
await db.deleteIndex(existingIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.eventEmitter &&
|
|
||||||
ctx.eventEmitter.emitTable(`table:delete`, appId, tableToDelete)
|
|
||||||
ctx.status = 200
|
|
||||||
ctx.message = `Table ${ctx.params.tableId} deleted.`
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.validateCSVSchema = async function(ctx) {
|
|
||||||
const { csvString, schema = {} } = ctx.request.body
|
|
||||||
const result = await csvParser.parse(csvString, schema)
|
|
||||||
ctx.body = { schema: result }
|
|
||||||
}
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
const CouchDB = require("../../../db")
|
||||||
|
const linkRows = require("../../../db/linkedRows")
|
||||||
|
const csvParser = require("../../../utilities/csvParser")
|
||||||
|
const {
|
||||||
|
getRowParams,
|
||||||
|
getTableParams,
|
||||||
|
generateTableID,
|
||||||
|
} = require("../../../db/utils")
|
||||||
|
const { FieldTypes } = require("../../../constants")
|
||||||
|
const { TableSaveFunctions } = require("./utils")
|
||||||
|
|
||||||
|
exports.fetch = async function(ctx) {
|
||||||
|
const db = new CouchDB(ctx.user.appId)
|
||||||
|
const body = await db.allDocs(
|
||||||
|
getTableParams(null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
ctx.body = body.rows.map(row => row.doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.find = async function(ctx) {
|
||||||
|
const db = new CouchDB(ctx.user.appId)
|
||||||
|
ctx.body = await db.get(ctx.params.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.save = async function(ctx) {
|
||||||
|
const appId = ctx.user.appId
|
||||||
|
const db = new CouchDB(appId)
|
||||||
|
const { dataImport, ...rest } = ctx.request.body
|
||||||
|
let tableToSave = {
|
||||||
|
type: "table",
|
||||||
|
_id: generateTableID(),
|
||||||
|
views: {},
|
||||||
|
...rest,
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the table obj had an _id then it will have been retrieved
|
||||||
|
let oldTable
|
||||||
|
if (ctx.request.body && ctx.request.body._id) {
|
||||||
|
oldTable = await db.get(ctx.request.body._id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// saving a table is a complex operation, involving many different steps, this
|
||||||
|
// has been broken out into a utility to make it more obvious/easier to manipulate
|
||||||
|
const tableSaveFunctions = new TableSaveFunctions({
|
||||||
|
db,
|
||||||
|
ctx,
|
||||||
|
oldTable,
|
||||||
|
dataImport,
|
||||||
|
})
|
||||||
|
tableToSave = await tableSaveFunctions.before(tableToSave)
|
||||||
|
|
||||||
|
// make sure that types don't change of a column, have to remove
|
||||||
|
// the column if you want to change the type
|
||||||
|
if (oldTable && oldTable.schema) {
|
||||||
|
for (let propKey of Object.keys(tableToSave.schema)) {
|
||||||
|
let column = tableToSave.schema[propKey]
|
||||||
|
let oldColumn = oldTable.schema[propKey]
|
||||||
|
if (oldColumn && oldColumn.type !== column.type) {
|
||||||
|
ctx.throw(400, "Cannot change the type of a column")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't rename if the name is the same
|
||||||
|
let { _rename } = tableToSave
|
||||||
|
if (_rename && _rename.old === _rename.updated) {
|
||||||
|
_rename = null
|
||||||
|
delete tableToSave._rename
|
||||||
|
}
|
||||||
|
|
||||||
|
// rename row fields when table column is renamed
|
||||||
|
if (_rename && tableToSave.schema[_rename.updated].type === FieldTypes.LINK) {
|
||||||
|
ctx.throw(400, "Cannot rename a linked column.")
|
||||||
|
} else if (_rename && tableToSave.primaryDisplay === _rename.old) {
|
||||||
|
ctx.throw(400, "Cannot rename the display column.")
|
||||||
|
}
|
||||||
|
|
||||||
|
tableToSave = await tableSaveFunctions.mid(tableToSave)
|
||||||
|
|
||||||
|
// update schema of non-statistics views when new columns are added
|
||||||
|
for (let view in tableToSave.views) {
|
||||||
|
const tableView = tableToSave.views[view]
|
||||||
|
if (!tableView) continue
|
||||||
|
|
||||||
|
if (tableView.schema.group || tableView.schema.field) continue
|
||||||
|
tableView.schema = tableToSave.schema
|
||||||
|
}
|
||||||
|
|
||||||
|
// update linked rows
|
||||||
|
const linkResp = await linkRows.updateLinks({
|
||||||
|
appId,
|
||||||
|
eventType: oldTable
|
||||||
|
? linkRows.EventType.TABLE_UPDATED
|
||||||
|
: linkRows.EventType.TABLE_SAVE,
|
||||||
|
table: tableToSave,
|
||||||
|
oldTable: oldTable,
|
||||||
|
})
|
||||||
|
if (linkResp != null && linkResp._rev) {
|
||||||
|
tableToSave._rev = linkResp._rev
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't perform any updates until relationships have been
|
||||||
|
// checked by the updateLinks function
|
||||||
|
const updatedRows = tableSaveFunctions.getUpdatedRows()
|
||||||
|
if (updatedRows && updatedRows.length !== 0) {
|
||||||
|
await db.bulkDocs(updatedRows)
|
||||||
|
}
|
||||||
|
const result = await db.post(tableToSave)
|
||||||
|
tableToSave._rev = result.rev
|
||||||
|
|
||||||
|
tableToSave = await tableSaveFunctions.after(tableToSave)
|
||||||
|
|
||||||
|
ctx.eventEmitter &&
|
||||||
|
ctx.eventEmitter.emitTable(`table:save`, appId, tableToSave)
|
||||||
|
|
||||||
|
ctx.status = 200
|
||||||
|
ctx.message = `Table ${ctx.request.body.name} saved successfully.`
|
||||||
|
ctx.body = tableToSave
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.destroy = async function(ctx) {
|
||||||
|
const appId = ctx.user.appId
|
||||||
|
const db = new CouchDB(appId)
|
||||||
|
const tableToDelete = await db.get(ctx.params.tableId)
|
||||||
|
|
||||||
|
// Delete all rows for that table
|
||||||
|
const rows = await db.allDocs(
|
||||||
|
getRowParams(ctx.params.tableId, null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
await db.bulkDocs(rows.rows.map(row => ({ ...row.doc, _deleted: true })))
|
||||||
|
|
||||||
|
// update linked rows
|
||||||
|
await linkRows.updateLinks({
|
||||||
|
appId,
|
||||||
|
eventType: linkRows.EventType.TABLE_DELETE,
|
||||||
|
table: tableToDelete,
|
||||||
|
})
|
||||||
|
|
||||||
|
// don't remove the table itself until very end
|
||||||
|
await db.remove(tableToDelete)
|
||||||
|
|
||||||
|
// remove table search index
|
||||||
|
const currentIndexes = await db.getIndexes()
|
||||||
|
const existingIndex = currentIndexes.indexes.find(
|
||||||
|
existing => existing.name === `search:${ctx.params.tableId}`
|
||||||
|
)
|
||||||
|
if (existingIndex) {
|
||||||
|
await db.deleteIndex(existingIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.eventEmitter &&
|
||||||
|
ctx.eventEmitter.emitTable(`table:delete`, appId, tableToDelete)
|
||||||
|
ctx.status = 200
|
||||||
|
ctx.message = `Table ${ctx.params.tableId} deleted.`
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.validateCSVSchema = async function(ctx) {
|
||||||
|
const { csvString, schema = {} } = ctx.request.body
|
||||||
|
const result = await csvParser.parse(csvString, schema)
|
||||||
|
ctx.body = { schema: result }
|
||||||
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
const CouchDB = require("../../../db")
|
||||||
|
const csvParser = require("../../../utilities/csvParser")
|
||||||
|
const { getRowParams, generateRowID, ViewNames } = require("../../../db/utils")
|
||||||
|
const { isEqual } = require("lodash/fp")
|
||||||
|
const { AutoFieldSubTypes } = require("../../../constants")
|
||||||
|
const { inputProcessing } = require("../../../utilities/rowProcessor")
|
||||||
|
const { USERS_TABLE_SCHEMA } = require("../../../constants")
|
||||||
|
|
||||||
|
exports.checkForColumnUpdates = async (db, oldTable, updatedTable) => {
|
||||||
|
let updatedRows = []
|
||||||
|
const rename = updatedTable._rename
|
||||||
|
let deletedColumns = []
|
||||||
|
if (oldTable && oldTable.schema && updatedTable.schema) {
|
||||||
|
deletedColumns = Object.keys(oldTable.schema).filter(
|
||||||
|
colName => updatedTable.schema[colName] == null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// check for renaming of columns or deleted columns
|
||||||
|
if (rename || deletedColumns.length !== 0) {
|
||||||
|
const rows = await db.allDocs(
|
||||||
|
getRowParams(updatedTable._id, null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
updatedRows = rows.rows.map(({ doc }) => {
|
||||||
|
if (rename) {
|
||||||
|
doc[rename.updated] = doc[rename.old]
|
||||||
|
delete doc[rename.old]
|
||||||
|
} else if (deletedColumns.length !== 0) {
|
||||||
|
deletedColumns.forEach(colName => delete doc[colName])
|
||||||
|
}
|
||||||
|
return doc
|
||||||
|
})
|
||||||
|
delete updatedTable._rename
|
||||||
|
}
|
||||||
|
return { rows: updatedRows, table: updatedTable }
|
||||||
|
}
|
||||||
|
|
||||||
|
// makes sure the passed in table isn't going to reset the auto ID
|
||||||
|
exports.makeSureTableUpToDate = (table, tableToSave) => {
|
||||||
|
if (!table) {
|
||||||
|
return tableToSave
|
||||||
|
}
|
||||||
|
// sure sure rev is up to date
|
||||||
|
tableToSave._rev = table._rev
|
||||||
|
// make sure auto IDs are always updated - these are internal
|
||||||
|
// so the client may not know they have changed
|
||||||
|
for (let [field, column] of Object.entries(table.schema)) {
|
||||||
|
if (
|
||||||
|
column.autocolumn &&
|
||||||
|
column.subtype === AutoFieldSubTypes.AUTO_ID &&
|
||||||
|
tableToSave.schema[field]
|
||||||
|
) {
|
||||||
|
tableToSave.schema[field].lastID = column.lastID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tableToSave
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.handleDataImport = async (user, table, dataImport) => {
|
||||||
|
const db = new CouchDB(user.appId)
|
||||||
|
if (dataImport && dataImport.csvString) {
|
||||||
|
// Populate the table with rows imported from CSV in a bulk update
|
||||||
|
const data = await csvParser.transform(dataImport)
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
let row = data[i]
|
||||||
|
row._id = generateRowID(table._id)
|
||||||
|
row.tableId = table._id
|
||||||
|
const processed = inputProcessing(user, table, row)
|
||||||
|
row = processed.row
|
||||||
|
// these auto-fields will never actually link anywhere (always builder)
|
||||||
|
for (let [fieldName, schema] of Object.entries(table.schema)) {
|
||||||
|
if (
|
||||||
|
schema.autocolumn &&
|
||||||
|
(schema.subtype === AutoFieldSubTypes.CREATED_BY ||
|
||||||
|
schema.subtype === AutoFieldSubTypes.UPDATED_BY)
|
||||||
|
) {
|
||||||
|
delete row[fieldName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table = processed.table
|
||||||
|
data[i] = row
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.bulkDocs(data)
|
||||||
|
let response = await db.put(table)
|
||||||
|
table._rev = response._rev
|
||||||
|
}
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.handleSearchIndexes = async (db, table) => {
|
||||||
|
// create relevant search indexes
|
||||||
|
if (table.indexes && table.indexes.length > 0) {
|
||||||
|
const currentIndexes = await db.getIndexes()
|
||||||
|
const indexName = `search:${table._id}`
|
||||||
|
|
||||||
|
const existingIndex = currentIndexes.indexes.find(
|
||||||
|
existing => existing.name === indexName
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existingIndex) {
|
||||||
|
const currentFields = existingIndex.def.fields.map(
|
||||||
|
field => Object.keys(field)[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
// if index fields have changed, delete the original index
|
||||||
|
if (!isEqual(currentFields, table.indexes)) {
|
||||||
|
await db.deleteIndex(existingIndex)
|
||||||
|
// create/recreate the index with fields
|
||||||
|
await db.createIndex({
|
||||||
|
index: {
|
||||||
|
fields: table.indexes,
|
||||||
|
name: indexName,
|
||||||
|
ddoc: "search_ddoc",
|
||||||
|
type: "json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// create/recreate the index with fields
|
||||||
|
await db.createIndex({
|
||||||
|
index: {
|
||||||
|
fields: table.indexes,
|
||||||
|
name: indexName,
|
||||||
|
ddoc: "search_ddoc",
|
||||||
|
type: "json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.checkStaticTables = table => {
|
||||||
|
// check user schema has all required elements
|
||||||
|
if (table._id === ViewNames.USERS) {
|
||||||
|
for (let [key, schema] of Object.entries(USERS_TABLE_SCHEMA.schema)) {
|
||||||
|
// check if the schema exists on the table to be created/updated
|
||||||
|
if (table.schema[key] == null) {
|
||||||
|
table.schema[key] = schema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
class TableSaveFunctions {
|
||||||
|
constructor({ db, ctx, oldTable, dataImport }) {
|
||||||
|
this.db = db
|
||||||
|
this.ctx = ctx
|
||||||
|
this.oldTable = oldTable
|
||||||
|
this.dataImport = dataImport
|
||||||
|
// any rows that need updated
|
||||||
|
this.rows = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// before anything is done
|
||||||
|
async before(table) {
|
||||||
|
if (this.oldTable) {
|
||||||
|
table = exports.makeSureTableUpToDate(this.oldTable, table)
|
||||||
|
}
|
||||||
|
table = exports.checkStaticTables(table)
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
// when confirmed valid
|
||||||
|
async mid(table) {
|
||||||
|
let response = await exports.checkForColumnUpdates(
|
||||||
|
this.db,
|
||||||
|
this.oldTable,
|
||||||
|
table
|
||||||
|
)
|
||||||
|
this.rows = this.rows.concat(response.rows)
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
// after saving
|
||||||
|
async after(table) {
|
||||||
|
table = await exports.handleSearchIndexes(this.db, table)
|
||||||
|
table = await exports.handleDataImport(
|
||||||
|
this.ctx.user,
|
||||||
|
table,
|
||||||
|
this.dataImport
|
||||||
|
)
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
getUpdatedRows() {
|
||||||
|
return this.rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.TableSaveFunctions = TableSaveFunctions
|
|
@ -2,6 +2,7 @@ const CouchDB = require("../../db")
|
||||||
const bcrypt = require("../../utilities/bcrypt")
|
const bcrypt = require("../../utilities/bcrypt")
|
||||||
const { generateUserID, getUserParams, ViewNames } = require("../../db/utils")
|
const { generateUserID, getUserParams, ViewNames } = require("../../db/utils")
|
||||||
const { getRole } = require("../../utilities/security/roles")
|
const { getRole } = require("../../utilities/security/roles")
|
||||||
|
const { UserStatus } = require("../../constants")
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const database = new CouchDB(ctx.user.appId)
|
const database = new CouchDB(ctx.user.appId)
|
||||||
|
@ -42,6 +43,10 @@ exports.create = async function(ctx) {
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
tableId: ViewNames.USERS,
|
tableId: ViewNames.USERS,
|
||||||
}
|
}
|
||||||
|
// add the active status to a user if its not provided
|
||||||
|
if (user.status == null) {
|
||||||
|
user.status = UserStatus.ACTIVE
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await db.post(user)
|
const response = await db.post(user)
|
||||||
|
@ -64,13 +69,21 @@ exports.create = async function(ctx) {
|
||||||
exports.update = async function(ctx) {
|
exports.update = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.appId)
|
const db = new CouchDB(ctx.user.appId)
|
||||||
const user = ctx.request.body
|
const user = ctx.request.body
|
||||||
|
let dbUser
|
||||||
|
// get user incase password removed
|
||||||
|
if (user._id) {
|
||||||
|
dbUser = await db.get(user._id)
|
||||||
|
}
|
||||||
if (user.password) {
|
if (user.password) {
|
||||||
user.password = await bcrypt.hash(user.password)
|
user.password = await bcrypt.hash(user.password)
|
||||||
} else {
|
} else {
|
||||||
delete user.password
|
delete user.password
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await db.put(user)
|
const response = await db.put({
|
||||||
|
password: dbUser.password,
|
||||||
|
...user,
|
||||||
|
})
|
||||||
user._rev = response.rev
|
user._rev = response.rev
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
|
|
|
@ -1,44 +1,5 @@
|
||||||
const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
|
const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
|
||||||
|
|
||||||
const AuthTypes = {
|
|
||||||
APP: "app",
|
|
||||||
BUILDER: "builder",
|
|
||||||
EXTERNAL: "external",
|
|
||||||
}
|
|
||||||
|
|
||||||
const USERS_TABLE_SCHEMA = {
|
|
||||||
_id: "ta_users",
|
|
||||||
type: "table",
|
|
||||||
views: {},
|
|
||||||
name: "Users",
|
|
||||||
schema: {
|
|
||||||
email: {
|
|
||||||
type: "string",
|
|
||||||
constraints: {
|
|
||||||
type: "string",
|
|
||||||
email: true,
|
|
||||||
length: {
|
|
||||||
maximum: "",
|
|
||||||
},
|
|
||||||
presence: true,
|
|
||||||
},
|
|
||||||
fieldName: "email",
|
|
||||||
name: "email",
|
|
||||||
},
|
|
||||||
roleId: {
|
|
||||||
fieldName: "roleId",
|
|
||||||
name: "roleId",
|
|
||||||
type: "options",
|
|
||||||
constraints: {
|
|
||||||
type: "string",
|
|
||||||
presence: false,
|
|
||||||
inclusion: Object.values(BUILTIN_ROLE_IDS),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
primaryDisplay: "email",
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.FieldTypes = {
|
exports.FieldTypes = {
|
||||||
STRING: "string",
|
STRING: "string",
|
||||||
LONGFORM: "longform",
|
LONGFORM: "longform",
|
||||||
|
@ -51,6 +12,60 @@ exports.FieldTypes = {
|
||||||
AUTO: "auto",
|
AUTO: "auto",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.AuthTypes = {
|
||||||
|
APP: "app",
|
||||||
|
BUILDER: "builder",
|
||||||
|
EXTERNAL: "external",
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.UserStatus = {
|
||||||
|
ACTIVE: "active",
|
||||||
|
INACTIVE: "inactive",
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.USERS_TABLE_SCHEMA = {
|
||||||
|
_id: "ta_users",
|
||||||
|
type: "table",
|
||||||
|
views: {},
|
||||||
|
name: "Users",
|
||||||
|
schema: {
|
||||||
|
email: {
|
||||||
|
type: exports.FieldTypes.STRING,
|
||||||
|
constraints: {
|
||||||
|
type: exports.FieldTypes.STRING,
|
||||||
|
email: true,
|
||||||
|
length: {
|
||||||
|
maximum: "",
|
||||||
|
},
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
fieldName: "email",
|
||||||
|
name: "email",
|
||||||
|
},
|
||||||
|
roleId: {
|
||||||
|
fieldName: "roleId",
|
||||||
|
name: "roleId",
|
||||||
|
type: exports.FieldTypes.OPTIONS,
|
||||||
|
constraints: {
|
||||||
|
type: exports.FieldTypes.STRING,
|
||||||
|
presence: false,
|
||||||
|
inclusion: Object.values(BUILTIN_ROLE_IDS),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
fieldName: "status",
|
||||||
|
name: "status",
|
||||||
|
type: exports.FieldTypes.OPTIONS,
|
||||||
|
constraints: {
|
||||||
|
type: exports.FieldTypes.STRING,
|
||||||
|
presence: false,
|
||||||
|
inclusion: Object.values(exports.UserStatus),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
primaryDisplay: "email",
|
||||||
|
}
|
||||||
|
|
||||||
exports.AutoFieldSubTypes = {
|
exports.AutoFieldSubTypes = {
|
||||||
CREATED_BY: "createdBy",
|
CREATED_BY: "createdBy",
|
||||||
CREATED_AT: "createdAt",
|
CREATED_AT: "createdAt",
|
||||||
|
@ -59,8 +74,6 @@ exports.AutoFieldSubTypes = {
|
||||||
AUTO_ID: "autoID",
|
AUTO_ID: "autoID",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.AuthTypes = AuthTypes
|
|
||||||
exports.USERS_TABLE_SCHEMA = USERS_TABLE_SCHEMA
|
|
||||||
exports.BUILDER_CONFIG_DB = "builder-config-db"
|
exports.BUILDER_CONFIG_DB = "builder-config-db"
|
||||||
exports.HOSTING_DOC = "hosting-doc"
|
exports.HOSTING_DOC = "hosting-doc"
|
||||||
exports.OBJ_STORE_DIRECTORY = "/app-assets/assets"
|
exports.OBJ_STORE_DIRECTORY = "/app-assets/assets"
|
||||||
|
|
|
@ -118,6 +118,11 @@
|
||||||
"label": "Empty Text",
|
"label": "Empty Text",
|
||||||
"key": "noRowsMessage",
|
"key": "noRowsMessage",
|
||||||
"defaultValue": "No rows found."
|
"defaultValue": "No rows found."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "filter",
|
||||||
|
"label": "Filtering",
|
||||||
|
"key": "filter"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd",
|
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.1.0",
|
"@adobe/spectrum-css-workflow-icons": "^1.1.0",
|
||||||
"@budibase/bbui": "^1.58.5",
|
"@budibase/bbui": "^1.58.13",
|
||||||
"@budibase/svelte-ag-grid": "^0.0.16",
|
"@budibase/svelte-ag-grid": "^0.0.16",
|
||||||
"@spectrum-css/actionbutton": "^1.0.0-beta.1",
|
"@spectrum-css/actionbutton": "^1.0.0-beta.1",
|
||||||
"@spectrum-css/button": "^3.0.0-beta.6",
|
"@spectrum-css/button": "^3.0.0-beta.6",
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
export let datasource
|
export let datasource
|
||||||
export let noRowsMessage
|
export let noRowsMessage
|
||||||
|
export let filter
|
||||||
|
|
||||||
const { API, styleable, Provider, builderStore, ActionTypes } = getContext(
|
const { API, styleable, Provider, builderStore, ActionTypes } = getContext(
|
||||||
"sdk"
|
"sdk"
|
||||||
|
@ -13,6 +14,7 @@
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
|
||||||
$: fetchData(datasource)
|
$: fetchData(datasource)
|
||||||
|
$: filteredRows = filterRows(rows, filter)
|
||||||
$: actions = [
|
$: actions = [
|
||||||
{
|
{
|
||||||
type: ActionTypes.RefreshDatasource,
|
type: ActionTypes.RefreshDatasource,
|
||||||
|
@ -21,21 +23,36 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
async function fetchData(datasource) {
|
const fetchData = async datasource => {
|
||||||
if (!isEmpty(datasource)) {
|
if (!isEmpty(datasource)) {
|
||||||
rows = await API.fetchDatasource(datasource)
|
rows = await API.fetchDatasource(datasource)
|
||||||
}
|
}
|
||||||
loaded = true
|
loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filterRows = (rows, filter) => {
|
||||||
|
if (!Object.keys(filter || {}).length) {
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
let filteredData = [...rows]
|
||||||
|
Object.entries(filter).forEach(([field, value]) => {
|
||||||
|
if (value != null && value !== "") {
|
||||||
|
filteredData = filteredData.filter(row => {
|
||||||
|
return row[field] === value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return filteredData
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Provider {actions}>
|
<Provider {actions}>
|
||||||
<div use:styleable={$component.styles}>
|
<div use:styleable={$component.styles}>
|
||||||
{#if rows.length > 0}
|
{#if filteredRows.length > 0}
|
||||||
{#if $component.children === 0 && $builderStore.inBuilder}
|
{#if $component.children === 0 && $builderStore.inBuilder}
|
||||||
<p><i class="ri-image-line" />Add some components to display.</p>
|
<p><i class="ri-image-line" />Add some components to display.</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#each rows as row}
|
{#each filteredRows as row}
|
||||||
<Provider data={row}>
|
<Provider data={row}>
|
||||||
<slot />
|
<slot />
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|
|
@ -18,25 +18,25 @@
|
||||||
let options = []
|
let options = []
|
||||||
let tableDefinition
|
let tableDefinition
|
||||||
let fieldText = ""
|
let fieldText = ""
|
||||||
|
|
||||||
const setFieldText = (value) => {
|
const setFieldText = value => {
|
||||||
if (fieldSchema?.relationshipType === 'one-to-many') {
|
if (fieldSchema?.relationshipType === "one-to-many") {
|
||||||
if (value?.length && options?.length) {
|
if (value?.length && options?.length) {
|
||||||
const row = options.find(row => row._id === value[0])
|
const row = options.find(row => row._id === value[0])
|
||||||
return row.name
|
return row.name
|
||||||
} else {
|
} else {
|
||||||
return placeholder || 'Choose an option'
|
return placeholder || "Choose an option"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (value?.length) {
|
if (value?.length) {
|
||||||
return `${value?.length ?? 0} selected rows`
|
return `${value?.length ?? 0} selected rows`
|
||||||
} else {
|
} else {
|
||||||
return placeholder || 'Choose some options'
|
return placeholder || "Choose some options"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: options, fieldText = setFieldText($fieldState?.value)
|
$: options, (fieldText = setFieldText($fieldState?.value))
|
||||||
$: valueLookupMap = getValueLookupMap($fieldState?.value)
|
$: valueLookupMap = getValueLookupMap($fieldState?.value)
|
||||||
$: isOptionSelected = option => valueLookupMap[option] === true
|
$: isOptionSelected = option => valueLookupMap[option] === true
|
||||||
$: linkedTableId = fieldSchema?.tableId
|
$: linkedTableId = fieldSchema?.tableId
|
||||||
|
@ -74,14 +74,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleOption = option => {
|
const toggleOption = option => {
|
||||||
if (fieldSchema.type === 'one-to-many') {
|
if (fieldSchema.type === "one-to-many") {
|
||||||
fieldApi.setValue([option])
|
fieldApi.setValue([option])
|
||||||
} else {
|
} else {
|
||||||
if ($fieldState.value.includes(option)) {
|
if ($fieldState.value.includes(option)) {
|
||||||
fieldApi.setValue($fieldState.value.filter(x => x !== option))
|
fieldApi.setValue($fieldState.value.filter(x => x !== option))
|
||||||
} else {
|
} else {
|
||||||
fieldApi.setValue([...$fieldState.value, option])
|
fieldApi.setValue([...$fieldState.value, option])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -44,10 +44,10 @@
|
||||||
lodash "^4.17.19"
|
lodash "^4.17.19"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@budibase/bbui@^1.58.5":
|
"@budibase/bbui@^1.58.13":
|
||||||
version "1.58.5"
|
version "1.58.13"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.5.tgz#c9ce712941760825c7774a1de77594e989db4561"
|
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.13.tgz#59df9c73def2d81c75dcbd2266c52c19db88dbd7"
|
||||||
integrity sha512-0j1I7BetJ2GzB1BXKyvvlkuFphLmADJh2U/Ihubwxx5qUDY8REoVzLgAB4c24zt0CGVTF9VMmOoMLd0zD0QwdQ==
|
integrity sha512-Zk6CKXdBfKsTVzA1Xs5++shdSSZLfphVpZuKVbjfzkgtuhyH7ruucexuSHEpFsxjW5rEKgKIBoRFzCK5vPvN0w==
|
||||||
dependencies:
|
dependencies:
|
||||||
markdown-it "^12.0.2"
|
markdown-it "^12.0.2"
|
||||||
quill "^1.3.7"
|
quill "^1.3.7"
|
||||||
|
|
Loading…
Reference in New Issue