mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
48 changed files with 1244 additions and 183 deletions
@ -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") |
|||
}) |
|||
}) |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
|
|||
@ -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> |
|||
@ -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, |
|||
}, |
|||
}, |
|||
] |
|||
`; |
|||
Loading…
Reference in new issue