Merge branch 'master' of github.com:Budibase/budibase into endpoint-renaming

This commit is contained in:
mike12345567 2020-10-12 13:34:32 +01:00
commit cfd4d9d34d
31 changed files with 900 additions and 308 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: "",
table: "",
},
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: "",
table: "",
},
route: "",
name: "screen-id",
})

View File

@ -0,0 +1,35 @@
import newRowScreen from "./newRowScreen"
import rowDetailScreen from "./rowDetailScreen"
import rowListScreen from "./rowListScreen"
import emptyNewRowScreen from "./emptyNewRowScreen"
import createFromScratchScreen from "./createFromScratchScreen"
import emptyRowDetailScreen from "./emptyRowDetailScreen"
import { generateNewIdsForComponent } from "../../storeUtils"
import { uuid } from "builderStore/uuid"
const allTemplates = tables => [
createFromScratchScreen,
...newRowScreen(tables),
...rowDetailScreen(tables),
...rowListScreen(tables),
emptyNewRowScreen,
emptyRowDetailScreen,
]
// 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, tables) =>
allTemplates(tables).map(template => ({
...template,
create: createTemplateOverride(frontendState, template.create),
}))

View File

@ -0,0 +1,135 @@
export default function(tables) {
return tables.map(table => {
const fields = Object.keys(table.schema)
const heading = fields.length > 0 ? `{{ data.${fields[0]} }}` : "Add Row"
return {
name: `${table.name} - New`,
create: () => createScreen(table, heading),
id: NEW_ROW_TEMPLATE,
}
})
}
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
const createScreen = (table, heading) => ({
props: {
_id: "",
_component: "@budibase/standard-components/newrow",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
table: table._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: `${table.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: `/${table.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",
tableId: table._id,
},
"##eventHandlerType": "Save Row",
},
],
_instanceName: "Save Button",
_children: [],
},
],
},
],
_instanceName: `${table.name} - New`,
_code: "",
},
route: `/${table.name.toLowerCase()}/new`,
name: "",
})

View File

@ -0,0 +1,135 @@
export default function(tables) {
return tables.map(table => {
const fields = Object.keys(table.schema)
const heading = fields.length > 0 ? `{{ data.${fields[0]} }}` : "Detail"
return {
name: `${table.name} - Detail`,
create: () => createScreen(table, heading),
id: ROW_DETAIL_TEMPLATE,
}
})
}
export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
const createScreen = (table, heading) => ({
props: {
_id: "",
_component: "@budibase/standard-components/rowdetail",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
table: table._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: `${table.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: `/${table.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",
tableId: table._id,
},
"##eventHandlerType": "Save Row",
},
],
_instanceName: "Save Button",
_children: [],
},
],
},
],
_instanceName: `${table.name} - Detail`,
_code: "",
},
route: `/${table.name.toLowerCase()}/:id`,
name: "",
})

View File

@ -0,0 +1,118 @@
export default function(tables) {
return tables.map(table => {
return {
name: `${table.name} - List`,
create: () => createScreen(table),
id: ROW_LIST_TEMPLATE,
}
})
}
export const ROW_LIST_TEMPLATE = "ROW_LIST_TEMPLATE"
const createScreen = table => ({
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: `${table.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: `/${table.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_${table._id}`,
tableId: table._id,
isTable: true,
},
stripeColor: "",
borderColor: "",
backgroundColor: "",
color: "",
_instanceName: `${table.name} Table`,
_children: [],
},
],
_instanceName: `${table.name} - List`,
_code: "",
className: "",
onLoad: [],
},
route: `/${table.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_ROW_TEMPLATE } from "builderStore/store/screenTemplates/newRowScreen"
import { ROW_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/rowDetailScreen"
import { ROW_LIST_TEMPLATE } from "builderStore/store/screenTemplates/rowListScreen"
const defaultScreens = [
NEW_ROW_TEMPLATE,
ROW_DETAIL_TEMPLATE,
ROW_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(`./table/${table._id}`) $goto(`./table/${table._id}`)
analytics.captureEvent("Table Created", { name }) analytics.captureEvent("Table Created", { name })
const screens = screenTemplates($store, [table])
.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
// NewRow component. So - this throws an exception
// because the currently released standard-components (on NPM)
// does not have NewRow
// we should remove this after this has been released
}
}
} }
</script> </script>

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 row 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,
tables: $backendUiStore.tables,
})
$: {
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 tableId
const tableInfo = instance[component.context]
const tableId =
typeof tableInfo === "string" ? tableInfo : tableInfo.tableId
if (!tableInfo) return []
const table = $backendUiStore.tables.find(m => m._id === tableId)
parameters.tableId = tableId
return Object.keys(table.schema).map(k => ({
name: k,
type: table.schema[k].type,
}))
}
const onFieldsChanged = e => {
parameters.fields = e.detail
}
</script>
<div class="root">
{#if idFields.length === 0}
<div class="cannot-use">
Update row 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 UpdateRow from "./UpdateRow.svelte" import SaveRow from "./SaveRow.svelte"
import CreateRow from "./CreateRow.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 CreateRow from "./CreateRow.svelte"
export default [ export default [
{ {
name: "Create Row", name: "Save Row",
component: CreateRow, component: SaveRow,
}, },
{ {
name: "Navigate To", name: "Navigate To",
component: NavigateTo, component: NavigateTo,
}, },
{
name: "Update Row",
component: UpdateRow,
},
] ]

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.tables)
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
draftScreen.props._instanceName = name
draftScreen.props._component = baseComponent
draftScreen.route = route
store.createScreen(draftScreen)
finished()
} }
store.createScreen(name, route, layoutComponent._component)
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

@ -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: "table",
control: TableSelect,
},
{
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: "table",
control: TableSelect,
},
{
label: "Title",
key: "title",
control: Input,
},
{
label: "Button Text",
key: "buttonText",
control: Input,
},
],
}, },
}, },
], ],
@ -1136,6 +1104,18 @@ export default {
}, },
children: [], children: [],
}, },
{
name: "New Row",
_component: "@budibase/standard-components/newrow",
description:
"Sets up a new row for creation, which can be used with {{ context }}, in children",
icon: "ri-profile-line",
properties: {
design: { ...all },
settings: [{ label: "Table", key: "table", control: TableSelect }],
},
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 createRow = async params => const saveRow = async (params, state) =>
await post({ await post({
url: `/api/${params.tableId}/rows`, url: `/api/${params.tableId}/rows`,
body: makeRowRequestBody(params), body: makeRowRequestBody(params, state),
}) })
const updateRow = async params => { const updateRow = async (params, state) => {
const row = makeRowRequestBody(params) const row = makeRowRequestBody(params, state)
row._id = params._id row._id = params._id
await patch({ await patch({
url: `/api/${params.tableId}/rows/${params._id}`, url: `/api/${params.tableId}/rows/${params._id}`,
@ -67,8 +67,14 @@ const updateRow = async params => {
}) })
} }
const makeRowRequestBody = parameters => { const makeRowRequestBody = (parameters, state) => {
const body = {} // start with the row thats currently in context
const body = { ...(state.data || {}) }
// dont send the table
if (body._table) delete body._table
// 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 makeRowRequestBody = parameters => {
export default { export default {
authenticate: authenticate(apiOpts), authenticate: authenticate(apiOpts),
createRow, saveRow,
updateRow, updateRow,
} }

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 Row": api.createRow, "Update Record": api.updateRow,
"Update Row": api.updateRow, "Save Record": api.saveRow,
"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

@ -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 rows with links are created, updated or deleted they are processed * This functionality makes sure that when rows with links are created, updated or deleted they are processed
@ -90,8 +91,7 @@ exports.attachLinkInfo = async (instanceId, rows) => {
} }
let tableIds = [...new Set(rows.map(el => el.tableId))] let tableIds = [...new Set(rows.map(el => el.tableId))]
// start by getting all the link values for performance reasons // start by getting all the link values for performance reasons
let responses = [].concat.apply( let responses = _.flatten(
[],
await Promise.all( await Promise.all(
tableIds.map(tableId => tableIds.map(tableId =>
getLinkDocuments({ getLinkDocuments({

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": {}
"table": "tables",
"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": {}
"table": "tables",
"title": "string",
"buttonText": "string"
}
}, },
"datalist": { "datalist": {
"description": "A configurable data list that attaches to your backend tables.", "description": "A configurable data list that attaches to your backend tables.",
@ -270,10 +262,26 @@
} }
}, },
"rowdetail": { "rowdetail": {
<<<<<<< HEAD
"description": "Loads a row, using an ID in the url", "description": "Loads a row, using an ID in the url",
"context": "table", "context": "table",
=======
"description": "Loads a record, using an ID in the url",
"context": "model",
>>>>>>> 3af1d8dc7f13091cc4673d53046919bad9ea28f7
"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": {
"table": "tables" "table": "tables"
} }
@ -715,7 +723,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,126 +1,25 @@
<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 LinkedRowSelector from "./LinkedRowSelector.svelte" import LinkedRowSelector from "./LinkedRowSelector.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 table export let table
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 row
let store = _bb.store let store = _bb.store
let schema = {} let schema = {}
let tableDef = {}
let saved = false
let rowId let rowId
let isNew = true
let errors = {} let errors = {}
$: schema = $store.data && $store.data._table.schema
$: fields = schema ? Object.keys(schema) : [] $: fields = schema ? Object.keys(schema) : []
$: if (table && table.length !== 0) {
fetchTable()
}
async function fetchTable() {
const FETCH_TABLE_URL = `/api/tables/${table}`
const response = await _bb.api.get(FETCH_TABLE_URL)
tableDef = await response.json()
schema = tableDef.schema
row = {
tableId: table,
}
}
const save = debounce(async () => {
for (let field of fields) {
// Assign defaults to empty fields to prevent validation issues
if (!(field in row)) {
row[field] = DEFAULTS_FOR_TYPE[schema[field].type]
}
}
const SAVE_ROW_URL = `/api/${table}/rows`
const response = await _bb.api.post(SAVE_ROW_URL, row)
const json = await response.json()
if (response.status === 200) {
store.update(state => {
state[table] = state[table] ? [...state[table], json] : [json]
return state
})
errors = {}
// wipe form, if new row, otherwise update
// table to get new _rev
row = isNew ? { tableId: table } : 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()
rowId =
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
isNew = !rowId || rowId === "new"
if (isNew) {
row = { tableId: table }
return
}
const GET_ROW_URL = `/api/${table}/rows/${rowId}`
const response = await _bb.api.get(GET_ROW_URL)
row = await response.json()
})
</script> </script>
<form class="form" on:submit|preventDefault> <div class="form-content">
{#if title} <ErrorsBox errors={$store.saveRowErrors || {}} />
<h1>{title}</h1>
{/if}
<div class="form-content">
<ErrorsBox {errors} />
{#each fields as field} {#each fields as field}
<div class="form-field" class:wide> <div class="form-field" class:wide>
{#if !(schema[field].type === 'boolean' && !wide)} {#if !(schema[field].type === 'boolean' && !wide)}
@ -129,40 +28,34 @@
</Label> </Label>
{/if} {/if}
{#if schema[field].type === 'options'} {#if schema[field].type === 'options'}
<Select secondary bind:value={row[field]}> <Select secondary bind:value={$store.data[field]}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#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 if schema[field].type === 'datetime'} {:else if schema[field].type === 'datetime'}
<DatePicker bind:value={row[field]} /> <DatePicker bind:value={$store.data[field]} />
{:else if schema[field].type === 'boolean'} {:else if schema[field].type === 'boolean'}
<Toggle <Toggle
text={wide ? null : capitalise(schema[field].name)} text={wide ? null : capitalise(schema[field].name)}
bind:checked={row[field]} /> bind:checked={$store.data[field]} />
{:else if schema[field].type === 'number'} {:else if schema[field].type === 'number'}
<Input type="number" bind:value={row[field]} /> <Input type="number" bind:value={$store.data[field]} />
{:else if schema[field].type === 'string'} {:else if schema[field].type === 'string'}
<Input bind:value={row[field]} /> <Input bind:value={$store.data[field]} />
{:else if schema[field].type === 'attachment'} {:else if schema[field].type === 'attachment'}
<Dropzone bind:files={row[field]} /> <Dropzone bind:files={$store.data[field]} />
{:else if schema[field].type === 'link'} {:else if schema[field].type === 'link'}
<LinkedRowSelector <LinkedRowSelector
secondary secondary
showLabel={false} showLabel={false}
bind:linkedRows={row[field]} bind:linkedRows={$store.data[field]}
schema={schema[field]} /> schema={schema[field]} />
{/if} {/if}
</div> </div>
{/each} {/each}
<div class="buttons"> </div>
<Button primary on:click={save} green={saved}>
{#if saved}Success{:else}{buttonText || 'Submit Form'}{/if}
</Button>
</div>
</div>
</form>
<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 table
let row = {}
$: {
row.tableId = table
}
let target
async function fetchTable(id) {
const FETCH_TABLE_URL = `/api/tables/${id}`
const response = await _bb.api.get(FETCH_TABLE_URL)
return await response.json()
}
onMount(async () => {
if (table) {
const tableObj = await fetchTable(table)
row.tableId = table
row._table = tableObj
_bb.attachChildren(target, {
context: row,
})
} 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 allRows = await response.json() const allRows = await response.json()
if (allRows.length > 0) return allRows[0] if (allRows.length > 0) return allRows[0]
return { tableId: table }
} }
} }
@ -29,31 +30,35 @@
let row let row
// 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") {
row = await fetchFirstRow() if (table) row = await fetchFirstRow()
} else { } else if (_bb.routeParams().id) {
const id = pathParts[pathParts.length - 1] const GET_ROW_URL = `/api/${table}/rows/${_bb.routeParams().id}`
const GET_ROW_URL = `/api/${table}/rows/${id}`
const response = await _bb.api.get(GET_ROW_URL) const response = await _bb.api.get(GET_ROW_URL)
if (response.status === 200) { if (response.status === 200) {
row = await response.json() row = await response.json()
} else {
throw new Error("Failed to fetch row.", response)
} }
} else {
throw new Exception("Row ID was not supplied to RowDetail")
} }
if (row) { if (row) {
// Fetch table schema so we can check for linked rows // Fetch table schema so we can check for linked rows
const table = await fetchTable(row.tableId) const tableObj = await fetchTable(row.tableId)
for (let key of Object.keys(table.schema)) { for (let key of Object.keys(tableObj.schema)) {
if (table.schema[key].type === "link") { if (tableObj.schema[key].type === "link") {
row[key] = Array.isArray(row[key]) ? row[key].length : 0 row[key] = Array.isArray(row[key]) ? row[key].length : 0
} }
} }
row._table = tableObj
_bb.attachChildren(target, { _bb.attachChildren(target, {
hydrate: false,
context: row, context: row,
}) })
} else { } else {
throw new Error("Failed to fetch row.", response) _bb.attachChildren(target)
} }
} }

View File

@ -27,6 +27,7 @@ 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 rowdetail } from "./RowDetail.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"