commit
2cea4be763
|
@ -0,0 +1,85 @@
|
|||
context('Create a View', () => {
|
||||
before(() => {
|
||||
cy.visit('localhost:4001/_builder')
|
||||
cy.createApp('View App', 'View App Description')
|
||||
cy.createTable('data')
|
||||
cy.addColumn('data', 'group', 'Plain Text')
|
||||
cy.addColumn('data', 'age', 'Number')
|
||||
cy.addColumn('data', 'rating', 'Number')
|
||||
|
||||
cy.addRecord(["Students", 25, 1])
|
||||
cy.addRecord(["Students", 20, 3])
|
||||
cy.addRecord(["Students", 18, 6])
|
||||
cy.addRecord(["Students", 25, 2])
|
||||
cy.addRecord(["Teachers", 49, 5])
|
||||
cy.addRecord(["Teachers", 36, 3])
|
||||
})
|
||||
|
||||
|
||||
it('creates a stats view based on age', () => {
|
||||
cy.contains("Create New View").click()
|
||||
cy.get("[placeholder='View Name']").type("Test View")
|
||||
cy.contains("Save View").click()
|
||||
cy.get("thead th").should(($headers) => {
|
||||
expect($headers).to.have.length(7)
|
||||
const headers = $headers.map((i, header) => Cypress.$(header).text())
|
||||
expect(headers.get()).to.deep.eq([
|
||||
"group",
|
||||
"sum",
|
||||
"min",
|
||||
"max",
|
||||
"sumsqr",
|
||||
"count",
|
||||
"avg",
|
||||
])
|
||||
})
|
||||
cy.get("tbody td").should(($values) => {
|
||||
const values = $values.map((i, value) => Cypress.$(value).text())
|
||||
expect(values.get()).to.deep.eq([
|
||||
"null",
|
||||
"173",
|
||||
"18",
|
||||
"49",
|
||||
"5671",
|
||||
"6",
|
||||
"28.833333333333332"
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('groups the stats view by group', () => {
|
||||
cy.contains("Group By").click()
|
||||
cy.get("select").select("group")
|
||||
cy.contains("Save").click()
|
||||
cy.contains("Students").should("be.visible")
|
||||
cy.contains("Teachers").should("be.visible")
|
||||
|
||||
cy.get("tbody tr").first().find("td").should(($values) => {
|
||||
const values = $values.map((i, value) => Cypress.$(value).text())
|
||||
expect(values.get()).to.deep.eq([
|
||||
"Students",
|
||||
"88",
|
||||
"18",
|
||||
"25",
|
||||
"1974",
|
||||
"4",
|
||||
"22"
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('renames a view', () => {
|
||||
cy.contains("[data-cy=model-nav-item]", "Test View").find(".ri-more-line").click()
|
||||
cy.contains("Edit").click()
|
||||
cy.get("[placeholder='View Name']").type(" Updated")
|
||||
cy.contains("Save").click()
|
||||
cy.contains("Test View Updated").should("be.visible")
|
||||
})
|
||||
|
||||
it('deletes a view', () => {
|
||||
cy.contains("[data-cy=model-nav-item]", "Test View Updated").find(".ri-more-line").click()
|
||||
cy.contains("Delete").click()
|
||||
cy.get(".content").contains("button", "Delete").click()
|
||||
cy.contains("TestView Updated").should("not.be.visible")
|
||||
})
|
||||
})
|
|
@ -57,7 +57,7 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.23.1",
|
||||
"@budibase/bbui": "^1.24.1",
|
||||
"@budibase/client": "^0.1.17",
|
||||
"@budibase/colorpicker": "^1.0.1",
|
||||
"@sentry/browser": "5.19.1",
|
||||
|
|
|
@ -23,13 +23,10 @@ export const getBackendUiStore = () => {
|
|||
database: {
|
||||
select: async db => {
|
||||
const modelsResponse = await api.get(`/api/models`)
|
||||
const viewsResponse = await api.get(`/api/views`)
|
||||
const models = await modelsResponse.json()
|
||||
const views = await viewsResponse.json()
|
||||
store.update(state => {
|
||||
state.selectedDatabase = db
|
||||
state.models = models
|
||||
state.views = views
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -59,8 +56,7 @@ export const getBackendUiStore = () => {
|
|||
store.update(state => {
|
||||
state.selectedModel = model
|
||||
state.draftModel = cloneDeep(model)
|
||||
state.selectedField = ""
|
||||
state.selectedView = `all_${model._id}`
|
||||
state.selectedView = { name: `all_${model._id}` }
|
||||
return state
|
||||
}),
|
||||
save: async model => {
|
||||
|
@ -99,10 +95,8 @@ export const getBackendUiStore = () => {
|
|||
// delete the original if renaming
|
||||
delete state.draftModel.schema[originalName]
|
||||
|
||||
state.draftModel.schema = {
|
||||
...state.draftModel.schema,
|
||||
[field.name]: cloneDeep(field),
|
||||
}
|
||||
state.draftModel.schema[field.name] = cloneDeep(field)
|
||||
|
||||
store.actions.models.save(state.draftModel)
|
||||
return state
|
||||
})
|
||||
|
@ -119,8 +113,30 @@ export const getBackendUiStore = () => {
|
|||
select: view =>
|
||||
store.update(state => {
|
||||
state.selectedView = view
|
||||
state.selectedModel = {}
|
||||
return state
|
||||
}),
|
||||
delete: async view => {
|
||||
await api.delete(`/api/views/${view}`)
|
||||
await store.actions.models.fetch()
|
||||
},
|
||||
save: async view => {
|
||||
await api.post(`/api/views`, view)
|
||||
|
||||
store.update(state => {
|
||||
const viewModel = state.models.find(
|
||||
model => model._id === view.modelId
|
||||
)
|
||||
// TODO: Cleaner?
|
||||
if (!viewModel.views) viewModel.views = {}
|
||||
if (view.originalName) delete viewModel.views[view.originalName]
|
||||
viewModel.views[view.name] = view
|
||||
|
||||
state.models = state.models
|
||||
state.selectedView = view
|
||||
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 ActionButton from "components/common/ActionButton.svelte"
|
||||
|
@ -9,6 +10,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"
|
||||
|
@ -20,12 +22,14 @@
|
|||
let modalOpen = false
|
||||
let data = []
|
||||
let headers = []
|
||||
let views = []
|
||||
let currentPage = 0
|
||||
let search
|
||||
|
||||
$: {
|
||||
if ($backendUiStore.selectedView) {
|
||||
if (
|
||||
$backendUiStore.selectedView &&
|
||||
$backendUiStore.selectedView.name.startsWith("all_")
|
||||
) {
|
||||
api.fetchDataForView($backendUiStore.selectedView).then(records => {
|
||||
data = records || []
|
||||
})
|
||||
|
@ -46,22 +50,6 @@
|
|||
)
|
||||
|
||||
$: schema = $backendUiStore.selectedModel.schema
|
||||
|
||||
const createNewRecord = () => {
|
||||
open(
|
||||
CreateEditRecordModal,
|
||||
{
|
||||
onClosed: close,
|
||||
},
|
||||
{ styleContent: { padding: "0" } }
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (views.length) {
|
||||
backendUiStore.actions.views.select(views[0])
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<section>
|
||||
|
@ -71,6 +59,7 @@
|
|||
<ColumnPopover />
|
||||
{#if Object.keys($backendUiStore.selectedModel.schema).length > 0}
|
||||
<RowPopover />
|
||||
<ViewPopover />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -101,7 +90,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>
|
|
@ -0,0 +1,141 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import fsort from "fast-sort"
|
||||
import getOr from "lodash/fp/getOr"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import api from "builderStore/api"
|
||||
import { Button, Icon } from "@budibase/bbui"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import LinkedRecord from "./LinkedRecord.svelte"
|
||||
import TablePagination from "./TablePagination.svelte"
|
||||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
||||
import RowPopover from "./popovers/Row.svelte"
|
||||
import ColumnPopover from "./popovers/Column.svelte"
|
||||
import ViewPopover from "./popovers/View.svelte"
|
||||
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte"
|
||||
import EditRowPopover from "./popovers/EditRow.svelte"
|
||||
import CalculationPopover from "./popovers/Calculate.svelte"
|
||||
|
||||
export let columns = []
|
||||
export let data = []
|
||||
export let title
|
||||
|
||||
const ITEMS_PER_PAGE = 10
|
||||
|
||||
let currentPage = 0
|
||||
|
||||
$: paginatedData =
|
||||
data && data.length
|
||||
? data.slice(
|
||||
currentPage * ITEMS_PER_PAGE,
|
||||
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
|
||||
)
|
||||
: []
|
||||
$: sort = $backendUiStore.sort
|
||||
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<div class="table-controls">
|
||||
<h2 class="title">{title}</h2>
|
||||
<div class="popovers">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<table class="uk-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{#each columns as header}
|
||||
<th>{header.name}</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if paginatedData.length === 0}
|
||||
<div class="no-data">No Data.</div>
|
||||
{/if}
|
||||
{#each paginatedData as row}
|
||||
<tr>
|
||||
{#each columns as header}
|
||||
<td>{getOr(row.default || '', header.key, row)}</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<TablePagination
|
||||
{data}
|
||||
bind:currentPage
|
||||
pageItemCount={data.length}
|
||||
{ITEMS_PER_PAGE} />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
text-rendering: optimizeLegibility;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid var(--grey-4);
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
thead {
|
||||
height: 40px;
|
||||
background: var(--grey-3);
|
||||
border: 1px solid var(--grey-4);
|
||||
}
|
||||
|
||||
thead th {
|
||||
color: var(--ink);
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
text-rendering: optimizeLegibility;
|
||||
transition: 0.5s all;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
th:hover {
|
||||
color: var(--blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
td {
|
||||
max-width: 200px;
|
||||
text-overflow: ellipsis;
|
||||
border: 1px solid var(--grey-4);
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
border-bottom: 1px solid var(--grey-4);
|
||||
transition: 0.3s background-color;
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: var(--grey-1);
|
||||
}
|
||||
|
||||
.table-controls {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.popovers {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.no-data {
|
||||
padding: 14px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,74 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import fsort from "fast-sort"
|
||||
import getOr from "lodash/fp/getOr"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import api from "builderStore/api"
|
||||
import { Button, Icon } from "@budibase/bbui"
|
||||
import Table from "./Table.svelte"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import LinkedRecord from "./LinkedRecord.svelte"
|
||||
import TablePagination from "./TablePagination.svelte"
|
||||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
||||
import RowPopover from "./popovers/Row.svelte"
|
||||
import ColumnPopover from "./popovers/Column.svelte"
|
||||
import ViewPopover from "./popovers/View.svelte"
|
||||
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte"
|
||||
import EditRowPopover from "./popovers/EditRow.svelte"
|
||||
import CalculationPopover from "./popovers/Calculate.svelte"
|
||||
import GroupByPopover from "./popovers/GroupBy.svelte"
|
||||
|
||||
let COLUMNS = [
|
||||
{
|
||||
name: "group",
|
||||
key: "key",
|
||||
default: "All Records",
|
||||
},
|
||||
{
|
||||
name: "sum",
|
||||
key: "value.sum",
|
||||
},
|
||||
{
|
||||
name: "min",
|
||||
key: "value.min",
|
||||
},
|
||||
{
|
||||
name: "max",
|
||||
key: "value.max",
|
||||
},
|
||||
{
|
||||
name: "sumsqr",
|
||||
key: "value.sumsqr",
|
||||
},
|
||||
{
|
||||
name: "count",
|
||||
key: "value.count",
|
||||
},
|
||||
{
|
||||
name: "avg",
|
||||
key: "value.avg",
|
||||
},
|
||||
]
|
||||
|
||||
export let view = {}
|
||||
|
||||
let data = []
|
||||
|
||||
$: ({ name, groupBy } = view)
|
||||
$: !name.startsWith("all_") && fetchViewData(name, groupBy)
|
||||
|
||||
async function fetchViewData(name, groupBy) {
|
||||
let QUERY_VIEW_URL = `/api/views/${name}?stats=true`
|
||||
if (groupBy) {
|
||||
QUERY_VIEW_URL += `&group=${groupBy}`
|
||||
}
|
||||
|
||||
const response = await api.get(QUERY_VIEW_URL)
|
||||
data = await response.json()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Table title={decodeURI(view.name)} columns={COLUMNS} {data}>
|
||||
<CalculationPopover {view} />
|
||||
<GroupByPopover {view} />
|
||||
</Table>
|
|
@ -6,20 +6,6 @@ export async function createUser(user) {
|
|||
return await response.json()
|
||||
}
|
||||
|
||||
export async function createDatabase(appname, instanceName) {
|
||||
const CREATE_DATABASE_URL = `/api/${appname}/instances`
|
||||
const response = await api.post(CREATE_DATABASE_URL, {
|
||||
name: instanceName,
|
||||
})
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
export async function deleteRecord(record) {
|
||||
const DELETE_RECORDS_URL = `/api/${record.modelId}/records/${record._id}/${record._rev}`
|
||||
const response = await api.delete(DELETE_RECORDS_URL)
|
||||
return response
|
||||
}
|
||||
|
||||
export async function saveRecord(record, modelId) {
|
||||
const SAVE_RECORDS_URL = `/api/${modelId}/records`
|
||||
const response = await api.post(SAVE_RECORDS_URL, record)
|
||||
|
@ -27,8 +13,14 @@ export async function saveRecord(record, modelId) {
|
|||
return await response.json()
|
||||
}
|
||||
|
||||
export async function fetchDataForView(viewName) {
|
||||
const FETCH_RECORDS_URL = `/api/views/${viewName}`
|
||||
export async function deleteRecord(record) {
|
||||
const DELETE_RECORDS_URL = `/api/${record.modelId}/records/${record._id}/${record._rev}`
|
||||
const response = await api.delete(DELETE_RECORDS_URL)
|
||||
return response
|
||||
}
|
||||
|
||||
export async function fetchDataForView(view) {
|
||||
const FETCH_RECORDS_URL = `/api/views/${view.name}`
|
||||
|
||||
const response = await api.get(FETCH_RECORDS_URL)
|
||||
return await response.json()
|
|
@ -15,22 +15,20 @@
|
|||
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
|
||||
import * as api from "../api"
|
||||
|
||||
export let onClosed
|
||||
export let field = {}
|
||||
|
||||
let fieldDefinitions = cloneDeep(FIELDS)
|
||||
|
||||
export let onClosed
|
||||
export let field = {
|
||||
type: "string",
|
||||
constraints: fieldDefinitions.STRING.constraints,
|
||||
}
|
||||
|
||||
let originalName = field.name
|
||||
|
||||
$: required =
|
||||
field.constraints &&
|
||||
field.constraints.presence &&
|
||||
!field.constraints.presence.allowEmpty
|
||||
$: if (field.type) {
|
||||
field.constraints = merge(
|
||||
fieldDefinitions[field.type.toUpperCase()].constraints,
|
||||
field.constraints
|
||||
)
|
||||
}
|
||||
|
||||
async function saveColumn() {
|
||||
backendUiStore.update(state => {
|
||||
|
@ -43,14 +41,26 @@
|
|||
})
|
||||
onClosed()
|
||||
}
|
||||
|
||||
function handleFieldConstraints(event) {
|
||||
const { type, constraints } = fieldDefinitions[
|
||||
event.target.value.toUpperCase()
|
||||
]
|
||||
field.type = type
|
||||
field.constraints = constraints
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="actions">
|
||||
<Input placeholder="Name" thin bind:value={field.name} />
|
||||
|
||||
<Select secondary thin bind:value={field.type}>
|
||||
<Select
|
||||
secondary
|
||||
thin
|
||||
on:change={handleFieldConstraints}
|
||||
bind:value={field.value}>
|
||||
{#each Object.values(fieldDefinitions) as field}
|
||||
<option value={field.type}>{field.name}</option>
|
||||
<option value={field.value}>{field.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
|
||||
|
@ -63,28 +73,28 @@
|
|||
on:change={() => (field.constraints.presence.allowEmpty = required)} />
|
||||
</div>
|
||||
|
||||
{#if field.type === 'string'}
|
||||
{#if field.value === 'string' && field.constraints}
|
||||
<NumberBox
|
||||
label="Max Length"
|
||||
bind:value={field.constraints.length.maximum} />
|
||||
<ValuesList
|
||||
label="Categories"
|
||||
bind:values={field.constraints.inclusion} />
|
||||
{:else if field.type === 'datetime'}
|
||||
{:else if field.value === 'datetime' && field.constraints}
|
||||
<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'}
|
||||
{:else if field.value === 'number' && field.constraints}
|
||||
<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'}
|
||||
{:else if field.value === 'link'}
|
||||
<div class="field">
|
||||
<label>Link</label>
|
||||
<select class="budibase__input" bind:value={field.modelId}>
|
|
@ -10,10 +10,10 @@
|
|||
|
||||
<section>
|
||||
<div class="content">
|
||||
<heading>
|
||||
<header>
|
||||
<i class="ri-information-line alert" />
|
||||
<h4 class="budibase__title--4">Delete Record</h4>
|
||||
</heading>
|
||||
</header>
|
||||
<p>
|
||||
Are you sure you want to delete this record? All of your data will be
|
||||
permanently removed. This action cannot be undone.
|
||||
|
@ -47,7 +47,7 @@
|
|||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
heading {
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
|
@ -14,10 +14,10 @@
|
|||
|
||||
<section>
|
||||
<div class="content">
|
||||
<heading>
|
||||
<header>
|
||||
<i class="ri-information-line alert" />
|
||||
<h4 class="budibase__title--4">Delete Table</h4>
|
||||
</heading>
|
||||
</header>
|
||||
<p>
|
||||
Are you sure you want to delete this table? All of your data will be
|
||||
permanently removed. This action cannot be undone.
|
||||
|
@ -50,7 +50,7 @@
|
|||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
heading {
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<script>
|
||||
import { goto } from "@sveltech/routify"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import * as api from "../api"
|
||||
|
||||
export let viewName
|
||||
export let onClosed
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<div class="content">
|
||||
<header>
|
||||
<i class="ri-information-line alert" />
|
||||
<h4 class="budibase__title--4">Delete View</h4>
|
||||
</header>
|
||||
<p>
|
||||
Are you sure you want to delete this view? All of your data will be
|
||||
permanently removed. This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<ActionButton on:click={onClosed}>Cancel</ActionButton>
|
||||
<ActionButton
|
||||
alert
|
||||
on:click={async () => {
|
||||
await backendUiStore.actions.views.delete(viewName)
|
||||
notifier.danger(`View ${viewName} deleted.`)
|
||||
$goto(`./backend`)
|
||||
onClosed()
|
||||
}}>
|
||||
Delete
|
||||
</ActionButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.alert {
|
||||
color: rgba(255, 0, 31, 1);
|
||||
background: var(--grey-1);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
padding: 10px;
|
||||
background: var(--grey-1);
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
</style>
|
|
@ -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}
|
|
@ -0,0 +1,95 @@
|
|||
<script>
|
||||
import {
|
||||
Popover,
|
||||
TextButton,
|
||||
Button,
|
||||
Icon,
|
||||
Input,
|
||||
Select,
|
||||
} from "@budibase/bbui"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import CreateEditRecord from "../modals/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">
|
||||
<h5>Calculate</h5>
|
||||
<div class="input-group-row">
|
||||
<p>The</p>
|
||||
<Select secondary thin bind:value={view.calculation}>
|
||||
{#each CALCULATIONS as calculation}
|
||||
<option value={calculation.key}>{calculation.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
<p>of</p>
|
||||
<Select secondary thin bind:value={view.field}>
|
||||
{#each fields as field}
|
||||
<option value={field}>{field}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<Button secondary on:click={dropdown.hide}>Cancel</Button>
|
||||
<Button primary on:click={saveView}>Save</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
h5 {
|
||||
margin-bottom: var(--spacing-l);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-top: var(--spacing-l);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.input-group-row {
|
||||
display: grid;
|
||||
grid-template-columns: 50px 1fr 20px 1fr;
|
||||
gap: var(--spacing-s);
|
||||
margin-bottom: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,12 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
|
||||
import {
|
||||
DropdownMenu,
|
||||
TextButton as Button,
|
||||
Icon,
|
||||
Input,
|
||||
Select,
|
||||
} from "@budibase/bbui"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<script>
|
||||
import {
|
||||
Popover,
|
||||
TextButton,
|
||||
Button,
|
||||
Icon,
|
||||
Input,
|
||||
Select,
|
||||
} from "@budibase/bbui"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import CreateEditRecord from "../modals/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 By
|
||||
</TextButton>
|
||||
</div>
|
||||
<Popover bind:this={dropdown} {anchor} align="left">
|
||||
<h5>Group By</h5>
|
||||
<div class="input-group-row">
|
||||
<p>Group By</p>
|
||||
<Select secondary thin bind:value={view.groupBy}>
|
||||
<option value={false} />
|
||||
{#each fields as field}
|
||||
<option value={field}>{field}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<Button secondary on:click={dropdown.hide}>Cancel</Button>
|
||||
<Button primary on:click={saveView}>Save</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
h5 {
|
||||
margin-bottom: var(--spacing-l);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-top: var(--spacing-l);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.input-group-row {
|
||||
display: grid;
|
||||
grid-template-columns: 50px 1fr 20px 1fr;
|
||||
gap: var(--spacing-s);
|
||||
margin-bottom: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { DropdownMenu, Button, Icon } from "@budibase/bbui"
|
||||
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui"
|
||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
|
||||
|
||||
let anchor
|
|
@ -0,0 +1,82 @@
|
|||
<script>
|
||||
import {
|
||||
Popover,
|
||||
TextButton,
|
||||
Button,
|
||||
Icon,
|
||||
Input,
|
||||
Select,
|
||||
} from "@budibase/bbui"
|
||||
import { goto } from "@sveltech/routify"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
|
||||
|
||||
let anchor
|
||||
let dropdown
|
||||
|
||||
let name
|
||||
let field
|
||||
|
||||
$: fields = Object.keys($backendUiStore.selectedModel.schema).filter(key => {
|
||||
return $backendUiStore.selectedModel.schema[key].type === "number"
|
||||
})
|
||||
$: views = $backendUiStore.models.flatMap(model => Object.keys(model.views))
|
||||
|
||||
function saveView() {
|
||||
if (views.includes(name)) {
|
||||
notifier.danger(`View exists with name ${name}.`)
|
||||
return
|
||||
}
|
||||
backendUiStore.actions.views.save({
|
||||
name,
|
||||
modelId: $backendUiStore.selectedModel._id,
|
||||
field,
|
||||
})
|
||||
notifier.success(`View ${name} created`)
|
||||
dropdown.hide()
|
||||
$goto(`../../../view/${name}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={anchor}>
|
||||
<TextButton text small on:click={dropdown.show}>
|
||||
<Icon name="addrow" />
|
||||
Create New View
|
||||
</TextButton>
|
||||
</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} />
|
||||
<Select thin secondary bind:value={field}>
|
||||
{#each fields as field}
|
||||
<option value={field}>{field}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</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;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-top: var(--spacing-l);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.input-group-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
|
@ -3,7 +3,7 @@
|
|||
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"
|
||||
import DeleteTableModal from "components/database/DataTable/modals/DeleteTable.svelte"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import DeleteViewModal from "components/database/DataTable/modals/DeleteView.svelte"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
export let view
|
||||
|
||||
let anchor
|
||||
let dropdown
|
||||
|
||||
let editing
|
||||
let originalName = view.name
|
||||
|
||||
function showEditor() {
|
||||
editing = true
|
||||
}
|
||||
|
||||
function hideEditor() {
|
||||
dropdown.hide()
|
||||
editing = false
|
||||
close()
|
||||
}
|
||||
|
||||
const deleteView = () => {
|
||||
open(
|
||||
DeleteViewModal,
|
||||
{
|
||||
onClosed: close,
|
||||
viewName: view.name,
|
||||
},
|
||||
{ styleContent: { padding: "0" } }
|
||||
)
|
||||
}
|
||||
|
||||
function save() {
|
||||
backendUiStore.actions.views.save({
|
||||
originalName,
|
||||
...view,
|
||||
})
|
||||
notifier.success("Renamed View Successfully.")
|
||||
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="View Name" thin bind:value={view.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-view" on:click={deleteView}>
|
||||
<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,24 @@
|
|||
export let indented
|
||||
</script>
|
||||
|
||||
<div class:selected on:click class={className}>
|
||||
<i class:indented class={icon} />
|
||||
<div
|
||||
data-cy="model-nav-item"
|
||||
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,21 @@
|
|||
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
|
||||
$: selectedView =
|
||||
$backendUiStore.selectedView && $backendUiStore.selectedView.name
|
||||
|
||||
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.name}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -34,12 +35,26 @@
|
|||
<div class="hierarchy-items-container">
|
||||
{#each $backendUiStore.models as model}
|
||||
<ListItem
|
||||
selected={!$backendUiStore.selectedField && model._id === $backendUiStore.selectedModel._id}
|
||||
selected={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 viewName}
|
||||
<ListItem
|
||||
indented
|
||||
selected={selectedView === viewName}
|
||||
title={viewName}
|
||||
icon="ri-eye-line"
|
||||
on:click={() => selectView({
|
||||
name: viewName,
|
||||
...model.views[viewName],
|
||||
})}>
|
||||
<EditViewPopover
|
||||
view={{ name: viewName, ...model.views[viewName] }} />
|
||||
</ListItem>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
export let value
|
||||
</script>
|
||||
|
||||
<div class="uk-margin block-field">
|
||||
<div class="uk-form-controls">
|
||||
<select class="budibase__input" on:change {value}>
|
||||
<option value="" />
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<Select thin secondary wide on:change {value}>
|
||||
<option value="" />
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
|
|
|
@ -3,6 +3,7 @@ export const FIELDS = {
|
|||
name: "Plain Text",
|
||||
icon: "ri-text",
|
||||
type: "string",
|
||||
value: "string",
|
||||
constraints: {
|
||||
type: "string",
|
||||
length: {},
|
||||
|
@ -13,16 +14,18 @@ export const FIELDS = {
|
|||
name: "Number",
|
||||
icon: "ri-number-1",
|
||||
type: "number",
|
||||
value: "number",
|
||||
constraints: {
|
||||
type: "number",
|
||||
presence: { allowEmpty: true },
|
||||
numericality: {},
|
||||
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
|
||||
},
|
||||
},
|
||||
BOOLEAN: {
|
||||
name: "True/False",
|
||||
icon: "ri-toggle-line",
|
||||
type: "boolean",
|
||||
value: "boolean",
|
||||
constraints: {
|
||||
type: "boolean",
|
||||
presence: { allowEmpty: true },
|
||||
|
@ -41,10 +44,15 @@ export const FIELDS = {
|
|||
name: "Date/Time",
|
||||
icon: "ri-calendar-event-fill",
|
||||
type: "string",
|
||||
value: "datetime",
|
||||
constraints: {
|
||||
type: "string",
|
||||
length: {},
|
||||
presence: { allowEmpty: true },
|
||||
datetime: {
|
||||
latest: "",
|
||||
earliest: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
// IMAGE: {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import * as api from "components/database/ModelDataTable/api"
|
||||
|
||||
import * as api from "components/database/DataTable/api"
|
||||
import ModelNavigator from "components/nav/ModelNavigator/ModelNavigator.svelte"
|
||||
</script>
|
||||
|
||||
|
@ -23,12 +22,10 @@
|
|||
background: var(--grey-1);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
margin: 20px 40px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
flex: 0 1 auto;
|
||||
width: 300px;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { Button } from "@budibase/bbui"
|
||||
import ModelDataTable from "components/database/ModelDataTable"
|
||||
import ModelDataTable from "components/database/DataTable"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import * as api from "components/database/ModelDataTable/api"
|
||||
import { CreateEditRecordModal } from "components/database/ModelDataTable/modals"
|
||||
import * as api from "components/database/DataTable/api"
|
||||
import { CreateEditRecordModal } from "components/database/DataTable/modals"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
import { params } from "@sveltech/routify"
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
if ($params.selectedView) {
|
||||
let view
|
||||
const viewName = decodeURI($params.selectedView)
|
||||
for (let model of $backendUiStore.models) {
|
||||
if (model.views && model.views[viewName]) {
|
||||
view = model.views[viewName]
|
||||
}
|
||||
}
|
||||
if (view) {
|
||||
backendUiStore.actions.views.select({
|
||||
name: viewName,
|
||||
...view,
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -0,0 +1,27 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { Button } from "@budibase/bbui"
|
||||
import ViewDataTable from "components/database/DataTable/ViewDataTable"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import * as api from "components/database/DataTable/api"
|
||||
import { CreateEditRecordModal } from "components/database/DataTable/modals"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
$: selectedView = $backendUiStore.selectedView
|
||||
</script>
|
||||
|
||||
{#if $backendUiStore.selectedDatabase._id && selectedView}
|
||||
<ViewDataTable view={selectedView} />
|
||||
{:else}
|
||||
<i>create your first table to start building</i>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
i {
|
||||
font-size: 20px;
|
||||
margin-right: 10px;
|
||||
color: var(--grey-4);
|
||||
}
|
||||
</style>
|
|
@ -688,9 +688,10 @@
|
|||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@budibase/bbui@^1.23.1":
|
||||
version "1.23.1"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.23.1.tgz#232b0d31379ef64afe5e76f477d5489e67defc22"
|
||||
"@budibase/bbui@^1.24.1":
|
||||
version "1.24.1"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.24.1.tgz#d1c527990c3dcdef78080abfe6aaeef6e8fb2d66"
|
||||
integrity sha512-yZE4Uk6EB3MoIUd2oNN50u8KOXRX65yRFv49gN0T6iCjTjEAdEk8JtsQR9AL3VWn3EdPz5xOkrGsIvNtxqaAFw==
|
||||
dependencies:
|
||||
sirv-cli "^0.4.6"
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ exports.save = async function(ctx) {
|
|||
const modelToSave = {
|
||||
type: "model",
|
||||
_id: newid(),
|
||||
views: {},
|
||||
...ctx.request.body,
|
||||
}
|
||||
|
||||
|
|
|
@ -80,10 +80,24 @@ exports.save = async function(ctx) {
|
|||
|
||||
exports.fetchView = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const { stats, group } = ctx.query
|
||||
const response = await db.query(`database/${ctx.params.viewName}`, {
|
||||
include_docs: true,
|
||||
include_docs: !stats,
|
||||
group,
|
||||
})
|
||||
ctx.body = response.rows.map(row => row.doc)
|
||||
|
||||
if (stats) {
|
||||
for (let row of response.rows) {
|
||||
row.value = {
|
||||
...row.value,
|
||||
avg: row.value.sum / row.value.count,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.rows = response.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
ctx.body = response.rows
|
||||
}
|
||||
|
||||
exports.fetchModelRecords = async function(ctx) {
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
const CouchDB = require("../../db")
|
||||
|
||||
const controller = {
|
||||
query: async () => {},
|
||||
fetch: async ctx => {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const designDoc = await db.get("_design/database")
|
||||
const response = []
|
||||
|
||||
for (let name in designDoc.views) {
|
||||
if (
|
||||
!name.startsWith("all") &&
|
||||
name !== "by_type" &&
|
||||
name !== "by_username" &&
|
||||
name !== "by_workflow_trigger"
|
||||
) {
|
||||
response.push({
|
||||
name,
|
||||
...designDoc.views[name],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx.body = response
|
||||
},
|
||||
create: async ctx => {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const newView = ctx.request.body
|
||||
|
||||
const designDoc = await db.get("_design/database")
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[newView.name]: newView,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
|
||||
ctx.body = newView
|
||||
ctx.message = `View ${newView.name} created successfully.`
|
||||
},
|
||||
destroy: async ctx => {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
ctx.body = await db.destroy(ctx.params.userId)
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = controller
|
|
@ -0,0 +1,83 @@
|
|||
const CouchDB = require("../../../db")
|
||||
const statsViewTemplate = require("./viewBuilder")
|
||||
|
||||
const controller = {
|
||||
fetch: async ctx => {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const designDoc = await db.get("_design/database")
|
||||
const response = []
|
||||
|
||||
for (let name in designDoc.views) {
|
||||
if (
|
||||
!name.startsWith("all") &&
|
||||
name !== "by_type" &&
|
||||
name !== "by_username" &&
|
||||
name !== "by_workflow_trigger"
|
||||
) {
|
||||
response.push({
|
||||
name,
|
||||
...designDoc.views[name],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx.body = response
|
||||
},
|
||||
save: async ctx => {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const { originalName, ...newView } = ctx.request.body
|
||||
|
||||
const designDoc = await db.get("_design/database")
|
||||
|
||||
const view = statsViewTemplate(newView)
|
||||
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[newView.name]: view,
|
||||
}
|
||||
|
||||
// view has been renamed
|
||||
if (originalName) {
|
||||
delete designDoc.views[originalName]
|
||||
}
|
||||
|
||||
await db.put(designDoc)
|
||||
|
||||
// add views to model document
|
||||
const model = await db.get(ctx.request.body.modelId)
|
||||
model.views = {
|
||||
...(model.views ? model.views : {}),
|
||||
[newView.name]: view.meta,
|
||||
}
|
||||
|
||||
if (originalName) {
|
||||
delete model.views[originalName]
|
||||
}
|
||||
|
||||
await db.put(model)
|
||||
|
||||
ctx.body = view
|
||||
ctx.message = `View ${newView.name} saved successfully.`
|
||||
},
|
||||
destroy: async ctx => {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const designDoc = await db.get("_design/database")
|
||||
|
||||
const viewName = decodeURI(ctx.params.viewName)
|
||||
|
||||
const view = designDoc.views[viewName]
|
||||
|
||||
delete designDoc.views[viewName]
|
||||
|
||||
await db.put(designDoc)
|
||||
|
||||
const model = await db.get(view.meta.modelId)
|
||||
delete model.views[viewName]
|
||||
await db.put(model)
|
||||
|
||||
ctx.body = view
|
||||
ctx.message = `View ${ctx.params.viewName} saved successfully.`
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = controller
|
|
@ -0,0 +1,25 @@
|
|||
function statsViewTemplate({ field, modelId, groupBy }) {
|
||||
return {
|
||||
meta: {
|
||||
field,
|
||||
modelId,
|
||||
groupBy,
|
||||
schema: {
|
||||
sum: "number",
|
||||
min: "number",
|
||||
max: "number",
|
||||
count: "number",
|
||||
sumsqr: "number",
|
||||
avg: "number",
|
||||
},
|
||||
},
|
||||
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,
|
||||
schema: {
|
||||
sum: "number",
|
||||
min: "number",
|
||||
max: "number",
|
||||
count: "number",
|
||||
sumsqr: "number",
|
||||
avg: "number"
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
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
|
||||
.get(`/api/views/TestView?stats=true`)
|
||||
.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
|
||||
.get(`/api/views/TestView?stats=true&group=Category`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
expect(res.body.length).toBe(2)
|
||||
expect(res.body).toMatchSnapshot()
|
||||
})
|
||||
});
|
||||
});
|
|
@ -13,8 +13,7 @@ router
|
|||
recordController.fetchView
|
||||
)
|
||||
.get("/api/views", authorized(BUILDER), viewController.fetch)
|
||||
// .patch("/api/:databaseId/views", controller.update);
|
||||
// .delete("/api/:instanceId/views/:viewId/:revId", controller.destroy);
|
||||
.post("/api/views", authorized(BUILDER), viewController.create)
|
||||
.delete("/api/views/:viewName", authorized(BUILDER), viewController.destroy)
|
||||
.post("/api/views", authorized(BUILDER), viewController.save)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -45,6 +45,8 @@ const server = http.createServer(app.callback())
|
|||
|
||||
server.on("close", () => console.log("Server Closed"))
|
||||
|
||||
process.on("SIGINT", () => process.exit(1))
|
||||
|
||||
module.exports = server.listen(env.PORT || 4001, () => {
|
||||
console.log(`Budibase running on ${JSON.stringify(server.address())}`)
|
||||
})
|
||||
|
|
|
@ -36,7 +36,10 @@ async function startApp() {
|
|||
|
||||
async function createWindow() {
|
||||
app.server = require("./app")
|
||||
win = new BrowserWindow({ width: 1920, height: 1080 })
|
||||
win = new BrowserWindow({
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
})
|
||||
win.setTitle(APP_TITLE)
|
||||
win.loadURL(APP_URL)
|
||||
if (isDev) {
|
||||
|
|
|
@ -8,8 +8,7 @@
|
|||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-selection/1.2.0/d3-selection.js"></script>
|
||||
|
||||
<script src="../../../node_modules/britecharts/dist/umd/bar.min.js" type="text/javascript"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/britecharts/dist/css/britecharts.min.css" type="text/css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/britecharts/dist/css/britecharts.min.css" type="text/css" />
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
|
Loading…
Reference in New Issue