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

This commit is contained in:
mike12345567 2020-10-05 09:45:38 +01:00
commit 0db876ad53
65 changed files with 1055 additions and 1619 deletions

View File

@ -79,7 +79,6 @@
"shortid": "^2.2.15", "shortid": "^2.2.15",
"svelte-loading-spinners": "^0.1.1", "svelte-loading-spinners": "^0.1.1",
"svelte-portal": "^0.1.0", "svelte-portal": "^0.1.0",
"svelte-simple-modal": "^0.4.2",
"yup": "^0.29.2" "yup": "^0.29.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,32 +1,23 @@
<script> <script>
import Modal from "svelte-simple-modal" import { onMount } from "svelte"
import { notifier } from "builderStore/store/notifications" import { automationStore } from "builderStore"
import { onMount, getContext } from "svelte"
import { backendUiStore, automationStore } from "builderStore"
import CreateAutomationModal from "./CreateAutomationModal.svelte" import CreateAutomationModal from "./CreateAutomationModal.svelte"
import { Button } from "@budibase/bbui" import { Button } from "@budibase/bbui"
import { Modal } from "components/common/Modal"
const { open, close } = getContext("simple-modal") let modal
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id $: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
function newAutomation() {
open(
CreateAutomationModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
onMount(() => { onMount(() => {
automationStore.actions.fetch() automationStore.actions.fetch()
}) })
</script> </script>
<section> <section>
<Button primary wide on:click={newAutomation}>Create New Automation</Button> <Button primary wide on:click={() => modal.show()}>
Create New Automation
</Button>
<ul> <ul>
{#each $automationStore.automations as automation} {#each $automationStore.automations as automation}
<li <li
@ -39,6 +30,9 @@
{/each} {/each}
</ul> </ul>
</section> </section>
<Modal bind:this={modal}>
<CreateAutomationModal />
</Modal>
<style> <style>
section { section {

View File

@ -1,11 +1,9 @@
<script> <script>
import { store, backendUiStore, automationStore } from "builderStore" import { store, backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import ActionButton from "components/common/ActionButton.svelte"
import { Input } from "@budibase/bbui" import { Input } from "@budibase/bbui"
import analytics from "analytics" import analytics from "analytics"
import { ModalTitle, ModalFooter } from "components/common/Modal"
export let onClosed
let name let name
@ -13,71 +11,38 @@
$: instanceId = $backendUiStore.selectedDatabase._id $: instanceId = $backendUiStore.selectedDatabase._id
$: appId = $store.appId $: appId = $store.appId
function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
}
async function createAutomation() { async function createAutomation() {
await automationStore.actions.create({ await automationStore.actions.create({
name, name,
instanceId, instanceId,
}) })
onClosed()
notifier.success(`Automation ${name} created.`) notifier.success(`Automation ${name} created.`)
analytics.captureEvent("Automation Created", { name }) analytics.captureEvent("Automation Created", { name })
} }
</script> </script>
<div class="container"> <ModalTitle>Create Automation</ModalTitle>
<header> <Input bind:value={name} label="Name" />
<i class="ri-stackshare-line" /> <ModalFooter
<h3>Create Automation</h3> confirmText="Create"
</header> onConfirm={createAutomation}
<div class="content"> disabled={!valid}>
<Input bind:value={name} label="Name" /> <a
</div> target="_blank"
<footer> href="https://docs.budibase.com/automate/introduction-to-automate">
<a href="https://docs.budibase.com"> <i class="ri-information-line" />
<i class="ri-information-line" /> <span>Learn about automations</span>
<span>Learn about automations</span> </a>
</a> </ModalFooter>
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
<ActionButton disabled={!valid} on:click={createAutomation}>
Save
</ActionButton>
</footer>
</div>
<style> <style>
.container { a {
padding: var(--spacing-xl);
}
header {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
header h3 {
font-size: var(--font-size-xl);
color: var(--ink);
font-weight: 600;
margin: 0;
}
header i {
margin-right: var(--spacing-m);
font-size: 28px;
color: var(--grey-6);
}
.content {
padding: var(--spacing-xl) 0;
}
footer {
display: grid;
grid-auto-flow: column;
grid-gap: var(--spacing-m);
grid-auto-columns: 3fr 1fr 1fr;
}
footer a {
color: var(--ink); color: var(--ink);
font-size: 14px; font-size: 14px;
vertical-align: middle; vertical-align: middle;
@ -85,10 +50,10 @@
align-items: center; align-items: center;
text-decoration: none; text-decoration: none;
} }
footer a span { a span {
text-decoration: underline; text-decoration: underline;
} }
footer i { i {
font-size: 20px; font-size: 20px;
margin-right: var(--spacing-m); margin-right: var(--spacing-m);
text-decoration: none; text-decoration: none;

View File

@ -13,6 +13,7 @@
$: allowDeleteBlock = $: allowDeleteBlock =
$automationStore.selectedBlock?.type !== "TRIGGER" || $automationStore.selectedBlock?.type !== "TRIGGER" ||
!automation?.definition?.steps?.length !automation?.definition?.steps?.length
$: name = automation?.name ?? ""
function deleteAutomationBlock() { function deleteAutomationBlock() {
automationStore.actions.deleteAutomationBlock( automationStore.actions.deleteAutomationBlock(
@ -101,7 +102,7 @@
<ConfirmDialog <ConfirmDialog
bind:this={confirmDeleteDialog} bind:this={confirmDeleteDialog}
title="Confirm Delete" title="Confirm Delete"
body={`Are you sure you wish to delete the automation '${automation.name}'?`} body={`Are you sure you wish to delete the automation '${name}'?`}
okText="Delete Automation" okText="Delete Automation"
onOk={deleteAutomation} /> onOk={deleteAutomation} />

View File

@ -1,124 +0,0 @@
<script>
import { onMount } from "svelte"
import { fade } from "svelte/transition"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
export let ids = []
export let field
let records = []
let open = false
let model
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel.name]
async function fetchRecords() {
const response = await api.post("/api/records/search", {
keys: ids,
})
const modelResponse = await api.get(`/api/models/${field.modelId}`)
records = await response.json()
model = await modelResponse.json()
}
$: ids && fetchRecords()
function toggleOpen() {
open = !open
}
onMount(() => {
fetchRecords()
})
</script>
<section>
<a on:click={toggleOpen}>{records.length}</a>
{#if open}
<div class="popover" transition:fade>
<header>
<h3>{field.name}</h3>
<i class="ri-close-circle-fill" on:click={toggleOpen} />
</header>
{#each records as record}
<div class="linked-record">
<div class="fields">
{#each Object.keys(model.schema).filter(key => !FIELDS_TO_HIDE.includes(key)) as key}
<div class="field">
<span>{model.schema[key].name}</span>
<p>{record[key]}</p>
</div>
{/each}
</div>
</div>
{/each}
</div>
{/if}
</section>
<style>
section {
display: relative;
}
header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
i {
font-size: 24px;
color: var(--ink-lighter);
}
i:hover {
cursor: pointer;
}
a {
font-size: 14px;
}
.popover {
width: 500px;
position: absolute;
right: 15%;
padding: 20px;
background: var(--grey-1);
border: 1px solid var(--grey);
}
h3 {
font-size: 20px;
font-weight: bold;
margin: 0;
}
.fields {
padding: 15px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 20px;
background: var(--white);
border: 1px solid var(--grey);
border-radius: 5px;
margin-bottom: 8px;
}
.field span {
color: var(--ink-lighter);
font-size: 12px;
}
.field p {
color: var(--ink);
font-size: 14px;
word-break: break-word;
font-weight: 500;
margin-top: 4px;
}
</style>

View File

@ -1,8 +1,9 @@
<script> <script>
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import RowPopover from "./popovers/Row.svelte" import CreateRowButton from "./buttons/CreateRowButton.svelte"
import ColumnPopover from "./popovers/Column.svelte" import CreateColumnButton from "./buttons/CreateColumnButton.svelte"
import ViewPopover from "./popovers/View.svelte" import CreateViewButton from "./buttons/CreateViewButton.svelte"
import ExportButton from "./buttons/ExportButton.svelte"
import * as api from "./api" import * as api from "./api"
import Table from "./Table.svelte" import Table from "./Table.svelte"
@ -10,6 +11,10 @@
$: title = $backendUiStore.selectedModel.name $: title = $backendUiStore.selectedModel.name
$: schema = $backendUiStore.selectedModel.schema $: schema = $backendUiStore.selectedModel.schema
$: modelView = {
schema,
name: $backendUiStore.selectedView.name,
}
// Fetch records for specified model // Fetch records for specified model
$: { $: {
@ -22,9 +27,10 @@
</script> </script>
<Table {title} {schema} {data} allowEditing={true}> <Table {title} {schema} {data} allowEditing={true}>
<ColumnPopover /> <CreateColumnButton />
{#if Object.keys(schema).length > 0} {#if Object.keys(schema).length > 0}
<RowPopover /> <CreateRowButton />
<ViewPopover /> <CreateViewButton />
<ExportButton view={modelView} />
{/if} {/if}
</Table> </Table>

View File

@ -1,7 +1,7 @@
<script> <script>
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui" import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
import Dropzone from "components/common/Dropzone.svelte" import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "../../../../helpers" import { capitalise } from "../../../helpers"
export let meta export let meta
export let value = meta.type === "boolean" ? false : "" export let value = meta.type === "boolean" ? false : ""

View File

@ -9,13 +9,13 @@
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import AttachmentList from "./AttachmentList.svelte" import AttachmentList from "./AttachmentList.svelte"
import TablePagination from "./TablePagination.svelte" import TablePagination from "./TablePagination.svelte"
import CreateEditRecordModal from "./popovers/CreateEditRecord.svelte" import CreateEditRecordModal from "./modals/CreateEditRecordModal.svelte"
import RowPopover from "./popovers/Row.svelte" import RowPopover from "./buttons/CreateRowButton.svelte"
import ColumnPopover from "./popovers/Column.svelte" import ColumnPopover from "./buttons/CreateColumnButton.svelte"
import ViewPopover from "./popovers/View.svelte" import ViewPopover from "./buttons/CreateViewButton.svelte"
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte" import ColumnHeaderPopover from "./popovers/ColumnPopover.svelte"
import EditRowPopover from "./popovers/EditRow.svelte" import EditRowPopover from "./popovers/RowPopover.svelte"
import CalculationPopover from "./popovers/Calculate.svelte" import CalculationPopover from "./buttons/CalculateButton.svelte"
const ITEMS_PER_PAGE = 10 const ITEMS_PER_PAGE = 10
@ -32,9 +32,9 @@
$: paginatedData = $: paginatedData =
sorted && sorted.length sorted && sorted.length
? sorted.slice( ? sorted.slice(
currentPage * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
) )
: [] : []
$: modelId = data?.length ? data[0].modelId : null $: modelId = data?.length ? data[0].modelId : null
@ -42,7 +42,9 @@
if (!record?.[fieldName]?.length) { if (!record?.[fieldName]?.length) {
return return
} }
$goto(`/${$params.application}/backend/model/${modelId}/relationship/${record._id}/${fieldName}`) $goto(
`/${$params.application}/backend/model/${modelId}/relationship/${record._id}/${fieldName}`
)
} }
</script> </script>
@ -50,68 +52,68 @@
<div class="table-controls"> <div class="table-controls">
<h2 class="title">{title}</h2> <h2 class="title">{title}</h2>
<div class="popovers"> <div class="popovers">
<slot/> <slot />
</div> </div>
</div> </div>
<table class="bb-table"> <table class="bb-table">
<thead> <thead>
<tr>
{#if allowEditing}
<th class="edit-header">
<div>Edit</div>
</th>
{/if}
{#each columns as header}
<th>
{#if allowEditing}
<ColumnHeaderPopover field={schema[header]}/>
{:else}
<div class="header">{header}</div>
{/if}
</th>
{/each}
</tr>
</thead>
<tbody>
{#if paginatedData.length === 0}
{#if allowEditing}
<td class="no-border">No data.</td>
{/if}
{#each columns as header, idx}
<td class="no-border">
{#if idx === 0}No data.{/if}
</td>
{/each}
{/if}
{#each paginatedData as row}
<tr> <tr>
{#if allowEditing} {#if allowEditing}
<td> <th class="edit-header">
<EditRowPopover {row}/> <div>Edit</div>
</td> </th>
{/if} {/if}
{#each columns as header} {#each columns as header}
<td> <th>
{#if schema[header].type === 'link'} {#if allowEditing}
<div <ColumnHeaderPopover field={schema[header]} />
class:link={row[header] && row[header].length} {:else}
on:click={() => selectRelationship(row, header)}> <div class="header">{header}</div>
{row[header] ? row[header].length : 0} linked row(s) {/if}
</div> </th>
{:else if schema[header].type === 'attachment'}
<AttachmentList files={row[header] || []}/>
{:else}{getOr('', header, row)}{/if}
</td>
{/each} {/each}
</tr> </tr>
{/each} </thead>
<tbody>
{#if paginatedData.length === 0}
{#if allowEditing}
<td class="no-border">No data.</td>
{/if}
{#each columns as header, idx}
<td class="no-border">
{#if idx === 0 && !allowEditing}No data.{/if}
</td>
{/each}
{/if}
{#each paginatedData as row}
<tr>
{#if allowEditing}
<td>
<EditRowPopover {row} />
</td>
{/if}
{#each columns as header}
<td>
{#if schema[header].type === 'link'}
<div
class:link={row[header] && row[header].length}
on:click={() => selectRelationship(row, header)}>
{row[header] ? row[header].length : 0} related row(s)
</div>
{:else if schema[header].type === 'attachment'}
<AttachmentList files={row[header] || []} />
{:else}{getOr('', header, row)}{/if}
</td>
{/each}
</tr>
{/each}
</tbody> </tbody>
</table> </table>
<TablePagination <TablePagination
{data} {data}
bind:currentPage bind:currentPage
pageItemCount={paginatedData.length} pageItemCount={paginatedData.length}
{ITEMS_PER_PAGE}/> {ITEMS_PER_PAGE} />
</section> </section>
<style> <style>

View File

@ -1,9 +1,10 @@
<script> <script>
import api from "builderStore/api" import api from "builderStore/api"
import Table from "./Table.svelte" import Table from "./Table.svelte"
import CalculationPopover from "./popovers/Calculate.svelte" import CalculateButton from "./buttons/CalculateButton.svelte"
import GroupByPopover from "./popovers/GroupBy.svelte" import GroupByButton from "./buttons/GroupByButton.svelte"
import FilterPopover from "./popovers/Filter.svelte" import FilterButton from "./buttons/FilterButton.svelte"
import ExportButton from "./buttons/ExportButton.svelte"
export let view = {} export let view = {}
@ -34,9 +35,10 @@
</script> </script>
<Table title={decodeURI(name)} schema={view.schema} {data}> <Table title={decodeURI(name)} schema={view.schema} {data}>
<FilterPopover {view} /> <FilterButton {view} />
<CalculationPopover {view} /> <CalculateButton {view} />
{#if view.calculation} {#if view.calculation}
<GroupByPopover {view} /> <GroupByButton {view} />
{/if} {/if}
<ExportButton {view} />
</Table> </Table>

View File

@ -0,0 +1,19 @@
<script>
import { Popover, TextButton, Icon } from "@budibase/bbui"
import CalculatePopover from "../popovers/CalculatePopover.svelte"
export let view = {}
let anchor
let dropdown
</script>
<div bind:this={anchor}>
<TextButton text small on:click={dropdown.show} active={!!view.field}>
<Icon name="calculate" />
Calculate
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<CalculatePopover {view} onClosed={dropdown.hide} />
</Popover>

View File

@ -1,14 +1,6 @@
<script> <script>
import { backendUiStore } from "builderStore" import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui"
import { import CreateEditColumnPopover from "../popovers/CreateEditColumnPopover.svelte"
DropdownMenu,
TextButton as Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import CreateEditColumn from "./CreateEditColumn.svelte"
let anchor let anchor
let dropdown let dropdown
@ -23,7 +15,7 @@
</div> </div>
<DropdownMenu bind:this={dropdown} {anchor} align="left"> <DropdownMenu bind:this={dropdown} {anchor} align="left">
<h5>Create Column</h5> <h5>Create Column</h5>
<CreateEditColumn onClosed={dropdown.hide} /> <CreateEditColumnPopover onClosed={dropdown.hide} />
</DropdownMenu> </DropdownMenu>
<style> <style>

View File

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

View File

@ -0,0 +1,17 @@
<script>
import { Popover, TextButton, Icon } from "@budibase/bbui"
import CreateViewPopover from "../popovers/CreateViewPopover.svelte"
let anchor
let dropdown
</script>
<div bind:this={anchor}>
<TextButton text small on:click={dropdown.show}>
<Icon name="view" />
Create New View
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<CreateViewPopover onClosed={dropdown.hide} />
</Popover>

View File

@ -0,0 +1,20 @@
<script>
import { TextButton, Icon, Popover } from "@budibase/bbui"
import api from "builderStore/api"
import ExportPopover from "../popovers/ExportPopover.svelte"
export let view
let anchor
let dropdown
</script>
<div bind:this={anchor}>
<TextButton text small on:click={dropdown.show}>
<Icon name="download" />
Export
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<ExportPopover {view} onClosed={dropdown.hide} />
</Popover>

View File

@ -0,0 +1,23 @@
<script>
import { Popover, TextButton, Icon } from "@budibase/bbui"
import FilterPopover from "../popovers/FilterPopover.svelte"
export let view = {}
let anchor
let dropdown
</script>
<div bind:this={anchor}>
<TextButton
text
small
on:click={dropdown.show}
active={view.filters && view.filters.length}>
<Icon name="filter" />
Filter
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<FilterPopover {view} onClosed={dropdown.hide} />
</Popover>

View File

@ -0,0 +1,19 @@
<script>
import { Popover, TextButton, Icon } from "@budibase/bbui"
import GroupByPopover from "../popovers/GroupByPopover.svelte"
export let view = {}
let anchor
let dropdown
</script>
<div bind:this={anchor}>
<TextButton text small active={!!view.groupBy} on:click={dropdown.show}>
<Icon name="group" />
Group By
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<GroupByPopover {view} onClosed={dropdown.hide} />
</Popover>

View File

@ -1 +0,0 @@
export { default } from "./ModelDataTable.svelte"

View File

@ -0,0 +1,50 @@
<script>
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import RecordFieldControl from "../RecordFieldControl.svelte"
import * as api from "../api"
import { ModalTitle, ModalFooter } from "components/common/Modal"
import ErrorsBox from "components/common/ErrorsBox.svelte"
export let record = {}
export let visible = false
let modal
let errors = []
$: creating = record?._id == null
$: model = record.modelId
? $backendUiStore.models.find(model => model._id === record?.modelId)
: $backendUiStore.selectedModel
$: modelSchema = Object.entries(model?.schema ?? {})
async function saveRecord() {
const recordResponse = await api.saveRecord(
{ ...record, modelId: model._id },
model._id
)
if (recordResponse.errors) {
errors = Object.keys(recordResponse.errors)
.map(k => ({ dataPath: k, message: recordResponse.errors[k] }))
.flat()
// Prevent modal closing if there were errors
return false
}
notifier.success("Record saved successfully.")
backendUiStore.actions.records.save(recordResponse)
}
</script>
<ModalTitle>{creating ? 'Create Row' : 'Edit Row'}</ModalTitle>
<ErrorsBox {errors} />
{#each modelSchema as [key, meta]}
<div>
{#if meta.type === 'link'}
<LinkedRecordSelector bind:linkedRecords={record[key]} schema={meta} />
{:else}
<RecordFieldControl {meta} bind:value={record[key]} />
{/if}
</div>
{/each}
<ModalFooter confirmText={creating ? 'Add' : 'Save'} onConfirm={saveRecord} />

View File

@ -1,104 +0,0 @@
<script>
import {
Popover,
TextButton,
Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import CreateEditRecord from "./CreateEditRecord.svelte"
import analytics from "analytics"
const CALCULATIONS = [
{
name: "Statistics",
key: "stats",
},
]
export let view = {}
let anchor
let dropdown
$: viewModel = $backendUiStore.models.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId
)
$: fields =
viewModel &&
Object.keys(viewModel.schema).filter(
field => viewModel.schema[field].type === "number"
)
function saveView() {
backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`)
analytics.captureEvent("Added View Calculate", { field: view.field })
dropdown.hide()
}
</script>
<div bind:this={anchor}>
<TextButton text small on:click={dropdown.show} active={!!view.field}>
<Icon name="calculate" />
Calculate
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<div class="actions">
<h5>Calculate</h5>
<div class="input-group-row">
<p>The</p>
<Select secondary thin bind:value={view.calculation}>
<option value="">Choose an option</option>
{#each CALCULATIONS as calculation}
<option value={calculation.key}>{calculation.name}</option>
{/each}
</Select>
<p>of</p>
<Select secondary thin bind:value={view.field}>
<option value="">Choose an option</option>
{#each fields as field}
<option value={field}>{field}</option>
{/each}
</Select>
</div>
<div class="footer">
<Button secondary on:click={dropdown.hide}>Cancel</Button>
<Button primary on:click={saveView}>Save</Button>
</div>
</div>
</Popover>
<style>
.actions {
display: grid;
grid-gap: var(--spacing-xl);
}
h5 {
margin: 0;
font-weight: 500;
}
.footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
.input-group-row {
display: grid;
grid-template-columns: 30px 1fr 20px 1fr;
gap: var(--spacing-s);
align-items: center;
}
p {
margin: 0;
font-size: var(--font-size-xs);
}
</style>

View File

@ -0,0 +1,86 @@
<script>
import { Button, Input, Select } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import analytics from "analytics"
const CALCULATIONS = [
{
name: "Statistics",
key: "stats",
},
]
export let view = {}
export let onClosed
$: viewModel = $backendUiStore.models.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId
)
$: fields =
viewModel &&
Object.keys(viewModel.schema).filter(
field => viewModel.schema[field].type === "number"
)
function saveView() {
backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`)
onClosed()
analytics.captureEvent("Added View Calculate", { field: view.field })
}
</script>
<div class="actions">
<h5>Calculate</h5>
<div class="input-group-row">
<p>The</p>
<Select secondary thin bind:value={view.calculation}>
<option value="">Choose an option</option>
{#each CALCULATIONS as calculation}
<option value={calculation.key}>{calculation.name}</option>
{/each}
</Select>
<p>of</p>
<Select secondary thin bind:value={view.field}>
<option value="">Choose an option</option>
{#each fields as field}
<option value={field}>{field}</option>
{/each}
</Select>
</div>
<div class="footer">
<Button secondary on:click={onClosed}>Cancel</Button>
<Button primary on:click={saveView}>Save</Button>
</div>
</div>
<style>
.actions {
display: grid;
grid-gap: var(--spacing-xl);
}
h5 {
margin: 0;
font-weight: 500;
}
.footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
.input-group-row {
display: grid;
grid-template-columns: 30px 1fr 20px 1fr;
gap: var(--spacing-s);
align-items: center;
}
p {
margin: 0;
font-size: var(--font-size-xs);
}
</style>

View File

@ -2,7 +2,7 @@
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import CreateEditColumnModal from "./CreateEditColumn.svelte" import CreateEditColumnPopover from "./CreateEditColumnPopover.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { notifier } from "../../../../builderStore/store/notifications" import { notifier } from "../../../../builderStore/store/notifications"
@ -26,6 +26,11 @@
editing = false editing = false
} }
function showDelete() {
dropdown.hide()
confirmDeleteDialog.show()
}
function deleteColumn() { function deleteColumn() {
if (field.name === $backendUiStore.selectedModel.primaryDisplay) { if (field.name === $backendUiStore.selectedModel.primaryDisplay) {
notifier.danger("You cannot delete the primary display column") notifier.danger("You cannot delete the primary display column")
@ -52,7 +57,7 @@
<DropdownMenu bind:this={dropdown} {anchor} align="left"> <DropdownMenu bind:this={dropdown} {anchor} align="left">
{#if editing} {#if editing}
<h5>Edit Column</h5> <h5>Edit Column</h5>
<CreateEditColumnModal onClosed={hideEditor} {field} /> <CreateEditColumnPopover onClosed={hideEditor} {field} />
{:else} {:else}
<ul> <ul>
{#if type !== 'link'} {#if type !== 'link'}
@ -61,9 +66,7 @@
Edit Edit
</li> </li>
{/if} {/if}
<li <li data-cy="delete-column-header" on:click={showDelete}>
data-cy="delete-column-header"
on:click={() => confirmDeleteDialog.show()}>
<Icon name="delete" /> <Icon name="delete" />
Delete Delete
</li> </li>

View File

@ -1,81 +0,0 @@
<script>
import { onMount, tick } from "svelte"
import { store, backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { compose, map, get, flatten } from "lodash/fp"
import { Input, TextArea, Button } from "@budibase/bbui"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import RecordFieldControl from "./RecordFieldControl.svelte"
import * as api from "../api"
import ErrorsBox from "components/common/ErrorsBox.svelte"
export let record = {}
export let onClosed
let errors = []
$: model = record.modelId
? $backendUiStore.models.find(model => model._id === record?.modelId)
: $backendUiStore.selectedModel
$: modelSchema = Object.entries(model?.schema ?? {})
async function saveRecord() {
const recordResponse = await api.saveRecord(
{
...record,
modelId: model._id,
},
model._id
)
if (recordResponse.errors) {
errors = Object.keys(recordResponse.errors)
.map(k => ({ dataPath: k, message: recordResponse.errors[k] }))
.flat()
return
}
onClosed()
notifier.success("Record saved successfully.")
backendUiStore.actions.records.save(recordResponse)
}
</script>
<div class="actions">
<ErrorsBox {errors} />
<form on:submit|preventDefault>
{#each modelSchema as [key, meta]}
<div>
{#if meta.type === 'link'}
<LinkedRecordSelector
bind:linkedRecords={record[key]}
schema={meta} />
{:else}
<RecordFieldControl {meta} bind:value={record[key]} />
{/if}
</div>
{/each}
</form>
<footer>
<Button secondary on:click={onClosed}>Cancel</Button>
<Button primary on:click={saveRecord}>Save</Button>
</footer>
</div>
<style>
.actions {
padding: var(--spacing-xl);
display: grid;
grid-gap: var(--spacing-xl);
min-width: 400px;
}
form {
display: grid;
grid-gap: var(--spacing-xl);
}
footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
</style>

View File

@ -1,20 +1,11 @@
<script> <script>
import { import { Button, Input, Select } from "@budibase/bbui"
Popover,
TextButton,
Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import CreateEditRecord from "./CreateEditRecord.svelte"
import analytics from "analytics" import analytics from "analytics"
let anchor export let onClosed
let dropdown
let name let name
let field let field
@ -37,28 +28,20 @@
field, field,
}) })
notifier.success(`View ${name} created`) notifier.success(`View ${name} created`)
dropdown.hide() onClosed()
analytics.captureEvent("View Created", { name }) analytics.captureEvent("View Created", { name })
$goto(`../../../view/${name}`) $goto(`../../../view/${name}`)
} }
</script> </script>
<div bind:this={anchor}> <div class="actions">
<TextButton text small on:click={dropdown.show}> <h5>Create View</h5>
<Icon name="view" /> <Input label="View Name" thin bind:value={name} />
Create New View <div class="footer">
</TextButton> <Button secondary on:click={onClosed}>Cancel</Button>
</div> <Button primary on:click={saveView}>Save View</Button>
<Popover bind:this={dropdown} {anchor} align="left">
<div class="actions">
<h5>Create View</h5>
<Input label="View Name" thin bind:value={name} />
<div class="footer">
<Button secondary on:click={dropdown.hide}>Cancel</Button>
<Button primary on:click={saveView}>Save View</Button>
</div>
</div> </div>
</Popover> </div>
<style> <style>
h5 { h5 {

View File

@ -0,0 +1,61 @@
<script>
import api from "builderStore/api"
import { Button, Select } from "@budibase/bbui"
const FORMATS = [
{
name: "CSV",
key: "csv",
},
{
name: "JSON",
key: "json",
},
]
export let view
export let onClosed
let exportFormat = FORMATS[0].key
async function exportView() {
const response = await api.post(
`/api/views/export?format=${exportFormat}`,
view
)
const downloadInfo = await response.json()
onClosed()
window.location = downloadInfo.url
}
</script>
<div class="popover">
<h5>Export Data</h5>
<Select label="Format" secondary thin bind:value={exportFormat}>
{#each FORMATS as format}
<option value={format.key}>{format.name}</option>
{/each}
</Select>
<div class="footer">
<Button secondary on:click={onClosed}>Cancel</Button>
<Button primary on:click={exportView}>Export</Button>
</div>
</div>
<style>
.popover {
display: grid;
grid-gap: var(--spacing-xl);
}
h5 {
margin: 0;
font-weight: 500;
}
.footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
</style>

View File

@ -1,15 +1,7 @@
<script> <script>
import { import { Button, Input, Select } from "@budibase/bbui"
Popover,
TextButton,
Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import CreateEditRecord from "./CreateEditRecord.svelte"
import analytics from "analytics" import analytics from "analytics"
const CONDITIONS = [ const CONDITIONS = [
@ -51,9 +43,7 @@
] ]
export let view = {} export let view = {}
export let onClosed
let anchor
let dropdown
$: viewModel = $backendUiStore.models.find( $: viewModel = $backendUiStore.models.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId ({ _id }) => _id === $backendUiStore.selectedView.modelId
@ -63,7 +53,7 @@
function saveView() { function saveView() {
backendUiStore.actions.views.save(view) backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`) notifier.success(`View ${view.name} saved.`)
dropdown.hide() onClosed()
analytics.captureEvent("Added View Filter", { analytics.captureEvent("Added View Filter", {
filters: JSON.stringify(view.filters), filters: JSON.stringify(view.filters),
}) })
@ -88,67 +78,55 @@
} }
</script> </script>
<div bind:this={anchor}> <div class="actions">
<TextButton <h5>Filter</h5>
text {#if view.filters.length}
small <div class="input-group-row">
on:click={dropdown.show} {#each view.filters as filter, idx}
active={view.filters && view.filters.length}> {#if idx === 0}
<Icon name="filter" /> <p>Where</p>
Filter {:else}
</TextButton> <Select secondary thin bind:value={filter.conjunction}>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<div class="actions">
<h5>Filter</h5>
{#if view.filters.length}
<div class="input-group-row">
{#each view.filters as filter, idx}
{#if idx === 0}
<p>Where</p>
{:else}
<Select secondary thin bind:value={filter.conjunction}>
<option value="">Choose an option</option>
{#each CONJUNCTIONS as conjunction}
<option value={conjunction.key}>{conjunction.name}</option>
{/each}
</Select>
{/if}
<Select secondary thin bind:value={filter.key}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each fields as field} {#each CONJUNCTIONS as conjunction}
<option value={field}>{field}</option> <option value={conjunction.key}>{conjunction.name}</option>
{/each} {/each}
</Select> </Select>
<Select secondary thin bind:value={filter.condition}> {/if}
<Select secondary thin bind:value={filter.key}>
<option value="">Choose an option</option>
{#each fields as field}
<option value={field}>{field}</option>
{/each}
</Select>
<Select secondary thin bind:value={filter.condition}>
<option value="">Choose an option</option>
{#each CONDITIONS as condition}
<option value={condition.key}>{condition.name}</option>
{/each}
</Select>
{#if filter.key && isMultipleChoice(filter.key)}
<Select secondary thin bind:value={filter.value}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each CONDITIONS as condition} {#each viewModel.schema[filter.key].constraints.inclusion as option}
<option value={condition.key}>{condition.name}</option> <option value={option}>{option}</option>
{/each} {/each}
</Select> </Select>
{#if filter.key && isMultipleChoice(filter.key)} {:else}
<Select secondary thin bind:value={filter.value}> <Input thin placeholder="Value" bind:value={filter.value} />
<option value="">Choose an option</option> {/if}
{#each viewModel.schema[filter.key].constraints.inclusion as option} <i class="ri-close-circle-fill" on:click={() => removeFilter(idx)} />
<option value={option}>{option}</option> {/each}
{/each} </div>
</Select> {/if}
{:else} <div class="footer">
<Input thin placeholder="Value" bind:value={filter.value} /> <Button text on:click={addFilter}>Add Filter</Button>
{/if} <div class="buttons">
<i class="ri-close-circle-fill" on:click={() => removeFilter(idx)} /> <Button secondary on:click={onClosed}>Cancel</Button>
{/each} <Button primary on:click={saveView}>Save</Button>
</div>
{/if}
<div class="footer">
<Button text on:click={addFilter}>Add Filter</Button>
<div class="buttons">
<Button secondary on:click={dropdown.hide}>Cancel</Button>
<Button primary on:click={saveView}>Save</Button>
</div>
</div> </div>
</div> </div>
</Popover> </div>
<style> <style>
.actions { .actions {

View File

@ -1,91 +0,0 @@
<script>
import {
Popover,
TextButton,
Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import CreateEditRecord from "./CreateEditRecord.svelte"
const CALCULATIONS = [
{
name: "Statistics",
key: "stats",
},
]
export let view = {}
let anchor
let dropdown
$: viewModel = $backendUiStore.models.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId
)
$: fields = viewModel && Object.keys(viewModel.schema)
function saveView() {
backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`)
dropdown.hide()
}
</script>
<div bind:this={anchor}>
<TextButton text small active={!!view.groupBy} on:click={dropdown.show}>
<Icon name="group" />
Group
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<div class="actions">
<h5>Group</h5>
<div class="input-group-row">
<p>By</p>
<Select secondary thin bind:value={view.groupBy}>
<option value="">Choose an option</option>
{#each fields as field}
<option value={field}>{field}</option>
{/each}
</Select>
</div>
<div class="footer">
<Button secondary on:click={dropdown.hide}>Cancel</Button>
<Button primary on:click={saveView}>Save</Button>
</div>
</div>
</Popover>
<style>
.actions {
display: grid;
grid-gap: var(--spacing-xl);
}
h5 {
margin: 0;
font-weight: 500;
}
.footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
.input-group-row {
display: grid;
grid-template-columns: 20px 1fr;
gap: var(--spacing-s);
align-items: center;
}
p {
margin: 0;
font-size: var(--font-size-xs);
}
</style>

View File

@ -0,0 +1,66 @@
<script>
import { Button, Input, Select } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
export let view = {}
export let onClosed
$: viewModel = $backendUiStore.models.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId
)
$: fields = viewModel && Object.keys(viewModel.schema)
function saveView() {
backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`)
onClosed()
}
</script>
<div class="actions">
<h5>Group</h5>
<div class="input-group-row">
<p>By</p>
<Select secondary thin bind:value={view.groupBy}>
<option value="">Choose an option</option>
{#each fields as field}
<option value={field}>{field}</option>
{/each}
</Select>
</div>
<div class="footer">
<Button secondary on:click={onClosed}>Cancel</Button>
<Button primary on:click={saveView}>Save</Button>
</div>
</div>
<style>
.actions {
display: grid;
grid-gap: var(--spacing-xl);
}
h5 {
margin: 0;
font-weight: 500;
}
.footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
.input-group-row {
display: grid;
grid-template-columns: 20px 1fr;
gap: var(--spacing-s);
align-items: center;
}
p {
margin: 0;
font-size: var(--font-size-xs);
}
</style>

View File

@ -1,27 +0,0 @@
<script>
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui"
import CreateEditRecord from "./CreateEditRecord.svelte"
import { Modal } from "components/common/Modal"
let anchor
let dropdown
</script>
<div bind:this={anchor}>
<Button text small on:click={dropdown.show}>
<Icon name="addrow" />
Create New Row
</Button>
</div>
<Modal bind:this={dropdown}>
<h5>Add New Row</h5>
<CreateEditRecord onClosed={dropdown.hide} />
</Modal>
<style>
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
</style>

View File

@ -1,41 +1,33 @@
<script> <script>
import { getContext } from "svelte"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { import { DropdownMenu, Icon } from "@budibase/bbui"
DropdownMenu, import CreateEditRecordModal from "../modals/CreateEditRecordModal.svelte"
Button,
Icon,
Input,
Select,
Heading,
} from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import CreateEditRecordModal from "./CreateEditRecord.svelte"
import * as api from "../api" import * as api from "../api"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { Modal } from "components/common/Modal"
export let row export let row
let anchor let anchor
let dropdown let dropdown
let editing
let confirmDeleteDialog let confirmDeleteDialog
let modal
function showEditor() { function showModal() {
editing = true dropdown.hide()
modal.show()
} }
function hideEditor() { function showDelete() {
dropdown.hide() dropdown.hide()
editing = false confirmDeleteDialog.show()
} }
async function deleteRow() { async function deleteRow() {
await api.deleteRecord(row) await api.deleteRecord(row)
notifier.success("Record deleted") notifier.success("Record deleted")
backendUiStore.actions.records.delete(row) backendUiStore.actions.records.delete(row)
hideEditor()
} }
</script> </script>
@ -43,21 +35,16 @@
<i class="ri-more-line" /> <i class="ri-more-line" />
</div> </div>
<DropdownMenu bind:this={dropdown} {anchor} align="left"> <DropdownMenu bind:this={dropdown} {anchor} align="left">
{#if editing} <ul>
<h5>Edit Row</h5> <li data-cy="edit-row" on:click={showModal}>
<CreateEditRecordModal onClosed={hideEditor} record={row} /> <Icon name="edit" />
{:else} <span>Edit</span>
<ul> </li>
<li data-cy="edit-row" on:click={showEditor}> <li data-cy="delete-row" on:click={showDelete}>
<Icon name="edit" /> <Icon name="delete" />
<span>Edit</span> <span>Delete</span>
</li> </li>
<li data-cy="delete-row" on:click={() => confirmDeleteDialog.show()}> </ul>
<Icon name="delete" />
<span>Delete</span>
</li>
</ul>
{/if}
</DropdownMenu> </DropdownMenu>
<ConfirmDialog <ConfirmDialog
bind:this={confirmDeleteDialog} bind:this={confirmDeleteDialog}
@ -65,6 +52,9 @@
okText="Delete Row" okText="Delete Row"
onOk={deleteRow} onOk={deleteRow}
title="Confirm Delete" /> title="Confirm Delete" />
<Modal bind:this={modal}>
<CreateEditRecordModal record={row} />
</Modal>
<style> <style>
.ri-more-line:hover { .ri-more-line:hover {

View File

@ -1,5 +1,4 @@
<script> <script>
import { getContext } from "svelte"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
@ -24,6 +23,11 @@
editing = false editing = false
} }
function showModal() {
hideEditor()
confirmDeleteDialog.show()
}
async function deleteTable() { async function deleteTable() {
await backendUiStore.actions.models.delete(table) await backendUiStore.actions.models.delete(table)
notifier.success("Table deleted") notifier.success("Table deleted")
@ -66,7 +70,7 @@
<Icon name="edit" /> <Icon name="edit" />
Edit Edit
</li> </li>
<li data-cy="delete-table" on:click={() => confirmDeleteDialog.show()}> <li data-cy="delete-table" on:click={showModal}>
<Icon name="delete" /> <Icon name="delete" />
Delete Delete
</li> </li>
@ -78,7 +82,6 @@
body={`Are you sure you wish to delete the table '${table.name}'? Your data will be deleted and this action cannot be undone.`} body={`Are you sure you wish to delete the table '${table.name}'? Your data will be deleted and this action cannot be undone.`}
okText="Delete Table" okText="Delete Table"
onOk={deleteTable} onOk={deleteTable}
onCancel={hideEditor}
title="Confirm Delete" /> title="Confirm Delete" />
<style> <style>

View File

@ -1,6 +1,5 @@
<script> <script>
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { getContext } from "svelte"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
@ -24,6 +23,11 @@
editing = false editing = false
} }
function showDelete() {
dropdown.hide()
confirmDeleteDialog.show()
}
async function save() { async function save() {
await backendUiStore.actions.views.save({ await backendUiStore.actions.views.save({
originalName, originalName,
@ -61,7 +65,7 @@
<Icon name="edit" /> <Icon name="edit" />
Edit Edit
</li> </li>
<li data-cy="delete-view" on:click={() => confirmDeleteDialog.show()}> <li data-cy="delete-view" on:click={showDelete}>
<Icon name="delete" /> <Icon name="delete" />
Delete Delete
</li> </li>

View File

@ -1,19 +1,13 @@
<script> <script>
import { getContext } from "svelte"
import { slide } from "svelte/transition"
import { Switcher } from "@budibase/bbui"
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { store, backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import ListItem from "./ListItem.svelte" import ListItem from "./ListItem.svelte"
import { Button } from "@budibase/bbui"
import CreateTablePopover from "./CreateTable.svelte" import CreateTablePopover from "./CreateTable.svelte"
import EditTablePopover from "./EditTable.svelte" import EditTablePopover from "./EditTable.svelte"
import EditViewPopover from "./EditView.svelte" import EditViewPopover from "./EditView.svelte"
import { Heading } from "@budibase/bbui" import { Heading } from "@budibase/bbui"
import { Spacer } from "@budibase/bbui" import { Spacer } from "@budibase/bbui"
const { open, close } = getContext("simple-modal")
$: selectedView = $: selectedView =
$backendUiStore.selectedView && $backendUiStore.selectedView.name $backendUiStore.selectedView && $backendUiStore.selectedView.name

View File

@ -1,64 +1,31 @@
<script> <script>
import { Modal, Button, Heading, Spacer } from "@budibase/bbui" import { Modal, ModalTitle, ModalFooter } from "components/common/Modal"
export let title = "" export let title = ""
export let body = "" export let body = ""
export let okText = "OK" export let okText = "Confirm"
export let cancelText = "Cancel" export let cancelText = "Cancel"
export let onOk = () => {} export let onOk = () => {}
export let onCancel = () => {} export let onCancel = () => {}
let modal
export const show = () => { export const show = () => {
theModal.show() modal.show()
} }
export const hide = () => { export const hide = () => {
theModal.hide() modal.hide()
}
let theModal
const cancel = () => {
hide()
onCancel()
}
const ok = () => {
const result = onOk()
// allow caller to return false, to cancel the "ok"
if (result === false) return
hide()
} }
</script> </script>
<Modal id={title} bind:this={theModal}> <Modal id={title} bind:this={modal} on:hide={onCancel}>
<h2>{title}</h2> <ModalTitle>{title}</ModalTitle>
<Spacer extraLarge /> <div class="body">{body}</div>
<div class="content"> <ModalFooter confirmText={okText} {cancelText} onConfirm={onOk} red />
<slot class="rows">{body}</slot>
</div>
<Spacer extraLarge />
<div class="modal-footer">
<Button red wide on:click={ok}>{okText}</Button>
<Button secondary wide on:click={cancel}>{cancelText}</Button>
</div>
</Modal> </Modal>
<style> <style>
h2 { .body {
font-size: var(--font-size-xl);
margin: 0;
font-family: var(--font-sans);
font-weight: 600;
}
.modal-footer {
display: grid;
grid-gap: var(--spacing-s);
}
.content {
white-space: normal;
font-size: var(--font-size-s); font-size: var(--font-size-s);
} }
</style> </style>

View File

@ -5,9 +5,25 @@
</script> </script>
{#if hasErrors} {#if hasErrors}
<div class="bb__alert bb__alert--danger"> <div class="container bb__alert bb__alert--danger">
{#each errors as error} {#each errors as error}
<div>{error.dataPath} {error.message}</div> <div class="error">{error.dataPath} {error.message}</div>
{/each} {/each}
</div> </div>
{/if} {/if}
<style>
.container {
border-radius: var(--border-radius-m);
margin: 0;
padding: var(--spacing-m);
}
.error {
font-size: var(--font-size-xs);
font-weight: 500;
}
.error:first-letter {
text-transform: uppercase;
}
</style>

View File

@ -20,7 +20,6 @@
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records` const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
const response = await api.get(FETCH_RECORDS_URL) const response = await api.get(FETCH_RECORDS_URL)
const result = await response.json() const result = await response.json()
console.log(result)
return result return result
} }

View File

@ -1,17 +1,26 @@
<script> <script>
import { createEventDispatcher } from "svelte" /**
* 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 { fade, fly } from "svelte/transition"
import { portal } from "./portal" import Portal from "svelte-portal"
import { ContextKey } from "./context"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let visible = false export let wide = false
export let cancelText = "Cancel" export let padded = true
export let confirmText = "Confirm"
export let showCancelButton = true let visible
export let showConfirmButton = true
export function show() { export function show() {
console.log("show")
if (visible) { if (visible) {
return return
} }
@ -26,10 +35,12 @@
visible = false visible = false
dispatch("hide") dispatch("hide")
} }
setContext(ContextKey, { show, hide })
</script> </script>
{#if visible} {#if visible}
<div class="portal-wrapper" use:portal={'#modal-container'}> <Portal target="#modal-container">
<div <div
class="overlay" class="overlay"
on:click|self={hide} on:click|self={hide}
@ -37,15 +48,16 @@
<div <div
class="scroll-wrapper" class="scroll-wrapper"
on:click|self={hide} on:click|self={hide}
transition:fly={{ y: 100 }}> transition:fly={{ y: 50 }}>
<div class="content-wrapper" on:click|self={hide}> <div class="content-wrapper" on:click|self={hide}>
<div class="content"> <div class="content" class:wide class:padded>
<slot /> <slot />
<i class="ri-close-line" on:click={hide} />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </Portal>
{/if} {/if}
<style> <style>
@ -66,7 +78,6 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: rgba(0, 0, 0, 0.25); background-color: rgba(0, 0, 0, 0.25);
z-index: 999;
} }
.scroll-wrapper { .scroll-wrapper {
@ -98,5 +109,24 @@
flex: 0 0 400px; flex: 0 0 400px;
margin: 2rem 0; margin: 2rem 0;
border-radius: var(--border-radius-m); border-radius: var(--border-radius-m);
gap: var(--spacing-xl);
}
.content.wide {
flex: 0 0 600px;
}
.content.padded {
padding: var(--spacing-xl);
}
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;
} }
</style> </style>

View File

@ -5,5 +5,6 @@
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: 999;
} }
</style> </style>

View File

@ -0,0 +1,63 @@
<script>
import { getContext } from "svelte"
import { Button } from "@budibase/bbui"
import { ContextKey } from "./context"
export let cancelText = "Cancel"
export let confirmText = "Confirm"
export let showCancelButton = true
export let showConfirmButton = true
export let onConfirm
const modalContext = getContext(ContextKey)
let loading = false
function hide() {
modalContext.hide()
}
async function confirm() {
loading = true
if (!onConfirm || (await onConfirm()) !== false) {
hide()
}
loading = false
}
</script>
<footer>
<div class="content">
<slot />
</div>
<div class="buttons">
{#if showCancelButton}
<Button secondary on:click={hide}>{cancelText}</Button>
{/if}
{#if showConfirmButton}
<Button
primary
{...$$restProps}
disabled={$$props.disabled || loading}
on:click={confirm}>
{confirmText}
</Button>
{/if}
</div>
</footer>
<style>
footer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--spacing-m);
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: var(--spacing-m);
}
</style>

View File

@ -0,0 +1,10 @@
<h5>
<slot />
</h5>
<style>
h5 {
margin: 0;
font-weight: 500;
}
</style>

View File

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

View File

@ -1,2 +1,5 @@
export { default as Modal } from "./Modal.svelte" export { default as Modal } from "./Modal.svelte"
export { default as ModalContainer } from "./ModalContainer.svelte" export { default as ModalContainer } from "./ModalContainer.svelte"
export { default as ModalTitle } from "./ModalTitle.svelte"
export { default as ModalFooter } from "./ModalFooter.svelte"
export { ContextKey } from "./context"

View File

@ -1,14 +0,0 @@
export function portal(node, targetNodeOrSelector) {
const targetNode =
typeof targetNodeOrSelector == "string"
? document.querySelector(targetNodeOrSelector)
: targetNodeOrSelector
const portalChildren = [...node.children]
targetNode.append(...portalChildren)
return {
destroy() {
for (const portalChild of portalChildren) portalChild.remove()
},
}
}

View File

@ -6,83 +6,68 @@
"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
export let value = [] export let value = []
export let readonly = false
export let label export let label
let placeholder = "Type to search" let placeholder = "Type to search"
let input
let inputValue
let options = [] let options = []
let activeOption
let optionsVisible = false let optionsVisible = false
let selected = {} let selected = {}
let first = true let first = true
let slot let slot
onMount(() => { onMount(() => {
slot.querySelectorAll("option").forEach(o => { const domOptions = Array.from(slot.querySelectorAll("option"))
o.selected && !value.includes(o.value) && (value = [...value, o.value]) options = domOptions.map(option => ({
options = [...options, { value: o.value, name: o.textContent }] value: option.value,
}) name: option.textContent,
value && }))
(selected = options.reduce( if (value) {
(obj, op) => options.forEach(option => {
value.includes(op.value) ? { ...obj, [op.value]: op } : obj, if (value.includes(option.value)) {
{} selected[option.value] = option
)) }
})
}
first = false first = false
}) })
$: if (!first) value = Object.values(selected).map(o => o.value) // Keep value up to date with selected options
$: filtered = options.filter(o => $: {
inputValue ? o.name.toLowerCase().includes(inputValue.toLowerCase()) : o if (!first) {
) value = Object.values(selected).map(option => option.value)
$: if ( }
(activeOption && !filtered.includes(activeOption)) || }
(!activeOption && inputValue)
)
activeOption = filtered[0]
function add(token) { function add(token) {
if (!readonly) selected[token.value] = token selected[token.value] = token
} }
function remove(value) { function remove(value) {
if (!readonly) { const { [value]: val, ...rest } = selected
const { [value]: val, ...rest } = selected selected = rest
selected = rest
}
} }
function removeAll() { function removeAll() {
selected = [] selected = []
inputValue = ""
} }
function showOptions(show) { function showOptions(show) {
optionsVisible = show optionsVisible = show
if (!show) {
activeOption = undefined
} else {
input.focus()
}
} }
function handleBlur() { function handleClick() {
showOptions(false) showOptions(!optionsVisible)
}
function handleFocus() {
showOptions(true)
} }
function handleOptionMousedown(e) { function handleOptionMousedown(e) {
const value = e.target.dataset.value const value = e.target.dataset.value
if (value == null) {
return
}
if (selected[value]) { if (selected[value]) {
remove(value) remove(value)
} else { } else {
add(options.filter(option => option.value === value)[0]) add(options.filter(option => option.value === value)[0])
input.focus()
} }
} }
</script> </script>
@ -91,76 +76,52 @@
{#if label} {#if label}
<Label extraSmall grey>{label}</Label> <Label extraSmall grey>{label}</Label>
{/if} {/if}
<div class="multiselect" class:readonly> <div class="multiselect">
<div class="tokens-wrapper"> <div class="tokens-wrapper">
<div class="tokens" class:showOptions> <div
class="tokens"
class:optionsVisible
on:click|self={handleClick}
class:empty={!value || !value.length}>
{#each Object.values(selected) as option} {#each Object.values(selected) as option}
<div class="token" data-id={option.value}> <div class="token" data-id={option.value} on:click|self={handleClick}>
<span>{option.name}</span> <span>{option.name}</span>
{#if !readonly} <div
<div class="token-remove"
class="token-remove" title="Remove {option.name}"
title="Remove {option.name}" on:click={() => remove(option.value)}>
on:click={() => remove(option.value)}> <svg
<svg class="icon-clear"
class="icon-clear" xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" width="18"
width="18" height="18"
height="18" viewBox="0 0 24 24">
viewBox="0 0 24 24"> <path d={xPath} />
<path d={xPath} /> </svg>
</svg> </div>
</div>
{/if}
</div> </div>
{/each} {/each}
{#if !value || !value.length}&nbsp;{/if}
</div> </div>
</div> </div>
<div class="actions">
{#if !readonly}
<input
autocomplete="off"
bind:value={inputValue}
bind:this={input}
on:blur={handleBlur}
on:focus={handleFocus}
{placeholder} />
<div
class="remove-all"
title="Remove All"
class:hidden={!Object.keys(selected).length}
on:click={removeAll}>
<svg
class="icon-clear"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24">
<path d={xPath} />
</svg>
</div>
{/if}
</div>
<select bind:this={slot} type="multiple" class="hidden"> <select bind:this={slot} type="multiple" class="hidden">
<slot /> <slot />
</select> </select>
{#if optionsVisible} {#if optionsVisible}
<div class="options-overlay" on:click|self={() => showOptions(false)} />
<ul <ul
class="options" class="options"
transition:fly={{ duration: 200, y: 5 }} transition:fly={{ duration: 200, y: 5 }}
on:mousedown|preventDefault={handleOptionMousedown}> on:mousedown|preventDefault={handleOptionMousedown}>
{#each filtered as option} {#each options as option}
<li <li class:selected={selected[option.value]} data-value={option.value}>
class:selected={selected[option.value]}
class:active={activeOption === option}
data-value={option.value}>
{option.name} {option.name}
</li> </li>
{/each} {/each}
{#if !filtered.length && inputValue.length} {#if !options.length}
<li>No results</li> <li class="no-results">No results</li>
{/if} {/if}
</ul> </ul>
{/if} {/if}
@ -175,7 +136,7 @@
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
} }
.multiselect:not(.readonly):hover { .multiselect:hover {
border-bottom-color: hsl(0, 0%, 50%); border-bottom-color: hsl(0, 0%, 50%);
} }
@ -185,6 +146,7 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
flex: 0 1 auto; flex: 0 1 auto;
z-index: 2;
} }
.tokens { .tokens {
@ -194,6 +156,14 @@
position: relative; position: relative;
width: 0; width: 0;
flex: 1 1 auto; flex: 1 1 auto;
background-color: var(--grey-2);
border-radius: var(--border-radius-m);
padding: 0 var(--spacing-m) calc(var(--spacing-m) - var(--spacing-xs))
calc(var(--spacing-m) / 2);
border: var(--border-transparent);
}
.tokens:hover {
cursor: pointer;
} }
.tokens::after { .tokens::after {
background: none repeat scroll 0 0 transparent; background: none repeat scroll 0 0 transparent;
@ -206,74 +176,72 @@
transition: width 0.3s ease 0s, left 0.3s ease 0s; transition: width 0.3s ease 0s, left 0.3s ease 0s;
width: 0; width: 0;
} }
.tokens.showOptions::after { .tokens.optionsVisible {
border: var(--border-blue);
}
.tokens.empty {
padding: var(--spacing-m);
font-size: var(--font-size-xs);
user-select: none;
}
.tokens::after {
width: 100%; width: 100%;
left: 0; left: 0;
} }
.token { .token {
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
align-items: center; align-items: center;
background-color: var(--grey-3); background-color: white;
border-radius: var(--border-radius-l); border-radius: var(--border-radius-l);
display: flex; display: flex;
margin: 0.25rem 0.5rem 0.25rem 0; margin: calc(var(--spacing-m) - var(--spacing-xs)) 0 0
calc(var(--spacing-m) / 2);
max-height: 1.3rem; max-height: 1.3rem;
padding: var(--spacing-s) var(--spacing-m); padding: var(--spacing-xs) var(--spacing-s);
transition: background-color 0.3s; transition: background-color 0.3s;
white-space: nowrap; white-space: nowrap;
} }
.token:hover { .token span {
background-color: var(--grey-4); pointer-events: none;
user-select: none;
} }
.readonly .token { .token-remove {
color: hsl(0, 0%, 40%);
}
.token-remove,
.remove-all {
align-items: center; align-items: center;
background-color: var(--grey-5); background-color: var(--grey-4);
border-radius: 50%; border-radius: 50%;
color: var(--white); color: var(--white);
display: flex; display: flex;
justify-content: center; justify-content: center;
height: 1.25rem; height: 1rem;
margin-left: 0.25rem; width: 1rem;
min-width: 1.25rem; margin: calc(-1 * var(--spacing-xs)) 0 calc(-1 * var(--spacing-xs))
var(--spacing-xs);
} }
.token-remove:hover, .token-remove:hover {
.remove-all:hover { background-color: var(--grey-5);
background-color: var(--grey-6);
cursor: pointer; cursor: pointer;
} }
.actions {
align-items: center;
display: flex;
flex: 1;
}
.actions > * {
flex: 0 0 auto;
}
.actions > input {
border: none;
font-size: var(--font-size-xs);
line-height: 1.5rem;
outline: none;
background-color: transparent;
flex: 1 1 auto;
}
.icon-clear path { .icon-clear path {
fill: white; fill: white;
} }
.options-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 1;
}
.options { .options {
z-index: 2;
left: 0; left: 0;
list-style: none; list-style: none;
margin-block-end: 0; margin-block-end: 0;
margin-block-start: 0; margin-block-start: 0;
max-height: 70vh; overflow-y: auto;
overflow: auto;
padding-inline-start: 0; padding-inline-start: 0;
position: absolute; position: absolute;
top: calc(100% + 1px); top: calc(100% + 1px);
@ -283,8 +251,8 @@
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 5px 12px rgba(0, 0, 0, 0.15);
margin-top: var(--spacing-xs); margin-top: var(--spacing-xs);
padding: var(--spacing-s) 0; padding: var(--spacing-s) 0;
z-index: 1;
background-color: white; background-color: white;
max-height: 200px;
} }
li { li {
background-color: white; background-color: white;
@ -299,6 +267,10 @@
li:not(.selected):hover { li:not(.selected):hover {
background-color: var(--grey-1); background-color: var(--grey-1);
} }
li.no-results:hover {
background-color: white;
cursor: initial;
}
.hidden { .hidden {
display: none; display: none;

View File

@ -1,7 +1,7 @@
<script> <script>
import { notificationStore } from "builderStore/store/notifications" import { notificationStore } from "builderStore/store/notifications"
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import { fade } from "svelte/transition" import { fly } from "svelte/transition"
export let themes = { export let themes = {
danger: "#E26D69", danger: "#E26D69",
@ -24,36 +24,41 @@
} }
</script> </script>
<ul class="notifications"> <div class="notifications">
{#each $notificationStore.notifications as notification (notification.id)} {#each $notificationStore.notifications as notification (notification.id)}
<li <div
class="toast" class="toast"
style="background: {themes[notification.type]};" style="background: {themes[notification.type]};"
transition:fade> transition:fly={{ y: -30 }}>
<div class="content">{notification.message}</div> <div class="content">{notification.message}</div>
{#if notification.icon} {#if notification.icon}
<i class={notification.icon} /> <i class={notification.icon} />
{/if} {/if}
</li> </div>
{/each} {/each}
</ul> </div>
<style> <style>
.notifications { .notifications {
width: 40vw;
list-style: none;
position: fixed; position: fixed;
top: 0; top: 10px;
left: 0; left: 0;
right: 0; right: 0;
margin-left: auto; margin: 0 auto;
margin-right: auto;
padding: 0; padding: 0;
z-index: 9999; z-index: 9999;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
} }
.toast { .toast {
flex: 0 0 auto;
margin-bottom: 10px; margin-bottom: 10px;
border-radius: var(--border-radius-s);
/* The toasts now support being auto sized, so this static width could be removed */
width: 40vw;
} }
.content { .content {

View File

@ -1,196 +0,0 @@
<script>
import { onMount } from "svelte"
import fsort from "fast-sort"
import getOr from "lodash/fp/getOr"
import { store, backendUiStore } from "builderStore"
import { Button, Icon } from "@budibase/bbui"
import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecord from "./LinkedRecord.svelte"
import AttachmentList from "./AttachmentList.svelte"
import TablePagination from "./TablePagination.svelte"
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import RowPopover from "./popovers/Row.svelte"
import ColumnPopover from "./popovers/Column.svelte"
import ViewPopover from "./popovers/View.svelte"
import ExportPopover from "./popovers/Export.svelte"
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte"
import EditRowPopover from "./popovers/EditRow.svelte"
import * as api from "./api"
const ITEMS_PER_PAGE = 10
// Internal headers we want to hide from the user
const INTERNAL_HEADERS = ["_id", "_rev", "modelId", "type"]
let modalOpen = false
let data = []
let headers = []
let currentPage = 0
let search
$: {
if (
$backendUiStore.selectedView &&
$backendUiStore.selectedView.name.startsWith("all_")
) {
api.fetchDataForView($backendUiStore.selectedView).then(records => {
data = records || []
})
}
}
$: sort = $backendUiStore.sort
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
$: paginatedData = sorted
? sorted.slice(
currentPage * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
)
: []
$: headers = Object.keys($backendUiStore.selectedModel.schema)
.sort()
.filter(id => !INTERNAL_HEADERS.includes(id))
$: schema = $backendUiStore.selectedModel.schema
$: modelView = {
schema: $backendUiStore.selectedModel.schema,
name: $backendUiStore.selectedView.name,
}
</script>
<section>
<div class="table-controls">
<h2 class="title">{$backendUiStore.selectedModel.name}</h2>
<div class="popovers">
<ColumnPopover />
{#if Object.keys($backendUiStore.selectedModel.schema).length > 0}
<RowPopover />
<ViewPopover />
<ExportPopover view={modelView} />
{/if}
</div>
</div>
<table class="bb-table">
<thead>
<tr>
<th class="edit-header">
<div>Edit</div>
</th>
{#each headers as header}
<th>
<ColumnHeaderPopover
field={$backendUiStore.selectedModel.schema[header]} />
</th>
{/each}
</tr>
</thead>
<tbody>
{#if paginatedData.length === 0}
<div class="no-data">No Data.</div>
{/if}
{#each paginatedData as row}
<tr>
<td>
<EditRowPopover {row} />
</td>
{#each headers as header}
<td>
{#if schema[header].type === 'link'}
<LinkedRecord field={schema[header]} ids={row[header]} />
{:else if schema[header].type === 'attachment'}
<AttachmentList files={row[header] || []} />
{:else}{getOr('', header, row)}{/if}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
<TablePagination
{data}
bind:currentPage
pageItemCount={paginatedData.length}
{ITEMS_PER_PAGE} />
</section>
<style>
section {
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: 600;
text-rendering: optimizeLegibility;
text-transform: capitalize;
}
table {
border: 1px solid var(--grey-4);
background: #fff;
border-radius: 3px;
border-collapse: collapse;
}
thead {
height: 40px;
background: var(--grey-3);
border: 1px solid var(--grey-4);
}
thead th {
color: var(--ink);
text-transform: capitalize;
font-weight: 500;
font-size: 14px;
text-rendering: optimizeLegibility;
transition: 0.5s all;
vertical-align: middle;
}
.edit-header {
width: 100px;
cursor: default;
}
.edit-header:hover {
color: var(--ink);
}
th:hover {
color: var(--blue);
cursor: pointer;
}
td {
max-width: 200px;
text-overflow: ellipsis;
border: 1px solid var(--grey-4);
overflow: hidden;
white-space: pre;
box-sizing: border-box;
}
tbody tr {
border-bottom: 1px solid var(--grey-4);
transition: 0.3s background-color;
color: var(--ink);
font-size: 12px;
}
tbody tr:hover {
background: var(--grey-1);
}
.table-controls {
width: 100%;
}
.popovers {
display: flex;
}
.no-data {
padding: 14px;
}
</style>

View File

@ -1,56 +0,0 @@
<script>
import { onMount } from "svelte"
import fsort from "fast-sort"
import getOr from "lodash/fp/getOr"
import { store, backendUiStore } from "builderStore"
import api from "builderStore/api"
import { Button, Icon } from "@budibase/bbui"
import Table from "./Table.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecord from "./LinkedRecord.svelte"
import TablePagination from "./TablePagination.svelte"
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import RowPopover from "./popovers/Row.svelte"
import ColumnPopover from "./popovers/Column.svelte"
import ViewPopover from "./popovers/View.svelte"
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte"
import EditRowPopover from "./popovers/EditRow.svelte"
import CalculationPopover from "./popovers/Calculate.svelte"
import GroupByPopover from "./popovers/GroupBy.svelte"
import FilterPopover from "./popovers/Filter.svelte"
import ExportPopover from "./popovers/Export.svelte"
export let view = {}
let data = []
$: name = view.name
$: filters = view.filters
$: field = view.field
$: groupBy = view.groupBy
$: !name.startsWith("all_") && filters && fetchViewData(name, field, groupBy)
async function fetchViewData(name, field, groupBy) {
const params = new URLSearchParams()
if (field) {
params.set("field", field)
params.set("stats", true)
}
if (groupBy) params.set("group", groupBy)
let QUERY_VIEW_URL = `/api/views/${name}?${params}`
const response = await api.get(QUERY_VIEW_URL)
data = await response.json()
}
</script>
<Table title={decodeURI(name)} schema={view.schema} {data}>
<FilterPopover {view} />
<CalculationPopover {view} />
{#if view.calculation}
<GroupByPopover {view} />
{/if}
<ExportPopover {view} />
</Table>

View File

@ -1,71 +0,0 @@
<script>
import {
TextButton,
Button,
Icon,
Input,
Select,
Popover,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import api from "builderStore/api"
const FORMATS = [
{
name: "CSV",
key: "csv",
},
{
name: "JSON",
key: "json",
},
]
export let view
let anchor
let dropdown
let exportFormat
async function exportView() {
const response = await api.post(
`/api/views/export?format=${exportFormat}`,
view
)
const downloadInfo = await response.json()
window.location = downloadInfo.url
}
</script>
<div bind:this={anchor}>
<TextButton text small on:click={dropdown.show}>
<Icon name="download" />
Export
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<h5>Export Format</h5>
<Select secondary thin bind:value={exportFormat}>
<option value={''}>Select an option</option>
{#each FORMATS as format}
<option value={format.key}>{format.name}</option>
{/each}
</Select>
<div class="button-group">
<Button secondary on:click={dropdown.hide}>Cancel</Button>
<Button primary on:click={exportView}>Export</Button>
</div>
</Popover>
<style>
h5 {
margin-top: 0;
}
.button-group {
margin-top: var(--spacing-l);
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -1,31 +1,17 @@
<script> <script>
import Modal from "./Modal.svelte" import SettingsModal from "./SettingsModal.svelte"
import { SettingsIcon } from "components/common/Icons/" import { SettingsIcon } from "components/common/Icons/"
import { getContext } from "svelte" import { Modal } from "components/common/Modal"
import { isActive, goto, layout } from "@sveltech/routify"
// Handle create app modal let modal
const { open, close } = getContext("simple-modal")
const showSettingsModal = () => {
open(
Modal,
{
name: "Placeholder App Name",
description: "This is a hardcoded description that needs to change",
},
{
closeOnEsc: true,
styleContent: { padding: 0 },
closeOnOuterClick: true,
}
)
}
</script> </script>
<span class="topnavitemright settings" on:click={showSettingsModal}> <span class="topnavitemright settings" on:click={modal.show}>
<SettingsIcon /> <SettingsIcon />
</span> </span>
<Modal bind:this={modal} wide>
<SettingsModal />
</Modal>
<style> <style>
span:first-letter { span:first-letter {

View File

@ -1,13 +1,8 @@
<script> <script>
import { General, Users, DangerZone, APIKeys } from "./tabs" import { General, Users, DangerZone, APIKeys } from "./tabs"
import { Switcher } from "@budibase/bbui"
import { ModalTitle } from "components/common/Modal"
import { Input, TextArea, Button, Switcher } from "@budibase/bbui"
import { SettingsIcon, CloseIcon } from "components/common/Icons/"
import { getContext } from "svelte"
import { post } from "builderStore/api"
export let name = ""
export let description = ""
const tabs = [ const tabs = [
{ {
title: "General", title: "General",
@ -30,15 +25,16 @@
component: DangerZone, component: DangerZone,
}, },
] ]
let value = "GENERAL" let value = "GENERAL"
$: selectedTab = tabs.find(tab => tab.key === value).component $: selectedTab = tabs.find(tab => tab.key === value).component
function hide() {}
</script> </script>
<div class="container"> <div class="container">
<div class="heading"> <ModalTitle>Settings</ModalTitle>
<i class="ri-settings-2-fill" />
<h3>Settings</h3>
</div>
<Switcher headings={tabs} bind:value> <Switcher headings={tabs} bind:value>
<svelte:component this={selectedTab} /> <svelte:component this={selectedTab} />
</Switcher> </Switcher>
@ -46,7 +42,6 @@
<style> <style>
.container { .container {
padding: 30px;
display: grid; display: grid;
grid-gap: var(--spacing-xl); grid-gap: var(--spacing-xl);
} }
@ -54,21 +49,7 @@
/* Fix margin defined in BBUI as L rather than XL */ /* Fix margin defined in BBUI as L rather than XL */
margin-bottom: var(--spacing-xl); margin-bottom: var(--spacing-xl);
} }
.container :global(textarea) {
.heading { min-height: 60px;
display: flex;
flex-direction: row;
align-items: center;
}
.heading h3 {
font-size: var(--font-size-xl);
color: var(--ink);
font-weight: 600;
margin: 0;
}
.heading i {
margin-right: var(--spacing-m);
font-size: 28px;
color: var(--grey-6);
} }
</style> </style>

View File

@ -2,6 +2,7 @@
import { params, goto } from "@sveltech/routify" import { params, goto } from "@sveltech/routify"
import { Input, TextArea, Button, Body } from "@budibase/bbui" import { Input, TextArea, Button, Body } from "@budibase/bbui"
import { del } from "builderStore/api" import { del } from "builderStore/api"
import { ModalFooter } from "components/common/Modal"
let value = "" let value = ""
let loading = false let loading = false
@ -9,16 +10,9 @@
async function deleteApp() { async function deleteApp() {
loading = true loading = true
const id = $params.application const id = $params.application
const res = await del(`/api/${id}`) await del(`/api/${id}`)
const json = await res.json()
loading = false loading = false
if (res.ok) { $goto("/")
$goto("/")
return json
} else {
throw new Error(json)
}
} }
</script> </script>
@ -35,13 +29,12 @@
thin thin
disabled={loading} disabled={loading}
placeholder="" /> placeholder="" />
<Button <ModalFooter
disabled={value !== 'DELETE' || loading} disabled={value !== 'DELETE' || loading}
red red
wide showCancelButton={false}
on:click={deleteApp}> confirmText="Delete Entire App"
Delete Entire Web App onConfirm={deleteApp} />
</Button>
</div> </div>
<style> <style>
@ -53,7 +46,4 @@
line-height: 1.2; line-height: 1.2;
margin: 0; margin: 0;
} }
.background :global(button) {
max-width: 100%;
}
</style> </style>

View File

@ -11,12 +11,10 @@
import { Input, TextArea, Button } from "@budibase/bbui" import { Input, TextArea, Button } from "@budibase/bbui"
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/" import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
import { getContext } from "svelte"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { post } from "builderStore/api" import { post } from "builderStore/api"
import analytics from "analytics" import analytics from "analytics"
const { open, close } = getContext("simple-modal")
//Move this to context="module" once svelte-forms is updated so that it can bind to stores correctly //Move this to context="module" once svelte-forms is updated so that it can bind to stores correctly
const createAppStore = writable({ currentStep: 0, values: {} }) const createAppStore = writable({ currentStep: 0, values: {} })
@ -169,7 +167,7 @@
} }
const userResp = await api.post(`/api/users`, user) const userResp = await api.post(`/api/users`, user)
const json = await userResp.json() const json = await userResp.json()
$goto(`./${appJson._id}`) $goto(`/${appJson._id}`)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
@ -194,10 +192,6 @@
let onChange = () => {} let onChange = () => {}
function _onCancel() {
close()
}
async function _onOkay() { async function _onOkay() {
await createNewApp() await createNewApp()
} }
@ -249,9 +243,6 @@
{/if} {/if}
</div> </div>
</div> </div>
<div class="close-button" on:click={_onCancel}>
<CloseIcon />
</div>
<img src="/_builder/assets/bb-logo.svg" alt="budibase icon" /> <img src="/_builder/assets/bb-logo.svg" alt="budibase icon" />
{#if submitting} {#if submitting}
<div in:fade class="spinner-container"> <div in:fade class="spinner-container">
@ -276,16 +267,6 @@
align-content: center; align-content: center;
background: #f5f5f5; background: #f5f5f5;
} }
.close-button {
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
}
.close-button :global(svg) {
width: 24px;
height: 24px;
}
.heading { .heading {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -1,5 +1,4 @@
<script> <script>
import { MoreIcon } from "components/common/Icons"
import { store } from "builderStore" import { store } from "builderStore"
import { getComponentDefinition } from "builderStore/storeUtils" import { getComponentDefinition } from "builderStore/storeUtils"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -109,9 +108,9 @@
</script> </script>
<div bind:this={anchor} on:click|stopPropagation={() => {}}> <div bind:this={anchor} on:click|stopPropagation={() => {}}>
<button on:click={dropdown.show}> <div class="icon" on:click={dropdown.show}>
<MoreIcon /> <i class="ri-more-line" />
</button> </div>
</div> </div>
<DropdownMenu <DropdownMenu
class="menu" class="menu"
@ -122,27 +121,27 @@
align="left"> align="left">
<ul> <ul>
<li class="item" on:click={() => confirmDeleteDialog.show()}> <li class="item" on:click={() => confirmDeleteDialog.show()}>
<i class="icon ri-delete-bin-2-line" /> <i class="ri-delete-bin-2-line" />
Delete Delete
</li> </li>
<li class="item" on:click={moveUpComponent}> <li class="item" on:click={moveUpComponent}>
<i class="icon ri-arrow-up-line" /> <i class="ri-arrow-up-line" />
Move up Move up
</li> </li>
<li class="item" on:click={moveDownComponent}> <li class="item" on:click={moveDownComponent}>
<i class="icon ri-arrow-down-line" /> <i class="ri-arrow-down-line" />
Move down Move down
</li> </li>
<li class="item" on:click={copyComponent}> <li class="item" on:click={copyComponent}>
<i class="icon ri-repeat-one-line" /> <i class="ri-repeat-one-line" />
Duplicate Duplicate
</li> </li>
<li class="item" on:click={() => storeComponentForCopy(true)}> <li class="item" on:click={() => storeComponentForCopy(true)}>
<i class="icon ri-scissors-cut-line" /> <i class="ri-scissors-cut-line" />
Cut Cut
</li> </li>
<li class="item" on:click={() => storeComponentForCopy(false)}> <li class="item" on:click={() => storeComponentForCopy(false)}>
<i class="icon ri-file-copy-line" /> <i class="ri-file-copy-line" />
Copy Copy
</li> </li>
<hr class="hr-style" /> <hr class="hr-style" />
@ -150,21 +149,21 @@
class="item" class="item"
class:disabled={noPaste} class:disabled={noPaste}
on:click={() => pasteComponent('above')}> on:click={() => pasteComponent('above')}>
<i class="icon ri-insert-row-top" /> <i class="ri-insert-row-top" />
Paste above Paste above
</li> </li>
<li <li
class="item" class="item"
class:disabled={noPaste} class:disabled={noPaste}
on:click={() => pasteComponent('below')}> on:click={() => pasteComponent('below')}>
<i class="icon ri-insert-row-bottom" /> <i class="ri-insert-row-bottom" />
Paste below Paste below
</li> </li>
<li <li
class="item" class="item"
class:disabled={noPaste || noChildrenAllowed} class:disabled={noPaste || noChildrenAllowed}
on:click={() => pasteComponent('inside')}> on:click={() => pasteComponent('inside')}>
<i class="icon ri-insert-column-right" /> <i class="ri-insert-column-right" />
Paste inside Paste inside
</li> </li>
</ul> </ul>
@ -181,7 +180,7 @@
ul { ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: var(--spacing-xs) 0; margin: var(--spacing-s) 0;
} }
li { li {
@ -190,44 +189,29 @@
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--ink); color: var(--ink);
padding: var(--spacing-s) var(--spacing-m); padding: var(--spacing-s) var(--spacing-m);
margin: auto 0px; margin: auto 0;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
} }
li:not(.disabled):hover {
button {
border-style: none;
border-radius: 2px;
padding: 0;
background: transparent;
cursor: pointer;
color: var(--ink);
outline: none;
}
li:hover {
background-color: var(--grey-2); background-color: var(--grey-2);
} }
li:active { li:active {
color: var(--blue); color: var(--blue);
} }
li i {
.item {
display: flex;
align-items: center;
font-size: 14px;
}
.icon {
margin-right: 8px; margin-right: 8px;
font-size: var(--font-size-s);
} }
li.disabled {
.disabled {
color: var(--grey-4); color: var(--grey-4);
cursor: default; cursor: default;
} }
.icon i {
font-size: 16px;
}
.hr-style { .hr-style {
margin: 8px 0; margin: 8px 0;
color: var(--grey-4); color: var(--grey-4);

View File

@ -190,9 +190,9 @@
.item { .item {
display: grid; display: grid;
grid-template-columns: 1fr auto auto auto; grid-template-columns: 1fr auto auto auto;
padding: 0px 5px 0px 15px; padding: 0 var(--spacing-m);
margin: auto 0px; margin: 0;
border-radius: 5px; border-radius: var(--border-radius-m);
height: 36px; height: 36px;
align-items: center; align-items: center;
} }
@ -205,9 +205,6 @@
.actions { .actions {
display: none; display: none;
color: var(--ink); color: var(--ink);
padding: 0 5px;
width: 24px;
height: 24px;
border-style: none; border-style: none;
background: rgba(0, 0, 0, 0); background: rgba(0, 0, 0, 0);
cursor: pointer; cursor: pointer;

View File

@ -1,29 +1,14 @@
<script> <script>
import { getContext } from "svelte" import { keys, map, includes, filter } from "lodash/fp"
import {
keys,
map,
some,
includes,
cloneDeep,
isEqual,
sortBy,
filter,
difference,
} from "lodash/fp"
import { pipe } from "components/common/core"
import Checkbox from "components/common/Checkbox.svelte"
import EventEditorModal from "./EventEditorModal.svelte" import EventEditorModal from "./EventEditorModal.svelte"
import { Modal } from "components/common/Modal"
import { PencilIcon } from "components/common/Icons"
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
export const EVENT_TYPE = "event" export const EVENT_TYPE = "event"
export let component export let component
let events = [] let events = []
let selectedEvent = null let selectedEvent = null
let modal
$: { $: {
events = Object.keys(component) events = Object.keys(component)
@ -35,28 +20,9 @@
})) }))
} }
// Handle create app modal
const { open, close } = getContext("simple-modal")
const openModal = event => { const openModal = event => {
selectedEvent = event selectedEvent = event
open( modal.show()
EventEditorModal,
{
eventOptions: events,
event: selectedEvent,
onClose: () => {
close()
selectedEvent = null
},
},
{
closeButton: false,
closeOnEsc: false,
styleContent: { padding: 0 },
closeOnOuterClick: true,
}
)
} }
</script> </script>
@ -81,6 +47,13 @@
</form> </form>
</div> </div>
<Modal bind:this={modal}>
<EventEditorModal
eventOptions={events}
event={selectedEvent}
on:hide={() => (selectedEvent = null)} />
</Modal>
<style> <style>
.root { .root {
font-size: 10pt; font-size: 10pt;

View File

@ -10,7 +10,7 @@
} from "builderStore/replaceBindings" } from "builderStore/replaceBindings"
import { DropdownMenu } from "@budibase/bbui" import { DropdownMenu } from "@budibase/bbui"
import BindingDropdown from "components/userInterface/BindingDropdown.svelte" import BindingDropdown from "components/userInterface/BindingDropdown.svelte"
import { onMount, getContext } from "svelte" import { onMount } from "svelte"
export let label = "" export let label = ""
export let componentInstance = {} export let componentInstance = {}

View File

@ -1,5 +1,4 @@
<script> <script>
import { MoreIcon } from "components/common/Icons"
import { store } from "builderStore" import { store } from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import api from "builderStore/api" import api from "builderStore/api"
@ -10,7 +9,7 @@
let confirmDeleteDialog let confirmDeleteDialog
let dropdown let dropdown
let buttonForDropdown let anchor
const hideDropdown = () => { const hideDropdown = () => {
dropdown.hide() dropdown.hide()
@ -34,14 +33,17 @@
} }
</script> </script>
<div class="root boundary" on:click|stopPropagation={() => {}}> <div
<button on:click={() => dropdown.show()} bind:this={buttonForDropdown}> bind:this={anchor}
<MoreIcon /> class="root boundary"
</button> on:click|stopPropagation={() => {}}>
<DropdownMenu bind:this={dropdown} anchor={buttonForDropdown}> <div class="icon" on:click={() => dropdown.show()}>
<ul class="menu" on:click={hideDropdown}> <i class="ri-more-line" />
<li class="item" on:click={() => confirmDeleteDialog.show()}> </div>
<i class="icon ri-delete-bin-2-line" /> <DropdownMenu bind:this={dropdown} {anchor} align="left">
<ul on:click={hideDropdown}>
<li on:click={() => confirmDeleteDialog.show()}>
<i class="ri-delete-bin-2-line" />
Delete Delete
</li> </li>
</ul> </ul>
@ -71,40 +73,40 @@
outline: none; outline: none;
} }
.menu { ul {
z-index: 100000; z-index: 100000;
overflow: visible; overflow: visible;
padding: var(--spacing-xs) 0; margin: var(--spacing-s) 0;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
padding: 0;
} }
.menu li { li {
border-style: none;
background-color: transparent;
list-style-type: none;
padding: 4px 16px;
margin: 0;
width: 100%;
box-sizing: border-box;
}
.item {
display: flex; display: flex;
font-family: var(--font-sans);
font-size: var(--font-size-xs);
color: var(--ink);
padding: var(--spacing-s) var(--spacing-m);
margin: auto 0;
align-items: center; align-items: center;
cursor: pointer;
}
li:not(.disabled):hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
li i {
margin-right: 8px;
font-size: var(--font-size-s); font-size: var(--font-size-s);
} }
li.disabled {
.icon { color: var(--grey-4);
margin-right: 8px; cursor: default;
} }
.menu li:not(.disabled) { .icon i {
cursor: pointer; font-size: 16px;
color: var(--ink);
}
.menu li:not(.disabled):hover {
color: var(--ink);
background-color: var(--grey-1);
} }
</style> </style>

View File

@ -1,13 +1,9 @@
<script> <script>
import Modal from "svelte-simple-modal"
import { store, automationStore, backendUiStore } from "builderStore" import { store, automationStore, backendUiStore } from "builderStore"
import SettingsLink from "components/settings/Link.svelte" import SettingsLink from "components/settings/Link.svelte"
import { get } from "builderStore/api" import { get } from "builderStore/api"
import { isActive, goto, layout } from "@sveltech/routify"
import { fade } from "svelte/transition" import { PreviewIcon } from "components/common/Icons/"
import { isActive, goto, layout, url } from "@sveltech/routify"
import { SettingsIcon, PreviewIcon } from "components/common/Icons/"
// Get Package and set store // Get Package and set store
export let application export let application
@ -47,50 +43,46 @@
} }
</script> </script>
<Modal> <div class="root">
<div class="root"> <div class="top-nav">
<div class="topleftnav">
<button class="home-logo">
<img
src="/_builder/assets/bb-logo.svg"
alt="budibase icon"
on:click={() => $goto(`/`)} />
</button>
<div class="top-nav"> <!-- This gets all indexable subroutes and sticks them in the top nav. -->
<div class="topleftnav"> {#each $layout.children as { path, title }}
<button class="home-logo">
<img
src="/_builder/assets/bb-logo.svg"
alt="budibase icon"
on:click={() => $goto(`/`)} />
</button>
<!-- This gets all indexable subroutes and sticks them in the top nav. -->
{#each $layout.children as { path, title }}
<span
class:active={$isActive(path)}
class="topnavitem"
on:click={topItemNavigate(path)}>
{title}
</span>
{/each}
</div>
<div class="toprightnav">
<SettingsLink />
<span <span
class:active={false} class:active={$isActive(path)}
class="topnavitemright" class="topnavitem"
on:click={() => window.open(`/${application}`)}> on:click={topItemNavigate(path)}>
<PreviewIcon /> {title}
</span> </span>
</div> {/each}
</div>
<div class="toprightnav">
<SettingsLink />
<span
class:active={false}
class="topnavitemright"
on:click={() => window.open(`/${application}`)}>
<PreviewIcon />
</span>
</div> </div>
{#await promise}
<!-- This should probably be some kind of loading state? -->
<div />
{:then}
<slot />
{:catch error}
<p>Something went wrong: {error.message}</p>
{/await}
</div> </div>
</Modal>
{#await promise}
<!-- This should probably be some kind of loading state? -->
<div />
{:then _}
<slot />
{:catch error}
<p>Something went wrong: {error.message}</p>
{/await}
</div>
<style> <style>
.root { .root {
@ -138,7 +130,7 @@
margin: 0px 00px 0px 20px; margin: 0px 00px 0px 20px;
padding-top: 4px; padding-top: 4px;
font-weight: 500; font-weight: 500;
font-size: var(--font-size-md); font-size: var(--font-size-m);
height: 100%; height: 100%;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
@ -183,10 +175,6 @@
align-items: center; align-items: center;
} }
.home-logo:hover {
color: var(--hovercolor);
}
.home-logo:active { .home-logo:active {
outline: none; outline: none;
} }

View File

@ -1,5 +1,4 @@
<script> <script>
import { getContext } from "svelte"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import * as api from "components/backend/DataTable/api" import * as api from "components/backend/DataTable/api"
import ModelNavigator from "components/backend/ModelNavigator/ModelNavigator.svelte" import ModelNavigator from "components/backend/ModelNavigator/ModelNavigator.svelte"

View File

@ -1,13 +1,6 @@
<script> <script>
import { getContext } from "svelte" import ModelDataTable from "components/backend/DataTable/ModelDataTable.svelte"
import { Button } from "@budibase/bbui"
import ModelDataTable from "components/backend/DataTable"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import ActionButton from "components/common/ActionButton.svelte"
import * as api from "components/backend/DataTable/api"
import CreateEditRecordModal from "components/backend/DataTable/popovers/CreateEditRecord.svelte"
const { open, close } = getContext("simple-modal")
$: selectedModel = $backendUiStore.selectedModel $: selectedModel = $backendUiStore.selectedModel
</script> </script>

View File

@ -1,13 +1,6 @@
<script> <script>
import { getContext } from "svelte"
import { Button } from "@budibase/bbui"
import ViewDataTable from "components/backend/DataTable/ViewDataTable" import ViewDataTable from "components/backend/DataTable/ViewDataTable"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import ActionButton from "components/common/ActionButton.svelte"
import * as api from "components/backend/DataTable/api"
import CreateEditRecord from "components/backend/DataTable/popovers/CreateEditRecord.svelte"
const { open, close } = getContext("simple-modal")
$: selectedView = $backendUiStore.selectedView $: selectedView = $backendUiStore.selectedView
</script> </script>

View File

@ -1,5 +1,4 @@
<script> <script>
import Modal from "svelte-simple-modal"
import { Home as Link } from "@budibase/bbui" import { Home as Link } from "@budibase/bbui"
import { import {
AppsIcon, AppsIcon,
@ -7,43 +6,41 @@
DocumentationIcon, DocumentationIcon,
CommunityIcon, CommunityIcon,
BugIcon, BugIcon,
} from "components/common/Icons/" } from "components/common/Icons"
</script> </script>
<Modal> <div class="root">
<div class="root"> <div class="ui-nav">
<div class="ui-nav"> <div class="home-logo">
<div class="home-logo"> <img src="/_builder/assets/budibase-logo.svg" alt="Budibase icon" />
<img src="/_builder/assets/budibase-logo.svg" alt="Budibase icon" />
</div>
<div class="nav-section">
<Link icon={AppsIcon} title="Apps" href="/" active />
<Link
icon={HostingIcon}
title="Hosting"
href="https://portal.budi.live/" />
<Link
icon={DocumentationIcon}
title="Documentation"
href="https://docs.budibase.com/" />
<Link
icon={CommunityIcon}
title="Community"
href="https://forum.budibase.com/" />
<Link
icon={BugIcon}
title="Raise an issue"
href="https://github.com/Budibase/budibase" />
</div>
</div> </div>
<div class="main"> <div class="nav-section">
<slot /> <Link icon={AppsIcon} title="Apps" href="/" active />
<Link
icon={HostingIcon}
title="Hosting"
href="https://portal.budi.live/" />
<Link
icon={DocumentationIcon}
title="Documentation"
href="https://docs.budibase.com/" />
<Link
icon={CommunityIcon}
title="Community"
href="https://forum.budibase.com/" />
<Link
icon={BugIcon}
title="Raise an issue"
href="https://github.com/Budibase/budibase" />
</div> </div>
</div> </div>
</Modal>
<div class="main">
<slot />
</div>
</div>
<style> <style>
.root { .root {

View File

@ -1,5 +1,4 @@
<script> <script>
import { getContext } from "svelte"
import { store } from "builderStore" import { store } from "builderStore"
import api from "builderStore/api" import api from "builderStore/api"
import AppList from "components/start/AppList.svelte" import AppList from "components/start/AppList.svelte"
@ -10,8 +9,11 @@
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
import { Button, Heading } from "@budibase/bbui" import { Button, Heading } from "@budibase/bbui"
import analytics from "analytics" import analytics from "analytics"
import { Modal } from "components/common/Modal"
let promise = getApps() let promise = getApps()
let hasKey
let modal
async function getApps() { async function getApps() {
const res = await get("/api/applications") const res = await get("/api/applications")
@ -24,8 +26,6 @@
} }
} }
let hasKey
async function fetchKeys() { async function fetchKeys() {
const response = await api.get(`/api/keys/`) const response = await api.get(`/api/keys/`)
return await response.json() return await response.json()
@ -40,37 +40,16 @@
} }
if (!keys.budibase) { if (!keys.budibase) {
showCreateAppModal() modal.show()
} }
} }
// Handle create app modal
const { open } = getContext("simple-modal")
const showCreateAppModal = () => {
open(
CreateAppModal,
{
hasKey,
},
{
closeButton: false,
closeOnEsc: false,
closeOnOuterClick: false,
styleContent: { padding: 0 },
closeOnOuterClick: true,
}
)
}
checkIfKeysAndApps() checkIfKeysAndApps()
</script> </script>
<div class="header"> <div class="header">
<Heading medium black>Welcome to the Budibase Beta</Heading> <Heading medium black>Welcome to the Budibase Beta</Heading>
<Button primary black on:click={showCreateAppModal}> <Button primary black on:click={modal.show}>Create New Web App</Button>
Create New Web App
</Button>
</div> </div>
<div class="banner"> <div class="banner">
@ -80,6 +59,10 @@
</div> </div>
</div> </div>
<Modal bind:this={modal} wide padded={false}>
<CreateAppModal {hasKey} />
</Modal>
{#await promise} {#await promise}
<div class="spinner-container"> <div class="spinner-container">
<Spinner /> <Spinner />

View File

@ -18,8 +18,8 @@ validateJs.extend(validateJs.validators.datetime, {
exports.patch = async function(ctx) { exports.patch = async function(ctx) {
const instanceId = ctx.user.instanceId const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId) const db = new CouchDB(instanceId)
const model = await db.get(record.modelId)
let record = await db.get(ctx.params.id) let record = await db.get(ctx.params.id)
const model = await db.get(record.modelId)
const patchfields = ctx.request.body const patchfields = ctx.request.body
for (let key of Object.keys(patchfields)) { for (let key of Object.keys(patchfields)) {
@ -159,7 +159,7 @@ exports.fetchModelRecords = async function(ctx) {
}) })
) )
ctx.body = response.rows.map(row => row.doc) ctx.body = response.rows.map(row => row.doc)
ctx.body = await linkRecords.attachLinkInfo( ctx.body = await linkRecords.attachLinkInfo(
instanceId, instanceId,
response.rows.map(row => row.doc) response.rows.map(row => row.doc)
) )

View File

@ -4,6 +4,7 @@ const fs = require("fs")
const path = require("path") const path = require("path")
const os = require("os") const os = require("os")
const exporters = require("./exporters") const exporters = require("./exporters")
const { fetchView } = require("../record")
const controller = { const controller = {
fetch: async ctx => { fetch: async ctx => {
@ -12,6 +13,10 @@ const controller = {
const response = [] const response = []
for (let name of Object.keys(designDoc.views)) { for (let name of Object.keys(designDoc.views)) {
// Only return custom views
if (name === "by_link") {
continue
}
response.push({ response.push({
name, name,
...designDoc.views[name], ...designDoc.views[name],
@ -77,36 +82,24 @@ const controller = {
ctx.message = `View ${ctx.params.viewName} saved successfully.` ctx.message = `View ${ctx.params.viewName} saved successfully.`
}, },
exportView: async ctx => { exportView: async ctx => {
const db = new CouchDB(ctx.user.instanceId)
const view = ctx.request.body const view = ctx.request.body
const format = ctx.query.format const format = ctx.query.format
// fetch records for the view // Fetch view records
const response = await db.query(`database/${view.name}`, { ctx.params.viewName = view.name
include_docs: !view.calculation, ctx.query.group = view.groupBy
group: view.groupBy, if (view.field) {
}) ctx.query.stats = true
ctx.query.field = view.field
if (view.calculation === "stats") {
response.rows = response.rows.map(row => ({
group: row.key,
field: view.field,
...row.value,
avg: row.value.sum / row.value.count,
}))
} else {
response.rows = response.rows.map(row => row.doc)
} }
await fetchView(ctx)
// Export part
let headers = Object.keys(view.schema) let headers = Object.keys(view.schema)
const exporter = exporters[format] const exporter = exporters[format]
const exportedFile = exporter(headers, response.rows) const exportedFile = exporter(headers, ctx.body)
const filename = `${view.name}.${format}` const filename = `${view.name}.${format}`
fs.writeFileSync(path.join(os.tmpdir(), filename), exportedFile) fs.writeFileSync(path.join(os.tmpdir(), filename), exportedFile)
ctx.body = { ctx.body = {
url: `/api/views/export/download/${filename}`, url: `/api/views/export/download/${filename}`,
name: view.name, name: view.name,