forked from tsai/budibase
21 changed files with 427 additions and 414 deletions
@ -0,0 +1,41 @@ |
|||||
|
<script> |
||||
|
import { Popover, Button, Icon, Input, Select } from "@budibase/bbui" |
||||
|
import { backendUiStore } from "builderStore" |
||||
|
import CreateEditRecord from "../modals/CreateEditRecord.svelte" |
||||
|
|
||||
|
let anchor |
||||
|
let dropdown |
||||
|
|
||||
|
let name |
||||
|
|
||||
|
function saveView() { |
||||
|
backendUiStore.actions.views.save({ |
||||
|
name, |
||||
|
modelId: $backendUiStore.selectedModel._id |
||||
|
}) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div bind:this={anchor}> |
||||
|
<Button text small on:click={dropdown.show}> |
||||
|
<Icon name="addrow" /> |
||||
|
Create New View |
||||
|
</Button> |
||||
|
</div> |
||||
|
<Popover bind:this={dropdown} {anchor} align="left"> |
||||
|
<h5>Create View</h5> |
||||
|
<div class="input-group-column"> |
||||
|
<Input placeholder="View Name" thin bind:value={name} /> |
||||
|
</div> |
||||
|
<div class="button-group"> |
||||
|
<Button secondary on:click={dropdown.hide}>Cancel</Button> |
||||
|
<Button primary on:click={saveView}>Save View</Button> |
||||
|
</div> |
||||
|
</Popover> |
||||
|
|
||||
|
<style> |
||||
|
h5 { |
||||
|
margin-bottom: var(--spacing-l); |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,137 @@ |
|||||
|
<script> |
||||
|
import { getContext } from "svelte" |
||||
|
import { backendUiStore } from "builderStore" |
||||
|
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" |
||||
|
import { FIELDS } from "constants/backend" |
||||
|
import DeleteTableModal from "components/database/ModelDataTable/modals/DeleteTable.svelte" |
||||
|
|
||||
|
const { open, close } = getContext("simple-modal") |
||||
|
|
||||
|
export let table |
||||
|
|
||||
|
let anchor |
||||
|
let dropdown |
||||
|
|
||||
|
let editing |
||||
|
|
||||
|
function showEditor() { |
||||
|
editing = true |
||||
|
} |
||||
|
|
||||
|
function hideEditor() { |
||||
|
dropdown.hide() |
||||
|
editing = false |
||||
|
close() |
||||
|
} |
||||
|
|
||||
|
const deleteTable = () => { |
||||
|
open( |
||||
|
DeleteTableModal, |
||||
|
{ |
||||
|
onClosed: close, |
||||
|
table, |
||||
|
}, |
||||
|
{ styleContent: { padding: "0" } } |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function save() { |
||||
|
backendUiStore.actions.models.save(table) |
||||
|
hideEditor() |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div bind:this={anchor} on:click={dropdown.show}> |
||||
|
<i class="ri-more-line" /> |
||||
|
</div> |
||||
|
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
||||
|
{#if editing} |
||||
|
<h5>Edit View</h5> |
||||
|
<div class="container"> |
||||
|
<Input placeholder="Table Name" thin bind:value={table.name} /> |
||||
|
</div> |
||||
|
<footer> |
||||
|
<div class="button-margin-3"> |
||||
|
<Button secondary on:click={hideEditor}>Cancel</Button> |
||||
|
</div> |
||||
|
<div class="button-margin-4"> |
||||
|
<Button primary on:click={save}>Save</Button> |
||||
|
</div> |
||||
|
</footer> |
||||
|
{:else} |
||||
|
<ul> |
||||
|
<li on:click={showEditor}> |
||||
|
<Icon name="edit" /> |
||||
|
Edit |
||||
|
</li> |
||||
|
<li data-cy="delete-table" on:click={deleteTable}> |
||||
|
<Icon name="delete" /> |
||||
|
Delete |
||||
|
</li> |
||||
|
</ul> |
||||
|
{/if} |
||||
|
</DropdownMenu> |
||||
|
|
||||
|
<style> |
||||
|
h5 { |
||||
|
padding: var(--spacing-xl) 0 0 var(--spacing-xl); |
||||
|
margin: 0; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.container { |
||||
|
padding: var(--spacing-xl); |
||||
|
} |
||||
|
|
||||
|
ul { |
||||
|
padding: var(--spacing-xl) 0 0 var(--spacing-xl); |
||||
|
list-style: none; |
||||
|
padding-left: 0; |
||||
|
margin: 0; |
||||
|
padding: var(--spacing-s) 0; |
||||
|
} |
||||
|
|
||||
|
li { |
||||
|
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 0px; |
||||
|
align-items: center; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
li:hover { |
||||
|
background-color: var(--grey-2); |
||||
|
} |
||||
|
|
||||
|
li:active { |
||||
|
color: var(--blue); |
||||
|
} |
||||
|
|
||||
|
footer { |
||||
|
padding: 20px; |
||||
|
display: grid; |
||||
|
grid-template-columns: 1fr 1fr 1fr 1fr; |
||||
|
gap: 20px; |
||||
|
background: var(--grey-1); |
||||
|
border-bottom-left-radius: 0.5rem; |
||||
|
border-bottom-left-radius: 0.5rem; |
||||
|
} |
||||
|
|
||||
|
.button-margin-1 { |
||||
|
grid-column-start: 1; |
||||
|
display: grid; |
||||
|
} |
||||
|
|
||||
|
.button-margin-3 { |
||||
|
grid-column-start: 3; |
||||
|
display: grid; |
||||
|
} |
||||
|
|
||||
|
.button-margin-4 { |
||||
|
grid-column-start: 4; |
||||
|
display: grid; |
||||
|
} |
||||
|
</style> |
||||
@ -1,120 +0,0 @@ |
|||||
<script> |
|
||||
import { backendUiStore } from "builderStore" |
|
||||
import { Button } from "@budibase/bbui" |
|
||||
import Dropdown from "components/common/Dropdown.svelte" |
|
||||
import Textbox from "components/common/Textbox.svelte" |
|
||||
import ButtonGroup from "components/common/ButtonGroup.svelte" |
|
||||
import NumberBox from "components/common/NumberBox.svelte" |
|
||||
import ValuesList from "components/common/ValuesList.svelte" |
|
||||
import ErrorsBox from "components/common/ErrorsBox.svelte" |
|
||||
import Checkbox from "components/common/Checkbox.svelte" |
|
||||
import ActionButton from "components/common/ActionButton.svelte" |
|
||||
import DatePicker from "components/common/DatePicker.svelte" |
|
||||
import { keys, cloneDeep } from "lodash/fp" |
|
||||
|
|
||||
const FIELD_TYPES = ["string", "number", "boolean", "link"] |
|
||||
|
|
||||
let field = {} |
|
||||
|
|
||||
$: field = |
|
||||
$backendUiStore.draftModel.schema[$backendUiStore.selectedField] || {} |
|
||||
$: required = |
|
||||
field.constraints && |
|
||||
field.constraints.presence && |
|
||||
!field.constraints.presence.allowEmpty |
|
||||
</script> |
|
||||
|
|
||||
<div class="info"> |
|
||||
<div class="field-box"> |
|
||||
<header>Name</header> |
|
||||
<input class="budibase__input" type="text" bind:value={field.name} /> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<div class="info"> |
|
||||
<div class="field-box"> |
|
||||
<header>Type</header> |
|
||||
<span>{field.type}</span> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<div class="info"> |
|
||||
<div class="field"> |
|
||||
<label>Required</label> |
|
||||
<input |
|
||||
type="checkbox" |
|
||||
bind:checked={required} |
|
||||
on:change={() => (field.constraints.presence.allowEmpty = required)} /> |
|
||||
</div> |
|
||||
|
|
||||
{#if field.type === 'string'} |
|
||||
<NumberBox |
|
||||
label="Max Length" |
|
||||
bind:value={field.constraints.length.maximum} /> |
|
||||
<ValuesList label="Categories" bind:values={field.constraints.inclusion} /> |
|
||||
{:else if field.type === 'datetime'} |
|
||||
<DatePicker |
|
||||
label="Min Value" |
|
||||
bind:value={field.constraints.datetime.earliest} /> |
|
||||
<DatePicker |
|
||||
label="Max Value" |
|
||||
bind:value={field.constraints.datetime.latest} /> |
|
||||
{:else if field.type === 'number'} |
|
||||
<NumberBox |
|
||||
label="Min Value" |
|
||||
bind:value={field.constraints.numericality.greaterThanOrEqualTo} /> |
|
||||
<NumberBox |
|
||||
label="Max Value" |
|
||||
bind:value={field.constraints.numericality.lessThanOrEqualTo} /> |
|
||||
{:else if field.type === 'link'} |
|
||||
<div class="field"> |
|
||||
<label>Link</label> |
|
||||
<select class="budibase__input" bind:value={field.modelId}> |
|
||||
<option value={''} /> |
|
||||
{#each $backendUiStore.models as model} |
|
||||
{#if model._id !== $backendUiStore.draftModel._id} |
|
||||
<option value={model._id}>{model.name}</option> |
|
||||
{/if} |
|
||||
{/each} |
|
||||
</select> |
|
||||
</div> |
|
||||
{/if} |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.info { |
|
||||
margin-bottom: 16px; |
|
||||
border-radius: 5px; |
|
||||
} |
|
||||
|
|
||||
label { |
|
||||
font-size: 14px; |
|
||||
font-weight: 500; |
|
||||
margin-bottom: 8px; |
|
||||
} |
|
||||
|
|
||||
.field { |
|
||||
display: grid; |
|
||||
align-items: center; |
|
||||
margin-bottom: 16px; |
|
||||
} |
|
||||
|
|
||||
.field-box header { |
|
||||
font-size: 14px; |
|
||||
font-weight: 500; |
|
||||
margin-bottom: 8px; |
|
||||
} |
|
||||
|
|
||||
.field-box span { |
|
||||
background: var(--grey-2); |
|
||||
color: var(--grey-6); |
|
||||
font-weight: 400; |
|
||||
height: 36px; |
|
||||
display: grid; |
|
||||
align-items: center; |
|
||||
padding-left: 12px; |
|
||||
text-transform: capitalize; |
|
||||
border-radius: 5px; |
|
||||
cursor: not-allowed; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,156 +0,0 @@ |
|||||
<script> |
|
||||
import { getContext, onMount } from "svelte" |
|
||||
import { Button, Switcher } from "@budibase/bbui" |
|
||||
import { notifier } from "builderStore/store/notifications" |
|
||||
import { store, backendUiStore } from "builderStore" |
|
||||
import api from "builderStore/api" |
|
||||
import ModelFieldEditor from "./ModelFieldEditor.svelte" |
|
||||
|
|
||||
const { open, close } = getContext("simple-modal") |
|
||||
|
|
||||
const ITEMS = [ |
|
||||
{ |
|
||||
title: "Setup", |
|
||||
key: "SETUP", |
|
||||
}, |
|
||||
{ |
|
||||
title: "Delete", |
|
||||
key: "DELETE", |
|
||||
}, |
|
||||
] |
|
||||
|
|
||||
let edited = false |
|
||||
|
|
||||
$: selectedTab = $backendUiStore.tabs.SETUP_PANEL |
|
||||
|
|
||||
$: edited = |
|
||||
$backendUiStore.selectedField || |
|
||||
($backendUiStore.draftModel && |
|
||||
$backendUiStore.draftModel.name !== $backendUiStore.selectedModel.name) |
|
||||
|
|
||||
async function deleteModel() { |
|
||||
const model = $backendUiStore.selectedModel |
|
||||
const field = $backendUiStore.selectedField |
|
||||
|
|
||||
if (field) { |
|
||||
const name = model.schema[field].name |
|
||||
delete model.schema[field] |
|
||||
backendUiStore.actions.models.save(model) |
|
||||
notifier.danger(`Field ${name} deleted.`) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
const DELETE_MODEL_URL = `/api/models/${model._id}/${model._rev}` |
|
||||
const response = await api.delete(DELETE_MODEL_URL) |
|
||||
backendUiStore.update(state => { |
|
||||
state.selectedView = null |
|
||||
state.selectedModel = {} |
|
||||
state.draftModel = {} |
|
||||
state.models = state.models.filter(({ _id }) => _id !== model._id) |
|
||||
notifier.danger(`${model.name} deleted successfully.`) |
|
||||
return state |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
function validate() { |
|
||||
let errors = [] |
|
||||
for (let field of Object.values($backendUiStore.draftModel.schema)) { |
|
||||
const restrictedFieldNames = ["type", "modelId"] |
|
||||
if (field.name.startsWith("_")) { |
|
||||
errors.push(`field '${field.name}' - name cannot begin with '_''`) |
|
||||
} else if (restrictedFieldNames.includes(field.name)) { |
|
||||
errors.push( |
|
||||
`field '${field.name}' - is a restricted name, please rename` |
|
||||
) |
|
||||
} else if (!field.name || !field.name.trim()) { |
|
||||
errors.push("field name cannot be blank") |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (!$backendUiStore.draftModel.name) { |
|
||||
errors.push("Table name cannot be blank") |
|
||||
} |
|
||||
|
|
||||
return errors |
|
||||
} |
|
||||
|
|
||||
async function saveModel() { |
|
||||
const errors = validate() |
|
||||
if (errors.length > 0) { |
|
||||
notifier.danger(errors.join("/n")) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
await backendUiStore.actions.models.save($backendUiStore.draftModel) |
|
||||
notifier.success( |
|
||||
"Success! Your changes have been saved. Please continue on with your greatness." |
|
||||
) |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div class="items-root"> |
|
||||
<Switcher headings={ITEMS} bind:value={$backendUiStore.tabs.SETUP_PANEL}> |
|
||||
{#if selectedTab === 'SETUP'} |
|
||||
{#if $backendUiStore.selectedField} |
|
||||
<ModelFieldEditor /> |
|
||||
{:else if $backendUiStore.draftModel.schema} |
|
||||
<div class="titled-input"> |
|
||||
<header>Name</header> |
|
||||
<input |
|
||||
data-cy="table-name-input" |
|
||||
type="text" |
|
||||
class="budibase__input" |
|
||||
bind:value={$backendUiStore.draftModel.name} /> |
|
||||
</div> |
|
||||
<!-- dont have this capability yet.. |
|
||||
<div class="titled-input"> |
|
||||
<header>Import Data</header> |
|
||||
<Button wide secondary>Import CSV</Button> |
|
||||
</div> |
|
||||
--> |
|
||||
{/if} |
|
||||
<footer> |
|
||||
<Button disabled={!edited} green={edited} wide on:click={saveModel}> |
|
||||
Save |
|
||||
</Button> |
|
||||
</footer> |
|
||||
{:else if selectedTab === 'DELETE'} |
|
||||
<div class="titled-input"> |
|
||||
<header>Danger Zone</header> |
|
||||
<Button red wide on:click={deleteModel}>Delete</Button> |
|
||||
</div> |
|
||||
{/if} |
|
||||
</Switcher> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
header { |
|
||||
font-weight: 500; |
|
||||
} |
|
||||
|
|
||||
footer { |
|
||||
width: 260px; |
|
||||
position: fixed; |
|
||||
bottom: 20px; |
|
||||
} |
|
||||
|
|
||||
.items-root { |
|
||||
padding: 20px; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
max-height: 100%; |
|
||||
height: 100%; |
|
||||
background-color: var(--white); |
|
||||
} |
|
||||
|
|
||||
.titled-input { |
|
||||
margin-bottom: 16px; |
|
||||
display: grid; |
|
||||
} |
|
||||
|
|
||||
.titled-input header { |
|
||||
display: block; |
|
||||
font-size: 14px; |
|
||||
margin-bottom: 8px; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1 +0,0 @@ |
|||||
export { default as ModelSetupNav } from "./ModelSetupNav.svelte" |
|
||||
@ -1,38 +0,0 @@ |
|||||
<script> |
|
||||
import { getContext } from "svelte" |
|
||||
import { store, backendUiStore } from "builderStore" |
|
||||
import * as api from "components/database/ModelDataTable/api" |
|
||||
|
|
||||
import ModelNavigator from "components/nav/ModelNavigator/ModelNavigator.svelte" |
|
||||
import { ModelSetupNav } from "components/nav/ModelSetupNav" |
|
||||
</script> |
|
||||
|
|
||||
<div class="root"> |
|
||||
<div class="nav"> |
|
||||
<ModelNavigator /> |
|
||||
</div> |
|
||||
<div class="content"> |
|
||||
<slot /> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.root { |
|
||||
height: 100%; |
|
||||
display: grid; |
|
||||
grid-template-columns: 300px minmax(0, 1fr); |
|
||||
background: var(--grey-1); |
|
||||
line-height: 1; |
|
||||
} |
|
||||
|
|
||||
.content { |
|
||||
flex: 1 1 auto; |
|
||||
margin: 20px 40px; |
|
||||
} |
|
||||
|
|
||||
.nav { |
|
||||
flex: 0 1 auto; |
|
||||
width: 300px; |
|
||||
height: 100%; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,27 +0,0 @@ |
|||||
const FORMULA_MAP = { |
|
||||
SUM: "_sum", |
|
||||
COUNT: "_count", |
|
||||
STATS: "_stats" |
|
||||
}; |
|
||||
|
|
||||
function customViewTemplate({ |
|
||||
field, |
|
||||
formula, |
|
||||
modelId |
|
||||
}) { |
|
||||
return { |
|
||||
meta: { |
|
||||
field, |
|
||||
formula, |
|
||||
modelId |
|
||||
}, |
|
||||
map: `function (doc) {
|
|
||||
if (doc.modelId === "${modelId}") { |
|
||||
emit(doc._id, doc["${field}"]); |
|
||||
} |
|
||||
}`,
|
|
||||
reduce: "_stats" |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
module.exports = customViewTemplate |
|
||||
@ -0,0 +1,21 @@ |
|||||
|
function statsViewTemplate({ |
||||
|
field, |
||||
|
modelId, |
||||
|
groupBy |
||||
|
}) { |
||||
|
return { |
||||
|
meta: { |
||||
|
field, |
||||
|
modelId, |
||||
|
groupBy |
||||
|
}, |
||||
|
map: `function (doc) {
|
||||
|
if (doc.modelId === "${modelId}") { |
||||
|
emit(doc["${groupBy || "_id"}"], doc["${field}"]); |
||||
|
} |
||||
|
}`,
|
||||
|
reduce: "_stats" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = statsViewTemplate |
||||
@ -0,0 +1,44 @@ |
|||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
|
||||
|
exports[`/views query returns data for the created view 1`] = ` |
||||
|
Array [ |
||||
|
Object { |
||||
|
"key": null, |
||||
|
"value": Object { |
||||
|
"avg": 2333.3333333333335, |
||||
|
"count": 3, |
||||
|
"max": 4000, |
||||
|
"min": 1000, |
||||
|
"sum": 7000, |
||||
|
"sumsqr": 21000000, |
||||
|
}, |
||||
|
}, |
||||
|
] |
||||
|
`; |
||||
|
|
||||
|
exports[`/views query returns data for the created view using a group by 1`] = ` |
||||
|
Array [ |
||||
|
Object { |
||||
|
"key": "One", |
||||
|
"value": Object { |
||||
|
"avg": 1500, |
||||
|
"count": 2, |
||||
|
"max": 2000, |
||||
|
"min": 1000, |
||||
|
"sum": 3000, |
||||
|
"sumsqr": 5000000, |
||||
|
}, |
||||
|
}, |
||||
|
Object { |
||||
|
"key": "Two", |
||||
|
"value": Object { |
||||
|
"avg": 4000, |
||||
|
"count": 1, |
||||
|
"max": 4000, |
||||
|
"min": 4000, |
||||
|
"sum": 4000, |
||||
|
"sumsqr": 16000000, |
||||
|
}, |
||||
|
}, |
||||
|
] |
||||
|
`; |
||||
Loading…
Reference in new issue