diff --git a/packages/builder/cypress/integration/createView.spec.js b/packages/builder/cypress/integration/createView.spec.js new file mode 100644 index 0000000000..b605135ad4 --- /dev/null +++ b/packages/builder/cypress/integration/createView.spec.js @@ -0,0 +1,85 @@ +context('Create a View', () => { + before(() => { + cy.visit('localhost:4001/_builder') + cy.createApp('View App', 'View App Description') + cy.createTable('data') + cy.addColumn('data', 'group', 'Plain Text') + cy.addColumn('data', 'age', 'Number') + cy.addColumn('data', 'rating', 'Number') + + cy.addRecord(["Students", 25, 1]) + cy.addRecord(["Students", 20, 3]) + cy.addRecord(["Students", 18, 6]) + cy.addRecord(["Students", 25, 2]) + cy.addRecord(["Teachers", 49, 5]) + cy.addRecord(["Teachers", 36, 3]) + }) + + + it('creates a stats view based on age', () => { + cy.contains("Create New View").click() + cy.get("[placeholder='View Name']").type("Test View") + cy.contains("Save View").click() + cy.get("thead th").should(($headers) => { + expect($headers).to.have.length(7) + const headers = $headers.map((i, header) => Cypress.$(header).text()) + expect(headers.get()).to.deep.eq([ + "group", + "sum", + "min", + "max", + "sumsqr", + "count", + "avg", + ]) + }) + cy.get("tbody td").should(($values) => { + const values = $values.map((i, value) => Cypress.$(value).text()) + expect(values.get()).to.deep.eq([ + "null", + "173", + "18", + "49", + "5671", + "6", + "28.833333333333332" + ]) + }) + }) + + it('groups the stats view by group', () => { + cy.contains("Group By").click() + cy.get("select").select("group") + cy.contains("Save").click() + cy.contains("Students").should("be.visible") + cy.contains("Teachers").should("be.visible") + + cy.get("tbody tr").first().find("td").should(($values) => { + const values = $values.map((i, value) => Cypress.$(value).text()) + expect(values.get()).to.deep.eq([ + "Students", + "88", + "18", + "25", + "1974", + "4", + "22" + ]) + }) + }) + + it('renames a view', () => { + cy.contains("[data-cy=model-nav-item]", "Test View").find(".ri-more-line").click() + cy.contains("Edit").click() + cy.get("[placeholder='View Name']").type(" Updated") + cy.contains("Save").click() + cy.contains("Test View Updated").should("be.visible") + }) + + it('deletes a view', () => { + cy.contains("[data-cy=model-nav-item]", "Test View Updated").find(".ri-more-line").click() + cy.contains("Delete").click() + cy.get(".content").contains("button", "Delete").click() + cy.contains("TestView Updated").should("not.be.visible") + }) +}) diff --git a/packages/builder/package.json b/packages/builder/package.json index eb9fe4f8e5..e223397508 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -57,7 +57,7 @@ ] }, "dependencies": { - "@budibase/bbui": "^1.23.1", + "@budibase/bbui": "^1.24.1", "@budibase/client": "^0.1.17", "@budibase/colorpicker": "^1.0.1", "@sentry/browser": "5.19.1", diff --git a/packages/builder/src/builderStore/store/backend.js b/packages/builder/src/builderStore/store/backend.js index 28272d9893..80f931686b 100644 --- a/packages/builder/src/builderStore/store/backend.js +++ b/packages/builder/src/builderStore/store/backend.js @@ -23,13 +23,10 @@ export const getBackendUiStore = () => { database: { select: async db => { const modelsResponse = await api.get(`/api/models`) - const viewsResponse = await api.get(`/api/views`) const models = await modelsResponse.json() - const views = await viewsResponse.json() store.update(state => { state.selectedDatabase = db state.models = models - state.views = views return state }) }, @@ -59,8 +56,7 @@ export const getBackendUiStore = () => { store.update(state => { state.selectedModel = model state.draftModel = cloneDeep(model) - state.selectedField = "" - state.selectedView = `all_${model._id}` + state.selectedView = { name: `all_${model._id}` } return state }), save: async model => { @@ -99,10 +95,8 @@ export const getBackendUiStore = () => { // delete the original if renaming delete state.draftModel.schema[originalName] - state.draftModel.schema = { - ...state.draftModel.schema, - [field.name]: cloneDeep(field), - } + state.draftModel.schema[field.name] = cloneDeep(field) + store.actions.models.save(state.draftModel) return state }) @@ -119,8 +113,30 @@ export const getBackendUiStore = () => { select: view => store.update(state => { state.selectedView = view + state.selectedModel = {} return state }), + delete: async view => { + await api.delete(`/api/views/${view}`) + await store.actions.models.fetch() + }, + save: async view => { + await api.post(`/api/views`, view) + + store.update(state => { + const viewModel = state.models.find( + model => model._id === view.modelId + ) + // TODO: Cleaner? + if (!viewModel.views) viewModel.views = {} + if (view.originalName) delete viewModel.views[view.originalName] + viewModel.views[view.name] = view + + state.models = state.models + state.selectedView = view + return state + }) + }, }, users: { create: user => diff --git a/packages/builder/src/components/database/ModelDataTable/LinkedRecord.svelte b/packages/builder/src/components/database/DataTable/LinkedRecord.svelte similarity index 100% rename from packages/builder/src/components/database/ModelDataTable/LinkedRecord.svelte rename to packages/builder/src/components/database/DataTable/LinkedRecord.svelte diff --git a/packages/builder/src/components/database/ModelDataTable/ModelDataTable.svelte b/packages/builder/src/components/database/DataTable/ModelDataTable.svelte similarity index 91% rename from packages/builder/src/components/database/ModelDataTable/ModelDataTable.svelte rename to packages/builder/src/components/database/DataTable/ModelDataTable.svelte index 80abd5e94a..6233e8cc19 100644 --- a/packages/builder/src/components/database/ModelDataTable/ModelDataTable.svelte +++ b/packages/builder/src/components/database/DataTable/ModelDataTable.svelte @@ -1,6 +1,7 @@
@@ -71,6 +59,7 @@ {#if Object.keys($backendUiStore.selectedModel.schema).length > 0} + {/if} @@ -101,7 +90,7 @@ {#if schema[header].type === 'link'} - {:else}{row[header] || ''}{/if} + {:else}{getOr('', header, row)}{/if} {/each} diff --git a/packages/builder/src/components/database/DataTable/Table.svelte b/packages/builder/src/components/database/DataTable/Table.svelte new file mode 100644 index 0000000000..8068a270cb --- /dev/null +++ b/packages/builder/src/components/database/DataTable/Table.svelte @@ -0,0 +1,141 @@ + + +
+
+

{title}

+
+ +
+
+ + + + {#each columns as header} + + {/each} + + + + {#if paginatedData.length === 0} +
No Data.
+ {/if} + {#each paginatedData as row} + + {#each columns as header} + + {/each} + + {/each} + +
{header.name}
{getOr(row.default || '', header.key, row)}
+ +
+ + diff --git a/packages/builder/src/components/database/ModelDataTable/TablePagination.svelte b/packages/builder/src/components/database/DataTable/TablePagination.svelte similarity index 100% rename from packages/builder/src/components/database/ModelDataTable/TablePagination.svelte rename to packages/builder/src/components/database/DataTable/TablePagination.svelte diff --git a/packages/builder/src/components/database/DataTable/ViewDataTable.svelte b/packages/builder/src/components/database/DataTable/ViewDataTable.svelte new file mode 100644 index 0000000000..838512a872 --- /dev/null +++ b/packages/builder/src/components/database/DataTable/ViewDataTable.svelte @@ -0,0 +1,74 @@ + + + + + +
diff --git a/packages/builder/src/components/database/ModelDataTable/api.js b/packages/builder/src/components/database/DataTable/api.js similarity index 67% rename from packages/builder/src/components/database/ModelDataTable/api.js rename to packages/builder/src/components/database/DataTable/api.js index 3434cdb4f1..f4c9a44380 100644 --- a/packages/builder/src/components/database/ModelDataTable/api.js +++ b/packages/builder/src/components/database/DataTable/api.js @@ -6,20 +6,6 @@ export async function createUser(user) { return await response.json() } -export async function createDatabase(appname, instanceName) { - const CREATE_DATABASE_URL = `/api/${appname}/instances` - const response = await api.post(CREATE_DATABASE_URL, { - name: instanceName, - }) - return await response.json() -} - -export async function deleteRecord(record) { - const DELETE_RECORDS_URL = `/api/${record.modelId}/records/${record._id}/${record._rev}` - const response = await api.delete(DELETE_RECORDS_URL) - return response -} - export async function saveRecord(record, modelId) { const SAVE_RECORDS_URL = `/api/${modelId}/records` const response = await api.post(SAVE_RECORDS_URL, record) @@ -27,8 +13,14 @@ export async function saveRecord(record, modelId) { return await response.json() } -export async function fetchDataForView(viewName) { - const FETCH_RECORDS_URL = `/api/views/${viewName}` +export async function deleteRecord(record) { + const DELETE_RECORDS_URL = `/api/${record.modelId}/records/${record._id}/${record._rev}` + const response = await api.delete(DELETE_RECORDS_URL) + return response +} + +export async function fetchDataForView(view) { + const FETCH_RECORDS_URL = `/api/views/${view.name}` const response = await api.get(FETCH_RECORDS_URL) return await response.json() diff --git a/packages/builder/src/components/database/ModelDataTable/index.js b/packages/builder/src/components/database/DataTable/index.js similarity index 100% rename from packages/builder/src/components/database/ModelDataTable/index.js rename to packages/builder/src/components/database/DataTable/index.js diff --git a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/database/DataTable/modals/CreateEditColumn.svelte similarity index 84% rename from packages/builder/src/components/database/ModelDataTable/modals/CreateEditColumn.svelte rename to packages/builder/src/components/database/DataTable/modals/CreateEditColumn.svelte index b8eca01aad..f4c60fc8e1 100644 --- a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/database/DataTable/modals/CreateEditColumn.svelte @@ -15,22 +15,20 @@ import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte" import * as api from "../api" - export let onClosed - export let field = {} - let fieldDefinitions = cloneDeep(FIELDS) + + export let onClosed + export let field = { + type: "string", + constraints: fieldDefinitions.STRING.constraints, + } + let originalName = field.name $: required = field.constraints && field.constraints.presence && !field.constraints.presence.allowEmpty - $: if (field.type) { - field.constraints = merge( - fieldDefinitions[field.type.toUpperCase()].constraints, - field.constraints - ) - } async function saveColumn() { backendUiStore.update(state => { @@ -43,14 +41,26 @@ }) onClosed() } + + function handleFieldConstraints(event) { + const { type, constraints } = fieldDefinitions[ + event.target.value.toUpperCase() + ] + field.type = type + field.constraints = constraints + }
- {#each Object.values(fieldDefinitions) as field} - + {/each} @@ -63,28 +73,28 @@ on:change={() => (field.constraints.presence.allowEmpty = required)} />
- {#if field.type === 'string'} + {#if field.value === 'string' && field.constraints} - {:else if field.type === 'datetime'} + {:else if field.value === 'datetime' && field.constraints} - {:else if field.type === 'number'} + {:else if field.value === 'number' && field.constraints} - {:else if field.type === 'link'} + {:else if field.value === 'link'}
+ {#each CALCULATIONS as calculation} + + {/each} + +

of

+ +
+
+ + +
+ + + 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
+
+

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 @@ - - +