-
+
+
Are you sure you want to delete this record? All of your data will be
permanently removed. This action cannot be undone.
@@ -47,7 +47,7 @@
border-top: 1px solid #ccc;
}
- heading {
+ header {
display: flex;
align-items: center;
}
diff --git a/packages/builder/src/components/database/ModelDataTable/modals/DeleteTable.svelte b/packages/builder/src/components/database/DataTable/modals/DeleteTable.svelte
similarity index 96%
rename from packages/builder/src/components/database/ModelDataTable/modals/DeleteTable.svelte
rename to packages/builder/src/components/database/DataTable/modals/DeleteTable.svelte
index 65471a7b28..c47b9e632a 100644
--- a/packages/builder/src/components/database/ModelDataTable/modals/DeleteTable.svelte
+++ b/packages/builder/src/components/database/DataTable/modals/DeleteTable.svelte
@@ -14,10 +14,10 @@
-
+
+
Are you sure you want to delete this table? All of your data will be
permanently removed. This action cannot be undone.
@@ -50,7 +50,7 @@
border-top: 1px solid #ccc;
}
- heading {
+ header {
display: flex;
align-items: center;
}
diff --git a/packages/builder/src/components/database/DataTable/modals/DeleteView.svelte b/packages/builder/src/components/database/DataTable/modals/DeleteView.svelte
new file mode 100644
index 0000000000..9d6c11cdb0
--- /dev/null
+++ b/packages/builder/src/components/database/DataTable/modals/DeleteView.svelte
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+ Are you sure you want to delete this view? All of your data will be
+ permanently removed. This action cannot be undone.
+
+
+
+
Cancel
+
{
+ await backendUiStore.actions.views.delete(viewName)
+ notifier.danger(`View ${viewName} deleted.`)
+ $goto(`./backend`)
+ onClosed()
+ }}>
+ Delete
+
+
+
+
+
diff --git a/packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte b/packages/builder/src/components/database/DataTable/modals/RecordFieldControl.svelte
similarity index 91%
rename from packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte
rename to packages/builder/src/components/database/DataTable/modals/RecordFieldControl.svelte
index ecc79b2ba0..638625a830 100644
--- a/packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte
+++ b/packages/builder/src/components/database/DataTable/modals/RecordFieldControl.svelte
@@ -2,12 +2,10 @@
import { Input, Select } from "@budibase/bbui"
export let type = "text"
- export let value = ""
+ export let value = type === "checkbox" ? false : ""
export let label
export let options = []
- let checked = type === "checkbox" ? value : false
-
const handleInput = event => {
if (event.target.type === "checkbox") {
value = event.target.checked
@@ -38,7 +36,7 @@
thin
placeholder={label}
data-cy="{label}-input"
- {checked}
+ checked={value}
{type}
{value}
on:input={handleInput}
diff --git a/packages/builder/src/components/database/ModelDataTable/modals/index.js b/packages/builder/src/components/database/DataTable/modals/index.js
similarity index 100%
rename from packages/builder/src/components/database/ModelDataTable/modals/index.js
rename to packages/builder/src/components/database/DataTable/modals/index.js
diff --git a/packages/builder/src/components/database/DataTable/popovers/Calculate.svelte b/packages/builder/src/components/database/DataTable/popovers/Calculate.svelte
new file mode 100644
index 0000000000..4cd5931f76
--- /dev/null
+++ b/packages/builder/src/components/database/DataTable/popovers/Calculate.svelte
@@ -0,0 +1,95 @@
+
+
+
+
+
+ Calculate
+
+
+
+ Calculate
+
+
+
+
+
+
+
+
diff --git a/packages/builder/src/components/database/ModelDataTable/popovers/Column.svelte b/packages/builder/src/components/database/DataTable/popovers/Column.svelte
similarity index 84%
rename from packages/builder/src/components/database/ModelDataTable/popovers/Column.svelte
rename to packages/builder/src/components/database/DataTable/popovers/Column.svelte
index c0619fe68a..2047a9a31b 100644
--- a/packages/builder/src/components/database/ModelDataTable/popovers/Column.svelte
+++ b/packages/builder/src/components/database/DataTable/popovers/Column.svelte
@@ -1,6 +1,12 @@
+
+
+
+
+ Group By
+
+
+
+ Group By
+
+
+
+
+
+
+
+
diff --git a/packages/builder/src/components/database/ModelDataTable/popovers/Row.svelte b/packages/builder/src/components/database/DataTable/popovers/Row.svelte
similarity index 87%
rename from packages/builder/src/components/database/ModelDataTable/popovers/Row.svelte
rename to packages/builder/src/components/database/DataTable/popovers/Row.svelte
index afa97c369f..843c636063 100644
--- a/packages/builder/src/components/database/ModelDataTable/popovers/Row.svelte
+++ b/packages/builder/src/components/database/DataTable/popovers/Row.svelte
@@ -1,5 +1,5 @@
+
+
+
+
+ Create New View
+
+
+
+ Create View
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/builder/src/components/database/ModelDataTable/popovers/View.svelte b/packages/builder/src/components/database/ModelDataTable/popovers/View.svelte
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/packages/builder/src/components/nav/ModelNavigator/EditTable.svelte b/packages/builder/src/components/nav/ModelNavigator/EditTable.svelte
index 3bfa5892d1..e7926063dd 100644
--- a/packages/builder/src/components/nav/ModelNavigator/EditTable.svelte
+++ b/packages/builder/src/components/nav/ModelNavigator/EditTable.svelte
@@ -3,7 +3,7 @@
import { backendUiStore } from "builderStore"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
- import DeleteTableModal from "components/database/ModelDataTable/modals/DeleteTable.svelte"
+ import DeleteTableModal from "components/database/DataTable/modals/DeleteTable.svelte"
const { open, close } = getContext("simple-modal")
diff --git a/packages/builder/src/components/nav/ModelNavigator/EditView.svelte b/packages/builder/src/components/nav/ModelNavigator/EditView.svelte
new file mode 100644
index 0000000000..7faf9b980e
--- /dev/null
+++ b/packages/builder/src/components/nav/ModelNavigator/EditView.svelte
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+ {#if editing}
+ Edit View
+
+
+
+
+ {:else}
+
+ -
+
+ Edit
+
+ -
+
+ Delete
+
+
+ {/if}
+
+
+
diff --git a/packages/builder/src/components/nav/ModelNavigator/ListItem.svelte b/packages/builder/src/components/nav/ModelNavigator/ListItem.svelte
index 5fb2832162..54d9eef0e9 100644
--- a/packages/builder/src/components/nav/ModelNavigator/ListItem.svelte
+++ b/packages/builder/src/components/nav/ModelNavigator/ListItem.svelte
@@ -6,15 +6,24 @@
export let indented
-
-
+
+
{title}
diff --git a/packages/builder/yarn.lock b/packages/builder/yarn.lock
index 5f072c0f2c..0d8374600b 100644
--- a/packages/builder/yarn.lock
+++ b/packages/builder/yarn.lock
@@ -688,9 +688,10 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
-"@budibase/bbui@^1.23.1":
- version "1.23.1"
- resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.23.1.tgz#232b0d31379ef64afe5e76f477d5489e67defc22"
+"@budibase/bbui@^1.24.1":
+ version "1.24.1"
+ resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.24.1.tgz#d1c527990c3dcdef78080abfe6aaeef6e8fb2d66"
+ integrity sha512-yZE4Uk6EB3MoIUd2oNN50u8KOXRX65yRFv49gN0T6iCjTjEAdEk8JtsQR9AL3VWn3EdPz5xOkrGsIvNtxqaAFw==
dependencies:
sirv-cli "^0.4.6"
diff --git a/packages/server/src/api/controllers/model.js b/packages/server/src/api/controllers/model.js
index 36dd9730e5..f8c5ebefd7 100644
--- a/packages/server/src/api/controllers/model.js
+++ b/packages/server/src/api/controllers/model.js
@@ -21,6 +21,7 @@ exports.save = async function(ctx) {
const modelToSave = {
type: "model",
_id: newid(),
+ views: {},
...ctx.request.body,
}
diff --git a/packages/server/src/api/controllers/record.js b/packages/server/src/api/controllers/record.js
index eeb5f707b6..7e63da180d 100644
--- a/packages/server/src/api/controllers/record.js
+++ b/packages/server/src/api/controllers/record.js
@@ -80,10 +80,24 @@ exports.save = async function(ctx) {
exports.fetchView = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
+ const { stats, group } = ctx.query
const response = await db.query(`database/${ctx.params.viewName}`, {
- include_docs: true,
+ include_docs: !stats,
+ group,
})
- ctx.body = response.rows.map(row => row.doc)
+
+ if (stats) {
+ for (let row of response.rows) {
+ row.value = {
+ ...row.value,
+ avg: row.value.sum / row.value.count,
+ }
+ }
+ } else {
+ response.rows = response.rows.map(row => row.doc)
+ }
+
+ ctx.body = response.rows
}
exports.fetchModelRecords = async function(ctx) {
diff --git a/packages/server/src/api/controllers/view.js b/packages/server/src/api/controllers/view.js
deleted file mode 100644
index d449ae36f9..0000000000
--- a/packages/server/src/api/controllers/view.js
+++ /dev/null
@@ -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
diff --git a/packages/server/src/api/controllers/view/index.js b/packages/server/src/api/controllers/view/index.js
new file mode 100644
index 0000000000..2ee74838c5
--- /dev/null
+++ b/packages/server/src/api/controllers/view/index.js
@@ -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
diff --git a/packages/server/src/api/controllers/view/viewBuilder.js b/packages/server/src/api/controllers/view/viewBuilder.js
new file mode 100644
index 0000000000..1c8cc49b03
--- /dev/null
+++ b/packages/server/src/api/controllers/view/viewBuilder.js
@@ -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
diff --git a/packages/server/src/api/routes/tests/__snapshots__/view.spec.js.snap b/packages/server/src/api/routes/tests/__snapshots__/view.spec.js.snap
new file mode 100644
index 0000000000..96ce281ee1
--- /dev/null
+++ b/packages/server/src/api/routes/tests/__snapshots__/view.spec.js.snap
@@ -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,
+ },
+ },
+]
+`;
diff --git a/packages/server/src/api/routes/tests/couchTestUtils.js b/packages/server/src/api/routes/tests/couchTestUtils.js
index e87233d458..594c4a3121 100644
--- a/packages/server/src/api/routes/tests/couchTestUtils.js
+++ b/packages/server/src/api/routes/tests/couchTestUtils.js
@@ -193,9 +193,6 @@ const createUserWithPermissions = async (
accessLevelId: accessRes.body._id,
})
- //const db = new CouchDB(instanceId)
- //const designDoc = await db.get("_design/database")
-
const anonUser = {
userId: "ANON",
accessLevelId: ANON_LEVEL_ID,
diff --git a/packages/server/src/api/routes/tests/view.spec.js b/packages/server/src/api/routes/tests/view.spec.js
index 5a889bed67..24948da3bc 100644
--- a/packages/server/src/api/routes/tests/view.spec.js
+++ b/packages/server/src/api/routes/tests/view.spec.js
@@ -4,7 +4,8 @@ const {
createInstance,
createModel,
supertest,
- defaultHeaders
+ defaultHeaders,
+ getDocument
} = require("./couchTestUtils")
describe("/views", () => {
@@ -14,6 +15,25 @@ describe("/views", () => {
let instance
let model
+ const createView = async (config = {
+ name: "TestView",
+ field: "Price",
+ modelId: model._id
+ }) =>
+ await request
+ .post(`/api/views`)
+ .send(config)
+ .set(defaultHeaders(app._id, instance._id))
+ .expect('Content-Type', /json/)
+ .expect(200)
+
+ const createRecord = async record => request
+ .post(`/api/${model._id}/records`)
+ .send(record)
+ .set(defaultHeaders(app._id, instance._id))
+ .expect('Content-Type', /json/)
+ .expect(200)
+
beforeAll(async () => {
({ request, server } = await supertest())
await createClientDatabase(request)
@@ -28,46 +48,111 @@ describe("/views", () => {
server.close()
})
- const createView = async () =>
- await request
- .post(`/api/views`)
- .send({
- name: "TestView",
- map: `function(doc) {
- if (doc.id) {
- emit(doc.name, doc._id);
- }
- }`,
- reduce: `function(keys, values) { }`
- })
- .set(defaultHeaders(app._id, instance._id))
- .expect('Content-Type', /json/)
- .expect(200)
-
describe("create", () => {
+ beforeEach(async () => {
+ model = await createModel(request, app._id, instance._id);
+ })
it("returns a success message when the view is successfully created", async () => {
const res = await createView()
- expect(res.res.statusMessage).toEqual("View TestView created successfully.");
- expect(res.body.name).toEqual("TestView");
+ expect(res.res.statusMessage).toEqual("View TestView saved successfully.");
+ })
+
+ it("updates the model record with the new view metadata", async () => {
+ const res = await createView()
+ expect(res.res.statusMessage).toEqual("View TestView saved successfully.");
+ const updatedModel = await getDocument(instance._id, model._id)
+ expect(updatedModel.views).toEqual({
+ TestView: {
+ field: "Price",
+ modelId: model._id,
+ schema: {
+ sum: "number",
+ min: "number",
+ max: "number",
+ count: "number",
+ sumsqr: "number",
+ avg: "number"
+ }
+ }
+ });
})
});
describe("fetch", () => {
-
beforeEach(async () => {
model = await createModel(request, app._id, instance._id);
});
- it("should only return custom views", async () => {
- const view = await createView()
+ it("returns only custom views", async () => {
+ await createView()
const res = await request
.get(`/api/views`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.length).toBe(1)
- expect(res.body.find(v => v.name === view.body.name)).toBeDefined()
+ expect(res.body.find(({ name }) => name === "TestView")).toBeDefined()
+ })
+ });
+
+ describe("query", () => {
+ beforeEach(async () => {
+ model = await createModel(request, app._id, instance._id);
+ });
+
+ it("returns data for the created view", async () => {
+ await createView()
+ await createRecord({
+ modelId: model._id,
+ Price: 1000
+ })
+ await createRecord({
+ modelId: model._id,
+ Price: 2000
+ })
+ await createRecord({
+ modelId: model._id,
+ Price: 4000
+ })
+ const res = await request
+ .get(`/api/views/TestView?stats=true`)
+ .set(defaultHeaders(app._id, instance._id))
+ .expect('Content-Type', /json/)
+ .expect(200)
+ expect(res.body.length).toBe(1)
+ expect(res.body).toMatchSnapshot()
+ })
+
+ it("returns data for the created view using a group by", async () => {
+ await createView({
+ name: "TestView",
+ field: "Price",
+ groupBy: "Category",
+ modelId: model._id
+ })
+ await createRecord({
+ modelId: model._id,
+ Price: 1000,
+ Category: "One"
+ })
+ await createRecord({
+ modelId: model._id,
+ Price: 2000,
+ Category: "One"
+ })
+ await createRecord({
+ modelId: model._id,
+ Price: 4000,
+ Category: "Two"
+ })
+ const res = await request
+ .get(`/api/views/TestView?stats=true&group=Category`)
+ .set(defaultHeaders(app._id, instance._id))
+ .expect('Content-Type', /json/)
+ .expect(200)
+ expect(res.body.length).toBe(2)
+ expect(res.body).toMatchSnapshot()
})
});
});
\ No newline at end of file
diff --git a/packages/server/src/api/routes/view.js b/packages/server/src/api/routes/view.js
index 5a73eea22e..53d27b9268 100644
--- a/packages/server/src/api/routes/view.js
+++ b/packages/server/src/api/routes/view.js
@@ -13,8 +13,7 @@ router
recordController.fetchView
)
.get("/api/views", authorized(BUILDER), viewController.fetch)
- // .patch("/api/:databaseId/views", controller.update);
- // .delete("/api/:instanceId/views/:viewId/:revId", controller.destroy);
- .post("/api/views", authorized(BUILDER), viewController.create)
+ .delete("/api/views/:viewName", authorized(BUILDER), viewController.destroy)
+ .post("/api/views", authorized(BUILDER), viewController.save)
module.exports = router
diff --git a/packages/server/src/app.js b/packages/server/src/app.js
index 09016f5521..ee6fb3172d 100644
--- a/packages/server/src/app.js
+++ b/packages/server/src/app.js
@@ -45,6 +45,8 @@ const server = http.createServer(app.callback())
server.on("close", () => console.log("Server Closed"))
+process.on("SIGINT", () => process.exit(1))
+
module.exports = server.listen(env.PORT || 4001, () => {
console.log(`Budibase running on ${JSON.stringify(server.address())}`)
})
diff --git a/packages/server/src/electron.js b/packages/server/src/electron.js
index 32bcd2bd43..636ddc8b46 100644
--- a/packages/server/src/electron.js
+++ b/packages/server/src/electron.js
@@ -36,7 +36,10 @@ async function startApp() {
async function createWindow() {
app.server = require("./app")
- win = new BrowserWindow({ width: 1920, height: 1080 })
+ win = new BrowserWindow({
+ width: 1920,
+ height: 1080,
+ })
win.setTitle(APP_TITLE)
win.loadURL(APP_URL)
if (isDev) {
diff --git a/packages/standard-components/src/Chart/tests/bar.html b/packages/standard-components/src/Chart/tests/bar.html
index cf90c96c17..9828587375 100644
--- a/packages/standard-components/src/Chart/tests/bar.html
+++ b/packages/standard-components/src/Chart/tests/bar.html
@@ -8,8 +8,7 @@
-
-
+