Merge remote-tracking branch 'origin/master' into mike-fixes
This commit is contained in:
commit
cf1df37e71
|
@ -55,6 +55,11 @@ context("Create a View", () => {
|
|||
cy.get(".menu-container")
|
||||
.find("select")
|
||||
.eq(0)
|
||||
.select("Statistics")
|
||||
cy.wait(50)
|
||||
cy.get(".menu-container")
|
||||
.find("select")
|
||||
.eq(1)
|
||||
.select("age")
|
||||
cy.contains("Save").click()
|
||||
cy.get("thead th div").should($headers => {
|
||||
|
|
|
@ -9,21 +9,23 @@
|
|||
export let view = {}
|
||||
|
||||
let data = []
|
||||
let loading = false
|
||||
|
||||
$: name = view.name
|
||||
|
||||
// Fetch rows for specified view
|
||||
$: {
|
||||
if (!name.startsWith("all_")) {
|
||||
fetchViewData(name, view.field, view.groupBy)
|
||||
loading = true
|
||||
fetchViewData(name, view.field, view.groupBy, view.calculation)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchViewData(name, field, groupBy) {
|
||||
async function fetchViewData(name, field, groupBy, calculation) {
|
||||
const params = new URLSearchParams()
|
||||
if (field) {
|
||||
if (calculation) {
|
||||
params.set("field", field)
|
||||
params.set("stats", true)
|
||||
params.set("calculation", calculation)
|
||||
}
|
||||
if (groupBy) {
|
||||
params.set("group", groupBy)
|
||||
|
@ -31,10 +33,11 @@
|
|||
const QUERY_VIEW_URL = `/api/views/${name}?${params}`
|
||||
const response = await api.get(QUERY_VIEW_URL)
|
||||
data = await response.json()
|
||||
loading = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<Table title={decodeURI(name)} schema={view.schema} {data}>
|
||||
<Table title={decodeURI(name)} schema={view.schema} {data} {loading}>
|
||||
<FilterButton {view} />
|
||||
<CalculateButton {view} />
|
||||
{#if view.calculation}
|
||||
|
|
|
@ -9,7 +9,11 @@
|
|||
</script>
|
||||
|
||||
<div bind:this={anchor}>
|
||||
<TextButton text small on:click={dropdown.show} active={!!view.field}>
|
||||
<TextButton
|
||||
text
|
||||
small
|
||||
on:click={dropdown.show}
|
||||
active={view.field && view.calculation}>
|
||||
<Icon name="calculate" />
|
||||
Calculate
|
||||
</TextButton>
|
||||
|
|
|
@ -9,6 +9,14 @@
|
|||
name: "Statistics",
|
||||
key: "stats",
|
||||
},
|
||||
{
|
||||
name: "Count",
|
||||
key: "count",
|
||||
},
|
||||
{
|
||||
name: "Sum",
|
||||
key: "sum",
|
||||
},
|
||||
]
|
||||
|
||||
export let view = {}
|
||||
|
@ -20,11 +28,12 @@
|
|||
$: fields =
|
||||
viewTable &&
|
||||
Object.keys(viewTable.schema).filter(
|
||||
field => viewTable.schema[field].type === "number"
|
||||
field =>
|
||||
view.calculation === "count" ||
|
||||
viewTable.schema[field].type === "number"
|
||||
)
|
||||
|
||||
function saveView() {
|
||||
if (!view.calculation) view.calculation = "stats"
|
||||
backendUiStore.actions.views.save(view)
|
||||
notifier.success(`View ${view.name} saved.`)
|
||||
onClosed()
|
||||
|
@ -35,25 +44,26 @@
|
|||
<div class="actions">
|
||||
<h5>Calculate</h5>
|
||||
<div class="input-group-row">
|
||||
<!-- <p>The</p>
|
||||
<p>The</p>
|
||||
<Select secondary thin bind:value={view.calculation}>
|
||||
<option value="">Choose an option</option>
|
||||
<option value={''}>Choose an option</option>
|
||||
{#each CALCULATIONS as calculation}
|
||||
<option value={calculation.key}>{calculation.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
<p>of</p> -->
|
||||
<p>The statistics of</p>
|
||||
{#if view.calculation}
|
||||
<p>of</p>
|
||||
<Select secondary thin bind:value={view.field}>
|
||||
<option value="">Choose an option</option>
|
||||
<option value={''}>You must choose an option</option>
|
||||
{#each fields as field}
|
||||
<option value={field}>{field}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||
<Button primary on:click={saveView}>Save</Button>
|
||||
<Button primary on:click={saveView} disabled={!view.field}>Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -72,8 +72,8 @@
|
|||
<div class="actions">
|
||||
<Input label="Name" thin bind:value={field.name} />
|
||||
|
||||
{#if !originalName}
|
||||
<Select
|
||||
disabled={originalName}
|
||||
secondary
|
||||
thin
|
||||
label="Type"
|
||||
|
@ -83,7 +83,6 @@
|
|||
<option value={field.type}>{field.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{/if}
|
||||
|
||||
{#if field.type !== 'link'}
|
||||
<Toggle
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
name: "Equals",
|
||||
key: "EQUALS",
|
||||
},
|
||||
{
|
||||
name: "Not Equals",
|
||||
key: "NOT_EQUALS",
|
||||
},
|
||||
{
|
||||
name: "Less Than",
|
||||
key: "LT",
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
<li>No Users found</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:catch error}
|
||||
{:catch err}
|
||||
Something went wrong when trying to fetch users. Please refresh (CMD + R /
|
||||
CTRL + R) the page and try again.
|
||||
{/await}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
import { onMount } from "svelte"
|
||||
|
||||
export let label = ""
|
||||
export let bindable = true
|
||||
export let componentInstance = {}
|
||||
export let control = null
|
||||
export let key = ""
|
||||
|
@ -93,7 +94,7 @@
|
|||
{...props}
|
||||
name={key} />
|
||||
</div>
|
||||
{#if control === Input && !key.startsWith('_')}
|
||||
{#if bindable && control === Input && !key.startsWith('_')}
|
||||
<button data-cy={`${key}-binding-button`} on:click={dropdown.show}>
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
|
|
|
@ -88,6 +88,7 @@
|
|||
{#if screenOrPageInstance}
|
||||
{#each screenOrPageDefinition as def}
|
||||
<PropertyControl
|
||||
bindable={false}
|
||||
control={def.control}
|
||||
label={def.label}
|
||||
key={def.key}
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
{#await promise}
|
||||
<!-- This should probably be some kind of loading state? -->
|
||||
<div />
|
||||
{:then results}
|
||||
{:then _}
|
||||
<slot />
|
||||
{:catch error}
|
||||
<p>Something went wrong: {error.message}</p>
|
||||
|
|
|
@ -3,6 +3,8 @@ export const parseAppIdFromCookie = docCookie => {
|
|||
docCookie.split(";").find(c => c.trim().startsWith("budibase:token")) ||
|
||||
docCookie.split(";").find(c => c.trim().startsWith("builder:token"))
|
||||
|
||||
if (!cookie) return location.pathname.replace(/\//g, "")
|
||||
|
||||
const base64Token = cookie.substring(lengthOfKey)
|
||||
|
||||
const user = JSON.parse(atob(base64Token.split(".")[1]))
|
||||
|
|
|
@ -11,6 +11,12 @@ const { cloneDeep } = require("lodash")
|
|||
|
||||
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
|
||||
|
||||
const CALCULATION_TYPES = {
|
||||
SUM: "sum",
|
||||
COUNT: "count",
|
||||
STATS: "stats",
|
||||
}
|
||||
|
||||
validateJs.extend(validateJs.validators.datetime, {
|
||||
parse: function(value) {
|
||||
return new Date(value).getTime()
|
||||
|
@ -137,7 +143,7 @@ exports.save = async function(ctx) {
|
|||
exports.fetchView = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const { stats, group, field } = ctx.query
|
||||
const { calculation, group, field } = ctx.query
|
||||
const viewName = ctx.params.viewName
|
||||
|
||||
// if this is a table view being looked for just transfer to that
|
||||
|
@ -148,22 +154,35 @@ exports.fetchView = async function(ctx) {
|
|||
}
|
||||
|
||||
const response = await db.query(`database/${viewName}`, {
|
||||
include_docs: !stats,
|
||||
include_docs: !calculation,
|
||||
group,
|
||||
})
|
||||
|
||||
if (stats) {
|
||||
if (!calculation) {
|
||||
response.rows = response.rows.map(row => row.doc)
|
||||
ctx.body = await linkRows.attachLinkInfo(instanceId, response.rows)
|
||||
}
|
||||
|
||||
if (calculation === CALCULATION_TYPES.STATS) {
|
||||
response.rows = response.rows.map(row => ({
|
||||
group: row.key,
|
||||
field,
|
||||
...row.value,
|
||||
avg: row.value.sum / row.value.count,
|
||||
}))
|
||||
} else {
|
||||
response.rows = response.rows.map(row => row.doc)
|
||||
ctx.body = response.rows
|
||||
}
|
||||
|
||||
ctx.body = await linkRows.attachLinkInfo(instanceId, response.rows)
|
||||
if (
|
||||
calculation === CALCULATION_TYPES.COUNT ||
|
||||
calculation === CALCULATION_TYPES.SUM
|
||||
) {
|
||||
ctx.body = response.rows.map(row => ({
|
||||
group: row.key,
|
||||
field,
|
||||
value: row.value,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
exports.fetchTableRows = async function(ctx) {
|
||||
|
|
|
@ -45,7 +45,7 @@ exports[`viewBuilder Filter creates a view with multiple filters and conjunction
|
|||
Object {
|
||||
"map": "function (doc) {
|
||||
if (doc.tableId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" && doc[\\"Name\\"] === \\"Test\\" || doc[\\"Yes\\"] > \\"Value\\") {
|
||||
emit(doc._id);
|
||||
emit(doc[\\"_id\\"], doc[\\"undefined\\"]);
|
||||
}
|
||||
}",
|
||||
"meta": Object {
|
||||
|
@ -86,6 +86,5 @@ Object {
|
|||
"schema": null,
|
||||
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
},
|
||||
"reduce": "_stats",
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const TOKEN_MAP = {
|
||||
EQUALS: "===",
|
||||
NOT_EQUALS: "!==",
|
||||
LT: "<",
|
||||
LTE: "<=",
|
||||
MT: ">",
|
||||
|
@ -22,6 +23,14 @@ const FIELD_PROPERTY = {
|
|||
}
|
||||
|
||||
const SCHEMA_MAP = {
|
||||
sum: {
|
||||
field: "string",
|
||||
value: "number",
|
||||
},
|
||||
count: {
|
||||
field: "string",
|
||||
value: "number",
|
||||
},
|
||||
stats: {
|
||||
sum: {
|
||||
type: "number",
|
||||
|
@ -80,8 +89,7 @@ function parseFilterExpression(filters) {
|
|||
* @param {String?} groupBy - field to group calculation results on, if any
|
||||
*/
|
||||
function parseEmitExpression(field, groupBy) {
|
||||
if (field) return `emit(doc["${groupBy || "_id"}"], doc["${field}"]);`
|
||||
return `emit(doc._id);`
|
||||
return `emit(doc["${groupBy || "_id"}"], doc["${field}"]);`
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -101,7 +109,7 @@ function viewTemplate({ field, tableId, groupBy, filters = [], calculation }) {
|
|||
|
||||
const emitExpression = parseEmitExpression(field, groupBy)
|
||||
|
||||
const reduction = field ? { reduce: "_stats" } : {}
|
||||
const reduction = field && calculation ? { reduce: `_${calculation}` } : {}
|
||||
|
||||
let schema = null
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ describe("/views", () => {
|
|||
const createView = async (config = {
|
||||
name: "TestView",
|
||||
field: "Price",
|
||||
calculation: "stats",
|
||||
tableId: table._id
|
||||
}) =>
|
||||
await request
|
||||
|
@ -65,20 +66,30 @@ describe("/views", () => {
|
|||
expect(updatedTable.views).toEqual({
|
||||
TestView: {
|
||||
field: "Price",
|
||||
calculation: "stats",
|
||||
tableId: table._id,
|
||||
filters: [],
|
||||
schema: {
|
||||
name: {
|
||||
sum: {
|
||||
type: "number",
|
||||
},
|
||||
min: {
|
||||
type: "number",
|
||||
},
|
||||
max: {
|
||||
type: "number",
|
||||
},
|
||||
count: {
|
||||
type: "number",
|
||||
},
|
||||
sumsqr: {
|
||||
type: "number",
|
||||
},
|
||||
avg: {
|
||||
type: "number",
|
||||
},
|
||||
field: {
|
||||
type: "string",
|
||||
constraints: {
|
||||
type: "string"
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
constraints: {
|
||||
type: "string"
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +134,7 @@ describe("/views", () => {
|
|||
Price: 4000
|
||||
})
|
||||
const res = await request
|
||||
.get(`/api/views/TestView?stats=true`)
|
||||
.get(`/api/views/TestView?calculation=stats`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
@ -133,6 +144,7 @@ describe("/views", () => {
|
|||
|
||||
it("returns data for the created view using a group by", async () => {
|
||||
await createView({
|
||||
calculation: "stats",
|
||||
name: "TestView",
|
||||
field: "Price",
|
||||
groupBy: "Category",
|
||||
|
@ -154,10 +166,11 @@ describe("/views", () => {
|
|||
Category: "Two"
|
||||
})
|
||||
const res = await request
|
||||
.get(`/api/views/TestView?stats=true&group=Category`)
|
||||
.get(`/api/views/TestView?calculation=stats&group=Category`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.length).toBe(2)
|
||||
expect(res.body).toMatchSnapshot()
|
||||
})
|
||||
|
|
|
@ -34,13 +34,15 @@ module.exports = async (ctx, next) => {
|
|||
|
||||
let appId = process.env.CLOUD ? ctx.subdomains[1] : ctx.params.appId
|
||||
|
||||
if (!appId) {
|
||||
appId = ctx.referer && ctx.referer.split("/").pop()
|
||||
// if appId can't be determined from path param or subdomain
|
||||
if (!appId && ctx.request.headers.referer) {
|
||||
const url = new URL(ctx.request.headers.referer)
|
||||
// remove leading and trailing slashes from appId
|
||||
appId = url.pathname.replace(/\//g, "")
|
||||
}
|
||||
|
||||
ctx.user = {
|
||||
// if appId can't be determined from path param or subdomain
|
||||
appId: appId,
|
||||
appId,
|
||||
}
|
||||
await next()
|
||||
return
|
||||
|
|
|
@ -12,20 +12,25 @@
|
|||
|
||||
import AgGrid from "@budibase/svelte-ag-grid"
|
||||
import CreateRowButton from "./CreateRow/Button.svelte"
|
||||
import { TextButton as DeleteButton, Icon, Modal, ModalContent } from "@budibase/bbui"
|
||||
import {
|
||||
TextButton as DeleteButton,
|
||||
Icon,
|
||||
Modal,
|
||||
ModalContent,
|
||||
} from "@budibase/bbui"
|
||||
|
||||
export let _bb
|
||||
export let datasource = {}
|
||||
export let editable
|
||||
export let theme = "alpine"
|
||||
export let height = 500
|
||||
export let pagination
|
||||
export let pagination = true
|
||||
|
||||
// These can never change at runtime so don't need to be reactive
|
||||
let canEdit = editable && datasource && datasource.type !== "view"
|
||||
let canAddDelete = editable && datasource && datasource.type === "table"
|
||||
|
||||
let modal;
|
||||
let modal
|
||||
|
||||
let store = _bb.store
|
||||
let dataLoaded = false
|
||||
|
@ -153,7 +158,10 @@
|
|||
on:select={({ detail }) => (selectedRows = detail)} />
|
||||
{/if}
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent title="Confirm Row Deletion" confirmText="Delete" onConfirm={deleteRows} >
|
||||
<ModalContent
|
||||
title="Confirm Row Deletion"
|
||||
confirmText="Delete"
|
||||
onConfirm={deleteRows}>
|
||||
<span>Are you sure you want to delete {selectedRows.length} row(s)?</span>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
|
||||
{#await _appPromise}
|
||||
loading
|
||||
{:then _bb}
|
||||
{:then _}
|
||||
<div id="current_component" bind:this={currentComponent} />
|
||||
{/await}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { Modal, ModalContent, Icon } from '@budibase/bbui'
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { Modal, ModalContent, Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
import { FILE_TYPES } from "./fileTypes"
|
||||
|
@ -9,16 +9,16 @@
|
|||
export let height = "70"
|
||||
export let width = "70"
|
||||
|
||||
let modal;
|
||||
let currentFile;
|
||||
let modal
|
||||
let currentFile
|
||||
|
||||
const openModal = (file) => {
|
||||
const openModal = file => {
|
||||
currentFile = file
|
||||
modal.show()
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
dispatch('delete', currentFile)
|
||||
dispatch("delete", currentFile)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -31,12 +31,19 @@
|
|||
{:else}<i class="far fa-file" />{/if}
|
||||
</a>
|
||||
<span>{file.name}</span>
|
||||
<div class="button-placement"><button primary on:click|stopPropagation={() => openModal(file)}>×</button></div>
|
||||
<div class="button-placement">
|
||||
<button primary on:click|stopPropagation={() => openModal(file)}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent title="Confirm File Deletion" confirmText="Delete" onConfirm={handleConfirm} >
|
||||
<ModalContent
|
||||
title="Confirm File Deletion"
|
||||
confirmText="Delete"
|
||||
onConfirm={handleConfirm}>
|
||||
<span>Are you sure you want to delete this attachment?</span>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
|
Loading…
Reference in New Issue