1
0
Fork 0
mirror of synced 2024-06-30 03:50:37 +12:00

Merge branch 'linked-records' of github.com:Budibase/budibase into api-usage-tracking

This commit is contained in:
mike12345567 2020-10-08 13:30:18 +01:00
commit d2ae589151
43 changed files with 475 additions and 1047 deletions

View file

@ -66,12 +66,14 @@ Cypress.Commands.add("createTestTableWithData", () => {
Cypress.Commands.add("createTable", tableName => {
// Enter model name
cy.contains("Create New Table").click()
cy.get(".menu-container")
.get("input")
.first()
.type(tableName)
cy.contains("Save").click()
cy.get(".modal").within(() => {
cy.get("input")
.first()
.type(tableName)
cy.get(".buttons")
.contains("Create")
.click()
})
cy.contains(tableName).should("be.visible")
})

View file

@ -63,7 +63,7 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.40.1",
"@budibase/bbui": "^1.41.0",
"@budibase/client": "^0.1.25",
"@budibase/colorpicker": "^1.0.1",
"@fortawesome/fontawesome-free": "^5.14.0",

View file

@ -4,7 +4,6 @@
import { routes } from "../routify/routes"
import { initialise } from "builderStore"
import NotificationDisplay from "components/common/Notification/NotificationDisplay.svelte"
import { ModalContainer } from "components/common/Modal"
onMount(async () => {
await initialise()
@ -15,4 +14,3 @@
<NotificationDisplay />
<Router {routes} />
<ModalContainer />

View file

@ -78,6 +78,11 @@ const contextToBindables = (models, walkResult) => context => {
typeof context.model === "string" ? context.model : context.model.modelId
const model = models.find(model => model._id === modelId)
// Avoid crashing whenever no data source has been selected
if (model == null) {
return []
}
const newBindable = key => ({
type: "context",
instance: context.instance,

View file

@ -2,10 +2,9 @@
import { onMount } from "svelte"
import { automationStore } from "builderStore"
import CreateAutomationModal from "./CreateAutomationModal.svelte"
import { Button } from "@budibase/bbui"
import { Modal } from "components/common/Modal"
import { Button, Modal } from "@budibase/bbui"
let modalVisible = false
let modal
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
@ -15,9 +14,7 @@
</script>
<section>
<Button primary wide on:click={() => (modalVisible = true)}>
Create New Automation
</Button>
<Button primary wide on:click={modal.show}>Create New Automation</Button>
<ul>
{#each $automationStore.automations as automation}
<li
@ -30,9 +27,9 @@
{/each}
</ul>
</section>
{#if modalVisible}
<CreateAutomationModal bind:visible={modalVisible} />
{/if}
<Modal bind:this={modal}>
<CreateAutomationModal />
</Modal>
<style>
section {
@ -51,7 +48,7 @@
color: var(--grey-6);
}
i.live {
color: var(--purple);
color: var(--ink);
}
li {

View file

@ -1,11 +1,8 @@
<script>
import { store, backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { Input } from "@budibase/bbui"
import { Input, ModalContent } from "@budibase/bbui"
import analytics from "analytics"
import { Modal } from "components/common/Modal"
export let visible
let name
@ -29,22 +26,21 @@
}
</script>
<Modal
bind:visible
<ModalContent
title="Create Automation"
confirmText="Create"
onConfirm={createAutomation}
disabled={!valid}>
<Input bind:value={name} label="Name" />
<slot name="footer">
<div slot="footer">
<a
target="_blank"
href="https://docs.budibase.com/automate/introduction-to-automate">
<i class="ri-information-line" />
<span>Learn about automations</span>
</a>
</slot>
</Modal>
</div>
</ModalContent>
<style>
a {

View file

@ -9,6 +9,7 @@
export let fieldName
let record
let title
$: data = record?.[fieldName] ?? []
$: linkedModelId = data?.length ? data[0].modelId : null
@ -17,8 +18,15 @@
)
$: schema = linkedModel?.schema
$: model = $backendUiStore.models.find(model => model._id === modelId)
$: title = `${record?.[model?.primaryDisplay]} - ${fieldName}`
$: fetchData(modelId, recordId)
$: {
let recordLabel = record?.[model?.primaryDisplay]
if (recordLabel) {
title = `${recordLabel} - ${fieldName}`
} else {
title = fieldName
}
}
async function fetchData(modelId, recordId) {
const QUERY_VIEW_URL = `/api/${modelId}/${recordId}/enrich`

View file

@ -1,17 +1,16 @@
<script>
import { TextButton as Button, Icon } from "@budibase/bbui"
import { TextButton as Button, Icon, Modal } from "@budibase/bbui"
import CreateEditRecordModal from "../modals/CreateEditRecordModal.svelte"
import { Modal } from "components/common/Modal"
let modalVisible
let modal
</script>
<div>
<Button text small on:click={() => (modalVisible = true)}>
<Button text small on:click={modal.show}>
<Icon name="addrow" />
Create New Row
</Button>
</div>
{#if modalVisible}
<CreateEditRecordModal bind:visible={modalVisible} />
{/if}
<Modal bind:this={modal}>
<CreateEditRecordModal />
</Modal>

View file

@ -3,13 +3,11 @@
import { notifier } from "builderStore/store/notifications"
import RecordFieldControl from "../RecordFieldControl.svelte"
import * as api from "../api"
import { Modal } from "components/common/Modal"
import { ModalContent } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
export let record = {}
export let visible
let modal
let errors = []
$: creating = record?._id == null
@ -35,8 +33,7 @@
}
</script>
<Modal
bind:visible
<ModalContent
title={creating ? 'Create Row' : 'Edit Row'}
confirmText={creating ? 'Create Row' : 'Save Row'}
onConfirm={saveRecord}>
@ -46,4 +43,4 @@
<RecordFieldControl {meta} bind:value={record[key]} />
</div>
{/each}
</Modal>
</ModalContent>

View file

@ -1,22 +1,21 @@
<script>
import { backendUiStore } from "builderStore"
import { DropdownMenu, Icon } from "@budibase/bbui"
import { DropdownMenu, Icon, Modal } from "@budibase/bbui"
import CreateEditRecordModal from "../modals/CreateEditRecordModal.svelte"
import * as api from "../api"
import { notifier } from "builderStore/store/notifications"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { Modal } from "components/common/Modal"
export let row
let anchor
let dropdown
let confirmDeleteDialog
let editModalVisible
let modal
function showModal() {
dropdown.hide()
editModalVisible = true
modal.show()
}
function showDelete() {
@ -52,9 +51,9 @@
okText="Delete Row"
onOk={deleteRow}
title="Confirm Delete" />
{#if editModalVisible}
<CreateEditRecordModal bind:visible={editModalVisible} record={row} />
{/if}
<Modal bind:this={modal}>
<CreateEditRecordModal record={row} />
</Modal>
<style>
.ri-more-line:hover {

View file

@ -2,7 +2,7 @@
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import ListItem from "./ListItem.svelte"
import CreateTablePopover from "./popovers/CreateTablePopover.svelte"
import CreateTableModal from "./modals/CreateTableModal.svelte"
import EditTablePopover from "./popovers/EditTablePopover.svelte"
import EditViewPopover from "./popovers/EditViewPopover.svelte"
import { Heading } from "@budibase/bbui"
@ -28,7 +28,7 @@
<div class="components-list-container">
<Heading small>Tables</Heading>
<Spacer medium />
<CreateTablePopover />
<CreateTableModal />
<div class="hierarchy-items-container">
{#each $backendUiStore.models as model}
<ListItem

View file

@ -0,0 +1,48 @@
<script>
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { Button, Input, Label, ModalContent, Modal } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import TableDataImport from "../TableDataImport.svelte"
import analytics from "analytics"
let modal
let name
let dataImport
function resetState() {
name = ""
dataImport = undefined
}
async function saveTable() {
const model = await backendUiStore.actions.models.save({
name,
schema: dataImport.schema || {},
dataImport,
})
notifier.success(`Table ${name} created successfully.`)
$goto(`./model/${model._id}`)
analytics.captureEvent("Table Created", { name })
}
</script>
<Button primary wide on:click={modal.show}>Create New Table</Button>
<Modal bind:this={modal} on:hide={resetState}>
<ModalContent
title="Create Table"
confirmText="Create"
onConfirm={saveTable}
disabled={!name || (dataImport && !dataImport.valid)}>
<Input
data-cy="table-name-input"
thin
label="Table Name"
bind:value={name} />
<div>
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
<TableDataImport bind:dataImport />
</div>
</ModalContent>
</Modal>

View file

@ -1,84 +0,0 @@
<script>
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { Popover, Button, Icon, Input, Select, Label } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import TableDataImport from "../TableDataImport.svelte"
import analytics from "analytics"
let anchor
let dropdown
let name
let dataImport
let loading
async function saveTable() {
loading = true
const model = await backendUiStore.actions.models.save({
name,
schema: dataImport.schema || {},
dataImport,
})
notifier.success(`Table ${name} created successfully.`)
$goto(`./model/${model._id}`)
analytics.captureEvent("Table Created", { name })
name = ""
dropdown.hide()
loading = false
}
const onClosed = () => {
name = ""
dropdown.hide()
}
</script>
<div bind:this={anchor}>
<Button primary wide on:click={dropdown.show}>Create New Table</Button>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<div class="actions">
<h5>Create Table</h5>
<Input
data-cy="table-name-input"
thin
label="Table Name"
bind:value={name} />
<div>
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
<TableDataImport bind:dataImport />
</div>
<footer>
<Button secondary on:click={onClosed}>Cancel</Button>
<Button
disabled={!name || (dataImport && !dataImport.valid)}
primary
on:click={saveTable}>
<span style={`margin-right: ${loading ? '10px' : 0};`}>Save</span>
{#if loading}
<Spinner size="10" />
{/if}
</Button>
</footer>
</div>
</Popover>
<style>
.actions {
display: grid;
grid-gap: var(--spacing-xl);
min-width: 400px;
}
h5 {
margin: 0;
font-weight: 500;
}
footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
</style>

View file

@ -1,33 +1,27 @@
<script>
import { Modal } from "components/common/Modal"
import { Modal, ModalContent } from "@budibase/bbui"
export let title = ""
export let body = ""
export let okText = "Confirm"
export let cancelText = "Cancel"
export let onOk = () => {}
export let onCancel = () => {}
export let onOk = undefined
export let onCancel = undefined
let visible = false
let modal
export const show = () => {
visible = true
modal.show()
}
export const hide = () => {
visible = false
modal.hide()
}
</script>
<Modal
id={title}
bind:visible
on:hide={onCancel}
{title}
confirmText={okText}
{cancelText}
onConfirm={onOk}
red>
<div class="body">{body}</div>
<Modal bind:this={modal} on:hide={onCancel}>
<ModalContent onConfirm={onOk} {title} confirmText={okText} {cancelText} red>
<div class="body">{body}</div>
</ModalContent>
</Modal>
<style>

View file

@ -8,17 +8,24 @@
export let schema
export let linkedRecords = []
let records = []
$: label = capitalise(schema.name)
$: linkedModelId = schema.modelId
$: linkedModel = $backendUiStore.models.find(
model => model._id === linkedModelId
)
$: promise = fetchRecords(linkedModelId)
$: fetchRecords(linkedModelId)
async function fetchRecords(linkedModelId) {
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
const response = await api.get(FETCH_RECORDS_URL)
return await response.json()
try {
const response = await api.get(FETCH_RECORDS_URL)
records = await response.json()
} catch (error) {
console.log(error)
records = []
}
}
function getPrettyName(record) {
@ -34,15 +41,13 @@
table.
</Label>
{:else}
{#await promise then records}
<Multiselect
secondary
bind:value={linkedRecords}
{label}
placeholder="Choose some options">
{#each records as record}
<option value={record._id}>{getPrettyName(record)}</option>
{/each}
</Multiselect>
{/await}
<Multiselect
secondary
bind:value={linkedRecords}
{label}
placeholder="Choose some options">
{#each records as record}
<option value={record._id}>{getPrettyName(record)}</option>
{/each}
</Multiselect>
{/if}

View file

@ -1,215 +0,0 @@
<script>
/**
* Confirmation is handled as a callback rather than an event to allow
* handling the result - meaning a parent can prevent the modal closing.
*
* A show/hide API is exposed as part of the modal and also via context for
* children inside the modal.
* "show" and "hide" events are emitted as visibility changes.
*
* Modals are rendered at the top of the DOM tree.
*/
import { createEventDispatcher, setContext } from "svelte"
import { fade, fly } from "svelte/transition"
import Portal from "svelte-portal"
import { Button } from "@budibase/bbui"
import { ContextKey } from "./context"
const dispatch = createEventDispatcher()
export let wide = false
export let padded = true
export let title = undefined
export let cancelText = "Cancel"
export let confirmText = "Confirm"
export let showCancelButton = true
export let showConfirmButton = true
export let onConfirm = () => {}
export let visible = false
let loading = false
function show() {
if (visible) {
return
}
visible = true
dispatch("show")
}
function hide() {
if (!visible) {
return
}
visible = false
dispatch("hide")
}
async function confirm() {
loading = true
if (!onConfirm || (await onConfirm()) !== false) {
hide()
}
loading = false
}
setContext(ContextKey, { show, hide })
</script>
{#if visible}
<Portal target="#modal-container">
<div
class="overlay"
on:click|self={hide}
transition:fade={{ duration: 200 }}>
<div
class="scroll-wrapper"
on:click|self={hide}
transition:fly={{ y: 50 }}>
<div class="content-wrapper" on:click|self={hide}>
<div class="modal" class:wide class:padded>
{#if title}
<header>
<h5>{title}</h5>
<div class="header-content">
<slot name="header" />
</div>
</header>
{/if}
<slot />
{#if showCancelButton || showConfirmButton}
<footer>
<div class="footer-content">
<slot name="footer" />
</div>
<div class="buttons">
{#if showCancelButton}
<Button secondary on:click={hide}>{cancelText}</Button>
{/if}
{#if showConfirmButton}
<Button
primary
{...$$restProps}
disabled={$$restProps.disabled || loading}
on:click={confirm}>
{confirmText}
</Button>
{/if}
</div>
</footer>
{/if}
<i class="ri-close-line" on:click={hide} />
</div>
</div>
</div>
</div>
</Portal>
{/if}
<style>
.overlay {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.25);
}
.scroll-wrapper {
flex: 1 1 auto;
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
max-height: 100%;
}
.content-wrapper {
flex: 1 1 auto;
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
width: 0;
}
.modal {
background-color: white;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
box-shadow: 0 0 2.4rem 1.5rem rgba(0, 0, 0, 0.15);
position: relative;
flex: 0 0 400px;
margin: 2rem 0;
border-radius: var(--border-radius-m);
gap: var(--spacing-xl);
}
.modal.wide {
flex: 0 0 600px;
}
.modal.padded {
padding: var(--spacing-xl);
}
header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-right: 40px;
}
header h5 {
margin: 0;
font-weight: 500;
}
.header-content {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
i {
position: absolute;
top: var(--spacing-xl);
right: var(--spacing-xl);
color: var(--ink);
font-size: var(--font-size-xl);
}
i:hover {
color: var(--grey-6);
cursor: pointer;
}
footer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--spacing-m);
}
.footer-content {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: var(--spacing-m);
}
</style>

View file

@ -1,10 +0,0 @@
<div id="modal-container" />
<style>
#modal-container {
position: fixed;
top: 0;
left: 0;
z-index: 999;
}
</style>

View file

@ -1 +0,0 @@
export const ContextKey = "budibase-modal"

View file

@ -1,3 +0,0 @@
export { default as Modal } from "./Modal.svelte"
export { default as ModalContainer } from "./ModalContainer.svelte"
export { ContextKey } from "./context"

View file

@ -1,17 +1,17 @@
<script>
import SettingsModal from "./SettingsModal.svelte"
import { SettingsIcon } from "components/common/Icons/"
import { Modal } from "components/common/Modal"
import { Modal } from "@budibase/bbui"
let modalVisible
let modal
</script>
<span class="topnavitemright settings" on:click={() => (modalVisible = true)}>
<span class="topnavitemright settings" on:click={modal.show}>
<SettingsIcon />
</span>
{#if modalVisible}
<SettingsModal bind:visible={modalVisible} />
{/if}
<Modal bind:this={modal} width="600px">
<SettingsModal />
</Modal>
<style>
span:first-letter {
@ -20,8 +20,7 @@
.topnavitemright {
cursor: pointer;
color: var(--grey-7);
margin: 0px 20px 0px 0px;
padding-top: 4px;
margin: 0 20px 0 0;
font-weight: 500;
font-size: 1rem;
height: 100%;

View file

@ -1,7 +1,6 @@
<script>
import { General, Users, DangerZone, APIKeys } from "./tabs"
import { Switcher } from "@budibase/bbui"
import { Modal } from "components/common/Modal"
import { Switcher, ModalContent } from "@budibase/bbui"
const tabs = [
{
@ -26,17 +25,13 @@
},
]
export let visible
let value = "GENERAL"
$: selectedTab = tabs.find(tab => tab.key === value).component
</script>
<Modal
<ModalContent
title="Settings"
wide
bind:visible
showConfirmButton={false}
showCancelButton={false}>
<div class="container">
@ -44,7 +39,7 @@
<svelte:component this={selectedTab} />
</Switcher>
</div>
</Modal>
</ModalContent>
<style>
.container :global(section > header) {

View file

@ -1,6 +1,5 @@
<script>
import { writable } from "svelte/store"
import { Modal } from "components/common/Modal"
import { store, automationStore, backendUiStore } from "builderStore"
import { string, object } from "yup"
import api, { get } from "builderStore/api"
@ -18,7 +17,6 @@
//Move this to context="module" once svelte-forms is updated so that it can bind to stores correctly
const createAppStore = writable({ currentStep: 0, values: {} })
export let visible
export let hasKey
export let template
@ -201,68 +199,61 @@
}
</script>
<Modal
bind:visible
wide
padded={false}
showCancelButton={false}
showConfirmButton={false}>
<div class="container">
<div class="sidebar">
{#each steps as { active, done }, i}
<Indicator
active={$createAppStore.currentStep === i}
done={i < $createAppStore.currentStep}
step={i + 1} />
{/each}
</div>
<div class="body">
<div class="heading">
<h3 class="header">Get Started with Budibase</h3>
</div>
<div class="step">
<Form bind:values={$createAppStore.values}>
{#each steps as step, i (i)}
<div class:hidden={$createAppStore.currentStep !== i}>
<svelte:component
this={step.component}
{template}
{validationErrors}
options={step.options}
name={step.name} />
</div>
{/each}
</Form>
</div>
<div class="footer">
{#if $createAppStore.currentStep > 0}
<Button medium secondary on:click={back}>Back</Button>
{/if}
{#if $createAppStore.currentStep < steps.length - 1}
<Button medium blue on:click={next} disabled={!currentStepIsValid}>
Next
</Button>
{/if}
{#if $createAppStore.currentStep === steps.length - 1}
<Button
medium
blue
on:click={signUp}
disabled={!fullFormIsValid || submitting}>
{submitting ? 'Loading...' : 'Submit'}
</Button>
{/if}
</div>
</div>
<img src="/_builder/assets/bb-logo.svg" alt="budibase icon" />
{#if submitting}
<div in:fade class="spinner-container">
<Spinner />
<span class="spinner-text">Creating your app...</span>
</div>
{/if}
<div class="container">
<div class="sidebar">
{#each steps as { active, done }, i}
<Indicator
active={$createAppStore.currentStep === i}
done={i < $createAppStore.currentStep}
step={i + 1} />
{/each}
</div>
</Modal>
<div class="body">
<div class="heading">
<h3 class="header">Get Started with Budibase</h3>
</div>
<div class="step">
<Form bind:values={$createAppStore.values}>
{#each steps as step, i (i)}
<div class:hidden={$createAppStore.currentStep !== i}>
<svelte:component
this={step.component}
{template}
{validationErrors}
options={step.options}
name={step.name} />
</div>
{/each}
</Form>
</div>
<div class="footer">
{#if $createAppStore.currentStep > 0}
<Button medium secondary on:click={back}>Back</Button>
{/if}
{#if $createAppStore.currentStep < steps.length - 1}
<Button medium blue on:click={next} disabled={!currentStepIsValid}>
Next
</Button>
{/if}
{#if $createAppStore.currentStep === steps.length - 1}
<Button
medium
blue
on:click={signUp}
disabled={!fullFormIsValid || submitting}>
{submitting ? 'Loading...' : 'Submit'}
</Button>
{/if}
</div>
</div>
<img src="/_builder/assets/bb-logo.svg" alt="budibase icon" />
{#if submitting}
<div in:fade class="spinner-container">
<Spinner />
<span class="spinner-text">Creating your app...</span>
</div>
{/if}
</div>
<style>
.container {

View file

@ -3,21 +3,18 @@
import {
TextButton,
Button,
Heading,
Body,
Spacer,
DropdownMenu,
ModalContent,
} from "@budibase/bbui"
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
import actionTypes from "./actions"
import { createEventDispatcher } from "svelte"
import { Modal } from "components/common/Modal"
const dispatch = createEventDispatcher()
export let event
export let visible
let addActionButton
let addActionDropdown
@ -60,12 +57,7 @@
}
</script>
<Modal
bind:visible
title="Actions"
wide
confirmText="Save"
onConfirm={saveEventData}>
<ModalContent title="Actions" confirmText="Save" onConfirm={saveEventData}>
<div slot="header">
<div bind:this={addActionButton}>
<TextButton text small blue on:click={addActionDropdown.show}>
@ -94,9 +86,7 @@
{#each actions as action, index}
<div class="action-container">
<div class="action-header" on:click={selectAction(action)}>
<Body extraSmall lh>
{index + 1}. {action[EVENT_TYPE_MEMBER_NAME]}
</Body>
<Body small lh>{index + 1}. {action[EVENT_TYPE_MEMBER_NAME]}</Body>
<div class="row-expander" class:rotate={action !== selectedAction}>
<ArrowDownIcon />
</div>
@ -121,7 +111,7 @@
<div slot="footer">
<a href="https://docs.budibase.com">Learn more about Actions</a>
</div>
</Modal>
</ModalContent>
<style>
.action-header {
@ -151,8 +141,7 @@
.actions-container {
flex: 1;
min-height: 0px;
padding-bottom: var(--spacing-s);
min-height: 0;
padding-top: 0;
border: var(--border-light);
border-width: 0 0 1px 0;
@ -162,10 +151,6 @@
.action-container {
border: var(--border-light);
border-width: 1px 0 0 0;
padding-left: var(--spacing-xl);
padding-right: var(--spacing-xl);
padding-top: 0;
padding-bottom: 0;
}
.selected-action-container {

View file

@ -1,23 +1,17 @@
<script>
import { Button, Modal } from "@budibase/bbui"
import EventEditorModal from "./EventEditorModal.svelte"
import { createEventDispatcher, onMount } from "svelte"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let value
export let name
let modalVisible = false
let modal
</script>
<Button secondary small on:click={() => (modalVisible = true)}>
Define Actions
</Button>
<Button secondary small on:click={modal.show}>Define Actions</Button>
{#if modalVisible}
<EventEditorModal
bind:visible={modalVisible}
event={value}
eventType={name}
on:change />
{/if}
<Modal bind:this={modal} width="600px">
<EventEditorModal event={value} eventType={name} on:change />
</Modal>

View file

@ -1,7 +1,7 @@
<script>
import { keys, map, includes, filter } from "lodash/fp"
import EventEditorModal from "./EventEditorModal.svelte"
import { Modal } from "components/common/Modal"
import { Modal } from "@budibase/bbui"
export const EVENT_TYPE = "event"
export let component
@ -47,11 +47,8 @@
</form>
</div>
<Modal bind:this={modal}>
<EventEditorModal
eventOptions={events}
event={selectedEvent}
on:hide={() => (selectedEvent = null)} />
<Modal bind:this={modal} width="600px">
<EventEditorModal eventOptions={events} event={selectedEvent} />
</Modal>
<style>

View file

@ -60,7 +60,7 @@
align-items: baseline;
}
.root :global(.relative:nth-child(2)) {
.root :global(> div:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}

View file

@ -22,7 +22,7 @@
align-items: baseline;
}
.root :global(.relative) {
.root :global(> div) {
flex: 1;
margin-left: var(--spacing-l);
}

View file

@ -119,7 +119,7 @@
align-items: baseline;
}
.root :global(.relative:nth-child(2)) {
.root :global(> div:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}

View file

@ -1,21 +1,18 @@
<script>
import { store, backendUiStore } from "builderStore"
import { store } from "builderStore"
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte"
import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte"
import NewScreen from "components/userInterface/NewScreen.svelte"
import { Button } from "@budibase/bbui"
import { Spacer } from "@budibase/bbui"
import NewScreenModal from "components/userInterface/NewScreenModal.svelte"
import { Button, Spacer, Modal } from "@budibase/bbui"
let modalVisible = false
let modal
</script>
<PagesList />
<Spacer medium />
<Button primary wide on:click={() => (modalVisible = true)}>
Create New Screen
</Button>
<Button primary wide on:click={modal.show}>Create New Screen</Button>
<Spacer medium />
<PageLayout layout={$store.pages[$store.currentPageName]} />
@ -23,38 +20,6 @@
<ComponentsHierarchy screens={$store.screens} />
</div>
{#if modalVisible}
<NewScreen bind:visible={modalVisible} />
{/if}
<style>
.newscreen {
cursor: pointer;
border: 1px solid var(--purple);
border-radius: 5px;
width: 100%;
height: 36px;
padding: 8px 16px;
margin: 20px 0px 12px 0px;
display: flex;
justify-content: center;
align-items: center;
background: var(--purple);
color: var(--white);
font-size: 14px;
font-weight: 500;
transition: all 3ms;
outline: none;
}
.newscreen:hover {
background: var(--purple-light);
color: var(--purple);
}
.icon {
color: var(--ink);
font-size: 16px;
margin-right: 4px;
}
</style>
<Modal bind:this={modal}>
<NewScreenModal />
</Modal>

View file

@ -1,14 +1,7 @@
<script>
import { store } from "builderStore"
import { pipe } from "components/common/core"
import { isRootComponent } from "./pagesParsing/searchComponents"
import { splitName } from "./pagesParsing/splitRootComponentName.js"
import { Input, Select, Button, Spacer } from "@budibase/bbui"
import { Modal } from "components/common/Modal"
import { find, filter, some, map, includes } from "lodash/fp"
import { assign } from "lodash"
export let visible
import { Input, Select, ModalContent } from "@budibase/bbui"
import { find, filter, some } from "lodash/fp"
let dialog
let layoutComponents
@ -60,11 +53,7 @@
}
</script>
<Modal
bind:visible
title="New Screen"
confirmText="Create Screen"
onConfirm={save}>
<ModalContent title="New Screen" confirmText="Create Screen" onConfirm={save}>
<Input label="Name" bind:value={name} />
<Input
label="Url"
@ -76,4 +65,4 @@
<option value={_component}>{name}</option>
{/each}
</Select>
</Modal>
</ModalContent>

View file

@ -10,8 +10,6 @@
export let onStyleChanged = () => {}
export let open = false
$: console.log(properties)
$: style = componentInstance["_styles"][styleCategory] || {}
</script>

View file

@ -4,19 +4,20 @@
import PageLayout from "./PageLayout.svelte"
import PagesList from "./PagesList.svelte"
import { store } from "builderStore"
import NewScreen from "./NewScreen.svelte"
import CurrentItemPreview from "./CurrentItemPreview.svelte"
import NewScreenModal from "./NewScreenModal.svelte"
import CurrentItemPreview from "./AppPreview/CurrentItemPreview.svelte"
import SettingsView from "./SettingsView.svelte"
import ComponentsPaneSwitcher from "./ComponentsPaneSwitcher.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last } from "lodash/fp"
import { AddIcon } from "components/common/Icons"
import { Modal } from "@budibase/bbui"
let newScreenPicker
let confirmDeleteDialog
let componentToDelete = ""
let settingsView
let modalVisible = false
let modal
const settings = () => {
settingsView.show()
@ -26,30 +27,24 @@
</script>
<div class="root">
<div class="ui-nav">
<div class="pages-list-container">
<div class="nav-header">
<span class="navigator-title">Navigator</span>
<span class="components-nav-page">Pages</span>
</div>
<div class="nav-items-container">
<PagesList />
</div>
</div>
<PageLayout layout={$store.pages[$store.currentPageName]} />
<div class="components-list-container">
<div class="nav-group-header">
<span class="components-nav-header" style="margin-top: 0;">
Screens
</span>
<div>
<button on:click={() => (modalVisible = true)}>
<button on:click={modal.show}>
<AddIcon />
</button>
</div>
@ -58,24 +53,21 @@
<ComponentsHierarchy screens={$store.screens} />
</div>
</div>
</div>
<div class="preview-pane">
<CurrentItemPreview />
</div>
{#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'}
<div class="components-pane">
<ComponentsPaneSwitcher />
</div>
{/if}
</div>
{#if modalVisible}
<NewScreen bind:visible={modalVisible} />
{/if}
<Modal bind:this={modal}>
<NewScreenModal />
</Modal>
<SettingsView bind:this={settingsView} />
<style>

View file

@ -127,8 +127,7 @@
.topnavitem {
cursor: pointer;
color: var(--grey-5);
margin: 0px 00px 0px 20px;
padding-top: 4px;
margin: 0 0 0 20px;
font-weight: 500;
font-size: var(--font-size-m);
height: 100%;
@ -149,8 +148,7 @@
.topnavitemright {
cursor: pointer;
color: var(--grey-7);
margin: 0px 20px 0px 0px;
padding-top: 4px;
margin: 0 20px 0 0;
font-weight: 500;
font-size: 1rem;
height: 100%;

View file

@ -3,5 +3,7 @@
import RelationshipDataTable from "components/backend/DataTable/RelationshipDataTable.svelte"
</script>
<RelationshipDataTable modelId={$params.selectedModel} recordId={$params.selectedRecord}
fieldName={$params.selectedField}/>
<RelationshipDataTable
modelId={$params.selectedModel}
recordId={$params.selectedRecord}
fieldName={decodeURI($params.selectedField)} />

View file

@ -7,15 +7,14 @@
import { get } from "builderStore/api"
import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte"
import { Button, Heading } from "@budibase/bbui"
import { Button, Heading, Modal } from "@budibase/bbui"
import TemplateList from "components/start/TemplateList.svelte"
import analytics from "analytics"
import { Modal } from "components/common/Modal"
let promise = getApps()
let hasKey
let template
let modalVisible = false
let modal
async function getApps() {
const res = await get("/api/applications")
@ -42,13 +41,13 @@
}
if (!keys.budibase) {
modalVisible = true
modal.show()
}
}
function selectTemplate(newTemplate) {
template = newTemplate
modalVisible = true
modal.show()
}
checkIfKeysAndApps()
@ -57,9 +56,7 @@
<div class="container">
<div class="header">
<Heading medium black>Welcome to the Budibase Beta</Heading>
<Button primary purple on:click={() => (modalVisible = true)}>
Create New Web App
</Button>
<Button primary purple on:click={modal.show}>Create New Web App</Button>
</div>
<div class="banner">
@ -72,12 +69,12 @@
<TemplateList onSelect={selectTemplate} />
<AppList />
{#if modalVisible}
<CreateAppModal bind:visible={modalVisible} {hasKey} {template} />
{/if}
</div>
<Modal bind:this={modal} padding={false} width="600px">
<CreateAppModal {hasKey} {template} />
</Modal>
<style>
.container {
display: grid;

View file

@ -709,13 +709,14 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@budibase/bbui@^1.40.1":
version "1.40.1"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.40.1.tgz#7ebfd52b4da822312d3395447a4f73caa41f8014"
integrity sha512-0t5Makyn5jOURKZIQPvd+8G4m6ps4GyfLUkAz5rKJSnAQSAgLiuZ+RihcEReDEJK8tnfW7h2echJTffJduQRRQ==
"@budibase/bbui@^1.41.0":
version "1.41.0"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.41.0.tgz#cb239db3071a4a6c6f0ef48ddde55f5eab9808ce"
integrity sha512-pT5u6HDdXcylWgSE1TBt3jETg92GwgAXpUsBVqX+OUE/2lNbmThb8egAckpemHDvm91FAL0nApQYpV7c/qLzvw==
dependencies:
sirv-cli "^0.4.6"
svelte-flatpickr "^2.4.0"
svelte-portal "^1.0.0"
"@budibase/colorpicker@^1.0.1":
version "1.0.1"
@ -5844,6 +5845,11 @@ svelte-portal@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-0.1.0.tgz#cc2821cc84b05ed5814e0218dcdfcbebc53c1742"
svelte-portal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3"
integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q==
svelte@^3.24.1:
version "3.25.1"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.25.1.tgz#218def1243fea5a97af6eb60f5e232315bb57ac4"

View file

@ -84,7 +84,7 @@
},
"devDependencies": {
"@jest/test-sequencer": "^24.8.0",
"electron": "8.2.5",
"electron": "10.1.3",
"electron-builder": "^22.7.0",
"electron-builder-notarize": "^1.1.2",
"eslint": "^6.8.0",

View file

@ -257,10 +257,8 @@ exports.fetchEnrichedRecord = async function(ctx) {
}
return
}
// // need model to work out where links go in record
const modelAndRecord = await Promise.all([db.get(modelId), db.get(recordId)])
const model = modelAndRecord[0]
const record = modelAndRecord[1]
// need model to work out where links go in record
const [model, record] = await Promise.all([db.get(modelId), db.get(recordId)])
// get the link docs
const linkVals = await linkRecords.getLinkDocuments({
instanceId,
@ -307,6 +305,16 @@ function coerceRecordValues(rec, model) {
}
const TYPE_TRANSFORM_MAP = {
link: {
"": [],
[null]: [],
[undefined]: undefined,
},
options: {
"": "",
[null]: "",
[undefined]: undefined,
},
string: {
"": "",
[null]: "",

View file

@ -2044,9 +2044,10 @@ electron-updater@^4.3.1:
lodash.isequal "^4.5.0"
semver "^7.1.3"
electron@8.2.5:
version "8.2.5"
resolved "https://registry.yarnpkg.com/electron/-/electron-8.2.5.tgz#ae3cb23d5517b2189fd35298e487198d65d1a291"
electron@10.1.3:
version "10.1.3"
resolved "https://registry.yarnpkg.com/electron/-/electron-10.1.3.tgz#7e276e373bf30078bd4cb1184850a91268dc0e6c"
integrity sha512-CR8LrlG47MdAp317SQ3vGYa2o2cIMdMSMPYH46OVitFLk35dwE9fn3VqvhUIXhCHYcNWIAPzMhkVHpkoFdKWuw==
dependencies:
"@electron/get" "^1.0.1"
"@types/node" "^12.0.12"

View file

@ -36,7 +36,7 @@
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
"dependencies": {
"@beyonk/svelte-googlemaps": "^2.2.0",
"@budibase/bbui": "^1.40.1",
"@budibase/bbui": "^1.41.0",
"@fortawesome/fontawesome-free": "^5.14.0",
"britecharts": "^2.16.1",
"d3-selection": "^1.4.2",

View file

@ -1,189 +1,10 @@
<script>
import { onMount } from "svelte"
import { fade } from "svelte/transition"
import {
Label,
DatePicker,
Input,
Select,
Button,
Toggle,
} from "@budibase/bbui"
import Dropzone from "./attachments/Dropzone.svelte"
import LinkedRecordSelector from "./LinkedRecordSelector.svelte"
import debounce from "lodash.debounce"
import ErrorsBox from "./ErrorsBox.svelte"
import { capitalise } from "./helpers"
import Form from "./Form.svelte"
export let _bb
export let model
export let title
export let buttonText
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 schema = {}
let modelDef = {}
let saved = false
let recordId
let isNew = true
let errors = {}
$: 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>
<form class="form" on:submit|preventDefault>
{#if title}
<h1>{title}</h1>
{/if}
<div class="form-content">
<ErrorsBox {errors} />
{#each fields as field}
{#if schema[field].type === 'options'}
<Select
secondary
label={capitalise(schema[field].name)}
bind:value={record[field]}>
<option value="">Choose an option</option>
{#each schema[field].constraints.inclusion as opt}
<option>{opt}</option>
{/each}
</Select>
{:else if schema[field].type === 'datetime'}
<DatePicker
label={capitalise(schema[field].name)}
bind:value={record[field]} />
{:else if schema[field].type === 'boolean'}
<Toggle
text={capitalise(schema[field].name)}
bind:checked={record[field]} />
{:else if schema[field].type === 'number'}
<Input
label={capitalise(schema[field].name)}
type="number"
bind:value={record[field]} />
{:else if schema[field].type === 'string'}
<Input
label={capitalise(schema[field].name)}
bind:value={record[field]} />
{:else if schema[field].type === 'attachment'}
<div>
<Label extraSmall grey>{schema[field].name}</Label>
<Dropzone bind:files={record[field]} />
</div>
{:else if schema[field].type === 'link'}
<LinkedRecordSelector
secondary
bind:linkedRecords={record[field]}
schema={schema[field]} />
{/if}
{/each}
<div class="buttons">
<Button primary on:click={save} green={saved}>
{#if saved}Success{:else}{buttonText || 'Submit Form'}{/if}
</Button>
</div>
</div>
</form>
<style>
.form {
display: flex;
flex-direction: column;
align-items: center;
}
.form-content {
margin-bottom: var(--spacing-xl);
display: grid;
gap: var(--spacing-xl);
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
}
</style>
<Form {_bb} {model} {title} {buttonText} wide={false} />

View file

@ -1,252 +1,10 @@
<script>
import { onMount } from "svelte"
import { fade } from "svelte/transition"
import { Label, DatePicker } from "@budibase/bbui"
import debounce from "lodash.debounce"
import Form from "./Form.svelte"
export let _bb
export let model
export let title
export let buttonText
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 schema = {}
let modelDef = {}
let saved = false
let recordId
let isNew = true
let errors = {}
$: if (model && model.length !== 0) {
fetchModel()
}
$: fields = schema ? Object.keys(schema) : []
$: errorMessages = Object.entries(errors).map(
([field, message]) => `${field} ${message}`
)
async function fetchModel() {
const FETCH_MODEL_URL = `/api/models/${model}`
const response = await _bb.api.get(FETCH_MODEL_URL)
modelDef = await response.json()
schema = modelDef.schema
record = {
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
}, 1000)
}
if (response.status === 400) {
errors = json.errors
}
})
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)
const json = await response.json()
record = json
})
</script>
<form class="form" on:submit|preventDefault>
{#if title}
<h1>{title}</h1>
{/if}
{#each errorMessages as error}
<p class="error">{error}</p>
{/each}
<hr />
<div class="form-content">
{#each fields as field}
<div class="form-item">
<Label small forAttr={'form-stacked-text'}>{field}</Label>
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
<select bind:value={record[field]}>
{#each schema[field].constraints.inclusion as opt}
<option>{opt}</option>
{/each}
</select>
{:else if schema[field].type === 'datetime'}
<DatePicker bind:value={record[field]} />
{:else if schema[field].type === 'boolean'}
<input class="input" type="checkbox" bind:checked={record[field]} />
{:else if schema[field].type === 'number'}
<input class="input" type="number" bind:value={record[field]} />
{:else if schema[field].type === 'string'}
<input class="input" type="text" bind:value={record[field]} />
{/if}
</div>
<hr />
{/each}
<div class="button-block">
<button on:click={save} class:saved>
{#if saved}
<div in:fade>
<span class:saved>Success</span>
</div>
{:else}
<div>{buttonText || 'Submit Form'}</div>
{/if}
</button>
</div>
</div>
</form>
<style>
.form {
display: flex;
flex-direction: column;
justify-content: center;
margin: auto;
padding: 40px;
}
.form-content {
margin-bottom: 20px;
}
.input {
border-radius: 5px;
border: 1px solid #e6e6e6;
padding: 1em;
font-size: 16px;
}
.form-item {
display: grid;
grid-template-columns: 30% 1fr;
margin-bottom: 16px;
align-items: center;
gap: 1em;
}
hr {
border: 1px solid #f5f5f5;
margin: 40px 0px;
}
hr:nth-last-child(2) {
border: 1px solid #f5f5f5;
margin: 40px 0px;
}
.button-block {
display: flex;
justify-content: flex-end;
}
button {
font-size: 16px;
padding: 8px 16px;
box-sizing: border-box;
border-radius: 4px;
color: white;
background-color: black;
outline: none;
height: 40px;
cursor: pointer;
transition: all 0.2s ease 0s;
overflow: hidden;
outline: none;
user-select: none;
white-space: nowrap;
text-align: center;
}
button.saved {
background-color: #84c991;
border: none;
}
button:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
input[type="checkbox"] {
transform: scale(2);
cursor: pointer;
}
select::-ms-expand {
display: none;
}
select {
cursor: pointer;
display: inline-block;
align-items: baseline;
box-sizing: border-box;
padding: 1em 1em;
border: 1px solid #eaeaea;
border-radius: 5px;
font: inherit;
line-height: inherit;
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
appearance: none;
background-repeat: no-repeat;
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
linear-gradient(135deg, currentColor 50%, transparent 50%);
background-position: right 17px top 1.5em, right 10px top 1.5em;
background-size: 7px 7px, 7px 7px;
}
.error {
color: red;
font-weight: 500;
}
</style>
<Form {_bb} {model} {title} {buttonText} wide={true} />

View file

@ -0,0 +1,197 @@
<script>
import { onMount } from "svelte"
import { fade } from "svelte/transition"
import {
Label,
DatePicker,
Input,
Select,
Button,
Toggle,
} from "@budibase/bbui"
import Dropzone from "./attachments/Dropzone.svelte"
import LinkedRecordSelector from "./LinkedRecordSelector.svelte"
import debounce from "lodash.debounce"
import ErrorsBox from "./ErrorsBox.svelte"
import { capitalise } from "./helpers"
export let _bb
export let model
export let title
export let buttonText
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 schema = {}
let modelDef = {}
let saved = false
let recordId
let isNew = true
let errors = {}
$: 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>
<form class="form" on:submit|preventDefault>
{#if title}
<h1>{title}</h1>
{/if}
<div class="form-content">
<ErrorsBox {errors} />
{#each fields as field}
<div class="form-field" class:wide>
{#if !(schema[field].type === 'boolean' && !wide)}
<Label extraSmall={!wide} grey={!wide}>
{capitalise(schema[field].name)}
</Label>
{/if}
{#if schema[field].type === 'options'}
<Select secondary bind:value={record[field]}>
<option value="">Choose an option</option>
{#each schema[field].constraints.inclusion as opt}
<option>{opt}</option>
{/each}
</Select>
{:else if schema[field].type === 'datetime'}
<DatePicker bind:value={record[field]} />
{:else if schema[field].type === 'boolean'}
<Toggle
text={wide ? null : capitalise(schema[field].name)}
bind:checked={record[field]} />
{:else if schema[field].type === 'number'}
<Input type="number" bind:value={record[field]} />
{:else if schema[field].type === 'string'}
<Input bind:value={record[field]} />
{:else if schema[field].type === 'attachment'}
<Dropzone bind:files={record[field]} />
{:else if schema[field].type === 'link'}
<LinkedRecordSelector
secondary
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>
</form>
<style>
.form {
display: flex;
flex-direction: column;
align-items: center;
}
.form-content {
margin-bottom: var(--spacing-xl);
display: grid;
gap: var(--spacing-xl);
width: 100%;
}
.form-field {
display: grid;
}
.form-field.wide {
align-items: center;
grid-template-columns: 30% 1fr;
gap: var(--spacing-xl);
}
.form-field.wide :global(label) {
margin-bottom: 0;
}
.buttons {
display: flex;
justify-content: flex-end;
}
</style>