Merge branch 'linked-records' of github.com:Budibase/budibase into linked-records
This commit is contained in:
commit
4ccd76fb5b
|
@ -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")
|
||||
})
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
|
@ -1,10 +0,0 @@
|
|||
<div id="modal-container" />
|
||||
|
||||
<style>
|
||||
#modal-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
</style>
|
|
@ -1 +0,0 @@
|
|||
export const ContextKey = "budibase-modal"
|
|
@ -1,3 +0,0 @@
|
|||
export { default as Modal } from "./Modal.svelte"
|
||||
export { default as ModalContainer } from "./ModalContainer.svelte"
|
||||
export { ContextKey } from "./context"
|
|
@ -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%;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
align-items: baseline;
|
||||
}
|
||||
|
||||
.root :global(.relative) {
|
||||
.root :global(> div) {
|
||||
flex: 1;
|
||||
margin-left: var(--spacing-l);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -10,8 +10,6 @@
|
|||
export let onStyleChanged = () => {}
|
||||
export let open = false
|
||||
|
||||
$: console.log(properties)
|
||||
|
||||
$: style = componentInstance["_styles"][styleCategory] || {}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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)} />
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -304,6 +304,16 @@ function coerceRecordValues(rec, model) {
|
|||
}
|
||||
|
||||
const TYPE_TRANSFORM_MAP = {
|
||||
link: {
|
||||
"": [],
|
||||
[null]: [],
|
||||
[undefined]: undefined,
|
||||
},
|
||||
options: {
|
||||
"": "",
|
||||
[null]: "",
|
||||
[undefined]: undefined,
|
||||
},
|
||||
string: {
|
||||
"": "",
|
||||
[null]: "",
|
||||
|
|
|
@ -172,10 +172,10 @@
|
|||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@budibase/client@^0.1.24":
|
||||
version "0.1.24"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.24.tgz#d2967c050af9f559791e0189137f80e621ea2d69"
|
||||
integrity sha512-2Plu9PpF3TOPTDkAAIkPFEjZFolGkty0Sc0vbLk8lee4yqeonBj5paXT44O6kpxLFW47YjN5VCA4+EnwGl358w==
|
||||
"@budibase/client@^0.1.25":
|
||||
version "0.1.25"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.25.tgz#f08c4a614f9018eb0f0faa6d20bb05f7a3215c70"
|
||||
integrity sha512-vZ0cqJwLYcs7MHihFnJO3qOe7qxibnB4Va1+IYNfnPc9kcxy4KvfQxCx/G/DDxP9CXfEvsguy9ymzR3RUAvBHw==
|
||||
dependencies:
|
||||
deep-equal "^2.0.1"
|
||||
mustache "^4.0.1"
|
||||
|
@ -2042,9 +2042,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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue