diff --git a/package.json b/package.json index 1ca7218bd4..6a70983bcf 100644 --- a/package.json +++ b/package.json @@ -35,4 +35,4 @@ "dependencies": { "@fortawesome/fontawesome": "^1.1.8" } -} +} \ No newline at end of file diff --git a/packages/builder/src/builderStore/store/index.js b/packages/builder/src/builderStore/store/index.js index 4eeb2e3842..bb61712f1a 100644 --- a/packages/builder/src/builderStore/store/index.js +++ b/packages/builder/src/builderStore/store/index.js @@ -53,6 +53,7 @@ export const getStore = () => { store.saveScreen = saveScreen(store) store.setCurrentScreen = setCurrentScreen(store) + store.deleteScreens = deleteScreens(store) store.setCurrentPage = setCurrentPage(store) store.createScreen = createScreen(store) store.addStylesheet = addStylesheet(store) @@ -185,6 +186,26 @@ const setCurrentScreen = store => screenName => { }) } +const deleteScreens = store => (screens, pageName = null) => { + if (!(screens instanceof Array)) { + screens = [screens] + } + store.update(state => { + if (pageName == null) { + pageName = state.pages.main.name + } + for (let screen of screens) { + state.screens = state.screens.filter(c => c.name !== screen.name) + // Remove screen from current page as well + state.pages[pageName]._screens = state.pages[pageName]._screens.filter( + scr => scr.name !== screen.name + ) + api.delete(`/_builder/api/pages/${pageName}/screens/${screen.name}`) + } + return state + }) +} + const savePage = store => async page => { store.update(state => { if (state.currentFrontEndType !== "page" || !state.currentPageName) { diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js index bd3dcef252..c01fc9e3ae 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js @@ -98,7 +98,7 @@ const createScreen = table => ({ label: "Deals", name: `all_${table._id}`, tableId: table._id, - isTable: true, + type: "table", }, _instanceName: `${table.name} Table`, _children: [], diff --git a/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte b/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte index 91aa9f07d8..5516b3e167 100644 --- a/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte +++ b/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte @@ -1,9 +1,11 @@ -
+
diff --git a/packages/builder/src/components/userInterface/propertyCategories.js b/packages/builder/src/components/userInterface/propertyCategories.js index bae44df7f5..efaa8fbd33 100644 --- a/packages/builder/src/components/userInterface/propertyCategories.js +++ b/packages/builder/src/components/userInterface/propertyCategories.js @@ -261,6 +261,16 @@ export const padding = [ ] export const size = [ + { + label: "Flex", + key: "flex", + control: OptionSelect, + defaultValue: "0 1 auto", + options: [ + { label: "Shrink", value: "0 1 auto" }, + { label: "Grow", value: "1 1 auto" }, + ], + }, { label: "Width", key: "width", diff --git a/packages/builder/tests/fetchBindableProperties.spec.js b/packages/builder/tests/fetchBindableProperties.spec.js index f89a2abbda..92b4b76a3b 100644 --- a/packages/builder/tests/fetchBindableProperties.spec.js +++ b/packages/builder/tests/fetchBindableProperties.spec.js @@ -1,13 +1,14 @@ import fetchBindableProperties from "../src/builderStore/fetchBindableProperties" describe("fetch bindable properties", () => { - it("should return bindable properties from screen components", () => { const result = fetchBindableProperties({ componentInstanceId: "heading-id", - ...testData() + ...testData(), }) - const componentBinding = result.find(r => r.instance._id === "search-input-id" && r.type === "instance") + const componentBinding = result.find( + r => r.instance._id === "search-input-id" && r.type === "instance" + ) expect(componentBinding).toBeDefined() expect(componentBinding.type).toBe("instance") expect(componentBinding.runtimeBinding).toBe("search-input-id.value") @@ -16,29 +17,39 @@ describe("fetch bindable properties", () => { it("should not return bindable components when not in their context", () => { const result = fetchBindableProperties({ componentInstanceId: "heading-id", - ...testData() + ...testData(), }) - const componentBinding = result.find(r => r.instance._id === "list-item-input-id") + const componentBinding = result.find( + r => r.instance._id === "list-item-input-id" + ) expect(componentBinding).not.toBeDefined() }) it("should return table schema, when inside a context", () => { const result = fetchBindableProperties({ componentInstanceId: "list-item-input-id", - ...testData() + ...testData(), }) - const contextBindings = result.filter(r => r.instance._id === "list-id" && r.type==="context") - // 2 fields + _id + _rev + const contextBindings = result.filter( + r => r.instance._id === "list-id" && r.type === "context" + ) + // 2 fields + _id + _rev expect(contextBindings.length).toBe(4) - - const namebinding = contextBindings.find(b => b.runtimeBinding === "data.name") + + const namebinding = contextBindings.find( + b => b.runtimeBinding === "data.name" + ) expect(namebinding).toBeDefined() expect(namebinding.readableBinding).toBe("list-name.Test Table.name") - - const descriptionbinding = contextBindings.find(b => b.runtimeBinding === "data.description") + + const descriptionbinding = contextBindings.find( + b => b.runtimeBinding === "data.description" + ) expect(descriptionbinding).toBeDefined() - expect(descriptionbinding.readableBinding).toBe("list-name.Test Table.description") - + expect(descriptionbinding.readableBinding).toBe( + "list-name.Test Table.description" + ) + const idbinding = contextBindings.find(b => b.runtimeBinding === "data._id") expect(idbinding).toBeDefined() expect(idbinding.readableBinding).toBe("list-name.Test Table._id") @@ -47,35 +58,51 @@ describe("fetch bindable properties", () => { it("should return table schema, for grantparent context", () => { const result = fetchBindableProperties({ componentInstanceId: "child-list-item-input-id", - ...testData() + ...testData(), }) - const contextBindings = result.filter(r => r.type==="context") + const contextBindings = result.filter(r => r.type === "context") // 2 fields + _id + _rev ... x 2 tables expect(contextBindings.length).toBe(8) - - const namebinding_parent = contextBindings.find(b => b.runtimeBinding === "parent.data.name") + + const namebinding_parent = contextBindings.find( + b => b.runtimeBinding === "parent.data.name" + ) expect(namebinding_parent).toBeDefined() expect(namebinding_parent.readableBinding).toBe("list-name.Test Table.name") - - const descriptionbinding_parent = contextBindings.find(b => b.runtimeBinding === "parent.data.description") + + const descriptionbinding_parent = contextBindings.find( + b => b.runtimeBinding === "parent.data.description" + ) expect(descriptionbinding_parent).toBeDefined() - expect(descriptionbinding_parent.readableBinding).toBe("list-name.Test Table.description") - - const namebinding_own = contextBindings.find(b => b.runtimeBinding === "data.name") + expect(descriptionbinding_parent.readableBinding).toBe( + "list-name.Test Table.description" + ) + + const namebinding_own = contextBindings.find( + b => b.runtimeBinding === "data.name" + ) expect(namebinding_own).toBeDefined() - expect(namebinding_own.readableBinding).toBe("child-list-name.Test Table.name") - - const descriptionbinding_own = contextBindings.find(b => b.runtimeBinding === "data.description") + expect(namebinding_own.readableBinding).toBe( + "child-list-name.Test Table.name" + ) + + const descriptionbinding_own = contextBindings.find( + b => b.runtimeBinding === "data.description" + ) expect(descriptionbinding_own).toBeDefined() - expect(descriptionbinding_own.readableBinding).toBe("child-list-name.Test Table.description") + expect(descriptionbinding_own.readableBinding).toBe( + "child-list-name.Test Table.description" + ) }) it("should return bindable component props, from components in same context", () => { const result = fetchBindableProperties({ componentInstanceId: "list-item-heading-id", - ...testData() + ...testData(), }) - const componentBinding = result.find(r => r.instance._id === "list-item-input-id" && r.type === "instance") + const componentBinding = result.find( + r => r.instance._id === "list-item-input-id" && r.type === "instance" + ) expect(componentBinding).toBeDefined() expect(componentBinding.runtimeBinding).toBe("list-item-input-id.value") }) @@ -83,125 +110,140 @@ describe("fetch bindable properties", () => { it("should not return components from child context", () => { const result = fetchBindableProperties({ componentInstanceId: "list-item-heading-id", - ...testData() + ...testData(), }) - const componentBinding = result.find(r => r.instance._id === "child-list-item-input-id" && r.type === "instance") + const componentBinding = result.find( + r => + r.instance._id === "child-list-item-input-id" && r.type === "instance" + ) expect(componentBinding).not.toBeDefined() }) - + it("should return bindable component props, from components in same context (when nested context)", () => { const result = fetchBindableProperties({ componentInstanceId: "child-list-item-heading-id", - ...testData() + ...testData(), }) - const componentBinding = result.find(r => r.instance._id === "child-list-item-input-id" && r.type === "instance") + const componentBinding = result.find( + r => + r.instance._id === "child-list-item-input-id" && r.type === "instance" + ) expect(componentBinding).toBeDefined() - }) - + }) }) const testData = () => { - const screen = { instanceName: "test screen", name: "screen-id", route: "/", props: { - _id:"screent-root-id", + _id: "screent-root-id", _component: "@budibase/standard-components/container", _children: [ { _id: "heading-id", _instanceName: "list item heading", _component: "@budibase/standard-components/heading", - text: "Screen Title" + text: "Screen Title", }, { _id: "search-input-id", _instanceName: "Search Input", _component: "@budibase/standard-components/input", - value: "search phrase" + value: "search phrase", }, { _id: "list-id", _component: "@budibase/standard-components/list", _instanceName: "list-name", - table: { isTable: true, tableId: "test-table-id", label: "Test Table", name: "all_test-table-id" }, + table: { + type: "table", + tableId: "test-table-id", + label: "Test Table", + name: "all_test-table-id", + }, _children: [ { _id: "list-item-heading-id", _instanceName: "list item heading", _component: "@budibase/standard-components/heading", - text: "hello" + text: "hello", }, { _id: "list-item-input-id", _instanceName: "List Item Input", _component: "@budibase/standard-components/input", - value: "list item" + value: "list item", }, { _id: "child-list-id", _component: "@budibase/standard-components/list", _instanceName: "child-list-name", - table: { isTable: true, tableId: "test-table-id", label: "Test Table", name: "all_test-table-id"}, + table: { + type: "table", + tableId: "test-table-id", + label: "Test Table", + name: "all_test-table-id", + }, _children: [ { _id: "child-list-item-heading-id", _instanceName: "child list item heading", _component: "@budibase/standard-components/heading", - text: "hello" + text: "hello", }, { _id: "child-list-item-input-id", _instanceName: "Child List Item Input", _component: "@budibase/standard-components/input", - value: "child list item" + value: "child list item", }, - ] + ], }, - ] + ], }, - ] - } + ], + }, } - const tables = [{ - _id: "test-table-id", - name: "Test Table", + const tables = [ + { + _id: "test-table-id", + name: "Test Table", schema: { name: { - type: "string" + type: "string", }, description: { - type: "string" - } - } - }] + type: "string", + }, + }, + }, + ] const components = { - "@budibase/standard-components/container" : { + "@budibase/standard-components/container": { props: {}, }, - "@budibase/standard-components/list" : { + "@budibase/standard-components/list": { context: "table", props: { - table: "string" + table: "string", }, }, - "@budibase/standard-components/input" : { + "@budibase/standard-components/input": { bindable: "value", props: { - value: "string" + value: "string", }, }, - "@budibase/standard-components/heading" : { + "@budibase/standard-components/heading": { props: { - text: "string" + text: "string", }, }, } return { screen, tables, components } - } diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index 53b8792f49..0d8fe021be 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -3,7 +3,7 @@ const ClientDb = require("../../db/clientDb") const { getPackageForBuilder, buildPage } = require("../../utilities/builder") const env = require("../../environment") const instanceController = require("./instance") -const { copy, exists, readFile, writeFile } = require("fs-extra") +const { copy, existsSync, readFile, writeFile } = require("fs-extra") const { budibaseAppsDir } = require("../../utilities/budibaseDir") const sqrl = require("squirrelly") const setBuilderToken = require("../../utilities/builder/setBuilderToken") @@ -116,6 +116,12 @@ exports.delete = async function(ctx) { const db = new CouchDB(ClientDb.name(getClientId(ctx))) const app = await db.get(ctx.params.applicationId) const result = await db.remove(app) + for (let instance of app.instances) { + const instanceDb = new CouchDB(instance._id) + await instanceDb.destroy() + } + + // remove top level directory await fs.rmdir(join(budibaseAppsDir(), ctx.params.applicationId), { recursive: true, }) @@ -137,7 +143,7 @@ const createEmptyAppPackage = async (ctx, app) => { const appsFolder = budibaseAppsDir() const newAppFolder = resolve(appsFolder, app._id) - if (await exists(newAppFolder)) { + if (existsSync(newAppFolder)) { ctx.throw(400, "App folder already exists for this application") } diff --git a/packages/server/src/api/controllers/page.js b/packages/server/src/api/controllers/page.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/server/src/api/controllers/screen.js b/packages/server/src/api/controllers/screen.js index e69de29bb2..91c3c83dd5 100644 --- a/packages/server/src/api/controllers/screen.js +++ b/packages/server/src/api/controllers/screen.js @@ -0,0 +1,17 @@ +/** + * This controller is not currently fully implemented. Screens are + * currently managed as part of the pages API, please look in api/routes/page.js + * for routes and controllers. + */ + +exports.fetch = async ctx => { + ctx.throw(501) +} + +exports.save = async ctx => { + ctx.throw(501) +} + +exports.destroy = async ctx => { + ctx.throw(501) +} diff --git a/packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json b/packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json index 916ad5d224..037c385f5c 100644 --- a/packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json +++ b/packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json @@ -1,9 +1,9 @@ { - "title": "{{ name }}", - "favicon": "./_shared/favicon.png", - "stylesheets": [], - "componentLibraries": ["@budibase/standard-components"], - "props": { + "title": "{{ name }}", + "favicon": "./_shared/favicon.png", + "stylesheets": [], + "componentLibraries": ["@budibase/standard-components"], + "props": { "_id": "private-master-root", "_component": "@budibase/standard-components/container", "_children": [ @@ -62,10 +62,11 @@ "_component": "##builtin/screenslot", "_styles": { "normal": { - "padding": "0px", - "align-items": "flex-start", - "height": "100vh", - "background-image": "None" + "flex": "1 1 auto", + "display": "flex", + "flex-direction": "column", + "justify-content": "flex-start", + "align-items": "stretch" }, "hover": {}, "active": {}, @@ -84,10 +85,9 @@ "flex-direction": "column", "align-items": "stretch", "justify-content": "flex-start", - "height": "", - "max-width": "", "margin-right": "auto", - "margin-left": "auto" + "margin-left": "auto", + "min-height": "100%" }, "selected": {} }, @@ -95,6 +95,6 @@ "className": "", "onLoad": [] }, - "_css": "", - "uiFunctions": "" + "_css": "", + "uiFunctions": "" } diff --git a/packages/server/src/utilities/initialiseBudibase.js b/packages/server/src/utilities/initialiseBudibase.js index c86b6083e9..be3253117c 100644 --- a/packages/server/src/utilities/initialiseBudibase.js +++ b/packages/server/src/utilities/initialiseBudibase.js @@ -1,4 +1,4 @@ -const { exists, readFile, writeFile, ensureDir } = require("fs-extra") +const { existsSync, readFile, writeFile, ensureDir } = require("fs-extra") const { join, resolve } = require("./centralPath") const Sqrl = require("squirrelly") const uuid = require("uuid") @@ -28,7 +28,7 @@ const setCouchDbUrl = async opts => { const createDevEnvFile = async opts => { const destConfigFile = join(opts.dir, "./.env") - let createConfig = !(await exists(destConfigFile)) || opts.quiet + let createConfig = !existsSync(destConfigFile) || opts.quiet if (createConfig) { const template = await readFile( resolve(__dirname, "..", "..", ".env.template"), diff --git a/packages/standard-components/src/DataGrid/Component.svelte b/packages/standard-components/src/DataGrid/Component.svelte index 6a2a6ded61..471d02cc68 100644 --- a/packages/standard-components/src/DataGrid/Component.svelte +++ b/packages/standard-components/src/DataGrid/Component.svelte @@ -21,6 +21,11 @@ export let height = 500 export let pagination + // 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 store = _bb.store let dataLoaded = false let data let columnDefs @@ -32,35 +37,42 @@ minWidth: 150, filter: true, }, - rowSelection: editable ? "multiple" : false, - suppressRowClickSelection: !editable, + rowSelection: canEdit ? "multiple" : false, + suppressRowClickSelection: !canEdit, paginationAutoPageSize: true, pagination, } - let store = _bb.store onMount(async () => { - if (datasource.tableId) { - const jsonTable = await _bb.api.get(`/api/tables/${datasource.tableId}`) - table = await jsonTable.json() - const { schema } = table - if (!isEmpty(datasource)) { - data = await fetchData(datasource, $store) - columnDefs = Object.keys(schema).map((key, i) => { - return { - headerCheckboxSelection: i === 0 && editable, - checkboxSelection: i === 0 && editable, - valueSetter: setters.get(schema[key].type), - headerName: key.charAt(0).toUpperCase() + key.slice(1), - field: key, - hide: shouldHideField(key), - sortable: true, - editable: editable, - cellRenderer: getRenderer(schema[key], editable), - autoHeight: true, - } - }) + if (!isEmpty(datasource)) { + data = await fetchData(datasource, $store) + let schema = {} + + // Get schema for datasource + // Views with "Calculate" applied provide their own schema. + // For everything else, use the tableId property to pull to table schema + if (datasource.schema) { + schema = datasource.schema + } else { + const jsonTable = await _bb.api.get(`/api/tables/${datasource.tableId}`) + table = await jsonTable.json() + schema = table.schema } + + columnDefs = Object.keys(schema).map((key, i) => { + return { + headerCheckboxSelection: i === 0 && canEdit, + checkboxSelection: i === 0 && canEdit, + valueSetter: setters.get(schema[key].type), + headerName: key.charAt(0).toUpperCase() + key.slice(1), + field: key, + hide: shouldHideField(key), + sortable: true, + editable: canEdit, + cellRenderer: getRenderer(schema[key], canEdit), + autoHeight: true, + } + }) dataLoaded = true } }) @@ -117,7 +129,7 @@
{#if dataLoaded} - {#if editable} + {#if canAddDelete}
{#if selectedRows.length > 0} diff --git a/packages/standard-components/src/DataTable.svelte b/packages/standard-components/src/DataTable.svelte index 896ca6a5ca..bdb0c76183 100644 --- a/packages/standard-components/src/DataTable.svelte +++ b/packages/standard-components/src/DataTable.svelte @@ -12,7 +12,7 @@ export let color export let stripeColor export let borderColor - export let datasource = {} + export let datasource export let _bb let data = [] @@ -35,14 +35,21 @@ const FETCH_TABLE_URL = `/api/tables/${tableId}` const response = await _bb.api.get(FETCH_TABLE_URL) const table = await response.json() - schema = table.schema + return table.schema } onMount(async () => { if (!isEmpty(datasource)) { data = await fetchData(datasource, $store) - if (data && data.length) { - await fetchTable(data[0].tableId) + + // Get schema for datasource + // Views with "Calculate" applied provide their own schema. + // For everything else, use the tableId property to pull to table schema + if (datasource.schema) { + schema = datasource.schema + headers = Object.keys(schema).filter(shouldDisplayField) + } else { + schema = await fetchTable(datasource.tableId) headers = Object.keys(schema).filter(shouldDisplayField) } } @@ -54,7 +61,6 @@ if (name === "type") return false // tables are always tied to a single tableId, this is irrelevant if (name === "tableId") return false - return true } @@ -95,11 +101,11 @@ {#each sorted as row (row._id)} {#each headers as header} - {#if schema[header]} + {#if schema[header] !== undefined} - {#if schema[header].type === 'attachment'} + {#if schema[header] && schema[header].type === 'attachment'} - {:else if schema[header].type === 'link'} + {:else if schema[header] && schema[header].type === 'link'} {row[header] ? row[header].length : 0} related row(s) {:else} {row[header] == null ? '' : row[header]} diff --git a/packages/standard-components/src/Form.svelte b/packages/standard-components/src/Form.svelte index ed9a393440..29401e8ad4 100644 --- a/packages/standard-components/src/Form.svelte +++ b/packages/standard-components/src/Form.svelte @@ -14,7 +14,7 @@ let rowId let errors = {} - $: schema = $store.data && $store.data._table.schema + $: schema = $store.data && $store.data._table && $store.data._table.schema $: fields = schema ? Object.keys(schema) : [] diff --git a/packages/standard-components/src/NewRow.svelte b/packages/standard-components/src/NewRow.svelte index 817e07e0d0..e7446f9bdc 100644 --- a/packages/standard-components/src/NewRow.svelte +++ b/packages/standard-components/src/NewRow.svelte @@ -19,7 +19,7 @@ } onMount(async () => { - if (table) { + if (table && typeof table === "string") { const tableObj = await fetchTable(table) row.tableId = table row._table = tableObj diff --git a/packages/standard-components/src/fetchData.js b/packages/standard-components/src/fetchData.js index 27f3fb9e12..68aca7eac6 100644 --- a/packages/standard-components/src/fetchData.js +++ b/packages/standard-components/src/fetchData.js @@ -6,16 +6,16 @@ export default async function fetchData(datasource, store) { if (name) { let rows = [] if (type === "table") { - rows = await fetchTableData() + rows = fetchTableData() } else if (type === "view") { - rows = await fetchViewData() + rows = fetchViewData() } else if (type === "link") { - rows = await fetchLinkedRowsData() + rows = fetchLinkedRowsData() } // Fetch table schema so we can check for linked rows if (rows && rows.length) { - const table = await fetchTable(rows[0].tableId) + const table = await fetchTable() const keys = Object.keys(table.schema) rows.forEach(row => { for (let key of keys) { @@ -27,10 +27,12 @@ export default async function fetchData(datasource, store) { } return rows + } else { + return [] } - async function fetchTable(id) { - const FETCH_TABLE_URL = `/api/tables/${id}` + async function fetchTable() { + const FETCH_TABLE_URL = `/api/tables/${datasource.tableId}` const response = await api.get(FETCH_TABLE_URL) return await response.json() }