Merge in master
This commit is contained in:
commit
d166d8072b
|
@ -21,7 +21,7 @@
|
|||
"publishdev": "lerna run publishdev",
|
||||
"publishnpm": "yarn build && lerna publish --force-publish",
|
||||
"clean": "lerna clean",
|
||||
"dev": "node ./scripts/symlinkDev.js && lerna run --parallel --stream dev:builder",
|
||||
"dev": "node ./scripts/symlinkDev.js && lerna run --parallel dev:builder",
|
||||
"test": "lerna run test",
|
||||
"lint": "eslint packages",
|
||||
"lint:fix": "eslint --fix packages",
|
||||
|
|
|
@ -5,4 +5,5 @@ package-lock.json
|
|||
release/
|
||||
dist/
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
routify
|
||||
|
|
|
@ -72,7 +72,6 @@
|
|||
"d3-selection": "^1.4.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fast-sort": "^2.2.0",
|
||||
"feather-icons": "^4.21.0",
|
||||
"lodash": "^4.17.13",
|
||||
"mustache": "^4.0.1",
|
||||
"posthog-js": "1.3.1",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Array.flat needs polyfilled in < Node 11
|
||||
if (!Array.prototype.flat) {
|
||||
Object.defineProperty(Array.prototype, "flat", {
|
||||
configurable: true,
|
||||
|
|
|
@ -37,7 +37,9 @@ export default function({ componentInstanceId, screen, components, models }) {
|
|||
.filter(isInstanceInSharedContext(walkResult))
|
||||
.map(componentInstanceToBindable(walkResult)),
|
||||
|
||||
...walkResult.target._contexts.map(contextToBindables(walkResult)).flat(),
|
||||
...walkResult.target._contexts
|
||||
.map(contextToBindables(models, walkResult))
|
||||
.flat(),
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -69,17 +71,31 @@ const componentInstanceToBindable = walkResult => i => {
|
|||
}
|
||||
}
|
||||
|
||||
const contextToBindables = walkResult => context => {
|
||||
const contextToBindables = (models, walkResult) => context => {
|
||||
const contextParentPath = getParentPath(walkResult, context)
|
||||
|
||||
return Object.keys(context.model.schema).map(k => ({
|
||||
const newBindable = key => ({
|
||||
type: "context",
|
||||
instance: context.instance,
|
||||
// how the binding expression persists, and is used in the app at runtime
|
||||
runtimeBinding: `${contextParentPath}data.${k}`,
|
||||
runtimeBinding: `${contextParentPath}data.${key}`,
|
||||
// how the binding exressions looks to the user of the builder
|
||||
readableBinding: `${context.instance._instanceName}.${context.model.name}.${k}`,
|
||||
}))
|
||||
readableBinding: `${context.instance._instanceName}.${context.model.label}.${key}`,
|
||||
})
|
||||
|
||||
// see ModelViewSelect.svelte for the format of context.model
|
||||
// ... this allows us to bind to Model scheams, or View schemas
|
||||
const model = models.find(m => m._id === context.model.modelId)
|
||||
const schema = context.model.isModel
|
||||
? model.schema
|
||||
: model.views[context.model.name].schema
|
||||
|
||||
return (
|
||||
Object.keys(schema)
|
||||
.map(newBindable)
|
||||
// add _id and _rev fields - not part of schema, but always valid
|
||||
.concat([newBindable("_id"), newBindable("_rev")])
|
||||
)
|
||||
}
|
||||
|
||||
const getParentPath = (walkResult, context) => {
|
||||
|
@ -135,7 +151,7 @@ const walk = ({ instance, targetId, components, models, result }) => {
|
|||
if (contextualInstance) {
|
||||
// add to currentContexts (ancestory of context)
|
||||
// before walking children
|
||||
const model = models.find(m => m._id === instance[component.context])
|
||||
const model = instance[component.context]
|
||||
result.currentContexts.push({ instance, model })
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
export const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g
|
||||
|
||||
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
|
||||
// Find all instances of mustasche
|
||||
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE)
|
||||
|
||||
let result = textWithBindings
|
||||
// Replace readableBindings with runtimeBindings
|
||||
boundValues &&
|
||||
boundValues.forEach(boundValue => {
|
||||
const binding = bindableProperties.find(({ readableBinding }) => {
|
||||
return boundValue === `{{ ${readableBinding} }}`
|
||||
})
|
||||
if (binding) {
|
||||
result = textWithBindings.replace(
|
||||
boundValue,
|
||||
`{{ ${binding.runtimeBinding} }}`
|
||||
)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
|
||||
let temp = textWithBindings
|
||||
const boundValues =
|
||||
(typeof textWithBindings === "string" &&
|
||||
textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE)) ||
|
||||
[]
|
||||
|
||||
// Replace runtimeBindings with readableBindings:
|
||||
boundValues.forEach(v => {
|
||||
const binding = bindableProperties.find(({ runtimeBinding }) => {
|
||||
return v === `{{ ${runtimeBinding} }}`
|
||||
})
|
||||
if (binding) {
|
||||
temp = temp.replace(v, `{{ ${binding.readableBinding} }}`)
|
||||
}
|
||||
})
|
||||
|
||||
return temp
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24">
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10
|
||||
10zm0-11.414L9.172 7.757 7.757 9.172 10.586 12l-2.829 2.828 1.415 1.415L12
|
||||
13.414l2.828 2.829 1.415-1.415L13.414 12l2.829-2.828-1.415-1.415L12 10.586z" />
|
||||
</svg>
|
After Width: | Height: | Size: 412 B |
|
@ -32,3 +32,4 @@ export { default as TwitterIcon } from "./Twitter.svelte"
|
|||
export { default as InfoIcon } from "./Info.svelte"
|
||||
export { default as CloseIcon } from "./Close.svelte"
|
||||
export { default as MoreIcon } from "./More.svelte"
|
||||
export { default as CloseCircleIcon } from "./CloseCircle.svelte"
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
import feather from "feather-icons"
|
||||
const getIcon = (icon, size) =>
|
||||
feather.icons[icon].toSvg({ height: size || "16", width: size || "16" })
|
||||
export default getIcon
|
|
@ -9,7 +9,6 @@
|
|||
CircleIndicator,
|
||||
EventsIcon,
|
||||
} from "components/common/Icons/"
|
||||
import EventsEditor from "./EventsEditor"
|
||||
import panelStructure from "./temporaryPanelStructure.js"
|
||||
import CategoryTab from "./CategoryTab.svelte"
|
||||
import DesignView from "./DesignView.svelte"
|
||||
|
@ -21,7 +20,6 @@
|
|||
let categories = [
|
||||
{ value: "settings", name: "Settings" },
|
||||
{ value: "design", name: "Design" },
|
||||
{ value: "events", name: "Events" },
|
||||
]
|
||||
let selectedCategory = categories[0]
|
||||
|
||||
|
@ -109,8 +107,6 @@
|
|||
displayNameField={displayName}
|
||||
onChange={onPropChanged}
|
||||
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
|
||||
{:else if selectedCategory.value === 'events'}
|
||||
<EventsEditor component={componentInstance} />
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
|
|
@ -1,168 +1,220 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import { Button, Select } from "@budibase/bbui"
|
||||
import HandlerSelector from "./HandlerSelector.svelte"
|
||||
import ActionButton from "../../common/ActionButton.svelte"
|
||||
import getIcon from "../../common/icon"
|
||||
import { CloseIcon } from "components/common/Icons/"
|
||||
|
||||
import { TextButton, Button, Heading, DropdownMenu } from "@budibase/bbui"
|
||||
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
||||
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
|
||||
import actionTypes from "./actions"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let event
|
||||
export let eventOptions = []
|
||||
export let onClose
|
||||
|
||||
let eventType = ""
|
||||
let addActionButton
|
||||
let addActionDropdown
|
||||
let selectedAction
|
||||
|
||||
let draftEventHandler = { parameters: [] }
|
||||
|
||||
$: eventData = event || { handlers: [] }
|
||||
$: if (!eventOptions.includes(eventType) && eventOptions.length > 0)
|
||||
eventType = eventOptions[0].name
|
||||
$: actions = event || []
|
||||
$: selectedActionComponent =
|
||||
selectedAction &&
|
||||
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_MEMBER_NAME])
|
||||
.component
|
||||
|
||||
const closeModal = () => {
|
||||
onClose()
|
||||
dispatch("close")
|
||||
draftEventHandler = { parameters: [] }
|
||||
eventData = { handlers: [] }
|
||||
actions = []
|
||||
}
|
||||
|
||||
const updateEventHandler = (updatedHandler, index) => {
|
||||
eventData.handlers[index] = updatedHandler
|
||||
actions[index] = updatedHandler
|
||||
}
|
||||
|
||||
const updateDraftEventHandler = updatedHandler => {
|
||||
draftEventHandler = updatedHandler
|
||||
const deleteAction = index => {
|
||||
actions.splice(index, 1)
|
||||
actions = actions
|
||||
}
|
||||
|
||||
const deleteEventHandler = index => {
|
||||
eventData.handlers.splice(index, 1)
|
||||
eventData = eventData
|
||||
}
|
||||
|
||||
const createNewEventHandler = handler => {
|
||||
const newHandler = handler || {
|
||||
const addAction = actionType => () => {
|
||||
const newAction = {
|
||||
parameters: {},
|
||||
[EVENT_TYPE_MEMBER_NAME]: "",
|
||||
[EVENT_TYPE_MEMBER_NAME]: actionType.name,
|
||||
}
|
||||
eventData.handlers.push(newHandler)
|
||||
eventData = eventData
|
||||
actions.push(newAction)
|
||||
selectedAction = newAction
|
||||
actions = actions
|
||||
addActionDropdown.hide()
|
||||
}
|
||||
|
||||
const deleteEvent = () => {
|
||||
store.setComponentProp(eventType, [])
|
||||
closeModal()
|
||||
const selectAction = action => () => {
|
||||
selectedAction = action
|
||||
}
|
||||
|
||||
const saveEventData = () => {
|
||||
store.setComponentProp(eventType, eventData.handlers)
|
||||
dispatch("change", actions)
|
||||
closeModal()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="body">
|
||||
<div class="heading">
|
||||
<h3>
|
||||
{eventData.name ? `${eventData.name} Event` : 'Create a New Component Event'}
|
||||
</h3>
|
||||
<div class="root">
|
||||
|
||||
<div class="header">
|
||||
<Heading small dark>Actions</Heading>
|
||||
<div bind:this={addActionButton}>
|
||||
<TextButton text small blue on:click={addActionDropdown.show}>
|
||||
Add Action
|
||||
<div style="height: 20px; width: 20px;">
|
||||
<AddIcon />
|
||||
</div>
|
||||
</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>
|
||||
<div class="event-options">
|
||||
<div class="section">
|
||||
<h4>Event Type</h4>
|
||||
<Select bind:value={eventType}>
|
||||
{#each eventOptions as option}
|
||||
<option value={option.name}>{option.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h4>Event Action(s)</h4>
|
||||
<HandlerSelector
|
||||
newHandler
|
||||
onChanged={updateDraftEventHandler}
|
||||
onCreate={() => {
|
||||
createNewEventHandler(draftEventHandler)
|
||||
draftEventHandler = { parameters: [] }
|
||||
}}
|
||||
handler={draftEventHandler} />
|
||||
<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)}>
|
||||
<p
|
||||
class="bb-body bb-body--small bb-body--color-dark"
|
||||
style="margin: var(--spacing-s) 0;">
|
||||
{index + 1}. {action[EVENT_TYPE_MEMBER_NAME]}
|
||||
</p>
|
||||
<div class="row-expander" class:rotate={action !== selectedAction}>
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
{#if eventData}
|
||||
{#each eventData.handlers as handler, index}
|
||||
<HandlerSelector
|
||||
{index}
|
||||
onChanged={updateEventHandler}
|
||||
onRemoved={() => deleteEventHandler(index)}
|
||||
{handler} />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
<div class="footer">
|
||||
{#if eventData.name}
|
||||
<Button
|
||||
outline
|
||||
on:click={deleteEvent}
|
||||
disabled={eventData.handlers.length === 0}>
|
||||
{#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
|
||||
</Button>
|
||||
</TextButton>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="save">
|
||||
<Button
|
||||
primary
|
||||
on:click={saveEventData}
|
||||
disabled={eventData.handlers.length === 0}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="close-button" on:click={closeModal}>
|
||||
<CloseIcon />
|
||||
|
||||
<div class="footer">
|
||||
<a href="https://docs.budibase.com">Learn more about Actions</a>
|
||||
<Button secondary on:click={closeModal}>Cancel</Button>
|
||||
<Button primary on:click={saveEventData}>Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
.heading {
|
||||
margin-bottom: 20px;
|
||||
.root {
|
||||
max-height: 50vh;
|
||||
width: 700px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-xl);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.action-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-header > p {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.row-expander {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.available-action {
|
||||
padding: var(--spacing-s);
|
||||
font-size: var(--font-size-m);
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
.close-button :global(svg) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 10px;
|
||||
.available-action:hover {
|
||||
background: var(--grey-2);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
.actions-container {
|
||||
flex: 1;
|
||||
min-height: 0px;
|
||||
padding-bottom: var(--spacing-s);
|
||||
padding-top: 0;
|
||||
border: var(--border-light);
|
||||
border-width: 0 0 1px 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.body {
|
||||
padding: 40px;
|
||||
display: grid;
|
||||
grid-gap: 20px;
|
||||
|
||||
.action-container {
|
||||
border: var(--border-light);
|
||||
border-width: 1px 0 0 0;
|
||||
padding-left: var(--spacing-xl);
|
||||
padding-right: var(--spacing-xl);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.footer {
|
||||
|
||||
.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;
|
||||
padding: 30px 40px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 50px;
|
||||
background-color: var(--grey-1);
|
||||
flex-direction: row;
|
||||
}
|
||||
.save {
|
||||
margin-left: 20px;
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-s);
|
||||
padding: var(--spacing-xl);
|
||||
padding-top: var(--spacing-m);
|
||||
}
|
||||
|
||||
.footer > a {
|
||||
flex: 1;
|
||||
color: var(--grey-5);
|
||||
font-size: var(--font-size-s);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer > a:hover {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.rotate :global(svg) {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
<script>
|
||||
import { Button, DropdownMenu } from "@budibase/bbui"
|
||||
import { Button, Modal } from "@budibase/bbui"
|
||||
import EventEditorModal from "./EventEditorModal.svelte"
|
||||
import { getContext } from "svelte"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value
|
||||
export let name
|
||||
|
||||
let button
|
||||
let dropdown
|
||||
let eventsModal
|
||||
</script>
|
||||
|
||||
<div bind:this={button}>
|
||||
<Button secondary small on:click={dropdown.show}>Define Actions</Button>
|
||||
</div>
|
||||
<DropdownMenu bind:this={dropdown} align="right" anchor={button}>
|
||||
<Button secondary small on:click={eventsModal.show}>Define Actions</Button>
|
||||
|
||||
<Modal bind:this={eventsModal} maxWidth="100vw" hideCloseButton>
|
||||
<EventEditorModal
|
||||
event={value}
|
||||
eventType={name}
|
||||
on:change
|
||||
on:close={dropdown.hide} />
|
||||
</DropdownMenu>
|
||||
on:close={eventsModal.hide} />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
<script>
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import fetchBindableProperties from "builderStore/fetchBindableProperties"
|
||||
import SaveFields from "./SaveFields.svelte"
|
||||
|
||||
export let parameters
|
||||
|
||||
$: bindableProperties = fetchBindableProperties({
|
||||
componentInstanceId: $store.currentComponentInfo._id,
|
||||
components: $store.components,
|
||||
screen: $store.currentPreviewItem,
|
||||
models: $backendUiStore.models,
|
||||
})
|
||||
|
||||
// just wraps binding in {{ ... }}
|
||||
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
|
||||
|
||||
const modelFields = modelId => {
|
||||
const model = $backendUiStore.models.find(m => m._id === modelId)
|
||||
|
||||
return Object.keys(model.schema).map(k => ({
|
||||
name: k,
|
||||
type: model.schema[k].type,
|
||||
}))
|
||||
}
|
||||
|
||||
$: schemaFields =
|
||||
parameters && parameters.modelId ? modelFields(parameters.modelId) : []
|
||||
|
||||
const onFieldsChanged = e => {
|
||||
parameters.fields = e.detail
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label size="m" color="dark">Table</Label>
|
||||
<Select secondary bind:value={parameters.modelId}>
|
||||
<option value="" />
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
|
||||
{#if parameters.modelId}
|
||||
<SaveFields
|
||||
parameterFields={parameters.fields}
|
||||
{schemaFields}
|
||||
on:fieldschanged={onFieldsChanged} />
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-s);
|
||||
row-gap: var(--spacing-s);
|
||||
grid-template-columns: auto 1fr auto 1fr auto;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.root :global(.relative:nth-child(2)) {
|
||||
grid-column-start: 2;
|
||||
grid-column-end: 6;
|
||||
}
|
||||
|
||||
.cannot-use {
|
||||
color: var(--red);
|
||||
font-size: var(--font-size-s);
|
||||
text-align: center;
|
||||
width: 70%;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,29 @@
|
|||
<script>
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
|
||||
export let parameters
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label size="m" color="dark">Screen</Label>
|
||||
<Select secondary bind:value={parameters.url}>
|
||||
<option value="" />
|
||||
{#each $store.screens as screen}
|
||||
<option value={screen.route}>{screen.props._instanceName}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.root :global(.relative) {
|
||||
flex: 1;
|
||||
margin-left: var(--spacing-l);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,119 @@
|
|||
<script>
|
||||
// accepts an array of field names, and outputs an object of { FieldName: value }
|
||||
import { Select, Label, TextButton, Spacer } from "@budibase/bbui"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import fetchBindableProperties from "builderStore/fetchBindableProperties"
|
||||
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/replaceBindings"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let parameterFields
|
||||
export let schemaFields
|
||||
|
||||
const emptyField = () => ({ name: "", value: "" })
|
||||
|
||||
// this statement initialises fields from parameters.fields
|
||||
$: fields =
|
||||
fields ||
|
||||
Object.keys(parameterFields || { "": "" }).map(name => ({
|
||||
name,
|
||||
value:
|
||||
(parameterFields &&
|
||||
runtimeToReadableBinding(
|
||||
bindableProperties,
|
||||
parameterFields[name].value
|
||||
)) ||
|
||||
"",
|
||||
}))
|
||||
|
||||
$: bindableProperties = fetchBindableProperties({
|
||||
componentInstanceId: $store.currentComponentInfo._id,
|
||||
components: $store.components,
|
||||
screen: $store.currentPreviewItem,
|
||||
models: $backendUiStore.models,
|
||||
})
|
||||
|
||||
const addField = () => {
|
||||
const newFields = fields.filter(f => f.name)
|
||||
newFields.push(emptyField())
|
||||
fields = newFields
|
||||
rebuildParameters()
|
||||
}
|
||||
|
||||
const removeField = field => () => {
|
||||
fields = fields.filter(f => f !== field)
|
||||
rebuildParameters()
|
||||
}
|
||||
|
||||
const rebuildParameters = () => {
|
||||
// rebuilds paramters.fields every time a field name or value is added
|
||||
// as UI below is bound to "fields" array, but we need to output a { key: value }
|
||||
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.find(f => f.name === field.name).type,
|
||||
value: readableToRuntimeBinding(bindableProperties, field.value),
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch("fieldschanged", newParameterFields)
|
||||
}
|
||||
|
||||
// just wraps binding in {{ ... }}
|
||||
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
|
||||
</script>
|
||||
|
||||
{#if fields}
|
||||
{#each fields as field}
|
||||
<Label size="m" color="dark">Field</Label>
|
||||
<Select secondary bind:value={field.name} on:blur={rebuildParameters}>
|
||||
<option value="" />
|
||||
{#each schemaFields as schemaField}
|
||||
<option value={schemaField.name}>{schemaField.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
<Label size="m" color="dark">Value</Label>
|
||||
<Select
|
||||
editable
|
||||
secondary
|
||||
bind:value={field.value}
|
||||
on:blur={rebuildParameters}>
|
||||
<option value="" />
|
||||
{#each bindableProperties as bindableProp}
|
||||
<option value={toBindingExpression(bindableProp.readableBinding)}>
|
||||
{bindableProp.readableBinding}
|
||||
</option>
|
||||
{/each}
|
||||
</Select>
|
||||
<div class="remove-field-container">
|
||||
<TextButton text small on:click={removeField(field)}>
|
||||
<CloseCircleIcon />
|
||||
</TextButton>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div>
|
||||
<Spacer small />
|
||||
|
||||
<TextButton text small blue on:click={addField}>
|
||||
Add Field
|
||||
<div style="height: 20px; width: 20px;">
|
||||
<AddIcon />
|
||||
</div>
|
||||
</TextButton>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.remove-field-container :global(button) {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,134 @@
|
|||
<script>
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import fetchBindableProperties from "builderStore/fetchBindableProperties"
|
||||
import SaveFields from "./SaveFields.svelte"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/replaceBindings"
|
||||
|
||||
export let parameters
|
||||
|
||||
$: bindableProperties = fetchBindableProperties({
|
||||
componentInstanceId: $store.currentComponentInfo._id,
|
||||
components: $store.components,
|
||||
screen: $store.currentPreviewItem,
|
||||
models: $backendUiStore.models,
|
||||
})
|
||||
|
||||
let idFields
|
||||
let recordId
|
||||
$: {
|
||||
idFields = bindableProperties.filter(
|
||||
bindable =>
|
||||
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id")
|
||||
)
|
||||
// ensure recordId is always defaulted - there is usually only one option
|
||||
if (idFields.length > 0 && !parameters._id) {
|
||||
recordId = idFields[0].runtimeBinding
|
||||
parameters = parameters
|
||||
} else if (!recordId && parameters._id) {
|
||||
recordId = parameters._id
|
||||
.replace("{{", "")
|
||||
.replace("}}", "")
|
||||
.trim()
|
||||
}
|
||||
}
|
||||
|
||||
$: parameters._id = `{{ ${recordId} }}`
|
||||
|
||||
// just wraps binding in {{ ... }}
|
||||
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
|
||||
|
||||
// finds the selected idBinding, then reads the table/view
|
||||
// from the component instance that it belongs to.
|
||||
// then returns the field names for that schema
|
||||
const schemaFromIdBinding = recordId => {
|
||||
if (!recordId) return []
|
||||
|
||||
const idBinding = bindableProperties.find(
|
||||
prop => prop.runtimeBinding === recordId
|
||||
)
|
||||
if (!idBinding) return []
|
||||
|
||||
const { instance } = idBinding
|
||||
|
||||
const component = $store.components[instance._component]
|
||||
|
||||
// component.context is the name of the prop that holds the modelId
|
||||
const modelInfo = instance[component.context]
|
||||
|
||||
if (!modelInfo) return []
|
||||
|
||||
const model = $backendUiStore.models.find(m => m._id === modelInfo.modelId)
|
||||
parameters.modelId = modelInfo.modelId
|
||||
return Object.keys(model.schema).map(k => ({
|
||||
name: k,
|
||||
type: model.schema[k].type,
|
||||
}))
|
||||
}
|
||||
|
||||
let schemaFields
|
||||
$: {
|
||||
if (parameters && recordId) {
|
||||
schemaFields = schemaFromIdBinding(recordId)
|
||||
} else {
|
||||
schemaFields = []
|
||||
}
|
||||
}
|
||||
|
||||
const onFieldsChanged = e => {
|
||||
parameters.fields = e.detail
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
{#if idFields.length === 0}
|
||||
<div class="cannot-use">
|
||||
Update record can only be used within a component that provides data, such
|
||||
as a List
|
||||
</div>
|
||||
{:else}
|
||||
<Label size="m" color="dark">Record Id</Label>
|
||||
<Select secondary bind:value={recordId}>
|
||||
<option value="" />
|
||||
{#each idFields as idField}
|
||||
<option value={idField.runtimeBinding}>
|
||||
{idField.readableBinding}
|
||||
</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{/if}
|
||||
|
||||
{#if recordId}
|
||||
<SaveFields
|
||||
parameterFields={parameters.fields}
|
||||
{schemaFields}
|
||||
on:fieldschanged={onFieldsChanged} />
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-s);
|
||||
row-gap: var(--spacing-s);
|
||||
grid-template-columns: auto 1fr auto 1fr auto;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.root :global(.relative:nth-child(2)) {
|
||||
grid-column-start: 2;
|
||||
grid-column-end: 6;
|
||||
}
|
||||
|
||||
.cannot-use {
|
||||
color: var(--red);
|
||||
font-size: var(--font-size-s);
|
||||
text-align: center;
|
||||
width: 70%;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,23 @@
|
|||
import NavigateTo from "./NavigateTo.svelte"
|
||||
import UpdateRecord from "./UpdateRecord.svelte"
|
||||
import CreateRecord from "./CreateRecord.svelte"
|
||||
|
||||
// defines what actions are available, when adding a new one
|
||||
// the component is the setup panel for the action
|
||||
// NOTE that the "name" is used by the client library,
|
||||
// so if you want to change it, you must change it client lib too
|
||||
|
||||
export default [
|
||||
{
|
||||
name: "Create Record",
|
||||
component: CreateRecord,
|
||||
},
|
||||
{
|
||||
name: "Navigate To",
|
||||
component: NavigateTo,
|
||||
},
|
||||
{
|
||||
name: "Update Record",
|
||||
component: UpdateRecord,
|
||||
},
|
||||
]
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./EventsEditor.svelte"
|
|
@ -3,6 +3,11 @@
|
|||
import Input from "./PropertyPanelControls/Input.svelte"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import fetchBindableProperties from "builderStore/fetchBindableProperties"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
CAPTURE_VAR_INSIDE_MUSTACHE,
|
||||
} from "builderStore/replaceBindings"
|
||||
import { DropdownMenu } from "@budibase/bbui"
|
||||
import BindingDropdown from "components/userInterface/BindingDropdown.svelte"
|
||||
import { onMount, getContext } from "svelte"
|
||||
|
@ -36,25 +41,12 @@
|
|||
})
|
||||
}
|
||||
|
||||
const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g
|
||||
function replaceBindings(textWithBindings) {
|
||||
getBindableProperties()
|
||||
// Find all instances of mustasche
|
||||
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE)
|
||||
|
||||
// Replace with names:
|
||||
boundValues &&
|
||||
boundValues.forEach(boundValue => {
|
||||
const binding = bindableProperties.find(({ readableBinding }) => {
|
||||
return boundValue === `{{ ${readableBinding} }}`
|
||||
})
|
||||
if (binding) {
|
||||
textWithBindings = textWithBindings.replace(
|
||||
boundValue,
|
||||
`{{ ${binding.runtimeBinding} }}`
|
||||
textWithBindings = readableToRuntimeBinding(
|
||||
bindableProperties,
|
||||
textWithBindings
|
||||
)
|
||||
}
|
||||
})
|
||||
onChange(key, textWithBindings)
|
||||
}
|
||||
|
||||
|
@ -67,7 +59,7 @@
|
|||
innerVal = props.valueKey ? v.target[props.valueKey] : v.target.value
|
||||
}
|
||||
}
|
||||
if (typeof innerVal !== "object") {
|
||||
if (typeof innerVal === "string") {
|
||||
replaceBindings(innerVal)
|
||||
} else {
|
||||
onChange(key, innerVal)
|
||||
|
@ -76,21 +68,9 @@
|
|||
|
||||
const safeValue = () => {
|
||||
getBindableProperties()
|
||||
let temp = value
|
||||
const boundValues =
|
||||
(typeof value === "string" && value.match(CAPTURE_VAR_INSIDE_MUSTACHE)) ||
|
||||
[]
|
||||
|
||||
// Replace with names:
|
||||
boundValues.forEach(v => {
|
||||
const binding = bindableProperties.find(({ runtimeBinding }) => {
|
||||
return v === `{{ ${runtimeBinding} }}`
|
||||
})
|
||||
if (binding) {
|
||||
temp = temp.replace(v, `{{ ${binding.readableBinding} }}`)
|
||||
}
|
||||
})
|
||||
// console.log(temp)
|
||||
let temp = runtimeToReadableBinding(bindableProperties, value)
|
||||
|
||||
return value === undefined && props.defaultValue !== undefined
|
||||
? props.defaultValue
|
||||
: temp
|
||||
|
|
|
@ -1051,6 +1051,16 @@ export default {
|
|||
key: "tooltipTitle",
|
||||
control: Input,
|
||||
},
|
||||
{
|
||||
label: "X Ticks",
|
||||
key: "xTicks",
|
||||
control: Input,
|
||||
},
|
||||
{
|
||||
label: "Y Ticks",
|
||||
key: "yTicks",
|
||||
control: Input,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -28,7 +28,8 @@ describe("fetch bindable properties", () => {
|
|||
...testData()
|
||||
})
|
||||
const contextBindings = result.filter(r => r.instance._id === "list-id" && r.type==="context")
|
||||
expect(contextBindings.length).toBe(2)
|
||||
// 2 fields + _id + _rev
|
||||
expect(contextBindings.length).toBe(4)
|
||||
|
||||
const namebinding = contextBindings.find(b => b.runtimeBinding === "data.name")
|
||||
expect(namebinding).toBeDefined()
|
||||
|
@ -37,6 +38,10 @@ describe("fetch bindable properties", () => {
|
|||
const descriptionbinding = contextBindings.find(b => b.runtimeBinding === "data.description")
|
||||
expect(descriptionbinding).toBeDefined()
|
||||
expect(descriptionbinding.readableBinding).toBe("list-name.Test Model.description")
|
||||
|
||||
const idbinding = contextBindings.find(b => b.runtimeBinding === "data._id")
|
||||
expect(idbinding).toBeDefined()
|
||||
expect(idbinding.readableBinding).toBe("list-name.Test Model._id")
|
||||
})
|
||||
|
||||
it("should return model schema, for grantparent context", () => {
|
||||
|
@ -45,7 +50,8 @@ describe("fetch bindable properties", () => {
|
|||
...testData()
|
||||
})
|
||||
const contextBindings = result.filter(r => r.type==="context")
|
||||
expect(contextBindings.length).toBe(4)
|
||||
// 2 fields + _id + _rev ... x 2 models
|
||||
expect(contextBindings.length).toBe(8)
|
||||
|
||||
const namebinding_parent = contextBindings.find(b => b.runtimeBinding === "parent.data.name")
|
||||
expect(namebinding_parent).toBeDefined()
|
||||
|
@ -120,7 +126,7 @@ const testData = () => {
|
|||
_id: "list-id",
|
||||
_component: "@budibase/standard-components/list",
|
||||
_instanceName: "list-name",
|
||||
model: "test-model-id",
|
||||
model: { isModel: true, modelId: "test-model-id", label: "Test Model", name: "all_test-model-id" },
|
||||
_children: [
|
||||
{
|
||||
_id: "list-item-heading-id",
|
||||
|
@ -138,7 +144,7 @@ const testData = () => {
|
|||
_id: "child-list-id",
|
||||
_component: "@budibase/standard-components/list",
|
||||
_instanceName: "child-list-name",
|
||||
model: "test-model-id",
|
||||
model: { isModel: true, modelId: "test-model-id", label: "Test Model", name: "all_test-model-id"},
|
||||
_children: [
|
||||
{
|
||||
_id: "child-list-item-heading-id",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -52,6 +52,49 @@ const apiOpts = {
|
|||
delete: del,
|
||||
}
|
||||
|
||||
const createRecord = async params =>
|
||||
await post({
|
||||
url: `/api/${params.modelId}/records`,
|
||||
body: makeRecordRequestBody(params),
|
||||
})
|
||||
|
||||
const updateRecord = async params => {
|
||||
const record = makeRecordRequestBody(params)
|
||||
record._id = params._id
|
||||
await patch({
|
||||
url: `/api/${params.modelId}/records/${params._id}`,
|
||||
body: record,
|
||||
})
|
||||
}
|
||||
|
||||
const makeRecordRequestBody = parameters => {
|
||||
const body = {}
|
||||
for (let fieldName in parameters.fields) {
|
||||
const field = parameters.fields[fieldName]
|
||||
|
||||
// ensure fields sent are of the correct type
|
||||
if (field.type === "boolean") {
|
||||
if (field.value === "true") body[fieldName] = true
|
||||
if (field.value === "false") body[fieldName] = false
|
||||
} else if (field.type === "number") {
|
||||
const val = parseFloat(field.value)
|
||||
if (!isNaN(val)) {
|
||||
body[fieldName] = val
|
||||
}
|
||||
} else if (field.type === "datetime") {
|
||||
const date = new Date(field.value)
|
||||
if (!isNaN(date.getTime())) {
|
||||
body[fieldName] = date.toISOString()
|
||||
}
|
||||
} else {
|
||||
body[fieldName] = field.value
|
||||
}
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
export default {
|
||||
authenticate: authenticate(apiOpts),
|
||||
createRecord,
|
||||
updateRecord,
|
||||
}
|
||||
|
|
|
@ -50,7 +50,6 @@ export const createApp = ({
|
|||
treeNode,
|
||||
onScreenSlotRendered,
|
||||
setupState: stateManager.setup,
|
||||
getCurrentState: stateManager.getCurrentState,
|
||||
})
|
||||
|
||||
return getInitialiseParams
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import setBindableComponentProp from "./setBindableComponentProp"
|
||||
import { attachChildren } from "../render/attachChildren"
|
||||
import store from "../state/store"
|
||||
|
||||
export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
|
||||
|
||||
export const bbFactory = ({
|
||||
componentLibraries,
|
||||
onScreenSlotRendered,
|
||||
getCurrentState,
|
||||
runEventActions,
|
||||
}) => {
|
||||
const apiCall = method => (url, body) => {
|
||||
return fetch(url, {
|
||||
|
@ -26,13 +27,6 @@ export const bbFactory = ({
|
|||
delete: apiCall("DELETE"),
|
||||
}
|
||||
|
||||
const safeCallEvent = (event, context) => {
|
||||
const isFunction = obj =>
|
||||
!!(obj && obj.constructor && obj.call && obj.apply)
|
||||
|
||||
if (isFunction(event)) event(context)
|
||||
}
|
||||
|
||||
return (treeNode, setupState) => {
|
||||
const attachParams = {
|
||||
componentLibraries,
|
||||
|
@ -44,12 +38,17 @@ export const bbFactory = ({
|
|||
return {
|
||||
attachChildren: attachChildren(attachParams),
|
||||
props: treeNode.props,
|
||||
call: safeCallEvent,
|
||||
call: async eventName =>
|
||||
eventName &&
|
||||
(await runEventActions(
|
||||
treeNode.props[eventName],
|
||||
store.getState(treeNode.contextStoreKey)
|
||||
)),
|
||||
setBinding: setBindableComponentProp(treeNode),
|
||||
api,
|
||||
parent,
|
||||
// these parameters are populated by screenRouter
|
||||
routeParams: () => getCurrentState()["##routeParams"],
|
||||
routeParams: () => store.getState()["##routeParams"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,38 @@
|
|||
import renderTemplateString from "./renderTemplateString"
|
||||
|
||||
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
||||
|
||||
export const eventHandlers = routeTo => {
|
||||
const handler = (parameters, execute) => ({
|
||||
execute,
|
||||
parameters,
|
||||
})
|
||||
|
||||
return {
|
||||
"Navigate To": handler(["url"], param => routeTo(param && param.url)),
|
||||
const handlers = {
|
||||
"Navigate To": param => routeTo(param && param.url),
|
||||
}
|
||||
|
||||
// when an event is called, this is what gets run
|
||||
const runEventActions = async (actions, state) => {
|
||||
if (!actions) return
|
||||
// calls event handlers sequentially
|
||||
for (let action of actions) {
|
||||
const handler = handlers[action[EVENT_TYPE_MEMBER_NAME]]
|
||||
const parameters = createParameters(action.parameters, state)
|
||||
if (handler) {
|
||||
await handler(parameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return runEventActions
|
||||
}
|
||||
|
||||
export const isEventType = prop =>
|
||||
Array.isArray(prop) &&
|
||||
prop.length > 0 &&
|
||||
!prop[0][EVENT_TYPE_MEMBER_NAME] === undefined
|
||||
// this will take a parameters obj, iterate all keys, and do a mustache render
|
||||
// for every string. It will work recursively if it encounnters an {}
|
||||
const createParameters = (parameterTemplateObj, state) => {
|
||||
const parameters = {}
|
||||
for (let key in parameterTemplateObj) {
|
||||
if (typeof parameterTemplateObj[key] === "string") {
|
||||
parameters[key] = renderTemplateString(parameterTemplateObj[key], state)
|
||||
} else if (typeof parameterTemplateObj[key] === "object") {
|
||||
parameters[key] = createParameters(parameterTemplateObj[key], state)
|
||||
}
|
||||
}
|
||||
return parameters
|
||||
}
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
import {
|
||||
isEventType,
|
||||
eventHandlers,
|
||||
EVENT_TYPE_MEMBER_NAME,
|
||||
} from "./eventHandlers"
|
||||
import { eventHandlers } from "./eventHandlers"
|
||||
import { bbFactory } from "./bbComponentApi"
|
||||
import renderTemplateString from "./renderTemplateString"
|
||||
import appStore from "./store"
|
||||
|
@ -25,33 +21,23 @@ export const createStateManager = ({
|
|||
onScreenSlotRendered,
|
||||
routeTo,
|
||||
}) => {
|
||||
let handlerTypes = eventHandlers(routeTo)
|
||||
|
||||
// creating a reference to the current state
|
||||
// this avoids doing store.get() ... which is expensive on
|
||||
// hot paths, according to the svelte docs.
|
||||
// the state object reference never changes (although it's internals do)
|
||||
// so this should work fine for us
|
||||
let currentState
|
||||
appStore.subscribe(s => (currentState = s))
|
||||
const getCurrentState = () => currentState
|
||||
let runEventActions = eventHandlers(routeTo)
|
||||
|
||||
const bb = bbFactory({
|
||||
getCurrentState,
|
||||
componentLibraries,
|
||||
onScreenSlotRendered,
|
||||
runEventActions,
|
||||
})
|
||||
|
||||
const setup = _setup({ handlerTypes, getCurrentState, bb })
|
||||
const setup = _setup(bb)
|
||||
|
||||
return {
|
||||
setup,
|
||||
destroy: () => {},
|
||||
getCurrentState,
|
||||
}
|
||||
}
|
||||
|
||||
const _setup = ({ handlerTypes, getCurrentState, bb }) => node => {
|
||||
const _setup = bb => node => {
|
||||
const props = node.props
|
||||
const initialProps = { ...props }
|
||||
|
||||
|
@ -70,53 +56,10 @@ const _setup = ({ handlerTypes, getCurrentState, bb }) => node => {
|
|||
node.stateBound = true
|
||||
}
|
||||
}
|
||||
|
||||
if (isEventType(propValue)) {
|
||||
const state = appStore.getState(node.contextStoreKey)
|
||||
const handlersInfos = []
|
||||
for (let event of propValue) {
|
||||
const handlerInfo = {
|
||||
handlerType: event[EVENT_TYPE_MEMBER_NAME],
|
||||
parameters: event.parameters,
|
||||
}
|
||||
|
||||
const resolvedParams = {}
|
||||
for (let paramName in handlerInfo.parameters) {
|
||||
const paramValue = handlerInfo.parameters[paramName]
|
||||
resolvedParams[paramName] = () =>
|
||||
renderTemplateString(paramValue, state)
|
||||
}
|
||||
|
||||
handlerInfo.parameters = resolvedParams
|
||||
handlersInfos.push(handlerInfo)
|
||||
}
|
||||
|
||||
if (handlersInfos.length === 0) {
|
||||
initialProps[propName] = doNothing
|
||||
} else {
|
||||
initialProps[propName] = async context => {
|
||||
for (let handlerInfo of handlersInfos) {
|
||||
const handler = makeHandler(handlerTypes, handlerInfo)
|
||||
await handler(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setup = _setup({ handlerTypes, getCurrentState, bb })
|
||||
const setup = _setup(bb)
|
||||
initialProps._bb = bb(node, setup)
|
||||
|
||||
return initialProps
|
||||
}
|
||||
|
||||
const makeHandler = (handlerTypes, handlerInfo) => {
|
||||
const handlerType = handlerTypes[handlerInfo.handlerType]
|
||||
return async context => {
|
||||
const parameters = {}
|
||||
for (let paramName in handlerInfo.parameters) {
|
||||
parameters[paramName] = handlerInfo.parameters[paramName](context)
|
||||
}
|
||||
await handlerType.execute(parameters)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -181,8 +181,7 @@ const maketestlib = window => ({
|
|||
currentProps = Object.assign(currentProps, props)
|
||||
if (currentProps.onClick) {
|
||||
node.addEventListener("click", () => {
|
||||
const testText = currentProps.testText || "hello"
|
||||
currentProps._bb.call(props.onClick, { testText })
|
||||
currentProps._bb.call("onClick")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,40 @@ validateJs.extend(validateJs.validators.datetime, {
|
|||
},
|
||||
})
|
||||
|
||||
exports.patch = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const record = await db.get(ctx.params.id)
|
||||
const model = await db.get(record.modelId)
|
||||
const patchfields = ctx.request.body
|
||||
|
||||
for (let key in patchfields) {
|
||||
if (!model.schema[key]) continue
|
||||
record[key] = patchfields[key]
|
||||
}
|
||||
|
||||
const validateResult = await validate({
|
||||
record,
|
||||
model,
|
||||
})
|
||||
|
||||
if (!validateResult.valid) {
|
||||
ctx.status = 400
|
||||
ctx.body = {
|
||||
status: 400,
|
||||
errors: validateResult.errors,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const response = await db.put(record)
|
||||
record._rev = response.rev
|
||||
record.type = "record"
|
||||
ctx.body = record
|
||||
ctx.status = 200
|
||||
ctx.message = `${model.name} updated successfully.`
|
||||
return
|
||||
}
|
||||
|
||||
exports.save = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const record = ctx.request.body
|
||||
|
|
|
@ -22,6 +22,11 @@ router
|
|||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.save
|
||||
)
|
||||
.patch(
|
||||
"/api/:modelId/records/:id",
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.patch
|
||||
)
|
||||
.post(
|
||||
"/api/:modelId/records/validate",
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
|
|
|
@ -51,6 +51,12 @@ exports.createModel = async (request, appId, instanceId, model) => {
|
|||
type: "string",
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: "text",
|
||||
constraints: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -30,13 +30,12 @@ describe("/records", () => {
|
|||
model = await createModel(request, app._id, instance._id)
|
||||
record = {
|
||||
name: "Test Contact",
|
||||
description: "original description",
|
||||
status: "new",
|
||||
modelId: model._id
|
||||
}
|
||||
})
|
||||
|
||||
describe("save, load, update, delete", () => {
|
||||
|
||||
const createRecord = async r =>
|
||||
await request
|
||||
.post(`/api/${model._id}/records`)
|
||||
|
@ -45,6 +44,17 @@ describe("/records", () => {
|
|||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const loadRecord = async id =>
|
||||
await request
|
||||
.get(`/api/${model._id}/records/${id}`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
||||
describe("save, load, update, delete", () => {
|
||||
|
||||
|
||||
it("returns a success message when the record is created", async () => {
|
||||
const res = await createRecord()
|
||||
expect(res.res.statusMessage).toEqual(`${model.name} created successfully`)
|
||||
|
@ -144,6 +154,35 @@ describe("/records", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("patch", () => {
|
||||
it("should update only the fields that are supplied", async () => {
|
||||
const rec = await createRecord()
|
||||
const existing = rec.body
|
||||
|
||||
const res = await request
|
||||
.patch(`/api/${model._id}/records/${existing._id}`)
|
||||
.send({
|
||||
_id: existing._id,
|
||||
_rev: existing._rev,
|
||||
modelId: model._id,
|
||||
name: "Updated Name",
|
||||
})
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.res.statusMessage).toEqual(`${model.name} updated successfully.`)
|
||||
expect(res.body.name).toEqual("Updated Name")
|
||||
expect(res.body.description).toEqual(existing.description)
|
||||
|
||||
const savedRecord = await loadRecord(res.body._id)
|
||||
|
||||
expect(savedRecord.body.description).toEqual(existing.description)
|
||||
expect(savedRecord.body.name).toEqual("Updated Name")
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe("validate", () => {
|
||||
it("should return no errors on valid record", async () => {
|
||||
const result = await request
|
||||
|
|
|
@ -72,8 +72,14 @@ describe("/views", () => {
|
|||
type: "text",
|
||||
constraints: {
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: "text",
|
||||
constraints: {
|
||||
type: "string"
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -252,7 +252,7 @@
|
|||
},
|
||||
"list": {
|
||||
"description": "A configurable data list that attaches to your backend models.",
|
||||
"context": "model",
|
||||
"context": "datasource",
|
||||
"children": true,
|
||||
"data": true,
|
||||
"props": {
|
||||
|
@ -537,23 +537,25 @@
|
|||
"height": "number",
|
||||
"axisTimeCombinations": "string",
|
||||
"color": "string",
|
||||
"grid": "string",
|
||||
"grid": {"type":"string", "default": "horizontal"},
|
||||
"aspectRatio": "number",
|
||||
"dateLabel": "string",
|
||||
"isAnimated": "bool",
|
||||
"isAnimated": {"type": "bool", "default": true},
|
||||
"lineCurve": "string",
|
||||
"locale": "string",
|
||||
"numberFormat": "string",
|
||||
"shouldShowAllDataPoints": "bool",
|
||||
"shouldShowAllDataPoints": {"type": "bool", "default": true},
|
||||
"topicLabel": "string",
|
||||
"valueLabel": "string",
|
||||
"xAxisValueType": "string",
|
||||
"xAxisValueType": {"type":"string", "default": "date"},
|
||||
"xAxisScale": "string",
|
||||
"xAxisFormat": "string",
|
||||
"xAxisFormat": {"type":"string", "default": "custom"},
|
||||
"xAxisCustomFormat": "string",
|
||||
"xAxisLabel": "string",
|
||||
"yAxisLabel": "string",
|
||||
"tooltipTitle": "string"
|
||||
"tooltipTitle": "string",
|
||||
"xTicks": "number",
|
||||
"yTicks": "number"
|
||||
}
|
||||
},
|
||||
"brush": {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -41,6 +41,7 @@
|
|||
"d3-selection": "^1.4.2",
|
||||
"fast-sort": "^2.2.0",
|
||||
"fusioncharts": "^3.15.1-sr.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"svelte-flatpickr": "^2.4.0",
|
||||
"svelte-fusioncharts": "^1.0.0"
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
export let className = "default"
|
||||
export let disabled = false
|
||||
export let text
|
||||
export let onClick
|
||||
|
||||
export let _bb
|
||||
let theButton
|
||||
|
@ -11,7 +10,7 @@
|
|||
theButton && _bb.attachChildren(theButton)
|
||||
|
||||
const clickHandler = () => {
|
||||
_bb.call(onClick)
|
||||
_bb.call("onClick")
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -62,6 +62,8 @@
|
|||
export let lines = null //not handled by setting prop
|
||||
export let tooltipThreshold = null
|
||||
export let tooltipTitle = ""
|
||||
export let xTicks = ""
|
||||
export let yTicks = ""
|
||||
|
||||
onMount(async () => {
|
||||
if (!isEmpty(datasource)) {
|
||||
|
@ -73,12 +75,15 @@
|
|||
bindChartEvents()
|
||||
chartContainer.datum(data).call(chart)
|
||||
|
||||
// X Axis Label gets cut off unless we do this 👇
|
||||
// Hack 🤮 X Axis Label and last tick label gets cut off unless we do this 👇
|
||||
const chartSvg = document.querySelector(`.${chartClass} .britechart`)
|
||||
if (chartSvg) {
|
||||
let height = chartSvg.getAttribute("height")
|
||||
let width = chartSvg.getAttribute("width")
|
||||
height = parseInt(height) + 35
|
||||
width = parseInt(width) + 15
|
||||
chartSvg.setAttribute("height", height)
|
||||
chartSvg.setAttribute("width", width)
|
||||
}
|
||||
|
||||
bindTooltip()
|
||||
|
@ -145,11 +150,10 @@
|
|||
}
|
||||
|
||||
function bindChartUIProps() {
|
||||
chart.grid("horizontal")
|
||||
chart.isAnimated(true)
|
||||
chart.tooltipThreshold(800)
|
||||
chart.aspectRatio(0.5)
|
||||
chart.xAxisCustomFormat("custom")
|
||||
chart.xAxisCustomFormat("%e %b %Y")
|
||||
chart.xTicks(data.dataByTopic.length)
|
||||
|
||||
if (notNull(color)) {
|
||||
chart.colorSchema(colorSchema)
|
||||
|
@ -214,6 +218,12 @@
|
|||
if (notNull(lines)) {
|
||||
chart.lines(lines)
|
||||
}
|
||||
if (notNull(xTicks)) {
|
||||
chart.xTicks(Number(xTicks))
|
||||
}
|
||||
if (notNull(yTicks)) {
|
||||
chart.yTicks(Number(yTicks))
|
||||
}
|
||||
if (notNull(tooltipTitle)) {
|
||||
tooltip.title(tooltipTitle)
|
||||
} else if (datasource.label) {
|
||||
|
@ -243,4 +253,4 @@
|
|||
$: chartGradient = getChartGradient(lineGradient)
|
||||
</script>
|
||||
|
||||
<div bind this:👇={chartElement} class={chartClass} />
|
||||
<div bind:this={chartElement} class={chartClass} />
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
if (containerElement) {
|
||||
_bb.attachChildren(containerElement)
|
||||
if (!hasLoaded) {
|
||||
_bb.call(onLoad)
|
||||
_bb.call("onLoad")
|
||||
hasLoaded = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { fade } from "svelte/transition"
|
||||
import { Label } from "@budibase/bbui"
|
||||
import { Label, DatePicker } from "@budibase/bbui"
|
||||
import debounce from "lodash.debounce"
|
||||
|
||||
export let _bb
|
||||
export let model
|
||||
|
@ -14,60 +15,49 @@
|
|||
number: "number",
|
||||
}
|
||||
|
||||
const DEFAULTS_FOR_TYPE = {
|
||||
string: "",
|
||||
boolean: false,
|
||||
number: null,
|
||||
link: [],
|
||||
}
|
||||
|
||||
let record
|
||||
let store = _bb.store
|
||||
let schema = {}
|
||||
let modelDef = {}
|
||||
let saved = false
|
||||
let saving = false
|
||||
let recordId
|
||||
let isNew = true
|
||||
|
||||
let inputElements = {}
|
||||
let errors = {}
|
||||
|
||||
$: if (model && model.length !== 0) {
|
||||
fetchModel()
|
||||
}
|
||||
|
||||
$: fields = Object.keys(schema)
|
||||
$: fields = schema ? Object.keys(schema) : []
|
||||
|
||||
$: Object.values(inputElements).length && setForm(record)
|
||||
|
||||
const createBlankRecord = () => {
|
||||
if (!schema) return
|
||||
const newrecord = {
|
||||
modelId: model,
|
||||
}
|
||||
for (let fieldName in schema) {
|
||||
const field = schema[fieldName]
|
||||
// defaulting to first one, as a blank value will fail validation
|
||||
if (
|
||||
field.type === "string" &&
|
||||
field.constraints &&
|
||||
field.constraints.inclusion &&
|
||||
field.constraints.inclusion.length > 0
|
||||
) {
|
||||
newrecord[fieldName] = field.constraints.inclusion[0]
|
||||
} else if (field.type === "number") newrecord[fieldName] = null
|
||||
else if (field.type === "boolean") newrecord[fieldName] = false
|
||||
else if (field.type === "link") newrecord[fieldName] = []
|
||||
else newrecord[fieldName] = ""
|
||||
}
|
||||
return newrecord
|
||||
}
|
||||
$: errorMessages = Object.entries(errors).map(
|
||||
([field, message]) => `${field} ${message}`
|
||||
)
|
||||
|
||||
async function fetchModel() {
|
||||
const FETCH_MODEL_URL = `/api/models/${model}`
|
||||
const response = await _bb.api.get(FETCH_MODEL_URL)
|
||||
modelDef = await response.json()
|
||||
schema = modelDef.schema
|
||||
record = createBlankRecord()
|
||||
record = {
|
||||
modelId: model,
|
||||
}
|
||||
}
|
||||
|
||||
const save = debounce(async () => {
|
||||
for (let field of fields) {
|
||||
// Assign defaults to empty fields to prevent validation issues
|
||||
if (!(field in record))
|
||||
record[field] = DEFAULTS_FOR_TYPE[schema[field].type]
|
||||
}
|
||||
|
||||
async function save() {
|
||||
// prevent double clicking firing multiple requests
|
||||
if (saving) return
|
||||
saving = true
|
||||
const SAVE_RECORD_URL = `/api/${model}/records`
|
||||
const response = await _bb.api.post(SAVE_RECORD_URL, record)
|
||||
|
||||
|
@ -79,13 +69,11 @@
|
|||
return state
|
||||
})
|
||||
|
||||
errors = {}
|
||||
|
||||
// wipe form, if new record, otherwise update
|
||||
// model to get new _rev
|
||||
if (isNew) {
|
||||
resetForm()
|
||||
} else {
|
||||
record = json
|
||||
}
|
||||
record = isNew ? { modelId: model } : json
|
||||
|
||||
// set saved, and unset after 1 second
|
||||
// i.e. make the success notifier appear, then disappear again after time
|
||||
|
@ -94,69 +82,27 @@
|
|||
saved = false
|
||||
}, 1000)
|
||||
}
|
||||
saving = false
|
||||
}
|
||||
|
||||
// we cannot use svelte bind on these inputs, as it does not allow
|
||||
// bind, when the input type is dynamic
|
||||
const resetForm = () => {
|
||||
for (let el of Object.values(inputElements)) {
|
||||
el.value = ""
|
||||
if (el.checked) {
|
||||
el.checked = false
|
||||
}
|
||||
}
|
||||
record = createBlankRecord()
|
||||
if (response.status === 400) {
|
||||
errors = json.errors
|
||||
}
|
||||
})
|
||||
|
||||
const setForm = rec => {
|
||||
if (isNew || !rec) return
|
||||
for (let fieldName in inputElements) {
|
||||
if (typeof rec[fieldName] === "boolean") {
|
||||
inputElements[fieldName].checked = rec[fieldName]
|
||||
} else {
|
||||
inputElements[fieldName].value = rec[fieldName]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleInput = field => event => {
|
||||
let value
|
||||
|
||||
if (event.target.type === "checkbox") {
|
||||
value = event.target.checked
|
||||
record[field] = value
|
||||
return
|
||||
}
|
||||
|
||||
if (event.target.type === "number") {
|
||||
value = parseInt(event.target.value)
|
||||
record[field] = value
|
||||
return
|
||||
}
|
||||
|
||||
value = event.target.value
|
||||
record[field] = value
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
const routeParams = _bb.routeParams()
|
||||
recordId =
|
||||
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
|
||||
isNew = !recordId || recordId === "new"
|
||||
|
||||
if (isNew) {
|
||||
record = createBlankRecord()
|
||||
} else {
|
||||
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
|
||||
_bb.api
|
||||
.get(GET_RECORD_URL)
|
||||
.then(response => response.json())
|
||||
.then(rec => {
|
||||
record = rec
|
||||
setForm(rec)
|
||||
})
|
||||
record = { modelId: model }
|
||||
return
|
||||
}
|
||||
|
||||
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
|
||||
const response = await _bb.api.get(GET_RECORD_URL)
|
||||
const json = await response.json()
|
||||
record = json
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -164,23 +110,28 @@
|
|||
{#if title}
|
||||
<h1>{title}</h1>
|
||||
{/if}
|
||||
{#each errorMessages as error}
|
||||
<p class="error">{error}</p>
|
||||
{/each}
|
||||
<hr />
|
||||
<div class="form-content">
|
||||
{#each fields as field}
|
||||
<div class="form-item">
|
||||
<Label small forAttr={'form-stacked-text'}>{field}</Label>
|
||||
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
|
||||
<select on:blur={handleInput(field)} bind:this={inputElements[field]}>
|
||||
<select bind:value={record[field]}>
|
||||
{#each schema[field].constraints.inclusion as opt}
|
||||
<option>{opt}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else}
|
||||
<input
|
||||
bind:this={inputElements[field]}
|
||||
class="input"
|
||||
type={TYPE_MAP[schema[field].type]}
|
||||
on:change={handleInput(field)} />
|
||||
{:else if schema[field].type === 'datetime'}
|
||||
<DatePicker bind:value={record[field]} />
|
||||
{:else if schema[field].type === 'boolean'}
|
||||
<input class="input" type="checkbox" bind:checked={record[field]} />
|
||||
{:else if schema[field].type === 'number'}
|
||||
<input class="input" type="number" bind:value={record[field]} />
|
||||
{:else if schema[field].type === 'string'}
|
||||
<input class="input" type="text" bind:value={record[field]} />
|
||||
{/if}
|
||||
</div>
|
||||
<hr />
|
||||
|
@ -302,4 +253,9 @@
|
|||
background-position: right 17px top 1.5em, right 10px top 1.5em;
|
||||
background-size: 7px 7px, 7px 7px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { fade } from "svelte/transition"
|
||||
import { Label } from "@budibase/bbui"
|
||||
import { Label, DatePicker } from "@budibase/bbui"
|
||||
import debounce from "lodash.debounce"
|
||||
|
||||
export let _bb
|
||||
export let model
|
||||
|
@ -14,60 +15,50 @@
|
|||
number: "number",
|
||||
}
|
||||
|
||||
const DEFAULTS_FOR_TYPE = {
|
||||
string: "",
|
||||
boolean: false,
|
||||
number: null,
|
||||
link: [],
|
||||
}
|
||||
|
||||
let record
|
||||
let store = _bb.store
|
||||
let schema = {}
|
||||
let modelDef = {}
|
||||
let saved = false
|
||||
let saving = false
|
||||
let recordId
|
||||
let isNew = true
|
||||
|
||||
let inputElements = {}
|
||||
let errors = {}
|
||||
|
||||
$: if (model && model.length !== 0) {
|
||||
fetchModel()
|
||||
}
|
||||
|
||||
$: fields = Object.keys(schema)
|
||||
$: fields = schema ? Object.keys(schema) : []
|
||||
|
||||
$: Object.values(inputElements).length && setForm(record)
|
||||
$: errorMessages = Object.entries(errors).map(
|
||||
([field, message]) => `${field} ${message}`
|
||||
)
|
||||
|
||||
const createBlankRecord = () => {
|
||||
if (!schema) return
|
||||
const newrecord = {
|
||||
modelId: model,
|
||||
}
|
||||
for (let fieldName in schema) {
|
||||
const field = schema[fieldName]
|
||||
// defaulting to first one, as a blank value will fail validation
|
||||
if (
|
||||
field.type === "string" &&
|
||||
field.constraints &&
|
||||
field.constraints.inclusion &&
|
||||
field.constraints.inclusion.length > 0
|
||||
) {
|
||||
newrecord[fieldName] = field.constraints.inclusion[0]
|
||||
} else if (field.type === "number") newrecord[fieldName] = null
|
||||
else if (field.type === "boolean") newrecord[fieldName] = false
|
||||
else if (field.type === "link") newrecord[fieldName] = []
|
||||
else newrecord[fieldName] = ""
|
||||
}
|
||||
return newrecord
|
||||
}
|
||||
|
||||
async function fetchModel() {
|
||||
const FETCH_MODEL_URL = `/api/models/${model}`
|
||||
const response = await _bb.api.get(FETCH_MODEL_URL)
|
||||
modelDef = await response.json()
|
||||
schema = modelDef.schema
|
||||
record = createBlankRecord()
|
||||
record = {
|
||||
modelId: model,
|
||||
}
|
||||
}
|
||||
|
||||
const save = debounce(async () => {
|
||||
for (let field of fields) {
|
||||
// Assign defaults to empty fields to prevent validation issues
|
||||
if (!(field in record))
|
||||
record[field] = DEFAULTS_FOR_TYPE[schema[field].type]
|
||||
}
|
||||
|
||||
async function save() {
|
||||
// prevent double clicking firing multiple requests
|
||||
if (saving) return
|
||||
saving = true
|
||||
const SAVE_RECORD_URL = `/api/${model}/records`
|
||||
const response = await _bb.api.post(SAVE_RECORD_URL, record)
|
||||
|
||||
|
@ -79,13 +70,11 @@
|
|||
return state
|
||||
})
|
||||
|
||||
errors = {}
|
||||
|
||||
// wipe form, if new record, otherwise update
|
||||
// model to get new _rev
|
||||
if (isNew) {
|
||||
resetForm()
|
||||
} else {
|
||||
record = json
|
||||
}
|
||||
record = isNew ? { modelId: model } : json
|
||||
|
||||
// set saved, and unset after 1 second
|
||||
// i.e. make the success notifier appear, then disappear again after time
|
||||
|
@ -94,69 +83,27 @@
|
|||
saved = false
|
||||
}, 1000)
|
||||
}
|
||||
saving = false
|
||||
}
|
||||
|
||||
// we cannot use svelte bind on these inputs, as it does not allow
|
||||
// bind, when the input type is dynamic
|
||||
const resetForm = () => {
|
||||
for (let el of Object.values(inputElements)) {
|
||||
el.value = ""
|
||||
if (el.checked) {
|
||||
el.checked = false
|
||||
}
|
||||
}
|
||||
record = createBlankRecord()
|
||||
if (response.status === 400) {
|
||||
errors = json.errors
|
||||
}
|
||||
})
|
||||
|
||||
const setForm = rec => {
|
||||
if (isNew || !rec) return
|
||||
for (let fieldName in inputElements) {
|
||||
if (typeof rec[fieldName] === "boolean") {
|
||||
inputElements[fieldName].checked = rec[fieldName]
|
||||
} else {
|
||||
inputElements[fieldName].value = rec[fieldName]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleInput = field => event => {
|
||||
let value
|
||||
|
||||
if (event.target.type === "checkbox") {
|
||||
value = event.target.checked
|
||||
record[field] = value
|
||||
return
|
||||
}
|
||||
|
||||
if (event.target.type === "number") {
|
||||
value = parseInt(event.target.value)
|
||||
record[field] = value
|
||||
return
|
||||
}
|
||||
|
||||
value = event.target.value
|
||||
record[field] = value
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
const routeParams = _bb.routeParams()
|
||||
recordId =
|
||||
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
|
||||
isNew = !recordId || recordId === "new"
|
||||
|
||||
if (isNew) {
|
||||
record = createBlankRecord()
|
||||
} else {
|
||||
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
|
||||
_bb.api
|
||||
.get(GET_RECORD_URL)
|
||||
.then(response => response.json())
|
||||
.then(rec => {
|
||||
record = rec
|
||||
setForm(rec)
|
||||
})
|
||||
record = { modelId: model }
|
||||
return
|
||||
}
|
||||
|
||||
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
|
||||
const response = await _bb.api.get(GET_RECORD_URL)
|
||||
const json = await response.json()
|
||||
record = json
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -164,23 +111,28 @@
|
|||
{#if title}
|
||||
<h1>{title}</h1>
|
||||
{/if}
|
||||
{#each errorMessages as error}
|
||||
<p class="error">{error}</p>
|
||||
{/each}
|
||||
<hr />
|
||||
<div class="form-content">
|
||||
{#each fields as field}
|
||||
<div class="form-item">
|
||||
<Label small forAttr={'form-stacked-text'}>{field}</Label>
|
||||
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
|
||||
<select on:blur={handleInput(field)} bind:this={inputElements[field]}>
|
||||
<select bind:value={record[field]}>
|
||||
{#each schema[field].constraints.inclusion as opt}
|
||||
<option>{opt}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else}
|
||||
<input
|
||||
bind:this={inputElements[field]}
|
||||
class="input"
|
||||
type={TYPE_MAP[schema[field].type]}
|
||||
on:change={handleInput(field)} />
|
||||
{:else if schema[field].type === 'datetime'}
|
||||
<DatePicker bind:value={record[field]} />
|
||||
{:else if schema[field].type === 'boolean'}
|
||||
<input class="input" type="checkbox" bind:checked={record[field]} />
|
||||
{:else if schema[field].type === 'number'}
|
||||
<input class="input" type="number" bind:value={record[field]} />
|
||||
{:else if schema[field].type === 'string'}
|
||||
<input class="input" type="text" bind:value={record[field]} />
|
||||
{/if}
|
||||
</div>
|
||||
<hr />
|
||||
|
@ -293,4 +245,9 @@
|
|||
background-position: right 17px top 1.5em, right 10px top 1.5em;
|
||||
background-size: 7px 7px, 7px 7px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
if (itemContainer) {
|
||||
_bb.attachChildren(itemContainer)
|
||||
if (!hasLoaded) {
|
||||
_bb.call(onLoad)
|
||||
_bb.call("onLoad")
|
||||
hasLoaded = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,10 @@
|
|||
export let _bb
|
||||
|
||||
const rowClickHandler = row => () => {
|
||||
_bb.call(onRowClick, row)
|
||||
// call currently only accepts one argument, so passing row does nothing
|
||||
// however, we do not expose this event anyway. I am leaving this
|
||||
// in for the future, as can and probably should hande this
|
||||
_bb.call("onRowClick", row)
|
||||
}
|
||||
|
||||
const cellValue = (colIndex, row) => {
|
||||
|
|
Loading…
Reference in New Issue