Merge in master
This commit is contained in:
commit
d166d8072b
|
@ -21,7 +21,7 @@
|
||||||
"publishdev": "lerna run publishdev",
|
"publishdev": "lerna run publishdev",
|
||||||
"publishnpm": "yarn build && lerna publish --force-publish",
|
"publishnpm": "yarn build && lerna publish --force-publish",
|
||||||
"clean": "lerna clean",
|
"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",
|
"test": "lerna run test",
|
||||||
"lint": "eslint packages",
|
"lint": "eslint packages",
|
||||||
"lint:fix": "eslint --fix packages",
|
"lint:fix": "eslint --fix packages",
|
||||||
|
|
|
@ -5,4 +5,5 @@ package-lock.json
|
||||||
release/
|
release/
|
||||||
dist/
|
dist/
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
|
cypress/videos
|
||||||
routify
|
routify
|
||||||
|
|
|
@ -72,7 +72,6 @@
|
||||||
"d3-selection": "^1.4.1",
|
"d3-selection": "^1.4.1",
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"fast-sort": "^2.2.0",
|
"fast-sort": "^2.2.0",
|
||||||
"feather-icons": "^4.21.0",
|
|
||||||
"lodash": "^4.17.13",
|
"lodash": "^4.17.13",
|
||||||
"mustache": "^4.0.1",
|
"mustache": "^4.0.1",
|
||||||
"posthog-js": "1.3.1",
|
"posthog-js": "1.3.1",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Array.flat needs polyfilled in < Node 11
|
||||||
if (!Array.prototype.flat) {
|
if (!Array.prototype.flat) {
|
||||||
Object.defineProperty(Array.prototype, "flat", {
|
Object.defineProperty(Array.prototype, "flat", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
|
|
|
@ -37,7 +37,9 @@ export default function({ componentInstanceId, screen, components, models }) {
|
||||||
.filter(isInstanceInSharedContext(walkResult))
|
.filter(isInstanceInSharedContext(walkResult))
|
||||||
.map(componentInstanceToBindable(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)
|
const contextParentPath = getParentPath(walkResult, context)
|
||||||
|
|
||||||
return Object.keys(context.model.schema).map(k => ({
|
const newBindable = key => ({
|
||||||
type: "context",
|
type: "context",
|
||||||
instance: context.instance,
|
instance: context.instance,
|
||||||
// how the binding expression persists, and is used in the app at runtime
|
// 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
|
// 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) => {
|
const getParentPath = (walkResult, context) => {
|
||||||
|
@ -135,7 +151,7 @@ const walk = ({ instance, targetId, components, models, result }) => {
|
||||||
if (contextualInstance) {
|
if (contextualInstance) {
|
||||||
// add to currentContexts (ancestory of context)
|
// add to currentContexts (ancestory of context)
|
||||||
// before walking children
|
// before walking children
|
||||||
const model = models.find(m => m._id === instance[component.context])
|
const model = instance[component.context]
|
||||||
result.currentContexts.push({ instance, model })
|
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 InfoIcon } from "./Info.svelte"
|
||||||
export { default as CloseIcon } from "./Close.svelte"
|
export { default as CloseIcon } from "./Close.svelte"
|
||||||
export { default as MoreIcon } from "./More.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,
|
CircleIndicator,
|
||||||
EventsIcon,
|
EventsIcon,
|
||||||
} from "components/common/Icons/"
|
} from "components/common/Icons/"
|
||||||
import EventsEditor from "./EventsEditor"
|
|
||||||
import panelStructure from "./temporaryPanelStructure.js"
|
import panelStructure from "./temporaryPanelStructure.js"
|
||||||
import CategoryTab from "./CategoryTab.svelte"
|
import CategoryTab from "./CategoryTab.svelte"
|
||||||
import DesignView from "./DesignView.svelte"
|
import DesignView from "./DesignView.svelte"
|
||||||
|
@ -21,7 +20,6 @@
|
||||||
let categories = [
|
let categories = [
|
||||||
{ value: "settings", name: "Settings" },
|
{ value: "settings", name: "Settings" },
|
||||||
{ value: "design", name: "Design" },
|
{ value: "design", name: "Design" },
|
||||||
{ value: "events", name: "Events" },
|
|
||||||
]
|
]
|
||||||
let selectedCategory = categories[0]
|
let selectedCategory = categories[0]
|
||||||
|
|
||||||
|
@ -109,8 +107,6 @@
|
||||||
displayNameField={displayName}
|
displayNameField={displayName}
|
||||||
onChange={onPropChanged}
|
onChange={onPropChanged}
|
||||||
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
|
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
|
||||||
{:else if selectedCategory.value === 'events'}
|
|
||||||
<EventsEditor component={componentInstance} />
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,168 +1,220 @@
|
||||||
<script>
|
<script>
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { Button, Select } from "@budibase/bbui"
|
import { TextButton, Button, Heading, DropdownMenu } from "@budibase/bbui"
|
||||||
import HandlerSelector from "./HandlerSelector.svelte"
|
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
||||||
import ActionButton from "../../common/ActionButton.svelte"
|
|
||||||
import getIcon from "../../common/icon"
|
|
||||||
import { CloseIcon } from "components/common/Icons/"
|
|
||||||
|
|
||||||
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
|
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
|
||||||
|
import actionTypes from "./actions"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let event
|
export let event
|
||||||
export let eventOptions = []
|
|
||||||
export let onClose
|
|
||||||
|
|
||||||
let eventType = ""
|
let addActionButton
|
||||||
|
let addActionDropdown
|
||||||
|
let selectedAction
|
||||||
|
|
||||||
let draftEventHandler = { parameters: [] }
|
let draftEventHandler = { parameters: [] }
|
||||||
|
|
||||||
$: eventData = event || { handlers: [] }
|
$: actions = event || []
|
||||||
$: if (!eventOptions.includes(eventType) && eventOptions.length > 0)
|
$: selectedActionComponent =
|
||||||
eventType = eventOptions[0].name
|
selectedAction &&
|
||||||
|
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_MEMBER_NAME])
|
||||||
|
.component
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
onClose()
|
dispatch("close")
|
||||||
draftEventHandler = { parameters: [] }
|
draftEventHandler = { parameters: [] }
|
||||||
eventData = { handlers: [] }
|
actions = []
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateEventHandler = (updatedHandler, index) => {
|
const updateEventHandler = (updatedHandler, index) => {
|
||||||
eventData.handlers[index] = updatedHandler
|
actions[index] = updatedHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateDraftEventHandler = updatedHandler => {
|
const deleteAction = index => {
|
||||||
draftEventHandler = updatedHandler
|
actions.splice(index, 1)
|
||||||
|
actions = actions
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteEventHandler = index => {
|
const addAction = actionType => () => {
|
||||||
eventData.handlers.splice(index, 1)
|
const newAction = {
|
||||||
eventData = eventData
|
|
||||||
}
|
|
||||||
|
|
||||||
const createNewEventHandler = handler => {
|
|
||||||
const newHandler = handler || {
|
|
||||||
parameters: {},
|
parameters: {},
|
||||||
[EVENT_TYPE_MEMBER_NAME]: "",
|
[EVENT_TYPE_MEMBER_NAME]: actionType.name,
|
||||||
}
|
}
|
||||||
eventData.handlers.push(newHandler)
|
actions.push(newAction)
|
||||||
eventData = eventData
|
selectedAction = newAction
|
||||||
|
actions = actions
|
||||||
|
addActionDropdown.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteEvent = () => {
|
const selectAction = action => () => {
|
||||||
store.setComponentProp(eventType, [])
|
selectedAction = action
|
||||||
closeModal()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveEventData = () => {
|
const saveEventData = () => {
|
||||||
store.setComponentProp(eventType, eventData.handlers)
|
dispatch("change", actions)
|
||||||
closeModal()
|
closeModal()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="root">
|
||||||
<div class="body">
|
|
||||||
<div class="heading">
|
|
||||||
<h3>
|
|
||||||
{eventData.name ? `${eventData.name} Event` : 'Create a New Component Event'}
|
|
||||||
</h3>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
<div class="header">
|
||||||
<h4>Event Action(s)</h4>
|
<Heading small dark>Actions</Heading>
|
||||||
<HandlerSelector
|
<div bind:this={addActionButton}>
|
||||||
newHandler
|
<TextButton text small blue on:click={addActionDropdown.show}>
|
||||||
onChanged={updateDraftEventHandler}
|
Add Action
|
||||||
onCreate={() => {
|
<div style="height: 20px; width: 20px;">
|
||||||
createNewEventHandler(draftEventHandler)
|
<AddIcon />
|
||||||
draftEventHandler = { parameters: [] }
|
</div>
|
||||||
}}
|
</TextButton>
|
||||||
handler={draftEventHandler} />
|
|
||||||
</div>
|
</div>
|
||||||
{#if eventData}
|
<DropdownMenu
|
||||||
{#each eventData.handlers as handler, index}
|
bind:this={addActionDropdown}
|
||||||
<HandlerSelector
|
anchor={addActionButton}
|
||||||
{index}
|
align="right">
|
||||||
onChanged={updateEventHandler}
|
<div class="available-actions-container">
|
||||||
onRemoved={() => deleteEventHandler(index)}
|
{#each actionTypes as actionType}
|
||||||
{handler} />
|
<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)}>
|
||||||
|
<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>
|
||||||
|
</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}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
{#if eventData.name}
|
<a href="https://docs.budibase.com">Learn more about Actions</a>
|
||||||
<Button
|
<Button secondary on:click={closeModal}>Cancel</Button>
|
||||||
outline
|
<Button primary on:click={saveEventData}>Save</Button>
|
||||||
on:click={deleteEvent}
|
|
||||||
disabled={eventData.handlers.length === 0}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
<div class="save">
|
|
||||||
<Button
|
|
||||||
primary
|
|
||||||
on:click={saveEventData}
|
|
||||||
disabled={eventData.handlers.length === 0}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="close-button" on:click={closeModal}>
|
|
||||||
<CloseIcon />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.root {
|
||||||
position: relative;
|
max-height: 50vh;
|
||||||
}
|
width: 700px;
|
||||||
.heading {
|
display: flex;
|
||||||
margin-bottom: 20px;
|
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;
|
cursor: pointer;
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
}
|
|
||||||
.close-button :global(svg) {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
.available-action:hover {
|
||||||
margin-bottom: 10px;
|
background: var(--grey-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
.actions-container {
|
||||||
margin: 0;
|
flex: 1;
|
||||||
font-size: 24px;
|
min-height: 0px;
|
||||||
font-weight: bold;
|
padding-bottom: var(--spacing-s);
|
||||||
|
padding-top: 0;
|
||||||
|
border: var(--border-light);
|
||||||
|
border-width: 0 0 1px 0;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.body {
|
|
||||||
padding: 40px;
|
.action-container {
|
||||||
display: grid;
|
border: var(--border-light);
|
||||||
grid-gap: 20px;
|
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;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding: 30px 40px;
|
flex-direction: row;
|
||||||
border-bottom-left-radius: 5px;
|
|
||||||
border-bottom-right-radius: 50px;
|
|
||||||
background-color: var(--grey-1);
|
|
||||||
}
|
}
|
||||||
.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>
|
</style>
|
||||||
|
|
|
@ -1,25 +1,24 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, DropdownMenu } from "@budibase/bbui"
|
import { Button, Modal } from "@budibase/bbui"
|
||||||
import EventEditorModal from "./EventEditorModal.svelte"
|
import EventEditorModal from "./EventEditorModal.svelte"
|
||||||
import { getContext } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let name
|
export let name
|
||||||
|
|
||||||
let button
|
let eventsModal
|
||||||
let dropdown
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={button}>
|
<Button secondary small on:click={eventsModal.show}>Define Actions</Button>
|
||||||
<Button secondary small on:click={dropdown.show}>Define Actions</Button>
|
|
||||||
</div>
|
<Modal bind:this={eventsModal} maxWidth="100vw" hideCloseButton>
|
||||||
<DropdownMenu bind:this={dropdown} align="right" anchor={button}>
|
|
||||||
<EventEditorModal
|
<EventEditorModal
|
||||||
event={value}
|
event={value}
|
||||||
eventType={name}
|
eventType={name}
|
||||||
on:change
|
on:change
|
||||||
on:close={dropdown.hide} />
|
on:close={eventsModal.hide} />
|
||||||
</DropdownMenu>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<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 Input from "./PropertyPanelControls/Input.svelte"
|
||||||
import { store, backendUiStore } from "builderStore"
|
import { store, backendUiStore } from "builderStore"
|
||||||
import fetchBindableProperties from "builderStore/fetchBindableProperties"
|
import fetchBindableProperties from "builderStore/fetchBindableProperties"
|
||||||
|
import {
|
||||||
|
readableToRuntimeBinding,
|
||||||
|
runtimeToReadableBinding,
|
||||||
|
CAPTURE_VAR_INSIDE_MUSTACHE,
|
||||||
|
} from "builderStore/replaceBindings"
|
||||||
import { DropdownMenu } from "@budibase/bbui"
|
import { DropdownMenu } from "@budibase/bbui"
|
||||||
import BindingDropdown from "components/userInterface/BindingDropdown.svelte"
|
import BindingDropdown from "components/userInterface/BindingDropdown.svelte"
|
||||||
import { onMount, getContext } from "svelte"
|
import { onMount, getContext } from "svelte"
|
||||||
|
@ -36,25 +41,12 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g
|
|
||||||
function replaceBindings(textWithBindings) {
|
function replaceBindings(textWithBindings) {
|
||||||
getBindableProperties()
|
getBindableProperties()
|
||||||
// Find all instances of mustasche
|
textWithBindings = readableToRuntimeBinding(
|
||||||
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE)
|
bindableProperties,
|
||||||
|
textWithBindings
|
||||||
// Replace with names:
|
)
|
||||||
boundValues &&
|
|
||||||
boundValues.forEach(boundValue => {
|
|
||||||
const binding = bindableProperties.find(({ readableBinding }) => {
|
|
||||||
return boundValue === `{{ ${readableBinding} }}`
|
|
||||||
})
|
|
||||||
if (binding) {
|
|
||||||
textWithBindings = textWithBindings.replace(
|
|
||||||
boundValue,
|
|
||||||
`{{ ${binding.runtimeBinding} }}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
onChange(key, textWithBindings)
|
onChange(key, textWithBindings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +59,7 @@
|
||||||
innerVal = props.valueKey ? v.target[props.valueKey] : v.target.value
|
innerVal = props.valueKey ? v.target[props.valueKey] : v.target.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof innerVal !== "object") {
|
if (typeof innerVal === "string") {
|
||||||
replaceBindings(innerVal)
|
replaceBindings(innerVal)
|
||||||
} else {
|
} else {
|
||||||
onChange(key, innerVal)
|
onChange(key, innerVal)
|
||||||
|
@ -76,21 +68,9 @@
|
||||||
|
|
||||||
const safeValue = () => {
|
const safeValue = () => {
|
||||||
getBindableProperties()
|
getBindableProperties()
|
||||||
let temp = value
|
|
||||||
const boundValues =
|
|
||||||
(typeof value === "string" && value.match(CAPTURE_VAR_INSIDE_MUSTACHE)) ||
|
|
||||||
[]
|
|
||||||
|
|
||||||
// Replace with names:
|
let temp = runtimeToReadableBinding(bindableProperties, value)
|
||||||
boundValues.forEach(v => {
|
|
||||||
const binding = bindableProperties.find(({ runtimeBinding }) => {
|
|
||||||
return v === `{{ ${runtimeBinding} }}`
|
|
||||||
})
|
|
||||||
if (binding) {
|
|
||||||
temp = temp.replace(v, `{{ ${binding.readableBinding} }}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// console.log(temp)
|
|
||||||
return value === undefined && props.defaultValue !== undefined
|
return value === undefined && props.defaultValue !== undefined
|
||||||
? props.defaultValue
|
? props.defaultValue
|
||||||
: temp
|
: temp
|
||||||
|
|
|
@ -1051,6 +1051,16 @@ export default {
|
||||||
key: "tooltipTitle",
|
key: "tooltipTitle",
|
||||||
control: Input,
|
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()
|
...testData()
|
||||||
})
|
})
|
||||||
const contextBindings = result.filter(r => r.instance._id === "list-id" && r.type==="context")
|
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")
|
const namebinding = contextBindings.find(b => b.runtimeBinding === "data.name")
|
||||||
expect(namebinding).toBeDefined()
|
expect(namebinding).toBeDefined()
|
||||||
|
@ -37,6 +38,10 @@ describe("fetch bindable properties", () => {
|
||||||
const descriptionbinding = contextBindings.find(b => b.runtimeBinding === "data.description")
|
const descriptionbinding = contextBindings.find(b => b.runtimeBinding === "data.description")
|
||||||
expect(descriptionbinding).toBeDefined()
|
expect(descriptionbinding).toBeDefined()
|
||||||
expect(descriptionbinding.readableBinding).toBe("list-name.Test Model.description")
|
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", () => {
|
it("should return model schema, for grantparent context", () => {
|
||||||
|
@ -45,7 +50,8 @@ describe("fetch bindable properties", () => {
|
||||||
...testData()
|
...testData()
|
||||||
})
|
})
|
||||||
const contextBindings = result.filter(r => r.type==="context")
|
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")
|
const namebinding_parent = contextBindings.find(b => b.runtimeBinding === "parent.data.name")
|
||||||
expect(namebinding_parent).toBeDefined()
|
expect(namebinding_parent).toBeDefined()
|
||||||
|
@ -120,7 +126,7 @@ const testData = () => {
|
||||||
_id: "list-id",
|
_id: "list-id",
|
||||||
_component: "@budibase/standard-components/list",
|
_component: "@budibase/standard-components/list",
|
||||||
_instanceName: "list-name",
|
_instanceName: "list-name",
|
||||||
model: "test-model-id",
|
model: { isModel: true, modelId: "test-model-id", label: "Test Model", name: "all_test-model-id" },
|
||||||
_children: [
|
_children: [
|
||||||
{
|
{
|
||||||
_id: "list-item-heading-id",
|
_id: "list-item-heading-id",
|
||||||
|
@ -138,7 +144,7 @@ const testData = () => {
|
||||||
_id: "child-list-id",
|
_id: "child-list-id",
|
||||||
_component: "@budibase/standard-components/list",
|
_component: "@budibase/standard-components/list",
|
||||||
_instanceName: "child-list-name",
|
_instanceName: "child-list-name",
|
||||||
model: "test-model-id",
|
model: { isModel: true, modelId: "test-model-id", label: "Test Model", name: "all_test-model-id"},
|
||||||
_children: [
|
_children: [
|
||||||
{
|
{
|
||||||
_id: "child-list-item-heading-id",
|
_id: "child-list-item-heading-id",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -52,6 +52,49 @@ const apiOpts = {
|
||||||
delete: del,
|
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 {
|
export default {
|
||||||
authenticate: authenticate(apiOpts),
|
authenticate: authenticate(apiOpts),
|
||||||
|
createRecord,
|
||||||
|
updateRecord,
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,6 @@ export const createApp = ({
|
||||||
treeNode,
|
treeNode,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
setupState: stateManager.setup,
|
setupState: stateManager.setup,
|
||||||
getCurrentState: stateManager.getCurrentState,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return getInitialiseParams
|
return getInitialiseParams
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import setBindableComponentProp from "./setBindableComponentProp"
|
import setBindableComponentProp from "./setBindableComponentProp"
|
||||||
import { attachChildren } from "../render/attachChildren"
|
import { attachChildren } from "../render/attachChildren"
|
||||||
|
import store from "../state/store"
|
||||||
|
|
||||||
export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
|
export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
|
||||||
|
|
||||||
export const bbFactory = ({
|
export const bbFactory = ({
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
getCurrentState,
|
runEventActions,
|
||||||
}) => {
|
}) => {
|
||||||
const apiCall = method => (url, body) => {
|
const apiCall = method => (url, body) => {
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
|
@ -26,13 +27,6 @@ export const bbFactory = ({
|
||||||
delete: apiCall("DELETE"),
|
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) => {
|
return (treeNode, setupState) => {
|
||||||
const attachParams = {
|
const attachParams = {
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
|
@ -44,12 +38,17 @@ export const bbFactory = ({
|
||||||
return {
|
return {
|
||||||
attachChildren: attachChildren(attachParams),
|
attachChildren: attachChildren(attachParams),
|
||||||
props: treeNode.props,
|
props: treeNode.props,
|
||||||
call: safeCallEvent,
|
call: async eventName =>
|
||||||
|
eventName &&
|
||||||
|
(await runEventActions(
|
||||||
|
treeNode.props[eventName],
|
||||||
|
store.getState(treeNode.contextStoreKey)
|
||||||
|
)),
|
||||||
setBinding: setBindableComponentProp(treeNode),
|
setBinding: setBindableComponentProp(treeNode),
|
||||||
api,
|
api,
|
||||||
parent,
|
parent,
|
||||||
// these parameters are populated by screenRouter
|
// 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 EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
||||||
|
|
||||||
export const eventHandlers = routeTo => {
|
export const eventHandlers = routeTo => {
|
||||||
const handler = (parameters, execute) => ({
|
const handlers = {
|
||||||
execute,
|
"Navigate To": param => routeTo(param && param.url),
|
||||||
parameters,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"Navigate To": handler(["url"], 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 =>
|
// this will take a parameters obj, iterate all keys, and do a mustache render
|
||||||
Array.isArray(prop) &&
|
// for every string. It will work recursively if it encounnters an {}
|
||||||
prop.length > 0 &&
|
const createParameters = (parameterTemplateObj, state) => {
|
||||||
!prop[0][EVENT_TYPE_MEMBER_NAME] === undefined
|
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 {
|
import { eventHandlers } from "./eventHandlers"
|
||||||
isEventType,
|
|
||||||
eventHandlers,
|
|
||||||
EVENT_TYPE_MEMBER_NAME,
|
|
||||||
} from "./eventHandlers"
|
|
||||||
import { bbFactory } from "./bbComponentApi"
|
import { bbFactory } from "./bbComponentApi"
|
||||||
import renderTemplateString from "./renderTemplateString"
|
import renderTemplateString from "./renderTemplateString"
|
||||||
import appStore from "./store"
|
import appStore from "./store"
|
||||||
|
@ -25,33 +21,23 @@ export const createStateManager = ({
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
routeTo,
|
routeTo,
|
||||||
}) => {
|
}) => {
|
||||||
let handlerTypes = eventHandlers(routeTo)
|
let runEventActions = 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
|
|
||||||
|
|
||||||
const bb = bbFactory({
|
const bb = bbFactory({
|
||||||
getCurrentState,
|
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
|
runEventActions,
|
||||||
})
|
})
|
||||||
|
|
||||||
const setup = _setup({ handlerTypes, getCurrentState, bb })
|
const setup = _setup(bb)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setup,
|
setup,
|
||||||
destroy: () => {},
|
destroy: () => {},
|
||||||
getCurrentState,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const _setup = ({ handlerTypes, getCurrentState, bb }) => node => {
|
const _setup = bb => node => {
|
||||||
const props = node.props
|
const props = node.props
|
||||||
const initialProps = { ...props }
|
const initialProps = { ...props }
|
||||||
|
|
||||||
|
@ -70,53 +56,10 @@ const _setup = ({ handlerTypes, getCurrentState, bb }) => node => {
|
||||||
node.stateBound = true
|
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)
|
initialProps._bb = bb(node, setup)
|
||||||
|
|
||||||
return initialProps
|
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)
|
currentProps = Object.assign(currentProps, props)
|
||||||
if (currentProps.onClick) {
|
if (currentProps.onClick) {
|
||||||
node.addEventListener("click", () => {
|
node.addEventListener("click", () => {
|
||||||
const testText = currentProps.testText || "hello"
|
currentProps._bb.call("onClick")
|
||||||
currentProps._bb.call(props.onClick, { testText })
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
exports.save = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const record = ctx.request.body
|
const record = ctx.request.body
|
||||||
|
|
|
@ -22,6 +22,11 @@ router
|
||||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||||
recordController.save
|
recordController.save
|
||||||
)
|
)
|
||||||
|
.patch(
|
||||||
|
"/api/:modelId/records/:id",
|
||||||
|
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||||
|
recordController.patch
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/:modelId/records/validate",
|
"/api/:modelId/records/validate",
|
||||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||||
|
|
|
@ -51,6 +51,12 @@ exports.createModel = async (request, appId, instanceId, model) => {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
description: {
|
||||||
|
type: "text",
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,20 +30,30 @@ describe("/records", () => {
|
||||||
model = await createModel(request, app._id, instance._id)
|
model = await createModel(request, app._id, instance._id)
|
||||||
record = {
|
record = {
|
||||||
name: "Test Contact",
|
name: "Test Contact",
|
||||||
|
description: "original description",
|
||||||
status: "new",
|
status: "new",
|
||||||
modelId: model._id
|
modelId: model._id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const createRecord = async r =>
|
||||||
|
await request
|
||||||
|
.post(`/api/${model._id}/records`)
|
||||||
|
.send(r || record)
|
||||||
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
|
.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", () => {
|
describe("save, load, update, delete", () => {
|
||||||
|
|
||||||
const createRecord = async r =>
|
|
||||||
await request
|
|
||||||
.post(`/api/${model._id}/records`)
|
|
||||||
.send(r || record)
|
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
it("returns a success message when the record is created", async () => {
|
it("returns a success message when the record is created", async () => {
|
||||||
const res = await createRecord()
|
const res = await createRecord()
|
||||||
|
@ -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", () => {
|
describe("validate", () => {
|
||||||
it("should return no errors on valid record", async () => {
|
it("should return no errors on valid record", async () => {
|
||||||
const result = await request
|
const result = await request
|
||||||
|
|
|
@ -72,8 +72,14 @@ describe("/views", () => {
|
||||||
type: "text",
|
type: "text",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string"
|
type: "string"
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
description: {
|
||||||
|
type: "text",
|
||||||
|
constraints: {
|
||||||
|
type: "string"
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -252,7 +252,7 @@
|
||||||
},
|
},
|
||||||
"list": {
|
"list": {
|
||||||
"description": "A configurable data list that attaches to your backend models.",
|
"description": "A configurable data list that attaches to your backend models.",
|
||||||
"context": "model",
|
"context": "datasource",
|
||||||
"children": true,
|
"children": true,
|
||||||
"data": true,
|
"data": true,
|
||||||
"props": {
|
"props": {
|
||||||
|
@ -537,23 +537,25 @@
|
||||||
"height": "number",
|
"height": "number",
|
||||||
"axisTimeCombinations": "string",
|
"axisTimeCombinations": "string",
|
||||||
"color": "string",
|
"color": "string",
|
||||||
"grid": "string",
|
"grid": {"type":"string", "default": "horizontal"},
|
||||||
"aspectRatio": "number",
|
"aspectRatio": "number",
|
||||||
"dateLabel": "string",
|
"dateLabel": "string",
|
||||||
"isAnimated": "bool",
|
"isAnimated": {"type": "bool", "default": true},
|
||||||
"lineCurve": "string",
|
"lineCurve": "string",
|
||||||
"locale": "string",
|
"locale": "string",
|
||||||
"numberFormat": "string",
|
"numberFormat": "string",
|
||||||
"shouldShowAllDataPoints": "bool",
|
"shouldShowAllDataPoints": {"type": "bool", "default": true},
|
||||||
"topicLabel": "string",
|
"topicLabel": "string",
|
||||||
"valueLabel": "string",
|
"valueLabel": "string",
|
||||||
"xAxisValueType": "string",
|
"xAxisValueType": {"type":"string", "default": "date"},
|
||||||
"xAxisScale": "string",
|
"xAxisScale": "string",
|
||||||
"xAxisFormat": "string",
|
"xAxisFormat": {"type":"string", "default": "custom"},
|
||||||
"xAxisCustomFormat": "string",
|
"xAxisCustomFormat": "string",
|
||||||
"xAxisLabel": "string",
|
"xAxisLabel": "string",
|
||||||
"yAxisLabel": "string",
|
"yAxisLabel": "string",
|
||||||
"tooltipTitle": "string"
|
"tooltipTitle": "string",
|
||||||
|
"xTicks": "number",
|
||||||
|
"yTicks": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"brush": {
|
"brush": {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -41,6 +41,7 @@
|
||||||
"d3-selection": "^1.4.2",
|
"d3-selection": "^1.4.2",
|
||||||
"fast-sort": "^2.2.0",
|
"fast-sort": "^2.2.0",
|
||||||
"fusioncharts": "^3.15.1-sr.1",
|
"fusioncharts": "^3.15.1-sr.1",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
"svelte-flatpickr": "^2.4.0",
|
"svelte-flatpickr": "^2.4.0",
|
||||||
"svelte-fusioncharts": "^1.0.0"
|
"svelte-fusioncharts": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
export let className = "default"
|
export let className = "default"
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let text
|
export let text
|
||||||
export let onClick
|
|
||||||
|
|
||||||
export let _bb
|
export let _bb
|
||||||
let theButton
|
let theButton
|
||||||
|
@ -11,7 +10,7 @@
|
||||||
theButton && _bb.attachChildren(theButton)
|
theButton && _bb.attachChildren(theButton)
|
||||||
|
|
||||||
const clickHandler = () => {
|
const clickHandler = () => {
|
||||||
_bb.call(onClick)
|
_bb.call("onClick")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,8 @@
|
||||||
export let lines = null //not handled by setting prop
|
export let lines = null //not handled by setting prop
|
||||||
export let tooltipThreshold = null
|
export let tooltipThreshold = null
|
||||||
export let tooltipTitle = ""
|
export let tooltipTitle = ""
|
||||||
|
export let xTicks = ""
|
||||||
|
export let yTicks = ""
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!isEmpty(datasource)) {
|
if (!isEmpty(datasource)) {
|
||||||
|
@ -73,12 +75,15 @@
|
||||||
bindChartEvents()
|
bindChartEvents()
|
||||||
chartContainer.datum(data).call(chart)
|
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`)
|
const chartSvg = document.querySelector(`.${chartClass} .britechart`)
|
||||||
if (chartSvg) {
|
if (chartSvg) {
|
||||||
let height = chartSvg.getAttribute("height")
|
let height = chartSvg.getAttribute("height")
|
||||||
|
let width = chartSvg.getAttribute("width")
|
||||||
height = parseInt(height) + 35
|
height = parseInt(height) + 35
|
||||||
|
width = parseInt(width) + 15
|
||||||
chartSvg.setAttribute("height", height)
|
chartSvg.setAttribute("height", height)
|
||||||
|
chartSvg.setAttribute("width", width)
|
||||||
}
|
}
|
||||||
|
|
||||||
bindTooltip()
|
bindTooltip()
|
||||||
|
@ -145,11 +150,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindChartUIProps() {
|
function bindChartUIProps() {
|
||||||
chart.grid("horizontal")
|
|
||||||
chart.isAnimated(true)
|
|
||||||
chart.tooltipThreshold(800)
|
chart.tooltipThreshold(800)
|
||||||
chart.aspectRatio(0.5)
|
chart.aspectRatio(0.5)
|
||||||
chart.xAxisCustomFormat("custom")
|
chart.xAxisCustomFormat("%e %b %Y")
|
||||||
|
chart.xTicks(data.dataByTopic.length)
|
||||||
|
|
||||||
if (notNull(color)) {
|
if (notNull(color)) {
|
||||||
chart.colorSchema(colorSchema)
|
chart.colorSchema(colorSchema)
|
||||||
|
@ -214,6 +218,12 @@
|
||||||
if (notNull(lines)) {
|
if (notNull(lines)) {
|
||||||
chart.lines(lines)
|
chart.lines(lines)
|
||||||
}
|
}
|
||||||
|
if (notNull(xTicks)) {
|
||||||
|
chart.xTicks(Number(xTicks))
|
||||||
|
}
|
||||||
|
if (notNull(yTicks)) {
|
||||||
|
chart.yTicks(Number(yTicks))
|
||||||
|
}
|
||||||
if (notNull(tooltipTitle)) {
|
if (notNull(tooltipTitle)) {
|
||||||
tooltip.title(tooltipTitle)
|
tooltip.title(tooltipTitle)
|
||||||
} else if (datasource.label) {
|
} else if (datasource.label) {
|
||||||
|
@ -243,4 +253,4 @@
|
||||||
$: chartGradient = getChartGradient(lineGradient)
|
$: chartGradient = getChartGradient(lineGradient)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind this:👇={chartElement} class={chartClass} />
|
<div bind:this={chartElement} class={chartClass} />
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
if (containerElement) {
|
if (containerElement) {
|
||||||
_bb.attachChildren(containerElement)
|
_bb.attachChildren(containerElement)
|
||||||
if (!hasLoaded) {
|
if (!hasLoaded) {
|
||||||
_bb.call(onLoad)
|
_bb.call("onLoad")
|
||||||
hasLoaded = true
|
hasLoaded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { fade } from "svelte/transition"
|
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 _bb
|
||||||
export let model
|
export let model
|
||||||
|
@ -14,60 +15,49 @@
|
||||||
number: "number",
|
number: "number",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULTS_FOR_TYPE = {
|
||||||
|
string: "",
|
||||||
|
boolean: false,
|
||||||
|
number: null,
|
||||||
|
link: [],
|
||||||
|
}
|
||||||
|
|
||||||
let record
|
let record
|
||||||
let store = _bb.store
|
let store = _bb.store
|
||||||
let schema = {}
|
let schema = {}
|
||||||
let modelDef = {}
|
let modelDef = {}
|
||||||
let saved = false
|
let saved = false
|
||||||
let saving = false
|
|
||||||
let recordId
|
let recordId
|
||||||
let isNew = true
|
let isNew = true
|
||||||
|
let errors = {}
|
||||||
let inputElements = {}
|
|
||||||
|
|
||||||
$: if (model && model.length !== 0) {
|
$: if (model && model.length !== 0) {
|
||||||
fetchModel()
|
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() {
|
async function fetchModel() {
|
||||||
const FETCH_MODEL_URL = `/api/models/${model}`
|
const FETCH_MODEL_URL = `/api/models/${model}`
|
||||||
const response = await _bb.api.get(FETCH_MODEL_URL)
|
const response = await _bb.api.get(FETCH_MODEL_URL)
|
||||||
modelDef = await response.json()
|
modelDef = await response.json()
|
||||||
schema = modelDef.schema
|
schema = modelDef.schema
|
||||||
record = createBlankRecord()
|
record = {
|
||||||
|
modelId: model,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
const save = debounce(async () => {
|
||||||
// prevent double clicking firing multiple requests
|
for (let field of fields) {
|
||||||
if (saving) return
|
// Assign defaults to empty fields to prevent validation issues
|
||||||
saving = true
|
if (!(field in record))
|
||||||
|
record[field] = DEFAULTS_FOR_TYPE[schema[field].type]
|
||||||
|
}
|
||||||
|
|
||||||
const SAVE_RECORD_URL = `/api/${model}/records`
|
const SAVE_RECORD_URL = `/api/${model}/records`
|
||||||
const response = await _bb.api.post(SAVE_RECORD_URL, record)
|
const response = await _bb.api.post(SAVE_RECORD_URL, record)
|
||||||
|
|
||||||
|
@ -79,13 +69,11 @@
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
|
||||||
// wipe form, if new record, otherwise update
|
// wipe form, if new record, otherwise update
|
||||||
// model to get new _rev
|
// model to get new _rev
|
||||||
if (isNew) {
|
record = isNew ? { modelId: model } : json
|
||||||
resetForm()
|
|
||||||
} else {
|
|
||||||
record = json
|
|
||||||
}
|
|
||||||
|
|
||||||
// set saved, and unset after 1 second
|
// set saved, and unset after 1 second
|
||||||
// i.e. make the success notifier appear, then disappear again after time
|
// i.e. make the success notifier appear, then disappear again after time
|
||||||
|
@ -94,69 +82,27 @@
|
||||||
saved = false
|
saved = false
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
saving = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// we cannot use svelte bind on these inputs, as it does not allow
|
if (response.status === 400) {
|
||||||
// bind, when the input type is dynamic
|
errors = json.errors
|
||||||
const resetForm = () => {
|
|
||||||
for (let el of Object.values(inputElements)) {
|
|
||||||
el.value = ""
|
|
||||||
if (el.checked) {
|
|
||||||
el.checked = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
record = createBlankRecord()
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const setForm = rec => {
|
onMount(async () => {
|
||||||
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(() => {
|
|
||||||
const routeParams = _bb.routeParams()
|
const routeParams = _bb.routeParams()
|
||||||
recordId =
|
recordId =
|
||||||
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
|
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
|
||||||
isNew = !recordId || recordId === "new"
|
isNew = !recordId || recordId === "new"
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
record = createBlankRecord()
|
record = { modelId: model }
|
||||||
} else {
|
return
|
||||||
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
|
|
||||||
_bb.api
|
|
||||||
.get(GET_RECORD_URL)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(rec => {
|
|
||||||
record = rec
|
|
||||||
setForm(rec)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
|
@ -164,23 +110,28 @@
|
||||||
{#if title}
|
{#if title}
|
||||||
<h1>{title}</h1>
|
<h1>{title}</h1>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#each errorMessages as error}
|
||||||
|
<p class="error">{error}</p>
|
||||||
|
{/each}
|
||||||
<hr />
|
<hr />
|
||||||
<div class="form-content">
|
<div class="form-content">
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<Label small forAttr={'form-stacked-text'}>{field}</Label>
|
<Label small forAttr={'form-stacked-text'}>{field}</Label>
|
||||||
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
|
{#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}
|
{#each schema[field].constraints.inclusion as opt}
|
||||||
<option>{opt}</option>
|
<option>{opt}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
{:else}
|
{:else if schema[field].type === 'datetime'}
|
||||||
<input
|
<DatePicker bind:value={record[field]} />
|
||||||
bind:this={inputElements[field]}
|
{:else if schema[field].type === 'boolean'}
|
||||||
class="input"
|
<input class="input" type="checkbox" bind:checked={record[field]} />
|
||||||
type={TYPE_MAP[schema[field].type]}
|
{:else if schema[field].type === 'number'}
|
||||||
on:change={handleInput(field)} />
|
<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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -302,4 +253,9 @@
|
||||||
background-position: right 17px top 1.5em, right 10px top 1.5em;
|
background-position: right 17px top 1.5em, right 10px top 1.5em;
|
||||||
background-size: 7px 7px, 7px 7px;
|
background-size: 7px 7px, 7px 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { fade } from "svelte/transition"
|
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 _bb
|
||||||
export let model
|
export let model
|
||||||
|
@ -14,60 +15,50 @@
|
||||||
number: "number",
|
number: "number",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULTS_FOR_TYPE = {
|
||||||
|
string: "",
|
||||||
|
boolean: false,
|
||||||
|
number: null,
|
||||||
|
link: [],
|
||||||
|
}
|
||||||
|
|
||||||
let record
|
let record
|
||||||
let store = _bb.store
|
let store = _bb.store
|
||||||
let schema = {}
|
let schema = {}
|
||||||
let modelDef = {}
|
let modelDef = {}
|
||||||
let saved = false
|
let saved = false
|
||||||
let saving = false
|
|
||||||
let recordId
|
let recordId
|
||||||
let isNew = true
|
let isNew = true
|
||||||
|
let errors = {}
|
||||||
let inputElements = {}
|
|
||||||
|
|
||||||
$: if (model && model.length !== 0) {
|
$: if (model && model.length !== 0) {
|
||||||
fetchModel()
|
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() {
|
async function fetchModel() {
|
||||||
const FETCH_MODEL_URL = `/api/models/${model}`
|
const FETCH_MODEL_URL = `/api/models/${model}`
|
||||||
const response = await _bb.api.get(FETCH_MODEL_URL)
|
const response = await _bb.api.get(FETCH_MODEL_URL)
|
||||||
modelDef = await response.json()
|
modelDef = await response.json()
|
||||||
schema = modelDef.schema
|
schema = modelDef.schema
|
||||||
record = createBlankRecord()
|
record = {
|
||||||
|
modelId: model,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
const save = debounce(async () => {
|
||||||
// prevent double clicking firing multiple requests
|
for (let field of fields) {
|
||||||
if (saving) return
|
// Assign defaults to empty fields to prevent validation issues
|
||||||
saving = true
|
if (!(field in record))
|
||||||
|
record[field] = DEFAULTS_FOR_TYPE[schema[field].type]
|
||||||
|
}
|
||||||
|
|
||||||
const SAVE_RECORD_URL = `/api/${model}/records`
|
const SAVE_RECORD_URL = `/api/${model}/records`
|
||||||
const response = await _bb.api.post(SAVE_RECORD_URL, record)
|
const response = await _bb.api.post(SAVE_RECORD_URL, record)
|
||||||
|
|
||||||
|
@ -79,13 +70,11 @@
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
|
||||||
// wipe form, if new record, otherwise update
|
// wipe form, if new record, otherwise update
|
||||||
// model to get new _rev
|
// model to get new _rev
|
||||||
if (isNew) {
|
record = isNew ? { modelId: model } : json
|
||||||
resetForm()
|
|
||||||
} else {
|
|
||||||
record = json
|
|
||||||
}
|
|
||||||
|
|
||||||
// set saved, and unset after 1 second
|
// set saved, and unset after 1 second
|
||||||
// i.e. make the success notifier appear, then disappear again after time
|
// i.e. make the success notifier appear, then disappear again after time
|
||||||
|
@ -94,69 +83,27 @@
|
||||||
saved = false
|
saved = false
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
saving = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// we cannot use svelte bind on these inputs, as it does not allow
|
if (response.status === 400) {
|
||||||
// bind, when the input type is dynamic
|
errors = json.errors
|
||||||
const resetForm = () => {
|
|
||||||
for (let el of Object.values(inputElements)) {
|
|
||||||
el.value = ""
|
|
||||||
if (el.checked) {
|
|
||||||
el.checked = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
record = createBlankRecord()
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const setForm = rec => {
|
onMount(async () => {
|
||||||
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(() => {
|
|
||||||
const routeParams = _bb.routeParams()
|
const routeParams = _bb.routeParams()
|
||||||
recordId =
|
recordId =
|
||||||
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
|
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
|
||||||
isNew = !recordId || recordId === "new"
|
isNew = !recordId || recordId === "new"
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
record = createBlankRecord()
|
record = { modelId: model }
|
||||||
} else {
|
return
|
||||||
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
|
|
||||||
_bb.api
|
|
||||||
.get(GET_RECORD_URL)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(rec => {
|
|
||||||
record = rec
|
|
||||||
setForm(rec)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
|
@ -164,23 +111,28 @@
|
||||||
{#if title}
|
{#if title}
|
||||||
<h1>{title}</h1>
|
<h1>{title}</h1>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#each errorMessages as error}
|
||||||
|
<p class="error">{error}</p>
|
||||||
|
{/each}
|
||||||
<hr />
|
<hr />
|
||||||
<div class="form-content">
|
<div class="form-content">
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<Label small forAttr={'form-stacked-text'}>{field}</Label>
|
<Label small forAttr={'form-stacked-text'}>{field}</Label>
|
||||||
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
|
{#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}
|
{#each schema[field].constraints.inclusion as opt}
|
||||||
<option>{opt}</option>
|
<option>{opt}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
{:else}
|
{:else if schema[field].type === 'datetime'}
|
||||||
<input
|
<DatePicker bind:value={record[field]} />
|
||||||
bind:this={inputElements[field]}
|
{:else if schema[field].type === 'boolean'}
|
||||||
class="input"
|
<input class="input" type="checkbox" bind:checked={record[field]} />
|
||||||
type={TYPE_MAP[schema[field].type]}
|
{:else if schema[field].type === 'number'}
|
||||||
on:change={handleInput(field)} />
|
<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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -293,4 +245,9 @@
|
||||||
background-position: right 17px top 1.5em, right 10px top 1.5em;
|
background-position: right 17px top 1.5em, right 10px top 1.5em;
|
||||||
background-size: 7px 7px, 7px 7px;
|
background-size: 7px 7px, 7px 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
if (itemContainer) {
|
if (itemContainer) {
|
||||||
_bb.attachChildren(itemContainer)
|
_bb.attachChildren(itemContainer)
|
||||||
if (!hasLoaded) {
|
if (!hasLoaded) {
|
||||||
_bb.call(onLoad)
|
_bb.call("onLoad")
|
||||||
hasLoaded = true
|
hasLoaded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,10 @@
|
||||||
export let _bb
|
export let _bb
|
||||||
|
|
||||||
const rowClickHandler = row => () => {
|
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) => {
|
const cellValue = (colIndex, row) => {
|
||||||
|
|
Loading…
Reference in New Issue