diff --git a/packages/builder/cypress/integration/createBinding.spec.js b/packages/builder/cypress/integration/createBinding.spec.js index 99d741ab46..d901dc608d 100644 --- a/packages/builder/cypress/integration/createBinding.spec.js +++ b/packages/builder/cypress/integration/createBinding.spec.js @@ -20,7 +20,7 @@ context("Create Bindings", () => { cy.get("[data-cy=setting-text] input") .type("{{}{{}{{} Current User._id {}}{}}") .blur() - cy.getComponent(componentId).should("have.text", "{{{ user._id }}") + cy.getComponent(componentId).should("have.text", "{{{ [user].[_id] }}") }) }) diff --git a/packages/builder/cypress/integration/createComponents.spec.js b/packages/builder/cypress/integration/createComponents.spec.js index 56aca88837..3fc61f8d1a 100644 --- a/packages/builder/cypress/integration/createComponents.spec.js +++ b/packages/builder/cypress/integration/createComponents.spec.js @@ -43,7 +43,7 @@ context("Create Components", () => { it("should create a form and reset to match schema", () => { cy.addComponent("Form", "Form").then(() => { cy.get("[data-cy=Settings]").click() - cy.get("[data-cy=setting-datasource]") + cy.get("[data-cy=setting-dataSource]") .contains("Choose option") .click() cy.get(".dropdown") diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index dee0b86fb3..9d6d0f3f6c 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -1,7 +1,7 @@ import { cloneDeep } from "lodash/fp" import { get } from "svelte/store" import { backendUiStore, store } from "builderStore" -import { findComponentPath } from "./storeUtils" +import { findComponent, findComponentPath } from "./storeUtils" import { makePropSafe } from "@budibase/string-templates" import { TableNames } from "../constants" @@ -35,7 +35,7 @@ export const getDataProviderComponents = (asset, componentId) => { // Filter by only data provider components return path.filter(component => { const def = store.actions.components.getDefinition(component._component) - return def?.dataProvider + return def?.context != null }) } @@ -62,14 +62,25 @@ export const getActionProviderComponents = (asset, componentId, actionType) => { /** * Gets a datasource object for a certain data provider component */ -export const getDatasourceForProvider = component => { +export const getDatasourceForProvider = (asset, component) => { const def = store.actions.components.getDefinition(component?._component) if (!def) { return null } + // If this component has a dataProvider setting, go up the stack and use it + const dataProviderSetting = def.settings.find(setting => { + return setting.type === "dataProvider" + }) + if (dataProviderSetting) { + const settingValue = component[dataProviderSetting.key] + const providerId = extractLiteralHandlebarsID(settingValue) + const provider = findComponent(asset.props, providerId) + return getDatasourceForProvider(asset, provider) + } + // Extract datasource from component instance - const validSettingTypes = ["datasource", "table", "schema"] + const validSettingTypes = ["dataSource", "table", "schema"] const datasourceSetting = def.settings.find(setting => { return validSettingTypes.includes(setting.type) }) @@ -101,53 +112,68 @@ const getContextBindings = (asset, componentId) => { // Create bindings for each data provider dataProviders.forEach(component => { - const isForm = component._component.endsWith("/form") - const datasource = getDatasourceForProvider(component) - let tableName, schema + const def = store.actions.components.getDefinition(component._component) + const contextDefinition = def.context + let schema + let readablePrefix - // Forms are an edge case which do not need table schemas - if (isForm) { + if (contextDefinition.type === "form") { + // Forms do not need table schemas + // Their schemas are built from their component field names schema = buildFormSchema(component) - tableName = "Fields" - } else { + readablePrefix = "Fields" + } else if (contextDefinition.type === "static") { + // Static contexts are fully defined by the components + schema = {} + const values = contextDefinition.values || [] + values.forEach(value => { + schema[value.key] = { name: value.label, type: "string" } + }) + } else if (contextDefinition.type === "schema") { + // Schema contexts are generated dynamically depending on their data + const datasource = getDatasourceForProvider(asset, component) if (!datasource) { return } - - // Get schema and table for the datasource - const info = getSchemaForDatasource(datasource, isForm) + const info = getSchemaForDatasource(datasource) schema = info.schema - tableName = info.table?.name - - // Add _id and _rev fields for certain types - if (schema && ["table", "link"].includes(datasource.type)) { - schema["_id"] = { type: "string" } - schema["_rev"] = { type: "string" } - } + readablePrefix = info.table?.name } - if (!schema || !tableName) { + if (!schema) { return } const keys = Object.keys(schema).sort() // Create bindable properties for each schema field + const safeComponentId = makePropSafe(component._id) keys.forEach(key => { const fieldSchema = schema[key] - // Replace certain bindings with a new property to help display components + + // Make safe runtime binding and replace certain bindings with a + // new property to help display components let runtimeBoundKey = key if (fieldSchema.type === "link") { runtimeBoundKey = `${key}_text` } else if (fieldSchema.type === "attachment") { runtimeBoundKey = `${key}_first` } + const runtimeBinding = `${safeComponentId}.${makePropSafe( + runtimeBoundKey + )}` + // Optionally use a prefix with readable bindings + let readableBinding = component._instanceName + if (readablePrefix) { + readableBinding += `.${readablePrefix}` + } + readableBinding += `.${fieldSchema.name || key}` + + // Create the binding object bindings.push({ type: "context", - runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe( - runtimeBoundKey - )}`, - readableBinding: `${component._instanceName}.${tableName}.${key}`, + runtimeBinding, + readableBinding, // Field schema and provider are required to construct relationship // datasource options, based on bindable properties fieldSchema, @@ -164,14 +190,12 @@ const getContextBindings = (asset, componentId) => { */ const getUserBindings = () => { let bindings = [] - const tables = get(backendUiStore).tables - const userTable = tables.find(table => table._id === TableNames.USERS) - const schema = { - ...userTable.schema, - _id: { type: "string" }, - _rev: { type: "string" }, - } + const { schema } = getSchemaForDatasource({ + type: "table", + tableId: TableNames.USERS, + }) const keys = Object.keys(schema).sort() + const safeUser = makePropSafe("user") keys.forEach(key => { const fieldSchema = schema[key] // Replace certain bindings with a new property to help display components @@ -184,7 +208,7 @@ const getUserBindings = () => { bindings.push({ type: "context", - runtimeBinding: `user.${runtimeBoundKey}`, + runtimeBinding: `${safeUser}.${makePropSafe(runtimeBoundKey)}`, readableBinding: `Current User.${key}`, // Field schema and provider are required to construct relationship // datasource options, based on bindable properties @@ -208,9 +232,10 @@ const getUrlBindings = asset => { params.push(part.replace(/:/g, "").replace(/\?/g, "")) } }) + const safeURL = makePropSafe("url") return params.map(param => ({ type: "context", - runtimeBinding: `url.${param}`, + runtimeBinding: `${safeURL}.${makePropSafe(param)}`, readableBinding: `URL.${param}`, })) } @@ -232,15 +257,6 @@ export const getSchemaForDatasource = (datasource, isForm = false) => { if (table) { if (type === "view") { schema = cloneDeep(table.views?.[datasource.name]?.schema) - - // Some calc views don't include a "name" property inside the schema - if (schema) { - Object.keys(schema).forEach(field => { - if (!schema[field].name) { - schema[field].name = field - } - }) - } } else if (type === "query" && isForm) { schema = {} const params = table.parameters || [] @@ -253,6 +269,21 @@ export const getSchemaForDatasource = (datasource, isForm = false) => { schema = cloneDeep(table.schema) } } + + // Add _id and _rev fields for certain types + if (schema && !isForm && ["table", "link"].includes(datasource.type)) { + schema["_id"] = { type: "string" } + schema["_rev"] = { type: "string" } + } + + // Ensure there are "name" properties for all fields + if (schema) { + Object.keys(schema).forEach(field => { + if (!schema[field].name) { + schema[field].name = field + } + }) + } } return { schema, table } } @@ -273,7 +304,7 @@ const buildFormSchema = component => { if (fieldSetting && component.field) { const type = fieldSetting.type.split("field/")[1] if (type) { - schema[component.field] = { name: component.field, type } + schema[component.field] = { type } } } component._children?.forEach(child => { @@ -326,6 +357,14 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) { return result } +/** + * Extracts a component ID from a handlebars expression setting of + * {{ literal [componentId] }} + */ +function extractLiteralHandlebarsID(value) { + return value?.match(/{{\s*literal[\s[]+([a-fA-F0-9]+)[\s\]]*}}/)?.[1] +} + /** * Converts a readable data binding into a runtime data binding */ diff --git a/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js b/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js index aeac80e7c1..7788245d46 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js @@ -37,7 +37,7 @@ const createScreen = table => { .customProps({ theme: "spectrum--lightest", size: "spectrum--medium", - datasource: { + dataSource: { label: table.name, tableId: table._id, type: "table", diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js index 0e48cf307e..67a1d1c916 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js @@ -25,7 +25,7 @@ export default function(tables) { export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE" export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`) -function generateTitleContainer(table, title, formId) { +function generateTitleContainer(table, title, formId, repeaterId) { // have to override style for this, its missing margin const saveButton = makeSaveButton(table, formId).normalStyle({ background: "#000000", @@ -61,10 +61,9 @@ function generateTitleContainer(table, title, formId) { onClick: [ { parameters: { - providerId: formId, - rowId: `{{ ${makePropSafe(formId)}._id }}`, - revId: `{{ ${makePropSafe(formId)}._rev }}`, tableId: table._id, + rowId: `{{ ${makePropSafe(repeaterId)}.${makePropSafe("_id")} }}`, + revId: `{{ ${makePropSafe(repeaterId)}.${makePropSafe("_rev")} }}`, }, "##eventHandlerType": "Delete Row", }, @@ -84,18 +83,33 @@ function generateTitleContainer(table, title, formId) { } const createScreen = table => { - const screen = new Screen() - .component("@budibase/standard-components/rowdetail") - .table(table._id) - .instanceName(`${table.name} - Detail`) - .route(rowDetailUrl(table)) + const provider = new Component("@budibase/standard-components/dataprovider") + .instanceName(`Data Provider`) + .customProps({ + dataSource: { + label: table.name, + name: `all_${table._id}`, + tableId: table._id, + type: "table", + }, + filter: { + _id: `{{ ${makePropSafe("url")}.${makePropSafe("id")} }}`, + }, + limit: 1, + }) + + const repeater = new Component("@budibase/standard-components/repeater") + .instanceName("Repeater") + .customProps({ + dataProvider: `{{ literal ${makePropSafe(provider._json._id)} }}`, + }) const form = makeMainForm() .instanceName("Form") .customProps({ theme: "spectrum--lightest", size: "spectrum--medium", - datasource: { + dataSource: { label: table.name, tableId: table._id, type: "table", @@ -116,14 +130,24 @@ const createScreen = table => { // Add all children to the form const formId = form._json._id - const rowDetailId = screen._json.props._id + const repeaterId = repeater._json._id const heading = table.primaryDisplay - ? `{{ ${makePropSafe(rowDetailId)}.${makePropSafe(table.primaryDisplay)} }}` + ? `{{ ${makePropSafe(repeaterId)}.${makePropSafe(table.primaryDisplay)} }}` : null form .addChild(makeBreadcrumbContainer(table.name, heading || "Edit")) - .addChild(generateTitleContainer(table, heading || "Edit Row", formId)) + .addChild( + generateTitleContainer(table, heading || "Edit Row", formId, repeaterId) + ) .addChild(fieldGroup) - return screen.addChild(form).json() + repeater.addChild(form) + provider.addChild(repeater) + + return new Screen() + .component("@budibase/standard-components/container") + .instanceName(`${table.name} - Detail`) + .route(rowDetailUrl(table)) + .addChild(provider) + .json() } diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js index c999c22647..603c730508 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js @@ -2,6 +2,7 @@ import sanitizeUrl from "./utils/sanitizeUrl" import { newRowUrl } from "./newRowScreen" import { Screen } from "./utils/Screen" import { Component } from "./utils/Component" +import { makePropSafe } from "@budibase/string-templates" export default function(tables) { return tables.map(table => { @@ -70,21 +71,56 @@ function generateTitleContainer(table) { } const createScreen = table => { - const datagrid = new Component("@budibase/standard-components/datagrid") + const provider = new Component("@budibase/standard-components/dataprovider") + .instanceName(`Data Provider`) .customProps({ - datasource: { + dataSource: { label: table.name, name: `all_${table._id}`, tableId: table._id, type: "table", }, - editable: false, - theme: "alpine", - height: "540", - pagination: true, - detailUrl: `${rowListUrl(table)}/:id`, }) - .instanceName("Grid") + + const spectrumTable = new Component("@budibase/standard-components/table") + .customProps({ + dataProvider: `{{ literal ${makePropSafe(provider._json._id)} }}`, + theme: "spectrum--lightest", + showAutoColumns: false, + quiet: false, + size: "spectrum--medium", + rowCount: 8, + }) + .instanceName(`${table.name} Table`) + + const safeTableId = makePropSafe(spectrumTable._json._id) + const safeRowId = makePropSafe("_id") + const viewButton = new Component("@budibase/standard-components/button") + .customProps({ + text: "View", + onClick: [ + { + "##eventHandlerType": "Navigate To", + parameters: { + url: `${rowListUrl(table)}/{{ ${safeTableId}.${safeRowId} }}`, + }, + }, + ], + }) + .instanceName("View Button") + .normalStyle({ + background: "transparent", + "font-family": "Inter, sans-serif", + "font-weight": "500", + color: "#888", + "border-width": "0", + }) + .hoverStyle({ + color: "#4285f4", + }) + + spectrumTable.addChild(viewButton) + provider.addChild(spectrumTable) const mainContainer = new Component("@budibase/standard-components/container") .normalStyle({ @@ -105,14 +141,12 @@ const createScreen = table => { .type("div") .instanceName("Container") .addChild(generateTitleContainer(table)) - .addChild(datagrid) + .addChild(provider) return new Screen() .component("@budibase/standard-components/container") - .mainType("div") .route(rowListUrl(table)) .instanceName(`${table.name} - List`) - .name("") .addChild(mainContainer) .json() } diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js index 1634556c8d..19f950226c 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js @@ -119,6 +119,7 @@ export function makeSaveButton(table, formId) { { parameters: { providerId: formId, + tableId: table._id, }, "##eventHandlerType": "Save Row", }, diff --git a/packages/builder/src/components/design/AppPreview/componentStructure.json b/packages/builder/src/components/design/AppPreview/componentStructure.json index 16109b5a96..267289a804 100644 --- a/packages/builder/src/components/design/AppPreview/componentStructure.json +++ b/packages/builder/src/components/design/AppPreview/componentStructure.json @@ -1,7 +1,8 @@ [ "container", - "datagrid", - "list", + "dataprovider", + "table", + "repeater", "button", "search", { @@ -62,8 +63,7 @@ "children": [ "screenslot", "navigation", - "login", - "rowdetail" + "login" ] } ] diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataProviderSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataProviderSelect.svelte new file mode 100644 index 0000000000..e773ba5e5d --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataProviderSelect.svelte @@ -0,0 +1,25 @@ + + + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DatasourceSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataSourceSelect.svelte similarity index 100% rename from packages/builder/src/components/design/PropertiesPanel/PropertyControls/DatasourceSelect.svelte rename to packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataSourceSelect.svelte diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/DeleteRow.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/DeleteRow.svelte index 425c6f9c58..9b6136ae1f 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/DeleteRow.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/DeleteRow.svelte @@ -1,50 +1,37 @@