forked from tsai/budibase
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> |
<script> |
||||
import { backendUiStore } from "builderStore" |
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 { FIELDS } from "constants/backend" |
||||
import CreateEditColumn from "../modals/CreateEditColumn.svelte" |
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> |
<script> |
||||
import { DropdownMenu, Button, Icon } from "@budibase/bbui" |
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui" |
||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte" |
import CreateEditRecord from "../modals/CreateEditRecord.svelte" |
||||
|
|
||||
let anchor |
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> |
<script> |
||||
|
import { Select } from "@budibase/bbui" |
||||
import { backendUiStore } from "builderStore" |
import { backendUiStore } from "builderStore" |
||||
|
|
||||
export let value |
export let value |
||||
</script> |
</script> |
||||
|
|
||||
<div class="uk-margin block-field"> |
<Select thin secondary wide on:change {value}> |
||||
<div class="uk-form-controls"> |
<option value="" /> |
||||
<select class="budibase__input" on:change {value}> |
{#each $backendUiStore.models as model} |
||||
<option value="" /> |
<option value={model._id}>{model.name}</option> |
||||
{#each $backendUiStore.models as model} |
{/each} |
||||
<option value={model._id}>{model.name}</option> |
</Select> |
||||
{/each} |
|
||||
</select> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|||||
@ -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