diff --git a/packages/builder/cypress/integration/createView.spec.js b/packages/builder/cypress/integration/createView.spec.js
index e35688c813..697bc35143 100644
--- a/packages/builder/cypress/integration/createView.spec.js
+++ b/packages/builder/cypress/integration/createView.spec.js
@@ -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 => {
diff --git a/packages/builder/src/builderStore/store/index.js b/packages/builder/src/builderStore/store/index.js
index 5a6c8581aa..277f57584e 100644
--- a/packages/builder/src/builderStore/store/index.js
+++ b/packages/builder/src/builderStore/store/index.js
@@ -24,6 +24,7 @@ import {
saveCurrentPreviewItem as _saveCurrentPreviewItem,
saveScreenApi as _saveScreenApi,
regenerateCssForCurrentScreen,
+ regenerateCssForScreen,
generateNewIdsForComponent,
getComponentDefinition,
} from "../storeUtils"
@@ -98,6 +99,29 @@ const setPackage = (store, initial) => async pkg => {
},
}
+ // if the app has just been created
+ // we need to build the CSS and save
+ if (pkg.justCreated) {
+ const generateInitialPageCss = async name => {
+ const page = pkg.pages[name]
+ regenerateCssForScreen(page)
+ for (let screen of page._screens) {
+ regenerateCssForScreen(screen)
+ }
+
+ await api.post(`/_builder/api/${pkg.application._id}/pages/${name}`, {
+ page: {
+ componentLibraries: pkg.application.componentLibraries,
+ ...page,
+ },
+ screens: page._screens,
+ })
+ }
+ generateInitialPageCss("main")
+ generateInitialPageCss("unauthenticated")
+ pkg.justCreated = false
+ }
+
initial.libraries = pkg.application.componentLibraries
initial.components = await fetchComponentLibDefinitions(pkg.application._id)
initial.name = pkg.application.name
@@ -156,8 +180,8 @@ const createScreen = store => async screen => {
state.currentPreviewItem = screen
state.currentComponentInfo = screen.props
state.currentFrontEndType = "screen"
- savePromise = _saveScreen(store, state, screen)
regenerateCssForCurrentScreen(state)
+ savePromise = _saveScreen(store, state, screen)
return state
})
await savePromise
diff --git a/packages/builder/src/builderStore/storeUtils.js b/packages/builder/src/builderStore/storeUtils.js
index 9111f06bd5..6155974d2b 100644
--- a/packages/builder/src/builderStore/storeUtils.js
+++ b/packages/builder/src/builderStore/storeUtils.js
@@ -79,10 +79,12 @@ export const walkProps = (props, action, cancelToken = null) => {
}
}
+export const regenerateCssForScreen = screen => {
+ screen._css = generate_screen_css([screen.props])
+}
+
export const regenerateCssForCurrentScreen = state => {
- state.currentPreviewItem._css = generate_screen_css([
- state.currentPreviewItem.props,
- ])
+ regenerateCssForScreen(state.currentPreviewItem)
return state
}
diff --git a/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte
index d3c82e4f7c..5b56e3d125 100644
--- a/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte
+++ b/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte
@@ -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
}
-
+
{#if view.calculation}
diff --git a/packages/builder/src/components/backend/DataTable/buttons/CalculateButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/CalculateButton.svelte
index efe3dcd79d..0452491247 100644
--- a/packages/builder/src/components/backend/DataTable/buttons/CalculateButton.svelte
+++ b/packages/builder/src/components/backend/DataTable/buttons/CalculateButton.svelte
@@ -9,7 +9,11 @@
-
+
Calculate
diff --git a/packages/builder/src/components/backend/DataTable/popovers/CalculatePopover.svelte b/packages/builder/src/components/backend/DataTable/popovers/CalculatePopover.svelte
index 1b079dbb4a..23965a63e0 100644
--- a/packages/builder/src/components/backend/DataTable/popovers/CalculatePopover.svelte
+++ b/packages/builder/src/components/backend/DataTable/popovers/CalculatePopover.svelte
@@ -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 @@
diff --git a/packages/builder/src/components/backend/DataTable/popovers/CreateEditColumnPopover.svelte b/packages/builder/src/components/backend/DataTable/popovers/CreateEditColumnPopover.svelte
index 3748e4ed98..e803ca21cf 100644
--- a/packages/builder/src/components/backend/DataTable/popovers/CreateEditColumnPopover.svelte
+++ b/packages/builder/src/components/backend/DataTable/popovers/CreateEditColumnPopover.svelte
@@ -72,18 +72,17 @@
- {#if !originalName}
-
- {/if}
+
{#if field.type !== 'link'}
No Users found
{/each}
- {:catch error}
+ {:catch err}
Something went wrong when trying to fetch users. Please refresh (CMD + R /
CTRL + R) the page and try again.
{/await}
diff --git a/packages/builder/src/components/start/CreateAppModal.svelte b/packages/builder/src/components/start/CreateAppModal.svelte
index e171dc836e..7ee707d490 100644
--- a/packages/builder/src/components/start/CreateAppModal.svelte
+++ b/packages/builder/src/components/start/CreateAppModal.svelte
@@ -154,6 +154,7 @@
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
backendUiStore.actions.reset()
+ pkg.justCreated = true
await store.setPackage(pkg)
automationStore.actions.fetch()
} else {
diff --git a/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte b/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte
index 74ec5694cd..8393299b12 100644
--- a/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte
+++ b/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte
@@ -11,6 +11,9 @@
export let screens = []
+ $: sortedScreens = screens.sort(
+ (s1, s2) => s1.props._instanceName > s2.props._instanceName
+ )
/*
Using a store here seems odd....
have a look in the code file to find out why.
@@ -24,12 +27,15 @@
const joinPath = join("/")
const normalizedName = name =>
- pipe(name, [
- trimCharsStart("./"),
- trimCharsStart("~/"),
- trimCharsStart("../"),
- trimChars(" "),
- ])
+ pipe(
+ name,
+ [
+ trimCharsStart("./"),
+ trimCharsStart("~/"),
+ trimCharsStart("../"),
+ trimChars(" "),
+ ]
+ )
const changeScreen = screen => {
store.setCurrentScreen(screen.props._instanceName)
@@ -38,7 +44,7 @@
- {#each screens as screen}
+ {#each sortedScreens as screen}
- {#if control === Input && !key.startsWith('_')}
+ {#if bindable && control === Input && !key.startsWith('_')}
diff --git a/packages/builder/src/components/userInterface/SettingsView.svelte b/packages/builder/src/components/userInterface/SettingsView.svelte
index 8698701bf3..d821f1f2e2 100644
--- a/packages/builder/src/components/userInterface/SettingsView.svelte
+++ b/packages/builder/src/components/userInterface/SettingsView.svelte
@@ -88,6 +88,7 @@
{#if screenOrPageInstance}
{#each screenOrPageDefinition as def}
- {:then results}
+ {:then _}
{:catch error}
Something went wrong: {error.message}
diff --git a/packages/client/src/render/getAppId.js b/packages/client/src/render/getAppId.js
index e6216e7be5..4cbb6692b5 100644
--- a/packages/client/src/render/getAppId.js
+++ b/packages/client/src/render/getAppId.js
@@ -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]))
diff --git a/packages/client/src/render/screenRouter.js b/packages/client/src/render/screenRouter.js
index 14337c4319..6af4f5491a 100644
--- a/packages/client/src/render/screenRouter.js
+++ b/packages/client/src/render/screenRouter.js
@@ -31,7 +31,9 @@ export const screenRouter = ({ screens, onScreenSelected, window }) => {
function route(url) {
const _url = makeRootedPath(url.state || url)
current = routes.findIndex(
- p => p !== "*" && new RegExp("^" + p + "$").test(_url)
+ p =>
+ p !== "*" &&
+ new RegExp("^" + p.toLowerCase() + "$").test(_url.toLowerCase())
)
const params = {}
diff --git a/packages/server/src/api/controllers/deploy/aws.js b/packages/server/src/api/controllers/deploy/aws.js
index 1337f23d1c..28bedb8893 100644
--- a/packages/server/src/api/controllers/deploy/aws.js
+++ b/packages/server/src/api/controllers/deploy/aws.js
@@ -2,6 +2,7 @@ const fs = require("fs")
const { join } = require("../../../utilities/centralPath")
const AWS = require("aws-sdk")
const fetch = require("node-fetch")
+const uuid = require("uuid")
const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
const PouchDB = require("../../../db")
const environment = require("../../../environment")
@@ -13,7 +14,7 @@ async function invalidateCDN(cfDistribution, appId) {
.createInvalidation({
DistributionId: cfDistribution,
InvalidationBatch: {
- CallerReference: appId,
+ CallerReference: `${appId}-${uuid.v4()}`,
Paths: {
Quantity: 1,
Items: [`/assets/${appId}/*`],
diff --git a/packages/server/src/api/controllers/row.js b/packages/server/src/api/controllers/row.js
index 4042336741..1e12bf7a78 100644
--- a/packages/server/src/api/controllers/row.js
+++ b/packages/server/src/api/controllers/row.js
@@ -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) {
diff --git a/packages/server/src/api/controllers/view/tests/__snapshots__/viewBuilder.spec.js.snap b/packages/server/src/api/controllers/view/tests/__snapshots__/viewBuilder.spec.js.snap
index cd66d040f3..dfe5f78c46 100644
--- a/packages/server/src/api/controllers/view/tests/__snapshots__/viewBuilder.spec.js.snap
+++ b/packages/server/src/api/controllers/view/tests/__snapshots__/viewBuilder.spec.js.snap
@@ -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",
}
`;
diff --git a/packages/server/src/api/controllers/view/viewBuilder.js b/packages/server/src/api/controllers/view/viewBuilder.js
index df1c3daef9..e9fa29856d 100644
--- a/packages/server/src/api/controllers/view/viewBuilder.js
+++ b/packages/server/src/api/controllers/view/viewBuilder.js
@@ -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
diff --git a/packages/server/src/api/routes/tests/view.spec.js b/packages/server/src/api/routes/tests/view.spec.js
index 3c10c7e320..1265b37f5b 100644
--- a/packages/server/src/api/routes/tests/view.spec.js
+++ b/packages/server/src/api/routes/tests/view.spec.js
@@ -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: {
- type: "string",
- constraints: {
- type: "string"
- },
+ sum: {
+ type: "number",
},
- description: {
+ min: {
+ type: "number",
+ },
+ max: {
+ type: "number",
+ },
+ count: {
+ type: "number",
+ },
+ sumsqr: {
+ type: "number",
+ },
+ avg: {
+ type: "number",
+ },
+ field: {
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()
})
diff --git a/packages/server/src/middleware/authenticated.js b/packages/server/src/middleware/authenticated.js
index aebe6c6bd7..ba5da28f63 100644
--- a/packages/server/src/middleware/authenticated.js
+++ b/packages/server/src/middleware/authenticated.js
@@ -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
diff --git a/packages/standard-components/components.json b/packages/standard-components/components.json
index 7d64f49c84..20929de9f4 100644
--- a/packages/standard-components/components.json
+++ b/packages/standard-components/components.json
@@ -245,7 +245,8 @@
"pagination": {
"type": "bool",
"default": true
- }
+ },
+ "detailUrl": "string"
}
},
"dataform": {
diff --git a/packages/standard-components/src/DataGrid/Component.svelte b/packages/standard-components/src/DataGrid/Component.svelte
index 3ccf826711..35b214f00f 100644
--- a/packages/standard-components/src/DataGrid/Component.svelte
+++ b/packages/standard-components/src/DataGrid/Component.svelte
@@ -25,6 +25,7 @@
export let theme = "alpine"
export let height = 500
export let pagination
+ export let detailUrl
// These can never change at runtime so don't need to be reactive
let canEdit = editable && datasource && datasource.type !== "view"
@@ -80,6 +81,19 @@
autoHeight: true,
}
})
+ columnDefs = [...columnDefs, {
+ headerName: 'Details',
+ field: '_id',
+ width: 25,
+ flex: 0,
+ editable: false,
+ sortable: false,
+ cellRenderer: getRenderer({
+ type: '_id',
+ options: detailUrl
+ }),
+ autoHeight: true,
+ }]
dataLoaded = true
}
})
diff --git a/packages/standard-components/src/DataGrid/ViewDetails/Cell.svelte b/packages/standard-components/src/DataGrid/ViewDetails/Cell.svelte
new file mode 100644
index 0000000000..58cf2a5408
--- /dev/null
+++ b/packages/standard-components/src/DataGrid/ViewDetails/Cell.svelte
@@ -0,0 +1,12 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/standard-components/src/DataGrid/customRenderer.js b/packages/standard-components/src/DataGrid/customRenderer.js
index 607a4f8432..aca4e7d39e 100644
--- a/packages/standard-components/src/DataGrid/customRenderer.js
+++ b/packages/standard-components/src/DataGrid/customRenderer.js
@@ -2,6 +2,7 @@
// https://www.ag-grid.com/javascript-grid-cell-rendering-components/
import AttachmentCell from "./AttachmentCell/Button.svelte"
+import ViewDetails from "./ViewDetails/Cell.svelte"
import Select from "./Select/Wrapper.svelte"
import DatePicker from "./DateTime/Wrapper.svelte"
import RelationshipDisplay from "./Relationship/RelationshipDisplay.svelte"
@@ -11,18 +12,19 @@ const renderers = new Map([
["attachment", attachmentRenderer],
["options", optionsRenderer],
["link", linkedRowRenderer],
+ ["_id", viewDetailsRenderer],
])
-export function getRenderer({ type, constraints }, editable) {
- if (renderers.get(type)) {
- return renderers.get(type)(constraints, editable)
+export function getRenderer(schema, editable) {
+ if (renderers.get(schema.type)) {
+ return renderers.get(schema.type)(schema.options, editable)
} else {
return false
}
}
/* eslint-disable no-unused-vars */
-function booleanRenderer(constraints, editable) {
+function booleanRenderer(options, editable) {
return params => {
const toggle = e => {
params.value = !params.value
@@ -44,7 +46,7 @@ function booleanRenderer(constraints, editable) {
}
}
/* eslint-disable no-unused-vars */
-function attachmentRenderer(constraints, editable) {
+function attachmentRenderer(options, editable) {
return params => {
const container = document.createElement("div")
@@ -66,7 +68,7 @@ function attachmentRenderer(constraints, editable) {
}
}
/* eslint-disable no-unused-vars */
-function dateRenderer(constraints, editable) {
+function dateRenderer(options, editable) {
return function(params) {
const container = document.createElement("div")
const toggle = e => {
@@ -111,7 +113,7 @@ function optionsRenderer({ inclusion }, editable) {
}
}
/* eslint-disable no-unused-vars */
-function linkedRowRenderer(constraints, editable) {
+function linkedRowRenderer(options, editable) {
return params => {
let container = document.createElement("div")
container.style.display = "grid"
@@ -129,3 +131,22 @@ function linkedRowRenderer(constraints, editable) {
return container
}
}
+
+/* eslint-disable no-unused-vars */
+function viewDetailsRenderer(options) {
+ return params => {
+ let container = document.createElement("div")
+ container.style.display = "grid"
+ container.style.placeItems = "center"
+ container.style.height = "100%"
+
+ new ViewDetails({
+ target: container,
+ props: {
+ url: `${options}/${params.data._id}`,
+ },
+ })
+
+ return container
+ }
+}
diff --git a/packages/standard-components/src/Test/TestApp.svelte b/packages/standard-components/src/Test/TestApp.svelte
index 13504e1e15..0c6da55601 100644
--- a/packages/standard-components/src/Test/TestApp.svelte
+++ b/packages/standard-components/src/Test/TestApp.svelte
@@ -35,7 +35,7 @@
{#await _appPromise}
loading
-{:then _bb}
+{:then _}
{/await}
diff --git a/packages/standard-components/src/attachments/AttachmentList.svelte b/packages/standard-components/src/attachments/AttachmentList.svelte
index 9d9b96246d..64a44ee53d 100644
--- a/packages/standard-components/src/attachments/AttachmentList.svelte
+++ b/packages/standard-components/src/attachments/AttachmentList.svelte
@@ -32,9 +32,9 @@
{file.name}
-
+
{/each}