1
0
Fork 0
mirror of synced 2024-07-16 03:35:56 +12:00

Merge branch 'master' into fixes

This commit is contained in:
Michael Shanks 2020-10-12 15:10:21 +01:00
commit 999e86f69c
39 changed files with 970 additions and 339 deletions

View file

@ -112,7 +112,7 @@
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"rollup-plugin-url": "^2.2.2", "rollup-plugin-url": "^2.2.2",
"start-server-and-test": "^1.11.0", "start-server-and-test": "^1.11.0",
"svelte": "^3.24.1", "svelte": "^3.29.0",
"svelte-jester": "^1.0.6" "svelte-jester": "^1.0.6"
}, },
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072" "gitHead": "115189f72a850bfb52b65ec61d932531bf327072"

View file

@ -8,8 +8,9 @@ export default function(component, state) {
const findMatches = props => { const findMatches = props => {
walkProps(props, c => { walkProps(props, c => {
if ((c._instanceName || "").startsWith(capitalised)) { const thisInstanceName = get_capitalised_name(c._instanceName)
matchingComponents.push(c._instanceName) if ((thisInstanceName || "").startsWith(capitalised)) {
matchingComponents.push(thisInstanceName)
} }
}) })
} }

View file

@ -124,17 +124,18 @@ const saveScreen = store => screen => {
} }
const _saveScreen = async (store, s, screen) => { const _saveScreen = async (store, s, screen) => {
const currentPageScreens = s.pages[s.currentPageName]._screens const pageName = s.currentPageName || "main"
const currentPageScreens = s.pages[pageName]._screens
await api await api
.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen) .post(`/_builder/api/${s.appId}/pages/${pageName}/screen`, screen)
.then(() => { .then(() => {
if (currentPageScreens.includes(screen)) return if (currentPageScreens.includes(screen)) return
const screens = [...currentPageScreens, screen] const screens = [...currentPageScreens, screen]
store.update(innerState => { store.update(innerState => {
innerState.pages[s.currentPageName]._screens = screens innerState.pages[pageName]._screens = screens
innerState.screens = screens innerState.screens = screens
innerState.currentPreviewItem = screen innerState.currentPreviewItem = screen
innerState.allScreens = [...innerState.allScreens, screen] innerState.allScreens = [...innerState.allScreens, screen]
@ -153,27 +154,17 @@ const _saveScreen = async (store, s, screen) => {
return s return s
} }
const createScreen = store => (screenName, route, layoutComponentName) => { const createScreen = store => async screen => {
let savePromise
store.update(state => { store.update(state => {
const rootComponent = state.components[layoutComponentName] state.currentPreviewItem = screen
state.currentComponentInfo = screen.props
const newScreen = {
description: "",
url: "",
_css: "",
props: createProps(rootComponent).props,
}
newScreen.route = route
newScreen.name = newScreen.props._id
newScreen.props._instanceName = screenName || ""
state.currentPreviewItem = newScreen
state.currentComponentInfo = newScreen.props
state.currentFrontEndType = "screen" state.currentFrontEndType = "screen"
savePromise = _saveScreen(store, state, screen)
_saveScreen(store, state, newScreen) regenerateCssForCurrentScreen(state)
return state return state
}) })
await savePromise
} }
const setCurrentScreen = store => screenName => { const setCurrentScreen = store => screenName => {

View file

@ -0,0 +1,22 @@
export default {
name: `Create from scratch`,
create: () => createScreen(),
}
const createScreen = () => ({
props: {
_id: "",
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
type: "div",
_children: [],
_instanceName: "",
},
route: "",
name: "screen-id",
})

View file

@ -0,0 +1,22 @@
export default {
name: `New Row (Empty)`,
create: () => createScreen(),
}
const createScreen = () => ({
props: {
_id: "",
_component: "@budibase/standard-components/newrow",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [],
_instanceName: "",
model: "",
},
route: "",
name: "screen-id",
})

View file

@ -0,0 +1,22 @@
export default {
name: `Row Detail (Empty)`,
create: () => createScreen(),
}
const createScreen = () => ({
props: {
_id: "",
_component: "@budibase/standard-components/rowdetail",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [],
_instanceName: "",
model: "",
},
route: "",
name: "screen-id",
})

View file

@ -0,0 +1,35 @@
import newRecordScreen from "./newRecordScreen"
import recordDetailScreen from "./recordDetailScreen"
import recordListScreen from "./recordListScreen"
import emptyNewRecordScreen from "./emptyNewRecordScreen"
import createFromScratchScreen from "./createFromScratchScreen"
import emptyRecordDetailScreen from "./emptyRecordDetailScreen"
import { generateNewIdsForComponent } from "../../storeUtils"
import { uuid } from "builderStore/uuid"
const allTemplates = models => [
createFromScratchScreen,
...newRecordScreen(models),
...recordDetailScreen(models),
...recordListScreen(models),
emptyNewRecordScreen,
emptyRecordDetailScreen,
]
// allows us to apply common behaviour to all create() functions
const createTemplateOverride = (frontendState, create) => () => {
const screen = create()
for (let component of screen.props._children) {
generateNewIdsForComponent(component, frontendState, false)
}
screen.props._id = uuid()
screen.name = screen.props._id
screen.route = screen.route.toLowerCase()
return screen
}
export default (frontendState, models) =>
allTemplates(models).map(template => ({
...template,
create: createTemplateOverride(frontendState, template.create),
}))

View file

@ -0,0 +1,135 @@
export default function(models) {
return models.map(model => {
const fields = Object.keys(model.schema)
const heading = fields.length > 0 ? `{{ data.${fields[0]} }}` : "Add Row"
return {
name: `${model.name} - New`,
create: () => createScreen(model, heading),
id: NEW_RECORD_TEMPLATE,
}
})
}
export const NEW_RECORD_TEMPLATE = "NEW_RECORD_TEMPLATE"
const createScreen = (model, heading) => ({
props: {
_id: "",
_component: "@budibase/standard-components/newrow",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
model: model._id,
_children: [
{
_id: "",
_component: "@budibase/standard-components/heading",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
text: heading,
type: "h1",
_instanceName: "Heading 1",
_children: [],
},
{
_id: "",
_component: "@budibase/standard-components/dataform",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
_instanceName: `${model.name} Form`,
_children: [],
},
{
_id: "",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
display: "flex",
"flex-direction": "row",
"align-items": "center",
"justify-content": "flex-end",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_instanceName: "Buttons Container",
_children: [
{
_id: "",
_component: "@budibase/standard-components/button",
_styles: {
normal: {
"margin-right": "20px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
text: "Back",
className: "",
disabled: false,
onClick: [
{
parameters: {
url: `/${model.name.toLowerCase()}`,
},
"##eventHandlerType": "Navigate To",
},
],
_instanceName: "Back Button",
_children: [],
},
{
_id: "",
_component: "@budibase/standard-components/button",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
text: "Save",
className: "",
disabled: false,
onClick: [
{
parameters: {
contextPath: "data",
modelId: model._id,
},
"##eventHandlerType": "Save Record",
},
],
_instanceName: "Save Button",
_children: [],
},
],
},
],
_instanceName: `${model.name} - New`,
_code: "",
},
route: `/${model.name.toLowerCase()}/new`,
name: "",
})

View file

@ -0,0 +1,135 @@
export default function(models) {
return models.map(model => {
const fields = Object.keys(model.schema)
const heading = fields.length > 0 ? `{{ data.${fields[0]} }}` : "Detail"
return {
name: `${model.name} - Detail`,
create: () => createScreen(model, heading),
id: RECORD_DETAIL_TEMPLATE,
}
})
}
export const RECORD_DETAIL_TEMPLATE = "RECORD_DETAIL_TEMPLATE"
const createScreen = (model, heading) => ({
props: {
_id: "",
_component: "@budibase/standard-components/rowdetail",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
model: model._id,
_children: [
{
_id: "",
_component: "@budibase/standard-components/heading",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
text: heading,
type: "h1",
_instanceName: "Heading 1",
_children: [],
},
{
_id: "",
_component: "@budibase/standard-components/dataform",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
_instanceName: `${model.name} Form`,
_children: [],
},
{
_id: "",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
display: "flex",
"flex-direction": "row",
"align-items": "center",
"justify-content": "flex-end",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_instanceName: "Buttons Container",
_children: [
{
_id: "",
_component: "@budibase/standard-components/button",
_styles: {
normal: {
"margin-right": "20px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
text: "Back",
className: "",
disabled: false,
onClick: [
{
parameters: {
url: `/${model.name.toLowerCase()}`,
},
"##eventHandlerType": "Navigate To",
},
],
_instanceName: "Back Button",
_children: [],
},
{
_id: "",
_component: "@budibase/standard-components/button",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
text: "Save",
className: "",
disabled: false,
onClick: [
{
parameters: {
contextPath: "data",
modelId: model._id,
},
"##eventHandlerType": "Save Record",
},
],
_instanceName: "Save Button",
_children: [],
},
],
},
],
_instanceName: `${model.name} - Detail`,
_code: "",
},
route: `/${model.name.toLowerCase()}/:id`,
name: "",
})

View file

@ -0,0 +1,118 @@
export default function(models) {
return models.map(model => {
return {
name: `${model.name} - List`,
create: () => createScreen(model),
id: RECORD_LIST_TEMPLATE,
}
})
}
export const RECORD_LIST_TEMPLATE = "RECORD_LIST_TEMPLATE"
const createScreen = model => ({
props: {
_id: "",
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
type: "div",
_children: [
{
_id: "",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
display: "flex",
"flex-direction": "row",
"justify-content": "space-between",
"align-items": "center",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_instanceName: "Header",
_children: [
{
_id: "",
_component: "@budibase/standard-components/heading",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
text: `${model.name} List`,
type: "h1",
_instanceName: "Heading 1",
_children: [],
},
{
_id: "",
_component: "@budibase/standard-components/button",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
text: "Create New",
className: "",
disabled: false,
onClick: [
{
parameters: {
url: `/${model.name}/new`,
},
"##eventHandlerType": "Navigate To",
},
],
_instanceName: "Create New Button",
_children: [],
},
],
},
{
_id: "",
_component: "@budibase/standard-components/datatable",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
datasource: {
label: "Deals",
name: `all_${model._id}`,
modelId: model._id,
isModel: true,
},
stripeColor: "",
borderColor: "",
backgroundColor: "",
color: "",
_instanceName: `${model.name} Table`,
_children: [],
},
],
_instanceName: `${model.name} - List`,
_code: "",
className: "",
onLoad: [],
},
route: `/${model.name.toLowerCase()}`,
name: "",
})

View file

@ -85,10 +85,10 @@ export const regenerateCssForCurrentScreen = state => {
return state return state
} }
export const generateNewIdsForComponent = (c, state) => export const generateNewIdsForComponent = (c, state, changeName = true) =>
walkProps(c, p => { walkProps(c, p => {
p._id = uuid() p._id = uuid()
p._instanceName = getNewComponentName(p._component, state) if (changeName) p._instanceName = getNewComponentName(p._component, state)
}) })
export const getComponentDefinition = (state, name) => export const getComponentDefinition = (state, name) =>

View file

@ -1,11 +1,21 @@
<script> <script>
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore" import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { Button, Input, Label, ModalContent, Modal } from "@budibase/bbui" import { Button, Input, Label, ModalContent, Modal } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import TableDataImport from "../TableDataImport.svelte" import TableDataImport from "../TableDataImport.svelte"
import analytics from "analytics" import analytics from "analytics"
import screenTemplates from "builderStore/store/screenTemplates"
import { NEW_RECORD_TEMPLATE } from "builderStore/store/screenTemplates/newRecordScreen"
import { RECORD_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/recordDetailScreen"
import { RECORD_LIST_TEMPLATE } from "builderStore/store/screenTemplates/recordListScreen"
const defaultScreens = [
NEW_RECORD_TEMPLATE,
RECORD_DETAIL_TEMPLATE,
RECORD_LIST_TEMPLATE,
]
let modal let modal
let name let name
@ -25,6 +35,24 @@
notifier.success(`Table ${name} created successfully.`) notifier.success(`Table ${name} created successfully.`)
$goto(`./model/${model._id}`) $goto(`./model/${model._id}`)
analytics.captureEvent("Table Created", { name }) analytics.captureEvent("Table Created", { name })
const screens = screenTemplates($store, [model])
.filter(template => defaultScreens.includes(template.id))
.map(template => template.create())
for (let screen of screens) {
console.log(JSON.stringify(screen))
try {
await store.createScreen(screen)
} catch (_) {
// TODO: this is temporary
// a cypress test is failing, because I added the
// NewRecord component. So - this throws an exception
// because the currently released standard-components (on NPM)
// does not have NewRecord
// we should remove this after this has been released
}
}
} }
</script> </script>

View file

@ -1,6 +1,7 @@
<script> <script>
import { Input } from "@budibase/bbui" import { Input, Label } from "@budibase/bbui"
import api from "builderStore/api" import api from "builderStore/api"
import { backendUiStore } from "builderStore"
import analytics from "analytics" import analytics from "analytics"
let keys = { budibase: "" } let keys = { budibase: "" }
@ -38,6 +39,10 @@
edit edit
value={keys.budibase} value={keys.budibase}
label="Budibase API Key" /> label="Budibase API Key" />
<div>
<Label extraSmall grey>Instance ID (Webhooks)</Label>
<span>{$backendUiStore.selectedDatabase._id}</span>
</div>
</div> </div>
<style> <style>
@ -45,4 +50,9 @@
display: grid; display: grid;
grid-gap: var(--spacing-xl); grid-gap: var(--spacing-xl);
} }
span {
font-size: var(--font-size-xs);
font-weight: 500;
}
</style> </style>

View file

@ -68,18 +68,8 @@
} }
const copyComponent = () => { const copyComponent = () => {
store.update(s => { storeComponentForCopy(false)
const parent = getParent(s.currentPreviewItem.props, component) pasteComponent("below")
const copiedComponent = cloneDeep(component)
walkProps(copiedComponent, p => {
p._id = uuid()
})
parent._children = [...parent._children, copiedComponent]
saveCurrentPreviewItem(s)
s.currentComponentInfo = copiedComponent
regenerateCssForCurrentScreen(s)
return s
})
} }
const deleteComponent = () => { const deleteComponent = () => {

View file

@ -41,14 +41,6 @@
const onStyleChanged = store.setComponentStyle const onStyleChanged = store.setComponentStyle
function onPropChanged(key, value) {
if ($store.currentView !== "component") {
store.setPageOrScreenProp(key, value)
return
}
store.setComponentProp(key, value)
}
$: isComponentOrScreen = $: isComponentOrScreen =
$store.currentView === "component" || $store.currentView === "component" ||
$store.currentFrontEndType === "screen" $store.currentFrontEndType === "screen"
@ -103,7 +95,8 @@
{componentDefinition} {componentDefinition}
{panelDefinition} {panelDefinition}
displayNameField={displayName} displayNameField={displayName}
onChange={onPropChanged} onChange={store.setComponentProp}
onScreenPropChange={store.setPageOrScreenProp}
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} /> screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
{/if} {/if}

View file

@ -73,7 +73,7 @@
{#if fields} {#if fields}
{#each fields as field} {#each fields as field}
<Label size="m" color="dark">Field</Label> <Label size="m" color="dark">Column</Label>
<Select secondary bind:value={field.name} on:blur={rebuildParameters}> <Select secondary bind:value={field.name} on:blur={rebuildParameters}>
<option value="" /> <option value="" />
{#each schemaFields as schemaField} {#each schemaFields as schemaField}

View file

@ -0,0 +1,136 @@
<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"
// parameters.contextPath used in the client handler to determine which record to save
// this could be "data" or "data.parent", "data.parent.parent" etc
export let parameters
let idFields
let schemaFields
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
components: $store.components,
screen: $store.currentPreviewItem,
models: $backendUiStore.models,
})
$: {
if (parameters && parameters.contextPath) {
schemaFields = schemaFromContextPath(parameters.contextPath)
} else {
schemaFields = []
}
}
const idBindingToContextPath = id => id.substring(0, id.length - 4)
const contextPathToId = path => `${path}._id`
$: {
idFields = bindableProperties.filter(
bindable =>
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id")
)
// ensure contextPath is always defaulted - there is usually only one option
if (idFields.length > 0 && !parameters.contextPath) {
parameters.contextPath = idBindingToContextPath(
idFields[0].runtimeBinding
)
parameters = parameters
}
}
// 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 schemaFromContextPath = contextPath => {
if (!contextPath) return []
const idBinding = bindableProperties.find(
prop => prop.runtimeBinding === contextPathToId(contextPath)
)
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]
const modelId =
typeof modelInfo === "string" ? modelInfo : modelInfo.modelId
if (!modelInfo) return []
const model = $backendUiStore.models.find(m => m._id === modelId)
parameters.modelId = modelId
return Object.keys(model.schema).map(k => ({
name: k,
type: model.schema[k].type,
}))
}
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">Datasource</Label>
<Select secondary bind:value={parameters.contextPath}>
<option value="" />
{#each idFields as idField}
<option value={idBindingToContextPath(idField.runtimeBinding)}>
{idField.instance._instanceName}
</option>
{/each}
</Select>
{/if}
{#if parameters.contextPath}
<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(> div: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>

View file

@ -1,6 +1,5 @@
import NavigateTo from "./NavigateTo.svelte" import NavigateTo from "./NavigateTo.svelte"
import UpdateRecord from "./UpdateRecord.svelte" import SaveRecord from "./SaveRecord.svelte"
import CreateRecord from "./CreateRecord.svelte"
// defines what actions are available, when adding a new one // defines what actions are available, when adding a new one
// the component is the setup panel for the action // the component is the setup panel for the action
@ -9,15 +8,11 @@ import CreateRecord from "./CreateRecord.svelte"
export default [ export default [
{ {
name: "Create Record", name: "Save Record",
component: CreateRecord, component: SaveRecord,
}, },
{ {
name: "Navigate To", name: "Navigate To",
component: NavigateTo, component: NavigateTo,
}, },
{
name: "Update Record",
component: UpdateRecord,
},
] ]

View file

@ -1,27 +1,49 @@
<script> <script>
import { store } from "builderStore" import { store, backendUiStore } from "builderStore"
import { Input, Select, ModalContent } from "@budibase/bbui" import { Input, Button, Spacer, Select, ModalContent } from "@budibase/bbui"
import { find, filter, some } from "lodash/fp" import getTemplates from "builderStore/store/screenTemplates"
import { some } from "lodash/fp"
const CONTAINER = "@budibase/standard-components/container"
let dialog
let layoutComponents
let layoutComponent
let screens
let name = "" let name = ""
let routeError let routeError
let baseComponent = CONTAINER
let templateIndex
let draftScreen
$: layoutComponents = Object.values($store.components).filter( $: templates = getTemplates($store, $backendUiStore.models)
componentDefinition => componentDefinition.container
)
$: layoutComponent = layoutComponent
? layoutComponents.find(
component => component._component === layoutComponent._component
)
: layoutComponents[0]
$: route = !route && $store.screens.length === 0 ? "*" : route $: route = !route && $store.screens.length === 0 ? "*" : route
$: baseComponents = Object.values($store.components)
.filter(componentDefinition => componentDefinition.baseComponent)
.map(c => c._component)
$: {
if (templates && templateIndex === undefined) {
templateIndex = 0
templateChanged(0)
}
}
const templateChanged = newTemplateIndex => {
if (newTemplateIndex === undefined) return
draftScreen = templates[newTemplateIndex].create()
if (draftScreen.props._instanceName) {
name = draftScreen.props._instanceName
}
if (draftScreen.props._component) {
baseComponent = draftScreen.props._component
}
if (draftScreen.route) {
route = draftScreen.route
}
}
const save = () => { const save = () => {
if (!route) { if (!route) {
routeError = "Url is required" routeError = "Url is required"
@ -32,12 +54,23 @@
routeError = "" routeError = ""
} }
} }
if (routeError) {
return false if (routeError) return false
}
store.createScreen(name, route, layoutComponent._component) draftScreen.props._instanceName = name
draftScreen.props._component = baseComponent
draftScreen.route = route
store.createScreen(draftScreen)
finished()
}
const finished = () => {
templateIndex = 0
name = "" name = ""
route = "" route = ""
baseComponent = CONTAINER
} }
const routeNameExists = route => { const routeNameExists = route => {
@ -54,15 +87,25 @@
</script> </script>
<ModalContent title="New Screen" confirmText="Create Screen" onConfirm={save}> <ModalContent title="New Screen" confirmText="Create Screen" onConfirm={save}>
<Select
label="Choose a Template"
bind:value={templateIndex}
secondary
on:change={ev => templateChanged(ev.target.value)}>
{#if templates}
{#each templates as template, index}
<option value={index}>{template.name}</option>
{/each}
{/if}
</Select>
<Input label="Name" bind:value={name} /> <Input label="Name" bind:value={name} />
<Input <Input
label="Url" label="Url"
error={routeError} error={routeError}
bind:value={route} bind:value={route}
on:change={routeChanged} /> on:change={routeChanged} />
<Select label="Layout Component" bind:value={layoutComponent} secondary>
{#each layoutComponents as { _component, name }}
<option value={_component}>{name}</option>
{/each}
</Select>
</ModalContent> </ModalContent>

View file

@ -11,6 +11,7 @@
export let componentDefinition = {} export let componentDefinition = {}
export let componentInstance = {} export let componentInstance = {}
export let onChange = () => {} export let onChange = () => {}
export let onScreenPropChange = () => {}
export let displayNameField = false export let displayNameField = false
export let screenOrPageInstance export let screenOrPageInstance
@ -91,7 +92,7 @@
label={def.label} label={def.label}
key={def.key} key={def.key}
value={screenOrPageInstance[def.key]} value={screenOrPageInstance[def.key]}
{onChange} onChange={onScreenPropChange}
props={{ ...excludeProps(def, ['control', 'label']) }} /> props={{ ...excludeProps(def, ['control', 'label']) }} />
{/each} {/each}
<hr /> <hr />

View file

@ -361,19 +361,18 @@ export const typography = [
label: "Font", label: "Font",
key: "font-family", key: "font-family",
control: OptionSelect, control: OptionSelect,
defaultValue: "initial", defaultValue: "Arial",
options: [ options: [
"initial",
"Arial", "Arial",
"Arial Black", "Arial Black",
"Cursive", "Cursive",
"Courier", "Courier",
"Comic Sans MS", "Comic Sans MS",
"Helvetica", "Helvetica",
"Helvetica Neue",
"Impact", "Impact",
"Inter", "Inter",
"Lucida Sans Unicode", "Lucida Sans Unicode",
"Open Sans",
"Roboto", "Roboto",
"Roboto Mono", "Roboto Mono",
"Times New Roman", "Times New Roman",
@ -467,9 +466,9 @@ export const background = [
label: "Gradient", label: "Gradient",
key: "background-image", key: "background-image",
control: OptionSelect, control: OptionSelect,
defaultValue: "None", defaultValue: "",
options: [ options: [
{ label: "None", value: "None" }, { label: "Select option", value: "" },
{ {
label: "Warm Flame", label: "Warm Flame",
value: "linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);", value: "linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);",
@ -518,9 +517,9 @@ export const background = [
}, },
{ {
label: "Image", label: "Image",
key: "background-image", key: "background",
control: Input, control: Input,
placeholder: "Src", placeholder: "url",
}, },
] ]
@ -665,7 +664,7 @@ export const transitions = [
control: OptionSelect, control: OptionSelect,
textAlign: "center", textAlign: "center",
placeholder: "sec", placeholder: "sec",
options: ["0.2ms", "0.4ms", "0.8ms", "1s", "2s", "4s"], options: ["0.4s", "0.6s", "0.8s", "1s", "2s", "4s"],
}, },
{ {
label: "Ease", label: "Ease",

View file

@ -583,23 +583,7 @@ export default {
icon: "ri-file-edit-line", icon: "ri-file-edit-line",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [ settings: [],
{
label: "Table",
key: "model",
control: ModelSelect,
},
{
label: "Title",
key: "title",
control: Input,
},
{
label: "Button Text",
key: "buttonText",
control: Input,
},
],
}, },
}, },
{ {
@ -608,23 +592,7 @@ export default {
icon: "ri-file-edit-line", icon: "ri-file-edit-line",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [ settings: [],
{
label: "Table",
key: "model",
control: ModelSelect,
},
{
label: "Title",
key: "title",
control: Input,
},
{
label: "Button Text",
key: "buttonText",
control: Input,
},
],
}, },
}, },
], ],
@ -1125,8 +1093,8 @@ export default {
// children: [], // children: [],
// }, // },
{ {
name: "Record Detail", name: "Row Detail",
_component: "@budibase/standard-components/recorddetail", _component: "@budibase/standard-components/rowdetail",
description: description:
"Loads a record, using an id from the URL, which can be used with {{ context }}, in children", "Loads a record, using an id from the URL, which can be used with {{ context }}, in children",
icon: "ri-profile-line", icon: "ri-profile-line",
@ -1136,6 +1104,18 @@ export default {
}, },
children: [], children: [],
}, },
{
name: "New Row",
_component: "@budibase/standard-components/newrow",
description:
"Sets up a new record for creation, which can be used with {{ context }}, in children",
icon: "ri-profile-line",
properties: {
design: { ...all },
settings: [{ label: "Table", key: "model", control: ModelSelect }],
},
children: [],
},
// { // {
// name: "Map", // name: "Map",
// _component: "@budibase/standard-components/datamap", // _component: "@budibase/standard-components/datamap",

View file

@ -5850,10 +5850,10 @@ svelte-portal@^1.0.0:
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3" resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3"
integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q== integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q==
svelte@^3.24.1: svelte@^3.29.0:
version "3.25.1" version "3.29.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.25.1.tgz#218def1243fea5a97af6eb60f5e232315bb57ac4" resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.29.0.tgz#80acac4254341ad8f3301e5ef03f4127ea967d96"
integrity sha512-IbrVKTmuR0BvDw4ii8/gBNy8REu7nWTRy9uhUz+Yuae5lIjWgSGwKlWtJGC2Vg95s+UnXPqDu0Kk/sUwe0t2GQ== integrity sha512-f+A65eyOQ5ujETLy+igNXtlr6AEjAQLYd1yJE1VwNiXMQO5Z/Vmiy3rL+zblV/9jd7rtTTWqO1IcuXsP2Qv0OA==
symbol-observable@^1.1.0: symbol-observable@^1.1.0:
version "1.2.0" version "1.2.0"

View file

@ -52,14 +52,14 @@ const apiOpts = {
delete: del, delete: del,
} }
const createRecord = async params => const saveRecord = async (params, state) =>
await post({ await post({
url: `/api/${params.modelId}/records`, url: `/api/${params.modelId}/records`,
body: makeRecordRequestBody(params), body: makeRecordRequestBody(params, state),
}) })
const updateRecord = async params => { const updateRecord = async (params, state) => {
const record = makeRecordRequestBody(params) const record = makeRecordRequestBody(params, state)
record._id = params._id record._id = params._id
await patch({ await patch({
url: `/api/${params.modelId}/records/${params._id}`, url: `/api/${params.modelId}/records/${params._id}`,
@ -67,8 +67,14 @@ const updateRecord = async params => {
}) })
} }
const makeRecordRequestBody = parameters => { const makeRecordRequestBody = (parameters, state) => {
const body = {} // start with the record thats currently in context
const body = { ...(state.data || {}) }
// dont send the model
if (body._model) delete body._model
// then override with supplied parameters
for (let fieldName in parameters.fields) { for (let fieldName in parameters.fields) {
const field = parameters.fields[fieldName] const field = parameters.fields[fieldName]
@ -95,6 +101,6 @@ const makeRecordRequestBody = parameters => {
export default { export default {
authenticate: authenticate(apiOpts), authenticate: authenticate(apiOpts),
createRecord, saveRecord,
updateRecord, updateRecord,
} }

View file

@ -6,8 +6,8 @@ export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
export const eventHandlers = routeTo => { export const eventHandlers = routeTo => {
const handlers = { const handlers = {
"Navigate To": param => routeTo(param && param.url), "Navigate To": param => routeTo(param && param.url),
"Create Record": api.createRecord,
"Update Record": api.updateRecord, "Update Record": api.updateRecord,
"Save Record": api.saveRecord,
"Trigger Workflow": api.triggerWorkflow, "Trigger Workflow": api.triggerWorkflow,
} }
@ -19,7 +19,7 @@ export const eventHandlers = routeTo => {
const handler = handlers[action[EVENT_TYPE_MEMBER_NAME]] const handler = handlers[action[EVENT_TYPE_MEMBER_NAME]]
const parameters = createParameters(action.parameters, state) const parameters = createParameters(action.parameters, state)
if (handler) { if (handler) {
await handler(parameters) await handler(parameters, state)
} }
} }
} }

View file

@ -95,7 +95,7 @@ const getState = contextStoreKey =>
contextStoreKey ? contextStores[contextStoreKey].state : rootState contextStoreKey ? contextStores[contextStoreKey].state : rootState
const getStore = contextStoreKey => const getStore = contextStoreKey =>
contextStoreKey ? contextStores[contextStoreKey] : rootStore contextStoreKey ? contextStores[contextStoreKey].store : rootStore
export default { export default {
subscribe, subscribe,

View file

@ -136,7 +136,7 @@ exports.performLocalFileProcessing = async function(ctx) {
} }
exports.serveApp = async function(ctx) { exports.serveApp = async function(ctx) {
const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated" const mainOrAuth = ctx.auth.authenticated ? "main" : "unauthenticated"
// default to homedir // default to homedir
const appPath = resolve( const appPath = resolve(
@ -154,7 +154,7 @@ exports.serveApp = async function(ctx) {
// only set the appId cookie for /appId .. we COULD check for valid appIds // only set the appId cookie for /appId .. we COULD check for valid appIds
// but would like to avoid that DB hit // but would like to avoid that DB hit
const looksLikeAppId = /^(app_)?[0-9a-f]{32}$/.test(appId) const looksLikeAppId = /^(app_)?[0-9a-f]{32}$/.test(appId)
if (looksLikeAppId && !ctx.isAuthenticated) { if (looksLikeAppId && !ctx.auth.authenticated) {
const anonUser = { const anonUser = {
userId: "ANON", userId: "ANON",
accessLevelId: ANON_LEVEL_ID, accessLevelId: ANON_LEVEL_ID,
@ -200,7 +200,7 @@ exports.serveAttachment = async function(ctx) {
exports.serveAppAsset = async function(ctx) { exports.serveAppAsset = async function(ctx) {
// default to homedir // default to homedir
const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated" const mainOrAuth = ctx.auth.authenticated ? "main" : "unauthenticated"
const appPath = resolve( const appPath = resolve(
budibaseAppsDir(), budibaseAppsDir(),

View file

@ -24,6 +24,7 @@ app.use(
) )
app.context.eventEmitter = eventEmitter app.context.eventEmitter = eventEmitter
app.context.auth = {}
// api routes // api routes
app.use(api.routes()) app.use(api.routes())

View file

@ -1,5 +1,6 @@
const LinkController = require("./LinkController") const LinkController = require("./LinkController")
const { IncludeDocs, getLinkDocuments, createLinkView } = require("./linkUtils") const { IncludeDocs, getLinkDocuments, createLinkView } = require("./linkUtils")
const _ = require("lodash")
/** /**
* This functionality makes sure that when records with links are created, updated or deleted they are processed * This functionality makes sure that when records with links are created, updated or deleted they are processed
@ -88,23 +89,23 @@ exports.attachLinkInfo = async (instanceId, records) => {
records = [records] records = [records]
wasArray = false wasArray = false
} }
let modelIds = [...new Set(records.map(el => el.modelId))]
// start by getting all the link values for performance reasons // start by getting all the link values for performance reasons
let responses = await Promise.all( let responses = _.flatten(
records.map(record => await Promise.all(
getLinkDocuments({ modelIds.map(modelId =>
instanceId, getLinkDocuments({
modelId: record.modelId, instanceId,
recordId: record._id, modelId: modelId,
includeDocs: IncludeDocs.EXCLUDE, includeDocs: IncludeDocs.EXCLUDE,
}) })
)
) )
) )
// can just use an index to access responses, order maintained
let index = 0
// now iterate through the records and all field information // now iterate through the records and all field information
for (let record of records) { for (let record of records) {
// get all links for record, ignore fieldName for now // get all links for record, ignore fieldName for now
const linkVals = responses[index++] const linkVals = responses.filter(el => el.thisId === record._id)
for (let linkVal of linkVals) { for (let linkVal of linkVals) {
// work out which link pertains to this record // work out which link pertains to this record
if (!(record[linkVal.fieldName] instanceof Array)) { if (!(record[linkVal.fieldName] instanceof Array)) {

View file

@ -27,10 +27,12 @@ exports.createLinkView = async instanceId => {
let doc2 = doc.doc2 let doc2 = doc.doc2
emit([doc1.modelId, doc1.recordId], { emit([doc1.modelId, doc1.recordId], {
id: doc2.recordId, id: doc2.recordId,
thisId: doc1.recordId,
fieldName: doc1.fieldName, fieldName: doc1.fieldName,
}) })
emit([doc2.modelId, doc2.recordId], { emit([doc2.modelId, doc2.recordId], {
id: doc1.recordId, id: doc1.recordId,
thisId: doc2.recordId,
fieldName: doc2.fieldName, fieldName: doc2.fieldName,
}) })
} }

View file

@ -20,8 +20,10 @@ module.exports = async (ctx, next) => {
if (builderToken) { if (builderToken) {
try { try {
const jwtPayload = jwt.verify(builderToken, ctx.config.jwtSecret) const jwtPayload = jwt.verify(builderToken, ctx.config.jwtSecret)
ctx.apiKey = jwtPayload.apiKey ctx.auth = {
ctx.isAuthenticated = jwtPayload.accessLevelId === BUILDER_LEVEL_ID apiKey: jwtPayload.apiKey,
authenticated: jwtPayload.accessLevelId === BUILDER_LEVEL_ID,
}
ctx.user = { ctx.user = {
...jwtPayload, ...jwtPayload,
accessLevel: await getAccessLevel( accessLevel: await getAccessLevel(
@ -38,14 +40,13 @@ module.exports = async (ctx, next) => {
} }
if (!appToken) { if (!appToken) {
ctx.isAuthenticated = false ctx.auth.authenticated = false
await next() await next()
return return
} }
try { try {
const jwtPayload = jwt.verify(appToken, ctx.config.jwtSecret) const jwtPayload = jwt.verify(appToken, ctx.config.jwtSecret)
ctx.apiKey = jwtPayload.apiKey
ctx.user = { ctx.user = {
...jwtPayload, ...jwtPayload,
accessLevel: await getAccessLevel( accessLevel: await getAccessLevel(
@ -53,7 +54,10 @@ module.exports = async (ctx, next) => {
jwtPayload.accessLevelId jwtPayload.accessLevelId
), ),
} }
ctx.isAuthenticated = ctx.user.accessLevelId !== ANON_LEVEL_ID ctx.auth = {
authenticated: ctx.user.accessLevelId !== ANON_LEVEL_ID,
apiKey: jwtPayload.apiKey,
}
} catch (err) { } catch (err) {
ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text) ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text)
} }

View file

@ -5,9 +5,36 @@ const {
BUILDER_LEVEL_ID, BUILDER_LEVEL_ID,
BUILDER, BUILDER,
} = require("../utilities/accessLevels") } = require("../utilities/accessLevels")
const environment = require("../environment")
const { apiKeyTable } = require("../db/dynamoClient")
module.exports = (permName, getItemId) => async (ctx, next) => { module.exports = (permName, getItemId) => async (ctx, next) => {
if (!ctx.isAuthenticated) { if (
environment.CLOUD &&
ctx.headers["x-api-key"] &&
ctx.headers["x-instanceid"]
) {
// api key header passed by external webhook
const apiKeyInfo = await apiKeyTable.get({
primary: ctx.headers["x-api-key"],
})
if (apiKeyInfo) {
ctx.auth = {
authenticated: true,
external: true,
apiKey: ctx.headers["x-api-key"],
}
ctx.user = {
instanceId: ctx.headers["x-instanceid"],
}
return next()
}
ctx.throw(403, "API key invalid")
}
if (!ctx.auth.authenticated) {
ctx.throw(403, "Session not authenticated") ctx.throw(403, "Session not authenticated")
} }

View file

@ -55,7 +55,7 @@ module.exports = async (ctx, next) => {
return next() return next()
} }
try { try {
await usageQuota.update(ctx.apiKey, property, usage) await usageQuota.update(ctx.auth.apiKey, property, usage)
return next() return next()
} catch (err) { } catch (err) {
ctx.throw(403, err) ctx.throw(403, err)

View file

@ -218,20 +218,12 @@
"dataform": { "dataform": {
"description": "an HTML table that fetches data from a table or view and displays it.", "description": "an HTML table that fetches data from a table or view and displays it.",
"data": true, "data": true,
"props": { "props": {}
"model": "models",
"title": "string",
"buttonText": "string"
}
}, },
"dataformwide": { "dataformwide": {
"description": "an HTML table that fetches data from a table or view and displays it.", "description": "an HTML table that fetches data from a table or view and displays it.",
"data": true, "data": true,
"props": { "props": {}
"model": "models",
"title": "string",
"buttonText": "string"
}
}, },
"datalist": { "datalist": {
"description": "A configurable data list that attaches to your backend models.", "description": "A configurable data list that attaches to your backend models.",
@ -269,11 +261,22 @@
"destinationUrl": "string" "destinationUrl": "string"
} }
}, },
"recorddetail": { "rowdetail": {
"description": "Loads a record, using an ID in the url", "description": "Loads a record, using an ID in the url",
"context": "model", "context": "model",
"children": true, "children": true,
"data": true, "data": true,
"baseComponent": true,
"props": {
"model": "models"
}
},
"newrow": {
"description": "Prepares a new record for creation",
"context": "model",
"children": true,
"data": true,
"baseComponent": true,
"props": { "props": {
"model": "models" "model": "models"
} }
@ -715,7 +718,7 @@
"default": "div" "default": "div"
} }
}, },
"container": true, "baseComponent": true,
"tags": [ "tags": [
"div", "div",
"container", "container",

View file

@ -16,17 +16,17 @@
"@budibase/client": "^0.2.0", "@budibase/client": "^0.2.0",
"@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-commonjs": "^11.1.0",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"rollup": "^1.11.0", "rollup": "^2.11.2",
"rollup-plugin-commonjs": "^10.0.2", "rollup-plugin-commonjs": "^10.0.2",
"rollup-plugin-json": "^4.0.0", "rollup-plugin-json": "^4.0.0",
"rollup-plugin-livereload": "^1.0.1", "rollup-plugin-livereload": "^1.0.1",
"rollup-plugin-node-resolve": "^5.0.0", "rollup-plugin-node-resolve": "^5.0.0",
"rollup-plugin-postcss": "^3.1.5", "rollup-plugin-postcss": "^3.1.5",
"rollup-plugin-svelte": "^5.0.0", "rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^5.1.1", "rollup-plugin-terser": "^7.0.2",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"sirv-cli": "^0.4.4", "sirv-cli": "^0.4.4",
"svelte": "^3.12.1" "svelte": "^3.29.0"
}, },
"keywords": [ "keywords": [
"svelte" "svelte"

View file

@ -1,168 +1,61 @@
<script> <script>
import { onMount } from "svelte" import { Label, DatePicker, Input, Select, Toggle } from "@budibase/bbui"
import { fade } from "svelte/transition"
import {
Label,
DatePicker,
Input,
Select,
Button,
Toggle,
} from "@budibase/bbui"
import Dropzone from "./attachments/Dropzone.svelte" import Dropzone from "./attachments/Dropzone.svelte"
import LinkedRecordSelector from "./LinkedRecordSelector.svelte" import LinkedRecordSelector from "./LinkedRecordSelector.svelte"
import debounce from "lodash.debounce"
import ErrorsBox from "./ErrorsBox.svelte" import ErrorsBox from "./ErrorsBox.svelte"
import { capitalise } from "./helpers" import { capitalise } from "./helpers"
export let _bb export let _bb
export let model export let model
export let title
export let buttonText
export let wide = false export let wide = false
const TYPE_MAP = {
string: "text",
boolean: "checkbox",
number: "number",
}
const DEFAULTS_FOR_TYPE = {
string: "",
boolean: false,
number: null,
link: [],
}
let record
let store = _bb.store let store = _bb.store
let schema = {} let schema = {}
let modelDef = {}
let saved = false
let recordId let recordId
let isNew = true
let errors = {} let errors = {}
$: schema = $store.data && $store.data._model.schema
$: fields = schema ? Object.keys(schema) : [] $: fields = schema ? Object.keys(schema) : []
$: if (model && model.length !== 0) {
fetchModel()
}
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 = {
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]
}
}
const SAVE_RECORD_URL = `/api/${model}/records`
const response = await _bb.api.post(SAVE_RECORD_URL, record)
const json = await response.json()
if (response.status === 200) {
store.update(state => {
state[model] = state[model] ? [...state[model], json] : [json]
return state
})
errors = {}
// wipe form, if new record, otherwise update
// model to get new _rev
record = isNew ? { modelId: model } : json
// set saved, and unset after 1 second
// i.e. make the success notifier appear, then disappear again after time
saved = true
setTimeout(() => {
saved = false
}, 3000)
}
if (response.status === 400) {
errors = Object.keys(json.errors)
.map(k => ({ dataPath: k, message: json.errors[k] }))
.flat()
}
})
onMount(async () => {
const routeParams = _bb.routeParams()
recordId =
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
isNew = !recordId || recordId === "new"
if (isNew) {
record = { modelId: model }
return
}
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
const response = await _bb.api.get(GET_RECORD_URL)
record = await response.json()
})
</script> </script>
<form class="form" on:submit|preventDefault> <div class="form-content">
{#if title} <ErrorsBox errors={$store.saveRecordErrors || {}} />
<h1>{title}</h1> {#each fields as field}
{/if} <div class="form-field" class:wide>
<div class="form-content"> {#if !(schema[field].type === 'boolean' && !wide)}
<ErrorsBox {errors} /> <Label extraSmall={!wide} grey={!wide}>
{#each fields as field} {capitalise(schema[field].name)}
<div class="form-field" class:wide> </Label>
{#if !(schema[field].type === 'boolean' && !wide)} {/if}
<Label extraSmall={!wide} grey={!wide}> {#if schema[field].type === 'options'}
{capitalise(schema[field].name)} <Select secondary bind:value={$store.data[field]}>
</Label> <option value="">Choose an option</option>
{/if} {#each schema[field].constraints.inclusion as opt}
{#if schema[field].type === 'options'} <option>{opt}</option>
<Select secondary bind:value={record[field]}> {/each}
<option value="">Choose an option</option> </Select>
{#each schema[field].constraints.inclusion as opt} {:else if schema[field].type === 'datetime'}
<option>{opt}</option> <DatePicker bind:value={$store.data[field]} />
{/each} {:else if schema[field].type === 'boolean'}
</Select> <Toggle
{:else if schema[field].type === 'datetime'} text={wide ? null : capitalise(schema[field].name)}
<DatePicker bind:value={record[field]} /> bind:checked={$store.data[field]} />
{:else if schema[field].type === 'boolean'} {:else if schema[field].type === 'number'}
<Toggle <Input type="number" bind:value={$store.data[field]} />
text={wide ? null : capitalise(schema[field].name)} {:else if schema[field].type === 'string'}
bind:checked={record[field]} /> <Input bind:value={$store.data[field]} />
{:else if schema[field].type === 'number'} {:else if schema[field].type === 'attachment'}
<Input type="number" bind:value={record[field]} /> <Dropzone bind:files={$store.data[field]} />
{:else if schema[field].type === 'string'} {:else if schema[field].type === 'link'}
<Input bind:value={record[field]} /> <LinkedRecordSelector
{:else if schema[field].type === 'attachment'} secondary
<Dropzone bind:files={record[field]} /> showLabel={false}
{:else if schema[field].type === 'link'} bind:linkedRecords={$store.data[field]}
<LinkedRecordSelector schema={schema[field]} />
secondary {/if}
showLabel={false}
bind:linkedRecords={record[field]}
schema={schema[field]} />
{/if}
</div>
{/each}
<div class="buttons">
<Button primary on:click={save} green={saved}>
{#if saved}Success{:else}{buttonText || 'Submit Form'}{/if}
</Button>
</div> </div>
</div> {/each}
</form> </div>
<style> <style>
.form { .form {
@ -189,9 +82,4 @@
.form-field.wide :global(label) { .form-field.wide :global(label) {
margin-bottom: 0; margin-bottom: 0;
} }
.buttons {
display: flex;
justify-content: flex-end;
}
</style> </style>

View file

@ -0,0 +1,37 @@
<script>
import { onMount } from "svelte"
export let _bb
export let model
let record = {}
$: {
record.modelId = model
}
let target
async function fetchModel(id) {
const FETCH_MODEL_URL = `/api/models/${id}`
const response = await _bb.api.get(FETCH_MODEL_URL)
return await response.json()
}
onMount(async () => {
if (model) {
const modelObj = await fetchModel(model)
record.modelId = model
record._model = modelObj
_bb.attachChildren(target, {
context: record,
})
} else {
_bb.attachChildren(target, {
context: {},
})
}
})
</script>
<section bind:this={target} />

View file

@ -20,6 +20,7 @@
if (response.status === 200) { if (response.status === 200) {
const allRecords = await response.json() const allRecords = await response.json()
if (allRecords.length > 0) return allRecords[0] if (allRecords.length > 0) return allRecords[0]
return { modelId: model }
} }
} }
@ -29,31 +30,35 @@
let record let record
// if srcdoc, then we assume this is the builder preview // if srcdoc, then we assume this is the builder preview
if (pathParts.length === 0 || pathParts[0] === "srcdoc") { if (pathParts.length === 0 || pathParts[0] === "srcdoc") {
record = await fetchFirstRecord() if (model) record = await fetchFirstRecord()
} else { } else if (_bb.routeParams().id) {
const id = pathParts[pathParts.length - 1] const GET_RECORD_URL = `/api/${model}/records/${_bb.routeParams().id}`
const GET_RECORD_URL = `/api/${model}/records/${id}`
const response = await _bb.api.get(GET_RECORD_URL) const response = await _bb.api.get(GET_RECORD_URL)
if (response.status === 200) { if (response.status === 200) {
record = await response.json() record = await response.json()
} else {
throw new Error("Failed to fetch record.", response)
} }
} else {
throw new Exception("Record ID was not supplied to RowDetail")
} }
if (record) { if (record) {
// Fetch model schema so we can check for linked records // Fetch model schema so we can check for linked records
const model = await fetchModel(record.modelId) const modelObj = await fetchModel(record.modelId)
for (let key of Object.keys(model.schema)) { for (let key of Object.keys(modelObj.schema)) {
if (model.schema[key].type === "link") { if (modelObj.schema[key].type === "link") {
record[key] = Array.isArray(record[key]) ? record[key].length : 0 record[key] = Array.isArray(record[key]) ? record[key].length : 0
} }
} }
record._model = modelObj
_bb.attachChildren(target, { _bb.attachChildren(target, {
hydrate: false,
context: record, context: record,
}) })
} else { } else {
throw new Error("Failed to fetch record.", response) _bb.attachChildren(target)
} }
} }

View file

@ -26,7 +26,8 @@ export { default as embed } from "./Embed.svelte"
export { default as stackedlist } from "./StackedList.svelte" export { default as stackedlist } from "./StackedList.svelte"
export { default as card } from "./Card.svelte" export { default as card } from "./Card.svelte"
export { default as cardhorizontal } from "./CardHorizontal.svelte" export { default as cardhorizontal } from "./CardHorizontal.svelte"
export { default as recorddetail } from "./RecordDetail.svelte" export { default as rowdetail } from "./RowDetail.svelte"
export { default as newrow } from "./NewRow.svelte"
export { default as datepicker } from "./DatePicker.svelte" export { default as datepicker } from "./DatePicker.svelte"
export * from "./Chart" export * from "./Chart"
export { default as icon } from "./Icon.svelte" export { default as icon } from "./Icon.svelte"