began UI for custom views

This commit is contained in:
Martin McKeaveney 2020-08-14 16:31:53 +01:00
parent bf46067fe1
commit 6ad5ebbe06
21 changed files with 427 additions and 414 deletions

View File

@ -59,7 +59,6 @@ export const getBackendUiStore = () => {
store.update(state => { store.update(state => {
state.selectedModel = model state.selectedModel = model
state.draftModel = cloneDeep(model) state.draftModel = cloneDeep(model)
state.selectedField = ""
state.selectedView = `all_${model._id}` state.selectedView = `all_${model._id}`
return state return state
}), }),
@ -87,9 +86,7 @@ export const getBackendUiStore = () => {
delete: async model => { delete: async model => {
await api.delete(`/api/models/${model._id}/${model._rev}`) await api.delete(`/api/models/${model._id}/${model._rev}`)
store.update(state => { store.update(state => {
state.models = state.models.filter( state.models = state.models.filter(existing => existing._id !== model._id)
existing => existing._id !== model._id
)
state.selectedModel = state.models[0] || {} state.selectedModel = state.models[0] || {}
return state return state
}) })
@ -113,14 +110,24 @@ export const getBackendUiStore = () => {
store.actions.models.save(state.draftModel) store.actions.models.save(state.draftModel)
return state return state
}) })
}, }
}, },
views: { views: {
select: view => select: view =>
store.update(state => { store.update(state => {
state.selectedView = view state.selectedView = view
state.selectedModel = {}
return state 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: { users: {
create: user => create: user =>

View File

@ -1,6 +1,7 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import fsort from "fast-sort" import fsort from "fast-sort"
import getOr from "lodash/fp/getOr"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import { Button, Icon } from "@budibase/bbui" import { Button, Icon } from "@budibase/bbui"
import Select from "components/common/Select.svelte" import Select from "components/common/Select.svelte"
@ -10,6 +11,7 @@
import { DeleteRecordModal, CreateEditRecordModal } from "./modals" import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import RowPopover from "./popovers/Row.svelte" import RowPopover from "./popovers/Row.svelte"
import ColumnPopover from "./popovers/Column.svelte" import ColumnPopover from "./popovers/Column.svelte"
import ViewPopover from "./popovers/View.svelte"
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte" import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte"
import EditRowPopover from "./popovers/EditRow.svelte" import EditRowPopover from "./popovers/EditRow.svelte"
import * as api from "./api" import * as api from "./api"
@ -72,6 +74,7 @@
<ColumnPopover /> <ColumnPopover />
{#if Object.keys($backendUiStore.selectedModel.schema).length > 0} {#if Object.keys($backendUiStore.selectedModel.schema).length > 0}
<RowPopover /> <RowPopover />
<ViewPopover />
{/if} {/if}
</div> </div>
</div> </div>
@ -102,7 +105,7 @@
<td> <td>
{#if schema[header].type === 'link'} {#if schema[header].type === 'link'}
<LinkedRecord field={schema[header]} ids={row[header]} /> <LinkedRecord field={schema[header]} ids={row[header]} />
{:else}{row[header] || ''}{/if} {:else}{getOr("", header, row)}{/if}
</td> </td>
{/each} {/each}
</tr> </tr>

View File

@ -2,12 +2,10 @@
import { Input, Select } from "@budibase/bbui" import { Input, Select } from "@budibase/bbui"
export let type = "text" export let type = "text"
export let value = "" export let value = type === "checkbox" ? false : ""
export let label export let label
export let options = [] export let options = []
let checked = type === "checkbox" ? value : false
const handleInput = event => { const handleInput = event => {
if (event.target.type === "checkbox") { if (event.target.type === "checkbox") {
value = event.target.checked value = event.target.checked
@ -38,7 +36,7 @@
thin thin
placeholder={label} placeholder={label}
data-cy="{label}-input" data-cy="{label}-input"
{checked} checked={value}
{type} {type}
{value} {value}
on:input={handleInput} on:input={handleInput}

View File

@ -2,8 +2,6 @@
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 { ModelSetupNav } from "components/nav/ModelSetupNav"
import ModelFieldEditor from "components/nav/ModelSetupNav/ModelFieldEditor.svelte"
import CreateEditColumn from "../modals/CreateEditColumn.svelte" import CreateEditColumn from "../modals/CreateEditColumn.svelte"
let anchor let anchor

View File

@ -2,8 +2,6 @@
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 { ModelSetupNav } from "components/nav/ModelSetupNav"
import ModelFieldEditor from "components/nav/ModelSetupNav/ModelFieldEditor.svelte"
import CreateEditColumn from "../modals/CreateEditColumn.svelte" import CreateEditColumn from "../modals/CreateEditColumn.svelte"
export let field export let field

View File

@ -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>

View File

@ -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>

View File

@ -6,15 +6,19 @@
export let indented export let indented
</script> </script>
<div class:selected on:click class={className}> <div class:indented class:selected on:click class={className}>
<i class:indented class={icon} /> <i class={icon} />
<span>{title}</span> <span>{title}</span>
<slot /> <slot />
</div> </div>
<style> <style>
.indented { .indented {
margin-left: 10px; grid-template-columns: 50px 1fr 20px;
}
.indented i {
justify-self: end;
} }
div { div {

View File

@ -8,20 +8,20 @@
import { Button } from "@budibase/bbui" 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"
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")
$: selectedTab = $backendUiStore.tabs.NAVIGATION_PANEL $: selectedTab = $backendUiStore.tabs.NAVIGATION_PANEL
function selectModel(model, fieldId) { function selectModel(model) {
backendUiStore.actions.models.select(model) backendUiStore.actions.models.select(model)
$goto(`./model/${model._id}`) $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> </script>
@ -34,12 +34,22 @@
<div class="hierarchy-items-container"> <div class="hierarchy-items-container">
{#each $backendUiStore.models as model} {#each $backendUiStore.models as model}
<ListItem <ListItem
selected={!$backendUiStore.selectedField && model._id === $backendUiStore.selectedModel._id} selected={$backendUiStore.selectedView === `all_${model._id}`}
title={model.name} title={model.name}
icon="ri-table-fill" icon="ri-table-fill"
on:click={() => selectModel(model)}> on:click={() => selectModel(model)}>
<EditTablePopover table={model} /> <EditTablePopover table={model} />
</ListItem> </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} {/each}
</div> </div>
</div> </div>

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -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>

View File

@ -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

View File

@ -1,23 +1,25 @@
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
const customViewTemplate = require("./customViews"); const statsViewTemplate = require("./viewBuilder");
const controller = { const controller = {
query: async ctx => { query: async ctx => {
// const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
const db = new CouchDB("inst_4e6f424_970ca7f2b9e24ec8896eb10862d7f22b") const { meta } = ctx.request.body
const response = await db.query(`database/${ctx.params.viewName}`, { 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 = response.rows
// ctx.body = {
// ...data,
// avg: data.sum / data.count
// }
}, },
fetch: async ctx => { fetch: async ctx => {
// const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
const db = new CouchDB("inst_4e6f424_970ca7f2b9e24ec8896eb10862d7f22b")
const designDoc = await db.get("_design/database") const designDoc = await db.get("_design/database")
const response = [] const response = []
@ -37,14 +39,13 @@ const controller = {
ctx.body = response ctx.body = response
}, },
create: async ctx => { save: async ctx => {
// const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
const db = new CouchDB("inst_4e6f424_970ca7f2b9e24ec8896eb10862d7f22b")
const newView = ctx.request.body const newView = ctx.request.body
const designDoc = await db.get("_design/database") const designDoc = await db.get("_design/database")
const view = customViewTemplate(ctx.request.body) const view = statsViewTemplate(newView)
designDoc.views = { designDoc.views = {
...designDoc.views, ...designDoc.views,
@ -53,8 +54,17 @@ const controller = {
await db.put(designDoc) 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 => { destroy: async ctx => {
const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)

View File

@ -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

View File

@ -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,
},
},
]
`;

View File

@ -193,9 +193,6 @@ const createUserWithPermissions = async (
accessLevelId: accessRes.body._id, accessLevelId: accessRes.body._id,
}) })
//const db = new CouchDB(instanceId)
//const designDoc = await db.get("_design/database")
const anonUser = { const anonUser = {
userId: "ANON", userId: "ANON",
accessLevelId: ANON_LEVEL_ID, accessLevelId: ANON_LEVEL_ID,

View File

@ -4,7 +4,8 @@ const {
createInstance, createInstance,
createModel, createModel,
supertest, supertest,
defaultHeaders defaultHeaders,
getDocument
} = require("./couchTestUtils") } = require("./couchTestUtils")
describe("/views", () => { describe("/views", () => {
@ -14,6 +15,25 @@ describe("/views", () => {
let instance let instance
let model 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 () => { beforeAll(async () => {
({ request, server } = await supertest()) ({ request, server } = await supertest())
await createClientDatabase(request) await createClientDatabase(request)
@ -28,46 +48,111 @@ describe("/views", () => {
server.close() 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", () => { describe("create", () => {
beforeEach(async () => {
model = await createModel(request, app._id, instance._id);
})
it("returns a success message when the view is successfully created", async () => { it("returns a success message when the view is successfully created", async () => {
const res = await createView() const res = await createView()
expect(res.res.statusMessage).toEqual("View TestView created successfully."); expect(res.res.statusMessage).toEqual("View TestView saved successfully.");
expect(res.body.name).toEqual("TestView"); })
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", () => { describe("fetch", () => {
beforeEach(async () => { beforeEach(async () => {
model = await createModel(request, app._id, instance._id); model = await createModel(request, app._id, instance._id);
}); });
it("should only return custom views", async () => { it("returns only custom views", async () => {
const view = await createView() await createView()
const res = await request const res = await request
.get(`/api/views`) .get(`/api/views`)
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
expect(res.body.length).toBe(1) 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()
}) })
}); });
}); });

View File

@ -13,9 +13,7 @@ router
recordController.fetchView recordController.fetchView
) )
.get("/api/views", viewController.fetch) .get("/api/views", viewController.fetch)
.get("/api/views/query/:viewName", viewController.query) .post("/api/views/query/:viewName", viewController.query)
// .patch("/api/:databaseId/views", controller.update); .post("/api/views", viewController.save)
// .delete("/api/:instanceId/views/:viewId/:revId", controller.destroy);
.post("/api/views", viewController.create)
module.exports = router module.exports = router

View File

@ -14,6 +14,10 @@ module.exports = async (ctx, next) => {
return return
} }
// ctx.user = {
// instanceId: "inst_4e6f424_970ca7f2b9e24ec8896eb10862d7f22b"
// }
const appToken = ctx.cookies.get("budibase:token") const appToken = ctx.cookies.get("budibase:token")
const builderToken = ctx.cookies.get("builder:token") const builderToken = ctx.cookies.get("builder:token")