began UI for custom views
This commit is contained in:
parent
f62e7e06d8
commit
6862be744b
|
@ -59,7 +59,6 @@ export const getBackendUiStore = () => {
|
|||
store.update(state => {
|
||||
state.selectedModel = model
|
||||
state.draftModel = cloneDeep(model)
|
||||
state.selectedField = ""
|
||||
state.selectedView = `all_${model._id}`
|
||||
return state
|
||||
}),
|
||||
|
@ -87,9 +86,7 @@ export const getBackendUiStore = () => {
|
|||
delete: async model => {
|
||||
await api.delete(`/api/models/${model._id}/${model._rev}`)
|
||||
store.update(state => {
|
||||
state.models = state.models.filter(
|
||||
existing => existing._id !== model._id
|
||||
)
|
||||
state.models = state.models.filter(existing => existing._id !== model._id)
|
||||
state.selectedModel = state.models[0] || {}
|
||||
return state
|
||||
})
|
||||
|
@ -113,14 +110,24 @@ export const getBackendUiStore = () => {
|
|||
store.actions.models.save(state.draftModel)
|
||||
return state
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
views: {
|
||||
select: view =>
|
||||
store.update(state => {
|
||||
state.selectedView = view
|
||||
state.selectedModel = {}
|
||||
return state
|
||||
}),
|
||||
save: async view => {
|
||||
const response = await api.post(`/api/views`, view)
|
||||
const savedView = await response.json()
|
||||
await store.actions.models.fetch()
|
||||
store.update(state => {
|
||||
state.selectedView = view.name
|
||||
return state
|
||||
})
|
||||
}
|
||||
},
|
||||
users: {
|
||||
create: user =>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<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 Select from "components/common/Select.svelte"
|
||||
|
@ -10,6 +11,7 @@
|
|||
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 * as api from "./api"
|
||||
|
@ -72,6 +74,7 @@
|
|||
<ColumnPopover />
|
||||
{#if Object.keys($backendUiStore.selectedModel.schema).length > 0}
|
||||
<RowPopover />
|
||||
<ViewPopover />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -102,7 +105,7 @@
|
|||
<td>
|
||||
{#if schema[header].type === 'link'}
|
||||
<LinkedRecord field={schema[header]} ids={row[header]} />
|
||||
{:else}{row[header] || ''}{/if}
|
||||
{:else}{getOr("", header, row)}{/if}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
|
|
|
@ -2,12 +2,10 @@
|
|||
import { Input, Select } from "@budibase/bbui"
|
||||
|
||||
export let type = "text"
|
||||
export let value = ""
|
||||
export let value = type === "checkbox" ? false : ""
|
||||
export let label
|
||||
export let options = []
|
||||
|
||||
let checked = type === "checkbox" ? value : false
|
||||
|
||||
const handleInput = event => {
|
||||
if (event.target.type === "checkbox") {
|
||||
value = event.target.checked
|
||||
|
@ -38,7 +36,7 @@
|
|||
thin
|
||||
placeholder={label}
|
||||
data-cy="{label}-input"
|
||||
{checked}
|
||||
checked={value}
|
||||
{type}
|
||||
{value}
|
||||
on:input={handleInput}
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
import { backendUiStore } from "builderStore"
|
||||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import { ModelSetupNav } from "components/nav/ModelSetupNav"
|
||||
import ModelFieldEditor from "components/nav/ModelSetupNav/ModelFieldEditor.svelte"
|
||||
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
||||
|
||||
let anchor
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
import { backendUiStore } from "builderStore"
|
||||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import { ModelSetupNav } from "components/nav/ModelSetupNav"
|
||||
import ModelFieldEditor from "components/nav/ModelSetupNav/ModelFieldEditor.svelte"
|
||||
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
||||
|
||||
export let field
|
||||
|
|
|
@ -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>
|
|
@ -6,15 +6,19 @@
|
|||
export let indented
|
||||
</script>
|
||||
|
||||
<div class:selected on:click class={className}>
|
||||
<i class:indented class={icon} />
|
||||
<div class:indented class:selected on:click class={className}>
|
||||
<i class={icon} />
|
||||
<span>{title}</span>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.indented {
|
||||
margin-left: 10px;
|
||||
grid-template-columns: 50px 1fr 20px;
|
||||
}
|
||||
|
||||
.indented i {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
div {
|
||||
|
|
|
@ -8,20 +8,20 @@
|
|||
import { Button } from "@budibase/bbui"
|
||||
import CreateTablePopover from "./CreateTable.svelte"
|
||||
import EditTablePopover from "./EditTable.svelte"
|
||||
import EditViewPopover from "./EditView.svelte"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
$: selectedTab = $backendUiStore.tabs.NAVIGATION_PANEL
|
||||
|
||||
function selectModel(model, fieldId) {
|
||||
function selectModel(model) {
|
||||
backendUiStore.actions.models.select(model)
|
||||
$goto(`./model/${model._id}`)
|
||||
if (fieldId) {
|
||||
backendUiStore.update(state => {
|
||||
state.selectedField = fieldId
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
function selectView(view) {
|
||||
backendUiStore.actions.views.select(view)
|
||||
$goto(`./view/${view}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -34,12 +34,22 @@
|
|||
<div class="hierarchy-items-container">
|
||||
{#each $backendUiStore.models as model}
|
||||
<ListItem
|
||||
selected={!$backendUiStore.selectedField && model._id === $backendUiStore.selectedModel._id}
|
||||
selected={$backendUiStore.selectedView === `all_${model._id}`}
|
||||
title={model.name}
|
||||
icon="ri-table-fill"
|
||||
on:click={() => selectModel(model)}>
|
||||
<EditTablePopover table={model} />
|
||||
</ListItem>
|
||||
{#each Object.keys(model.views || {}) as view}
|
||||
<ListItem
|
||||
indented
|
||||
selected={$backendUiStore.selectedView === view}
|
||||
title={view}
|
||||
icon="ri-eye-line"
|
||||
on:click={() => selectView(view)}>
|
||||
<EditViewPopover {view} />
|
||||
</ListItem>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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
|
|
@ -1,23 +1,25 @@
|
|||
const CouchDB = require("../../../db")
|
||||
const customViewTemplate = require("./customViews");
|
||||
const statsViewTemplate = require("./viewBuilder");
|
||||
|
||||
const controller = {
|
||||
query: async ctx => {
|
||||
// const db = new CouchDB(ctx.user.instanceId)
|
||||
const db = new CouchDB("inst_4e6f424_970ca7f2b9e24ec8896eb10862d7f22b")
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const { meta } = ctx.request.body
|
||||
const response = await db.query(`database/${ctx.params.viewName}`, {
|
||||
group: false
|
||||
group: !!meta.groupBy
|
||||
})
|
||||
|
||||
for (row of response.rows) {
|
||||
row.value = {
|
||||
...row.value,
|
||||
avg: row.value.sum / row.value.count
|
||||
}
|
||||
}
|
||||
|
||||
ctx.body = response.rows
|
||||
// ctx.body = {
|
||||
// ...data,
|
||||
// avg: data.sum / data.count
|
||||
// }
|
||||
},
|
||||
fetch: async ctx => {
|
||||
// const db = new CouchDB(ctx.user.instanceId)
|
||||
const db = new CouchDB("inst_4e6f424_970ca7f2b9e24ec8896eb10862d7f22b")
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const designDoc = await db.get("_design/database")
|
||||
const response = []
|
||||
|
||||
|
@ -37,14 +39,13 @@ const controller = {
|
|||
|
||||
ctx.body = response
|
||||
},
|
||||
create: async ctx => {
|
||||
// const db = new CouchDB(ctx.user.instanceId)
|
||||
const db = new CouchDB("inst_4e6f424_970ca7f2b9e24ec8896eb10862d7f22b")
|
||||
save: async ctx => {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const newView = ctx.request.body
|
||||
|
||||
const designDoc = await db.get("_design/database")
|
||||
|
||||
const view = customViewTemplate(ctx.request.body)
|
||||
const view = statsViewTemplate(newView)
|
||||
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
|
@ -53,8 +54,17 @@ const controller = {
|
|||
|
||||
await db.put(designDoc)
|
||||
|
||||
ctx.body = newView
|
||||
ctx.message = `View ${newView.name} created successfully.`
|
||||
|
||||
// add views to model document
|
||||
const model = await db.get(ctx.request.body.modelId)
|
||||
model.views = {
|
||||
...(model.views ? model.views : {}),
|
||||
[newView.name]: view.meta
|
||||
}
|
||||
await db.put(model)
|
||||
|
||||
ctx.body = view
|
||||
ctx.message = `View ${newView.name} saved successfully.`
|
||||
},
|
||||
destroy: async ctx => {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -193,9 +193,6 @@ const createUserWithPermissions = async (
|
|||
accessLevelId: accessRes.body._id,
|
||||
})
|
||||
|
||||
//const db = new CouchDB(instanceId)
|
||||
//const designDoc = await db.get("_design/database")
|
||||
|
||||
const anonUser = {
|
||||
userId: "ANON",
|
||||
accessLevelId: ANON_LEVEL_ID,
|
||||
|
|
|
@ -4,7 +4,8 @@ const {
|
|||
createInstance,
|
||||
createModel,
|
||||
supertest,
|
||||
defaultHeaders
|
||||
defaultHeaders,
|
||||
getDocument
|
||||
} = require("./couchTestUtils")
|
||||
|
||||
describe("/views", () => {
|
||||
|
@ -14,6 +15,25 @@ describe("/views", () => {
|
|||
let instance
|
||||
let model
|
||||
|
||||
const createView = async (config = {
|
||||
name: "TestView",
|
||||
field: "Price",
|
||||
modelId: model._id
|
||||
}) =>
|
||||
await request
|
||||
.post(`/api/views`)
|
||||
.send(config)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const createRecord = async record => request
|
||||
.post(`/api/${model._id}/records`)
|
||||
.send(record)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
beforeAll(async () => {
|
||||
({ request, server } = await supertest())
|
||||
await createClientDatabase(request)
|
||||
|
@ -28,46 +48,111 @@ describe("/views", () => {
|
|||
server.close()
|
||||
})
|
||||
|
||||
const createView = async () =>
|
||||
await request
|
||||
.post(`/api/views`)
|
||||
.send({
|
||||
name: "TestView",
|
||||
map: `function(doc) {
|
||||
if (doc.id) {
|
||||
emit(doc.name, doc._id);
|
||||
}
|
||||
}`,
|
||||
reduce: `function(keys, values) { }`
|
||||
})
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
describe("create", () => {
|
||||
beforeEach(async () => {
|
||||
model = await createModel(request, app._id, instance._id);
|
||||
})
|
||||
|
||||
it("returns a success message when the view is successfully created", async () => {
|
||||
const res = await createView()
|
||||
expect(res.res.statusMessage).toEqual("View TestView created successfully.");
|
||||
expect(res.body.name).toEqual("TestView");
|
||||
expect(res.res.statusMessage).toEqual("View TestView saved successfully.");
|
||||
})
|
||||
|
||||
it("updates the model record with the new view metadata", async () => {
|
||||
const res = await createView()
|
||||
expect(res.res.statusMessage).toEqual("View TestView saved successfully.");
|
||||
const updatedModel = await getDocument(instance._id, model._id)
|
||||
expect(updatedModel.views).toEqual({
|
||||
TestView: {
|
||||
field: "Price",
|
||||
modelId: model._id
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe("fetch", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
model = await createModel(request, app._id, instance._id);
|
||||
});
|
||||
|
||||
it("should only return custom views", async () => {
|
||||
const view = await createView()
|
||||
it("returns only custom views", async () => {
|
||||
await createView()
|
||||
const res = await request
|
||||
.get(`/api/views`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
expect(res.body.length).toBe(1)
|
||||
expect(res.body.find(v => v.name === view.body.name)).toBeDefined()
|
||||
expect(res.body.find(({ name }) => name === "TestView")).toBeDefined()
|
||||
})
|
||||
});
|
||||
|
||||
describe("query", () => {
|
||||
beforeEach(async () => {
|
||||
model = await createModel(request, app._id, instance._id);
|
||||
});
|
||||
|
||||
it("returns data for the created view", async () => {
|
||||
await createView()
|
||||
await createRecord({
|
||||
modelId: model._id,
|
||||
Price: 1000
|
||||
})
|
||||
await createRecord({
|
||||
modelId: model._id,
|
||||
Price: 2000
|
||||
})
|
||||
await createRecord({
|
||||
modelId: model._id,
|
||||
Price: 4000
|
||||
})
|
||||
const res = await request
|
||||
.post(`/api/views/query/TestView`)
|
||||
.send({
|
||||
meta: {}
|
||||
})
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
expect(res.body.length).toBe(1)
|
||||
expect(res.body).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("returns data for the created view using a group by", async () => {
|
||||
await createView({
|
||||
name: "TestView",
|
||||
field: "Price",
|
||||
groupBy: "Category",
|
||||
modelId: model._id
|
||||
})
|
||||
await createRecord({
|
||||
modelId: model._id,
|
||||
Price: 1000,
|
||||
Category: "One"
|
||||
})
|
||||
await createRecord({
|
||||
modelId: model._id,
|
||||
Price: 2000,
|
||||
Category: "One"
|
||||
})
|
||||
await createRecord({
|
||||
modelId: model._id,
|
||||
Price: 4000,
|
||||
Category: "Two"
|
||||
})
|
||||
const res = await request
|
||||
.post(`/api/views/query/TestView`)
|
||||
.send({
|
||||
meta: {
|
||||
groupBy: "Category"
|
||||
}
|
||||
})
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
expect(res.body.length).toBe(2)
|
||||
expect(res.body).toMatchSnapshot()
|
||||
})
|
||||
});
|
||||
});
|
|
@ -13,9 +13,7 @@ router
|
|||
recordController.fetchView
|
||||
)
|
||||
.get("/api/views", viewController.fetch)
|
||||
.get("/api/views/query/:viewName", viewController.query)
|
||||
// .patch("/api/:databaseId/views", controller.update);
|
||||
// .delete("/api/:instanceId/views/:viewId/:revId", controller.destroy);
|
||||
.post("/api/views", viewController.create)
|
||||
.post("/api/views/query/:viewName", viewController.query)
|
||||
.post("/api/views", viewController.save)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -14,6 +14,10 @@ module.exports = async (ctx, next) => {
|
|||
return
|
||||
}
|
||||
|
||||
// ctx.user = {
|
||||
// instanceId: "inst_4e6f424_970ca7f2b9e24ec8896eb10862d7f22b"
|
||||
// }
|
||||
|
||||
const appToken = ctx.cookies.get("budibase:token")
|
||||
const builderToken = ctx.cookies.get("builder:token")
|
||||
|
||||
|
|
Loading…
Reference in New Issue