Add new modal to backend and fix backend file structure

This commit is contained in:
Andrew Kingston 2020-10-02 12:00:17 +01:00
parent d4ebd3bb63
commit 42a7a21050
36 changed files with 562 additions and 598 deletions

View File

@ -1,8 +1,8 @@
<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 * as api from "./api" import * as api from "./api"
import Table from "./Table.svelte" import Table from "./Table.svelte"
@ -22,9 +22,9 @@
</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 />
{/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}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} linked 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,9 @@
<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"
export let view = {} export let view = {}
@ -34,9 +34,9 @@
</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}
</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,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,102 +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).filter(
field => viewModel.schema[field].type === "number"
)
function saveView() {
backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`)
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,84 @@
<script>
import { Button, Input, Select } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
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()
}
</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,19 +1,10 @@
<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"
let anchor export let onClosed
let dropdown
let name let name
let field let field
@ -36,27 +27,19 @@
field, field,
}) })
notifier.success(`View ${name} created`) notifier.success(`View ${name} created`)
dropdown.hide() onClosed()
$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

@ -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"
const CONDITIONS = [ const CONDITIONS = [
{ {
@ -50,9 +42,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
@ -62,7 +52,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()
} }
function removeFilter(idx) { function removeFilter(idx) {
@ -84,67 +74,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

@ -24,6 +24,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 +71,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 +83,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

@ -24,6 +24,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 +66,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
export let cancelText = "Cancel" export let cancelText
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

@ -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,23 @@
<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 "./portal"
import { ContextKey } from "./context"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let visible = false let visible
export let cancelText = "Cancel"
export let confirmText = "Confirm"
export let showCancelButton = true
export let showConfirmButton = true
export function show() { export function show() {
console.log("show")
if (visible) { if (visible) {
return return
} }
@ -26,6 +32,8 @@
visible = false visible = false
dispatch("hide") dispatch("hide")
} }
setContext(ContextKey, { show, hide })
</script> </script>
{#if visible} {#if visible}
@ -66,7 +74,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 +105,7 @@
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);
padding: var(--spacing-xl);
} }
</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,44 @@
<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)
function hide() {
modalContext.hide()
}
async function confirm() {
if (!onConfirm || (await onConfirm()) !== false) {
hide()
}
}
</script>
{#if showCancelButton || showConfirmButton}
<footer>
{#if showCancelButton}
<Button secondary on:click={hide}>{cancelText}</Button>
{/if}
{#if showConfirmButton}
<Button primary {...$$restProps} on:click={confirm}>{confirmText}</Button>
{/if}
</footer>
{/if}
<style>
footer {
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,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>