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/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 @@
Calculate
- -

The statistics of

- + {#if view.calculation} +

of

+ + {/if}
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/userInterface/PropertyControl.svelte b/packages/builder/src/components/userInterface/PropertyControl.svelte index 91e35f2321..8d21e6fc82 100644 --- a/packages/builder/src/components/userInterface/PropertyControl.svelte +++ b/packages/builder/src/components/userInterface/PropertyControl.svelte @@ -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} />
- {#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/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/src/DataGrid/AttachmentCell/Button.svelte b/packages/standard-components/src/DataGrid/AttachmentCell/Button.svelte index 139ffe02aa..12f3cc8e9f 100644 --- a/packages/standard-components/src/DataGrid/AttachmentCell/Button.svelte +++ b/packages/standard-components/src/DataGrid/AttachmentCell/Button.svelte @@ -3,4 +3,4 @@ export let files - \ No newline at end of file + diff --git a/packages/standard-components/src/DataGrid/Component.svelte b/packages/standard-components/src/DataGrid/Component.svelte index 0d86808961..9d249b2929 100644 --- a/packages/standard-components/src/DataGrid/Component.svelte +++ b/packages/standard-components/src/DataGrid/Component.svelte @@ -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} - + Are you sure you want to delete {selectedRows.length} row(s)? 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 e52eeef5a7..64a44ee53d 100644 --- a/packages/standard-components/src/attachments/AttachmentList.svelte +++ b/packages/standard-components/src/attachments/AttachmentList.svelte @@ -1,6 +1,6 @@ @@ -31,12 +31,19 @@ {:else}{/if} {file.name} -
+
+ +
{/each}
- + Are you sure you want to delete this attachment? @@ -67,7 +74,7 @@ position: relative; } - button { + button { display: block; box-sizing: border-box; position: absolute; @@ -85,18 +92,18 @@ border-radius: var(--border-radius-xl); background: black; transition: transform 0.2s cubic-bezier(0.25, 0.1, 0.25, 1), - background 0.2s cubic-bezier(0.25, 0.1, 0.25, 1); + background 0.2s cubic-bezier(0.25, 0.1, 0.25, 1); -webkit-appearance: none; outline: none; - } - button:hover { - background-color: var(--grey-8); - cursor: pointer; - } - button:active { - background-color: var(--grey-9); - cursor: pointer; - } + } + button:hover { + background-color: var(--grey-8); + cursor: pointer; + } + button:active { + background-color: var(--grey-9); + cursor: pointer; + } .file { position: relative;