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 @@
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/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 d0475a1225..20929de9f4 100644 --- a/packages/standard-components/components.json +++ b/packages/standard-components/components.json @@ -246,6 +246,7 @@ "type": "bool", "default": true }, + "detailUrl": "string" } }, "dataform": { 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 d2743cecea..f8fa5d4e5a 100644 --- a/packages/standard-components/src/DataGrid/Component.svelte +++ b/packages/standard-components/src/DataGrid/Component.svelte @@ -19,12 +19,13 @@ 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" let canAddDelete = editable && datasource && datasource.type === "table" - let modal; + let modal let store = _bb.store let dataLoaded = false @@ -74,6 +75,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 } }) @@ -140,7 +154,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/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 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;