diff --git a/packages/builder/package.json b/packages/builder/package.json index 09fd4dac48..267e4f5d72 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -63,7 +63,7 @@ } }, "dependencies": { - "@budibase/bbui": "^1.58.3", + "@budibase/bbui": "^1.58.5", "@budibase/client": "^0.7.6", "@budibase/colorpicker": "1.0.1", "@budibase/string-templates": "^0.7.6", diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 79785f4e8d..abd8fdce85 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 { findAllMatchingComponents, findComponentPath } from "./storeUtils" +import { findComponentPath } from "./storeUtils" import { makePropSafe } from "@budibase/string-templates" import { TableNames } from "../constants" @@ -12,9 +12,7 @@ const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g * Gets all bindable data context fields and instance fields. */ export const getBindableProperties = (rootComponent, componentId) => { - const contextBindings = getContextBindings(rootComponent, componentId) - const componentBindings = getComponentBindings(rootComponent) - return [...contextBindings, ...componentBindings] + return getContextBindings(rootComponent, componentId) } /** @@ -37,6 +35,30 @@ export const getDataProviderComponents = (rootComponent, componentId) => { }) } +/** + * Gets all data provider components above a component. + */ +export const getActionProviderComponents = ( + rootComponent, + componentId, + actionType +) => { + if (!rootComponent || !componentId) { + return [] + } + + // Get the component tree leading up to this component, ignoring the component + // itself + const path = findComponentPath(rootComponent, componentId) + path.pop() + + // Filter by only data provider components + return path.filter(component => { + const def = store.actions.components.getDefinition(component._component) + return def?.actions?.includes(actionType) + }) +} + /** * Gets a datasource object for a certain data provider component */ @@ -47,8 +69,9 @@ export const getDatasourceForProvider = component => { } // Extract datasource from component instance + const validSettingTypes = ["datasource", "table", "schema"] const datasourceSetting = def.settings.find(setting => { - return setting.type === "datasource" || setting.type === "table" + return validSettingTypes.includes(setting.type) }) if (!datasourceSetting) { return null @@ -58,15 +81,14 @@ export const getDatasourceForProvider = component => { // example an actual datasource object, or a table ID string. // Convert the datasource setting into a proper datasource object so that // we can use it properly - if (datasourceSetting.type === "datasource") { - return component[datasourceSetting?.key] - } else if (datasourceSetting.type === "table") { + if (datasourceSetting.type === "table") { return { tableId: component[datasourceSetting?.key], type: "table", } + } else { + return component[datasourceSetting?.key] } - return null } /** @@ -77,21 +99,37 @@ export const getContextBindings = (rootComponent, componentId) => { // Extract any components which provide data contexts const dataProviders = getDataProviderComponents(rootComponent, componentId) let contextBindings = [] + + // Create bindings for each data provider dataProviders.forEach(component => { + const isForm = component._component.endsWith("/form") const datasource = getDatasourceForProvider(component) - if (!datasource) { + let tableName, schema + + // Forms are an edge case which do not need table schemas + if (isForm) { + schema = buildFormSchema(component) + tableName = "Schema" + } else { + if (!datasource) { + return + } + + // Get schema and table for the datasource + const info = getSchemaForDatasource(datasource, isForm) + schema = info.schema + tableName = info.table?.name + + // Add _id and _rev fields for certain types + if (datasource.type === "table" || datasource.type === "link") { + schema["_id"] = { type: "string" } + schema["_rev"] = { type: "string" } + } + } + if (!schema || !tableName) { return } - // Get schema and add _id and _rev fields for certain types - let { schema, table } = getSchemaForDatasource(datasource) - if (!schema || !table) { - return - } - if (datasource.type === "table" || datasource.type === "link") { - schema["_id"] = { type: "string" } - schema["_rev"] = { type: "string " } - } const keys = Object.keys(schema).sort() // Create bindable properties for each schema field @@ -110,11 +148,11 @@ export const getContextBindings = (rootComponent, componentId) => { runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe( runtimeBoundKey )}`, - readableBinding: `${component._instanceName}.${table.name}.${key}`, + readableBinding: `${component._instanceName}.${tableName}.${key}`, + // Field schema and provider are required to construct relationship + // datasource options, based on bindable properties fieldSchema, providerId: component._id, - tableId: datasource.tableId, - field: key, }) }) }) @@ -142,44 +180,20 @@ export const getContextBindings = (rootComponent, componentId) => { type: "context", runtimeBinding: `user.${runtimeBoundKey}`, readableBinding: `Current User.${key}`, + // Field schema and provider are required to construct relationship + // datasource options, based on bindable properties fieldSchema, providerId: "user", - tableId: TableNames.USERS, - field: key, }) }) return contextBindings } -/** - * Gets all bindable components. These are form components which allow their - * values to be bound to. - */ -export const getComponentBindings = rootComponent => { - if (!rootComponent) { - return [] - } - const componentSelector = component => { - const type = component._component - const definition = store.actions.components.getDefinition(type) - return definition?.bindable - } - const components = findAllMatchingComponents(rootComponent, componentSelector) - return components.map(component => { - return { - type: "instance", - providerId: component._id, - runtimeBinding: `${makePropSafe(component._id)}`, - readableBinding: `${component._instanceName}`, - } - }) -} - /** * Gets a schema for a datasource object. */ -export const getSchemaForDatasource = datasource => { +export const getSchemaForDatasource = (datasource, isForm = false) => { let schema, table if (datasource) { const { type } = datasource @@ -193,6 +207,14 @@ export const getSchemaForDatasource = datasource => { if (table) { if (type === "view") { schema = cloneDeep(table.views?.[datasource.name]?.schema) + } else if (type === "query" && isForm) { + schema = {} + const params = table.parameters || [] + params.forEach(param => { + if (param?.name) { + schema[param.name] = { ...param, type: "string" } + } + }) } else { schema = cloneDeep(table.schema) } @@ -201,6 +223,32 @@ export const getSchemaForDatasource = datasource => { return { schema, table } } +/** + * Builds a form schema given a form component. + * A form schema is a schema of all the fields nested anywhere within a form. + */ +const buildFormSchema = component => { + let schema = {} + if (!component) { + return schema + } + const def = store.actions.components.getDefinition(component._component) + const fieldSetting = def?.settings?.find( + setting => setting.key === "field" && setting.type.startsWith("field/") + ) + if (fieldSetting && component.field) { + const type = fieldSetting.type.split("field/")[1] + if (type) { + schema[component.field] = { name: component.field, type } + } + } + component._children?.forEach(child => { + const childSchema = buildFormSchema(child) + schema = { ...schema, ...childSchema } + }) + return schema +} + /** * utility function for the readableToRuntimeBinding and runtimeToReadableBinding. */ diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 340e2829aa..27427c6ef0 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -416,7 +416,14 @@ export const getFrontendStore = () => { if (cut) { state.componentToPaste = null } else { - componentToPaste._id = uuid() + const randomizeIds = component => { + if (!component) { + return + } + component._id = uuid() + component._children?.forEach(randomizeIds) + } + randomizeIds(componentToPaste) } if (mode === "inside") { diff --git a/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js b/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js index b25562758e..1f30d974ef 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js @@ -9,5 +9,6 @@ const createScreen = () => { return new Screen() .mainType("div") .component("@budibase/standard-components/container") + .instanceName("New Screen") .json() } diff --git a/packages/builder/src/builderStore/store/screenTemplates/emptyNewRowScreen.js b/packages/builder/src/builderStore/store/screenTemplates/emptyNewRowScreen.js deleted file mode 100644 index a2f2f6df67..0000000000 --- a/packages/builder/src/builderStore/store/screenTemplates/emptyNewRowScreen.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Screen } from "./utils/Screen" - -export default { - name: `New Row (Empty)`, - create: () => createScreen(), -} - -const createScreen = () => { - return new Screen() - .component("@budibase/standard-components/newrow") - .table("") - .json() -} diff --git a/packages/builder/src/builderStore/store/screenTemplates/emptyRowDetailScreen.js b/packages/builder/src/builderStore/store/screenTemplates/emptyRowDetailScreen.js deleted file mode 100644 index 5dbdcf4e69..0000000000 --- a/packages/builder/src/builderStore/store/screenTemplates/emptyRowDetailScreen.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Screen } from "./utils/Screen" - -export default { - name: `Row Detail (Empty)`, - create: () => createScreen(), -} - -const createScreen = () => { - return new Screen() - .component("@budibase/standard-components/rowdetail") - .table("") - .json() -} diff --git a/packages/builder/src/builderStore/store/screenTemplates/index.js b/packages/builder/src/builderStore/store/screenTemplates/index.js index 7272f3514c..38ae434753 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/index.js +++ b/packages/builder/src/builderStore/store/screenTemplates/index.js @@ -1,17 +1,12 @@ import newRowScreen from "./newRowScreen" import rowDetailScreen from "./rowDetailScreen" import rowListScreen from "./rowListScreen" -import emptyNewRowScreen from "./emptyNewRowScreen" import createFromScratchScreen from "./createFromScratchScreen" -import emptyRowDetailScreen from "./emptyRowDetailScreen" const allTemplates = tables => [ - createFromScratchScreen, ...newRowScreen(tables), ...rowDetailScreen(tables), ...rowListScreen(tables), - emptyNewRowScreen, - emptyRowDetailScreen, ] // Allows us to apply common behaviour to all create() functions @@ -22,8 +17,18 @@ const createTemplateOverride = (frontendState, create) => () => { return screen } -export default (frontendState, tables) => - allTemplates(tables).map(template => ({ +export default (frontendState, tables) => { + const enrichTemplate = template => ({ ...template, create: createTemplateOverride(frontendState, template.create), - })) + }) + + const fromScratch = enrichTemplate(createFromScratchScreen) + const tableTemplates = allTemplates(tables).map(enrichTemplate) + return [ + fromScratch, + ...tableTemplates.sort((templateA, templateB) => { + return templateA.name > templateB.name ? 1 : -1 + }), + ] +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js b/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js index 2790a68677..aeac80e7c1 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js @@ -1,11 +1,12 @@ import sanitizeUrl from "./utils/sanitizeUrl" -import { Component } from "./utils/Component" import { Screen } from "./utils/Screen" +import { Component } from "./utils/Component" import { makeBreadcrumbContainer, - makeMainContainer, + makeMainForm, makeTitleContainer, makeSaveButton, + makeDatasourceFormComponents, } from "./utils/commonComponents" export default function(tables) { @@ -21,29 +22,46 @@ export default function(tables) { export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`) export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE" -function generateTitleContainer(table, providerId) { - return makeTitleContainer("New Row").addChild( - makeSaveButton(table, providerId) - ) +function generateTitleContainer(table, formId) { + return makeTitleContainer("New Row").addChild(makeSaveButton(table, formId)) } const createScreen = table => { const screen = new Screen() - .component("@budibase/standard-components/newrow") - .table(table._id) - .route(newRowUrl(table)) + .component("@budibase/standard-components/container") .instanceName(`${table.name} - New`) - .name("") + .route(newRowUrl(table)) - const dataform = new Component( - "@budibase/standard-components/dataformwide" - ).instanceName("Form") + const form = makeMainForm() + .instanceName("Form") + .customProps({ + theme: "spectrum--lightest", + size: "spectrum--medium", + datasource: { + label: table.name, + tableId: table._id, + type: "table", + }, + }) - const providerId = screen._json.props._id - const container = makeMainContainer() + const fieldGroup = new Component("@budibase/standard-components/fieldgroup") + .instanceName("Field Group") + .customProps({ + labelPosition: "left", + }) + + // Add all form fields from this schema to the field group + const datasource = { type: "table", tableId: table._id } + makeDatasourceFormComponents(datasource).forEach(component => { + fieldGroup.addChild(component) + }) + + // Add all children to the form + const formId = form._json._id + form .addChild(makeBreadcrumbContainer(table.name, "New")) - .addChild(generateTitleContainer(table, providerId)) - .addChild(dataform) + .addChild(generateTitleContainer(table, formId)) + .addChild(fieldGroup) - return screen.addChild(container).json() + return screen.addChild(form).json() } diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js index fd54405875..0e48cf307e 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js @@ -4,20 +4,19 @@ import { Screen } from "./utils/Screen" import { Component } from "./utils/Component" import { makePropSafe } from "@budibase/string-templates" import { - makeMainContainer, makeBreadcrumbContainer, makeTitleContainer, makeSaveButton, + makeMainForm, + spectrumColor, + makeDatasourceFormComponents, } from "./utils/commonComponents" export default function(tables) { return tables.map(table => { - const heading = table.primaryDisplay - ? `{{ data.${makePropSafe(table.primaryDisplay)} }}` - : null return { name: `${table.name} - Detail`, - create: () => createScreen(table, heading), + create: () => createScreen(table), id: ROW_DETAIL_TEMPLATE, } }) @@ -26,9 +25,9 @@ export default function(tables) { export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE" export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`) -function generateTitleContainer(table, title, providerId) { +function generateTitleContainer(table, title, formId) { // have to override style for this, its missing margin - const saveButton = makeSaveButton(table, providerId).normalStyle({ + const saveButton = makeSaveButton(table, formId).normalStyle({ background: "#000000", "border-width": "0", "border-style": "None", @@ -54,6 +53,7 @@ function generateTitleContainer(table, title, providerId) { background: "transparent", color: "#4285f4", }) + .customStyle(spectrumColor(700)) .text("Delete") .customProps({ className: "", @@ -61,8 +61,9 @@ function generateTitleContainer(table, title, providerId) { onClick: [ { parameters: { - rowId: `{{ ${makePropSafe(providerId)}._id }}`, - revId: `{{ ${makePropSafe(providerId)}._rev }}`, + providerId: formId, + rowId: `{{ ${makePropSafe(formId)}._id }}`, + revId: `{{ ${makePropSafe(formId)}._rev }}`, tableId: table._id, }, "##eventHandlerType": "Delete Row", @@ -82,23 +83,47 @@ function generateTitleContainer(table, title, providerId) { .addChild(saveButton) } -const createScreen = (table, heading) => { +const createScreen = table => { const screen = new Screen() .component("@budibase/standard-components/rowdetail") .table(table._id) .instanceName(`${table.name} - Detail`) .route(rowDetailUrl(table)) - .name("") - const dataform = new Component( - "@budibase/standard-components/dataformwide" - ).instanceName("Form") + const form = makeMainForm() + .instanceName("Form") + .customProps({ + theme: "spectrum--lightest", + size: "spectrum--medium", + datasource: { + label: table.name, + tableId: table._id, + type: "table", + }, + }) - const providerId = screen._json.props._id - const container = makeMainContainer() + const fieldGroup = new Component("@budibase/standard-components/fieldgroup") + .instanceName("Field Group") + .customProps({ + labelPosition: "left", + }) + + // Add all form fields from this schema to the field group + const datasource = { type: "table", tableId: table._id } + makeDatasourceFormComponents(datasource).forEach(component => { + fieldGroup.addChild(component) + }) + + // Add all children to the form + const formId = form._json._id + const rowDetailId = screen._json.props._id + const heading = table.primaryDisplay + ? `{{ ${makePropSafe(rowDetailId)}.${makePropSafe(table.primaryDisplay)} }}` + : null + form .addChild(makeBreadcrumbContainer(table.name, heading || "Edit")) - .addChild(generateTitleContainer(table, heading || "Edit Row", providerId)) - .addChild(dataform) + .addChild(generateTitleContainer(table, heading || "Edit Row", formId)) + .addChild(fieldGroup) - return screen.addChild(container).json() + return screen.addChild(form).json() } diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js b/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js index a74ea526f7..182736a1d5 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js @@ -14,17 +14,11 @@ export class Component extends BaseStructure { active: {}, selected: {}, }, - type: "", _instanceName: "", _children: [], } } - type(type) { - this._json.type = type - return this - } - normalStyle(styling) { this._json._styles.normal = styling return this @@ -35,14 +29,25 @@ export class Component extends BaseStructure { return this } - text(text) { - this._json.text = text + customStyle(styling) { + this._json._styles.custom = styling return this } - // TODO: do we need this instanceName(name) { this._json._instanceName = name return this } + + // Shorthand for custom props "type" + type(type) { + this._json.type = type + return this + } + + // Shorthand for custom props "text" + text(text) { + this._json.text = text + return this + } } diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js index a00f66f828..4c127fbe0b 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js @@ -1,5 +1,15 @@ import { Component } from "./Component" import { rowListUrl } from "../rowListScreen" +import { getSchemaForDatasource } from "../../../dataBinding" + +export function spectrumColor(number) { + // Acorn throws a parsing error in this file if the word g-l-o-b-a-l is found + // (without dashes - I can't even type it in a comment). + // God knows why. It seems to think optional chaining further down the + // file is invalid if the word g-l-o-b-a-l is found - hence the reason this + // statement is split into parts. + return "color: var(--spectrum-glo" + `bal-color-gray-${number});` +} export function makeLinkComponent(tableName) { return new Component("@budibase/standard-components/link") @@ -10,6 +20,7 @@ export function makeLinkComponent(tableName) { .hoverStyle({ color: "#4285f4", }) + .customStyle(spectrumColor(700)) .text(tableName) .customProps({ url: `/${tableName.toLowerCase()}`, @@ -22,13 +33,12 @@ export function makeLinkComponent(tableName) { }) } -export function makeMainContainer() { - return new Component("@budibase/standard-components/container") +export function makeMainForm() { + return new Component("@budibase/standard-components/form") .type("div") .normalStyle({ width: "700px", padding: "0px", - background: "white", "border-radius": "0.5rem", "box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)", margin: "auto", @@ -39,7 +49,7 @@ export function makeMainContainer() { "padding-left": "48px", "margin-bottom": "20px", }) - .instanceName("Container") + .instanceName("Form") } export function makeBreadcrumbContainer(tableName, text, capitalise = false) { @@ -51,6 +61,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) { "margin-right": "4px", "margin-left": "4px", }) + .customStyle(spectrumColor(700)) .text(">") .instanceName("Arrow") @@ -63,6 +74,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) { const identifierText = new Component("@budibase/standard-components/text") .type("none") .normalStyle(textStyling) + .customStyle(spectrumColor(700)) .text(text) .instanceName("Identifier") @@ -78,7 +90,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) { .addChild(identifierText) } -export function makeSaveButton(table, providerId) { +export function makeSaveButton(table, formId) { return new Component("@budibase/standard-components/button") .normalStyle({ background: "#000000", @@ -99,8 +111,14 @@ export function makeSaveButton(table, providerId) { disabled: false, onClick: [ { + "##eventHandlerType": "Validate Form", parameters: { - providerId, + componentId: formId, + }, + }, + { + parameters: { + providerId: formId, }, "##eventHandlerType": "Save Row", }, @@ -125,6 +143,7 @@ export function makeTitleContainer(title) { "margin-left": "0px", flex: "1 1 auto", }) + .customStyle(spectrumColor(900)) .type("h3") .instanceName("Title") .text(title) @@ -142,3 +161,44 @@ export function makeTitleContainer(title) { .instanceName("Title Container") .addChild(heading) } + +const fieldTypeToComponentMap = { + string: "stringfield", + number: "numberfield", + options: "optionsfield", + boolean: "booleanfield", + longform: "longformfield", + datetime: "datetimefield", + attachment: "attachmentfield", + link: "relationshipfield", +} + +export function makeDatasourceFormComponents(datasource) { + const { schema } = getSchemaForDatasource(datasource, true) + let components = [] + let fields = Object.keys(schema || {}) + fields.forEach(field => { + const fieldSchema = schema[field] + const fieldType = + typeof fieldSchema === "object" ? fieldSchema.type : fieldSchema + const componentType = fieldTypeToComponentMap[fieldType] + const fullComponentType = `@budibase/standard-components/${componentType}` + if (componentType) { + const component = new Component(fullComponentType) + .instanceName(field) + .customProps({ + field, + label: field, + placeholder: field, + }) + if (fieldType === "options") { + component.customProps({ placeholder: "Choose an option " }) + } + if (fieldType === "boolean") { + component.customProps({ text: field, label: "" }) + } + components.push(component) + } + }) + return components +} diff --git a/packages/builder/src/builderStore/storeUtils.js b/packages/builder/src/builderStore/storeUtils.js index 00f5a209a3..6d0f0beab0 100644 --- a/packages/builder/src/builderStore/storeUtils.js +++ b/packages/builder/src/builderStore/storeUtils.js @@ -59,8 +59,8 @@ export const findComponentPath = (rootComponent, id, path = []) => { } /** - * Recurses through the component tree and finds all components of a certain - * type. + * Recurses through the component tree and finds all components which match + * a certain selector */ export const findAllMatchingComponents = (rootComponent, selector) => { if (!rootComponent || !selector) { @@ -81,6 +81,26 @@ export const findAllMatchingComponents = (rootComponent, selector) => { return components.reverse() } +/** + * Finds the closes parent component which matches certain criteria + */ +export const findClosestMatchingComponent = ( + rootComponent, + componentId, + selector +) => { + if (!selector) { + return null + } + const componentPath = findComponentPath(rootComponent, componentId).reverse() + for (let component of componentPath) { + if (selector(component)) { + return component + } + } + return null +} + /** * Recurses through a component tree evaluating a matching function against * components until a match is found diff --git a/packages/builder/src/components/automation/SetupPanel/GenericBindingPopover.svelte b/packages/builder/src/components/automation/SetupPanel/GenericBindingPopover.svelte index 3ee6c0fd31..e6818518ad 100644 --- a/packages/builder/src/components/automation/SetupPanel/GenericBindingPopover.svelte +++ b/packages/builder/src/components/automation/SetupPanel/GenericBindingPopover.svelte @@ -2,7 +2,6 @@ import groupBy from "lodash/fp/groupBy" import { TextArea, - Label, Input, Heading, Body, diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index bddb66e4c9..3390b95288 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -36,7 +36,9 @@ {:else if type === 'boolean'} {:else if type === 'link'} - +
+ +
{:else if type === 'longform'}
diff --git a/packages/builder/src/components/design/AppPreview/componentStructure.json b/packages/builder/src/components/design/AppPreview/componentStructure.json index 88cb620f6e..87bef3d3a1 100644 --- a/packages/builder/src/components/design/AppPreview/componentStructure.json +++ b/packages/builder/src/components/design/AppPreview/componentStructure.json @@ -8,11 +8,16 @@ "name": "Form", "icon": "ri-file-edit-line", "children": [ - "dataform", - "dataformwide", - "input", - "richtext", - "datepicker" + "form", + "fieldgroup", + "stringfield", + "numberfield", + "optionsfield", + "booleanfield", + "longformfield", + "datetimefield", + "attachmentfield", + "relationshipfield" ] }, { @@ -56,8 +61,8 @@ "screenslot", "navigation", "login", - "rowdetail", - "newrow" + "rowdetail" ] } -] \ No newline at end of file +] + diff --git a/packages/builder/src/components/design/AppPreview/iframeTemplate.html b/packages/builder/src/components/design/AppPreview/iframeTemplate.html index 49df3b5c0b..166a978d01 100644 --- a/packages/builder/src/components/design/AppPreview/iframeTemplate.html +++ b/packages/builder/src/components/design/AppPreview/iframeTemplate.html @@ -11,9 +11,6 @@ *, *:before, *:after { box-sizing: border-box; } - * { - pointer-events: none; - } + + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/BooleanFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/BooleanFieldSelect.svelte new file mode 100644 index 0000000000..51b459bf23 --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/BooleanFieldSelect.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/TableViewSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DatasourceSelect.svelte similarity index 93% rename from packages/builder/src/components/design/PropertiesPanel/PropertyControls/TableViewSelect.svelte rename to packages/builder/src/components/design/PropertiesPanel/PropertyControls/DatasourceSelect.svelte index a467a954a2..75702b7cdb 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/TableViewSelect.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DatasourceSelect.svelte @@ -19,6 +19,7 @@ let drawer export let value = {} + export let otherSources $: tables = $backendUiStore.tables.map(m => ({ label: m.name, @@ -88,7 +89,7 @@ class="dropdownbutton" bind:this={anchorRight} on:click={dropdownRight.show}> - {value?.label ? value.label : 'Choose option'} + {value?.label ?? 'Choose option'}
{#if value?.type === 'query'} @@ -175,6 +176,22 @@ {/each} + + {#if otherSources?.length} +
+
+ Other +
+ + {/if} diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DateTimeFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DateTimeFieldSelect.svelte new file mode 100644 index 0000000000..5c0ed8cb2f --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DateTimeFieldSelect.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventEditor.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventEditor.svelte index 0e52aa8f76..7dd7fc5044 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventEditor.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventEditor.svelte @@ -1,15 +1,6 @@ + +
+ + +
+ + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveFields.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveFields.svelte index a27acab956..c8e3d5b4d5 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveFields.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveFields.svelte @@ -32,7 +32,7 @@ // this statement initialises fields from parameters.fields $: fields = fields || - Object.keys(parameterFields || { "": "" }).map(name => ({ + Object.keys(parameterFields || {}).map(name => ({ name, value: (parameterFields && diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/ValidateForm.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/ValidateForm.svelte new file mode 100644 index 0000000000..1d4f789a85 --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/ValidateForm.svelte @@ -0,0 +1,38 @@ + + +
+ + +
+ + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js index 677c646728..b267b8ab3e 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js @@ -3,6 +3,8 @@ import SaveRow from "./SaveRow.svelte" import DeleteRow from "./DeleteRow.svelte" import ExecuteQuery from "./ExecuteQuery.svelte" import TriggerAutomation from "./TriggerAutomation.svelte" +import ValidateForm from "./ValidateForm.svelte" +import RefreshDatasource from "./RefreshDatasource.svelte" // defines what actions are available, when adding a new one // the component is the setup panel for the action @@ -30,4 +32,12 @@ export default [ name: "Trigger Automation", component: TriggerAutomation, }, + { + name: "Validate Form", + component: ValidateForm, + }, + { + name: "Refresh Datasource", + component: RefreshDatasource, + }, ] diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/TableViewFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FieldSelect.svelte similarity index 100% rename from packages/builder/src/components/design/PropertiesPanel/PropertyControls/TableViewFieldSelect.svelte rename to packages/builder/src/components/design/PropertiesPanel/PropertyControls/FieldSelect.svelte diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte new file mode 100644 index 0000000000..0e72cc4665 --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte @@ -0,0 +1,59 @@ + + +
+ + + {/each} + +
+ + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/LongFormFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/LongFormFieldSelect.svelte new file mode 100644 index 0000000000..05a83fe387 --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/LongFormFieldSelect.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/MultiFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/MultiFieldSelect.svelte new file mode 100644 index 0000000000..a735cc5f18 --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/MultiFieldSelect.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/MultiTableViewFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/MultiTableViewFieldSelect.svelte deleted file mode 100644 index 7d2f17a02b..0000000000 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/MultiTableViewFieldSelect.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/NumberFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/NumberFieldSelect.svelte new file mode 100644 index 0000000000..ce2569cf91 --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/NumberFieldSelect.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionSelect.svelte index 3ee839fbd0..c464ed84e0 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionSelect.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionSelect.svelte @@ -106,7 +106,9 @@ } $: displayLabel = - selectedOption && selectedOption.label ? selectedOption.label : value || "" + selectedOption && selectedOption.label + ? selectedOption.label + : value || "Choose option"
    +
  • handleClick(null)} + class:selected={value == null || value === ''}> + Choose option +
  • {#if isOptionsObject} {#each options as { value: v, label }}
  • handleClick(v)} class:selected={value === v}> {label}
  • @@ -142,7 +149,7 @@ {#each options as v}
  • handleClick(v)} class:selected={value === v}> {v}
  • diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionsFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionsFieldSelect.svelte new file mode 100644 index 0000000000..526372e3d8 --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionsFieldSelect.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte index 865c1a662a..91d8758989 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte @@ -144,7 +144,7 @@ align-items: center; display: flex; box-sizing: border-box; - padding-left: var(--spacing-xs); + padding-left: 7px; border-left: 1px solid var(--grey-4); background-color: var(--grey-2); border-top-right-radius: var(--border-radius-m); diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyGroup.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyGroup.svelte index 9d0bb4a40d..93be958d55 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyGroup.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyGroup.svelte @@ -25,7 +25,7 @@ {#if open}
    - {#each properties as prop} + {#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)} + import FormFieldSelect from "./FormFieldSelect.svelte" + + + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/SchemaSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/SchemaSelect.svelte new file mode 100644 index 0000000000..d1890f883f --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/SchemaSelect.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/StringFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/StringFieldSelect.svelte new file mode 100644 index 0000000000..27815af91f --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/StringFieldSelect.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte b/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte index 6d3c0d07d3..430e622c4f 100644 --- a/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte @@ -1,22 +1,35 @@
    @@ -114,7 +151,7 @@ value={componentInstance[setting.key] ?? componentInstance[setting.key]?.defaultValue} {componentInstance} onChange={val => onChange(setting.key, val)} - props={{ options: setting.options }} /> + props={{ options: setting.options, placeholder: setting.placeholder }} /> {/if} {/each} {:else} @@ -122,7 +159,19 @@ This component doesn't have any additional settings.
    {/if} + + {#if componentDefinition?.component?.endsWith('/fieldgroup')} + + {/if}
    + diff --git a/packages/standard-components/src/Input.svelte b/packages/standard-components/src/Input.svelte deleted file mode 100644 index 18a8880529..0000000000 --- a/packages/standard-components/src/Input.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/packages/standard-components/src/List.svelte b/packages/standard-components/src/List.svelte index 61d0d3eaac..9923851abb 100644 --- a/packages/standard-components/src/List.svelte +++ b/packages/standard-components/src/List.svelte @@ -2,16 +2,24 @@ import { getContext } from "svelte" import { isEmpty } from "lodash/fp" - const { API, styleable, DataProvider, builderStore } = getContext("sdk") + export let datasource + export let noRowsMessage + + const { API, styleable, Provider, builderStore, ActionTypes } = getContext( + "sdk" + ) const component = getContext("component") - - export let datasource = [] - export let noRowsMessage = "Feed me some data" - let rows = [] let loaded = false $: fetchData(datasource) + $: actions = [ + { + type: ActionTypes.RefreshDatasource, + callback: () => fetchData(datasource), + metadata: { datasource }, + }, + ] async function fetchData(datasource) { if (!isEmpty(datasource)) { @@ -21,28 +29,38 @@ } -
    - {#if rows.length > 0} - {#if $component.children === 0 && $builderStore.inBuilder} -

    Add some components too

    - {:else} - {#each rows as row} - - - - {/each} + +
    + {#if rows.length > 0} + {#if $component.children === 0 && $builderStore.inBuilder} +

    Add some components to display.

    + {:else} + {#each rows as row} + + + + {/each} + {/if} + {:else if loaded && noRowsMessage} +

    {noRowsMessage}

    {/if} - {:else if loaded && $builderStore.inBuilder} -

    {noRowsMessage}

    - {/if} -
    +
    + diff --git a/packages/standard-components/src/Login.svelte b/packages/standard-components/src/Login.svelte index 40b83a7246..edec1f3992 100644 --- a/packages/standard-components/src/Login.svelte +++ b/packages/standard-components/src/Login.svelte @@ -1,9 +1,7 @@ diff --git a/packages/standard-components/src/NewRow.svelte b/packages/standard-components/src/NewRow.svelte deleted file mode 100644 index 68dcac0b11..0000000000 --- a/packages/standard-components/src/NewRow.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -
    - - - -
    diff --git a/packages/standard-components/src/RichText.svelte b/packages/standard-components/src/RichText.svelte deleted file mode 100644 index 3ad66e1703..0000000000 --- a/packages/standard-components/src/RichText.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - -
    - -
    diff --git a/packages/standard-components/src/RowDetail.svelte b/packages/standard-components/src/RowDetail.svelte index 7f01341665..8a413dfb43 100644 --- a/packages/standard-components/src/RowDetail.svelte +++ b/packages/standard-components/src/RowDetail.svelte @@ -1,46 +1,57 @@ {#if row} -
    - + +
    - -
    +
    + {/if} diff --git a/packages/standard-components/src/Search.svelte b/packages/standard-components/src/Search.svelte index cbe4ece78f..509205f8f1 100644 --- a/packages/standard-components/src/Search.svelte +++ b/packages/standard-components/src/Search.svelte @@ -1,6 +1,5 @@ -
    -
    - {#if schema} - {#each columns as field} -
    - - {#if schema[field].type === 'options'} - - {:else if schema[field].type === 'datetime'} - - {:else if schema[field].type === 'boolean'} - - {:else if schema[field].type === 'number'} - - {:else if schema[field].type === 'string'} - - {/if} -
    - {/each} - {/if} -
    - - -
    -
    - {#if loaded} - {#if rows.length > 0} - {#if $component.children === 0 && $builderStore.inBuilder} -

    Add some components too

    - {:else} - {#each rows as row} - - - + +
    +
    + {#if schema} + {#each columns as field} +
    + + {#if schema[field].type === 'options'} + + {:else if schema[field].type === 'datetime'} + + {:else if schema[field].type === 'boolean'} + + {:else if schema[field].type === 'number'} + + {:else if schema[field].type === 'string'} + + {/if} +
    {/each} {/if} - {:else if $builderStore.inBuilder} -

    Feed me some data

    - {:else} -

    {noRowsMessage}

    - {/if} - {/if} - + {#if loaded} + {#if rows.length > 0} + {#if $component.children === 0 && $builderStore.inBuilder} +

    Add some components to display.

    + {:else} + {#each rows as row} + + + + {/each} + {/if} + {:else if noRowsMessage} +

    {noRowsMessage}

    + {/if} {/if} +
    -
    +
    diff --git a/packages/standard-components/src/forms/DateTimeField.svelte b/packages/standard-components/src/forms/DateTimeField.svelte new file mode 100644 index 0000000000..1826c77724 --- /dev/null +++ b/packages/standard-components/src/forms/DateTimeField.svelte @@ -0,0 +1,141 @@ + + + + {#if fieldState} + +
    +
    + {#if !$fieldState.valid} + + {/if} + +
    + +
    +
    + {#if open} +
    + {/if} + {/if} + + + diff --git a/packages/standard-components/src/forms/Field.svelte b/packages/standard-components/src/forms/Field.svelte new file mode 100644 index 0000000000..597cad02c0 --- /dev/null +++ b/packages/standard-components/src/forms/Field.svelte @@ -0,0 +1,86 @@ + + + +
    + +
    + {#if !formContext} + Form components need to be wrapped in a Form + {:else if !fieldState} + + Add the Field setting to start using your component + + {:else if fieldSchema?.type && fieldSchema?.type !== type} + + This Field setting is the wrong data type for this component + + {:else} + + {#if $fieldState.error} +
    {$fieldState.error}
    + {/if} + {/if} +
    +
    +
    + + diff --git a/packages/standard-components/src/forms/FieldGroup.svelte b/packages/standard-components/src/forms/FieldGroup.svelte new file mode 100644 index 0000000000..79e1ecad62 --- /dev/null +++ b/packages/standard-components/src/forms/FieldGroup.svelte @@ -0,0 +1,27 @@ + + +
    +
    + +
    +
    + + diff --git a/packages/standard-components/src/forms/FieldGroupFallback.svelte b/packages/standard-components/src/forms/FieldGroupFallback.svelte new file mode 100644 index 0000000000..8fc22c9cc4 --- /dev/null +++ b/packages/standard-components/src/forms/FieldGroupFallback.svelte @@ -0,0 +1,14 @@ + + +{#if fieldGroupContext} + +{:else} +
    + +
    +{/if} diff --git a/packages/standard-components/src/forms/Form.svelte b/packages/standard-components/src/forms/Form.svelte new file mode 100644 index 0000000000..77008bd8b6 --- /dev/null +++ b/packages/standard-components/src/forms/Form.svelte @@ -0,0 +1,173 @@ + + + +
    + {#if loaded} + + {/if} +
    +
    + + diff --git a/packages/standard-components/src/forms/LongFormField.svelte b/packages/standard-components/src/forms/LongFormField.svelte new file mode 100644 index 0000000000..d22be1b6a9 --- /dev/null +++ b/packages/standard-components/src/forms/LongFormField.svelte @@ -0,0 +1,71 @@ + + + + {#if mounted} +
    + +
    + {/if} +
    + + diff --git a/packages/standard-components/src/forms/NumberField.svelte b/packages/standard-components/src/forms/NumberField.svelte new file mode 100644 index 0000000000..32eacd5b44 --- /dev/null +++ b/packages/standard-components/src/forms/NumberField.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/standard-components/src/forms/OptionsField.svelte b/packages/standard-components/src/forms/OptionsField.svelte new file mode 100644 index 0000000000..48feb77b54 --- /dev/null +++ b/packages/standard-components/src/forms/OptionsField.svelte @@ -0,0 +1,44 @@ + + + + {#if fieldState} + option === $fieldState.value} + onSelectOption={selectOption} /> + {/if} + diff --git a/packages/standard-components/src/forms/Picker.svelte b/packages/standard-components/src/forms/Picker.svelte new file mode 100644 index 0000000000..153d4f4ea7 --- /dev/null +++ b/packages/standard-components/src/forms/Picker.svelte @@ -0,0 +1,109 @@ + + +{#if fieldState} + + {#if open} +
    (open = false)} /> +
    +
      + {#if placeholderOption} +
    • onSelectOption(null)}> + {placeholderOption} + +
    • + {/if} + {#each options as option} +
    • onSelectOption(getOptionValue(option))}> + {getOptionLabel(option)} + +
    • + {/each} +
    +
    + {/if} +{/if} + + diff --git a/packages/standard-components/src/forms/Placeholder.svelte b/packages/standard-components/src/forms/Placeholder.svelte new file mode 100644 index 0000000000..517901e7c5 --- /dev/null +++ b/packages/standard-components/src/forms/Placeholder.svelte @@ -0,0 +1,30 @@ + + +{#if $builderStore.inBuilder} +
    + +
    +{/if} + + diff --git a/packages/standard-components/src/forms/RelationshipField.svelte b/packages/standard-components/src/forms/RelationshipField.svelte new file mode 100644 index 0000000000..f09595606f --- /dev/null +++ b/packages/standard-components/src/forms/RelationshipField.svelte @@ -0,0 +1,81 @@ + + + + option._id} /> + diff --git a/packages/standard-components/src/forms/StringField.svelte b/packages/standard-components/src/forms/StringField.svelte new file mode 100644 index 0000000000..416a037fb7 --- /dev/null +++ b/packages/standard-components/src/forms/StringField.svelte @@ -0,0 +1,66 @@ + + + + {#if fieldState} +
    + {#if !$fieldState.valid} + + {/if} + +
    + {/if} +
    + + diff --git a/packages/standard-components/src/forms/index.js b/packages/standard-components/src/forms/index.js new file mode 100644 index 0000000000..3998424db3 --- /dev/null +++ b/packages/standard-components/src/forms/index.js @@ -0,0 +1,10 @@ +export { default as form } from "./Form.svelte" +export { default as fieldgroup } from "./FieldGroup.svelte" +export { default as stringfield } from "./StringField.svelte" +export { default as numberfield } from "./NumberField.svelte" +export { default as optionsfield } from "./OptionsField.svelte" +export { default as booleanfield } from "./BooleanField.svelte" +export { default as longformfield } from "./LongFormField.svelte" +export { default as datetimefield } from "./DateTimeField.svelte" +export { default as attachmentfield } from "./AttachmentField.svelte" +export { default as relationshipfield } from "./RelationshipField.svelte" diff --git a/packages/standard-components/src/forms/validation.js b/packages/standard-components/src/forms/validation.js new file mode 100644 index 0000000000..5b49fa0848 --- /dev/null +++ b/packages/standard-components/src/forms/validation.js @@ -0,0 +1,112 @@ +import flatpickr from "flatpickr" + +export const createValidatorFromConstraints = (constraints, field, table) => { + let checks = [] + + if (constraints) { + // Required constraint + if ( + field === table?.primaryDisplay || + constraints.presence?.allowEmpty === false + ) { + checks.push(presenceConstraint) + } + + // String length constraint + if (exists(constraints.length?.maximum)) { + const length = constraints.length.maximum + checks.push(lengthConstraint(length)) + } + + // Min / max number constraint + if (exists(constraints.numericality?.greaterThanOrEqualTo)) { + const min = constraints.numericality.greaterThanOrEqualTo + checks.push(numericalConstraint(x => x >= min, `Minimum value is ${min}`)) + } + if (exists(constraints.numericality?.lessThanOrEqualTo)) { + const max = constraints.numericality.lessThanOrEqualTo + checks.push(numericalConstraint(x => x <= max, `Maximum value is ${max}`)) + } + + // Inclusion constraint + if (exists(constraints.inclusion)) { + const options = constraints.inclusion + checks.push(inclusionConstraint(options)) + } + + // Date constraint + if (exists(constraints.datetime?.earliest)) { + const limit = constraints.datetime.earliest + checks.push(dateConstraint(limit, true)) + } + if (exists(constraints.datetime?.latest)) { + const limit = constraints.datetime.latest + checks.push(dateConstraint(limit, false)) + } + } + + // Evaluate each constraint + return value => { + for (let check of checks) { + const error = check(value) + if (error) { + return error + } + } + return null + } +} + +const exists = value => value != null && value !== "" + +const presenceConstraint = value => { + let invalid + if (Array.isArray(value)) { + invalid = value.length === 0 + } else { + invalid = value == null || value === "" + } + return invalid ? "Required" : null +} + +const lengthConstraint = maxLength => value => { + if (value && value.length > maxLength) { + return `Maximum ${maxLength} characters` + } + return null +} + +const numericalConstraint = (constraint, error) => value => { + if (isNaN(value)) { + return "Must be a number" + } + const number = parseFloat(value) + if (!constraint(number)) { + return error + } + return null +} + +const inclusionConstraint = (options = []) => value => { + if (value == null || value === "") { + return null + } + if (!options.includes(value)) { + return "Invalid value" + } + return null +} + +const dateConstraint = (dateString, isEarliest) => { + const dateLimit = Date.parse(dateString) + return value => { + if (value == null || value === "") { + return null + } + const dateValue = Date.parse(value) + const valid = isEarliest ? dateValue >= dateLimit : dateValue <= dateLimit + const adjective = isEarliest ? "Earliest" : "Latest" + const limitString = flatpickr.formatDate(new Date(dateLimit), "F j Y, H:i") + return valid ? null : `${adjective} is ${limitString}` + } +} diff --git a/packages/standard-components/src/grid/Component.svelte b/packages/standard-components/src/grid/Component.svelte index 3004622bfd..2958282293 100644 --- a/packages/standard-components/src/grid/Component.svelte +++ b/packages/standard-components/src/grid/Component.svelte @@ -16,7 +16,6 @@ const setters = new Map([["number", number]]) const SDK = getContext("sdk") const component = getContext("component") - const dataContext = getContext("data") const { API, styleable } = SDK export let datasource = {} diff --git a/packages/standard-components/src/helpers.js b/packages/standard-components/src/helpers.js index 02f3229467..af98473ac9 100644 --- a/packages/standard-components/src/helpers.js +++ b/packages/standard-components/src/helpers.js @@ -31,3 +31,35 @@ export const cssVars = (node, props) => { }, } } + +/** + * Generates a short random ID. + * This is "nanoid" but rollup was derping attempting to bundle it, so the + * source has just been extracted manually since it's tiny. + */ +export const generateID = (size = 21) => { + let id = "" + let bytes = crypto.getRandomValues(new Uint8Array(size)) + + // A compact alternative for `for (var i = 0; i < step; i++)`. + while (size--) { + // It is incorrect to use bytes exceeding the alphabet size. + // The following mask reduces the random byte in the 0-255 value + // range to the 0-63 value range. Therefore, adding hacks, such + // as empty string fallback or magic numbers, is unneccessary because + // the bitmask trims bytes down to the alphabet size. + let byte = bytes[size] & 63 + if (byte < 36) { + // `0-9a-z` + id += byte.toString(36) + } else if (byte < 62) { + // `A-Z` + id += (byte - 26).toString(36).toUpperCase() + } else if (byte < 63) { + id += "_" + } else { + id += "-" + } + } + return id +} diff --git a/packages/standard-components/src/index.js b/packages/standard-components/src/index.js index 1adb482ea3..14886e2a17 100644 --- a/packages/standard-components/src/index.js +++ b/packages/standard-components/src/index.js @@ -1,18 +1,24 @@ import "@budibase/bbui/dist/bbui.css" -import "flatpickr/dist/flatpickr.css" +import "@spectrum-css/vars/dist/spectrum-global.css" +import "@spectrum-css/vars/dist/spectrum-medium.css" +import "@spectrum-css/vars/dist/spectrum-large.css" +import "@spectrum-css/vars/dist/spectrum-lightest.css" +import "@spectrum-css/vars/dist/spectrum-light.css" +import "@spectrum-css/vars/dist/spectrum-dark.css" +import "@spectrum-css/vars/dist/spectrum-darkest.css" +import "@spectrum-css/page/dist/index-vars.css" +import "@spectrum-css/button/dist/index-vars.css" + +import { loadSpectrumIcons } from "./spectrum-icons" +loadSpectrumIcons() export { default as container } from "./Container.svelte" export { default as datagrid } from "./grid/Component.svelte" export { default as screenslot } from "./ScreenSlot.svelte" export { default as button } from "./Button.svelte" -export { default as input } from "./Input.svelte" -export { default as richtext } from "./RichText.svelte" export { default as list } from "./List.svelte" export { default as stackedlist } from "./StackedList.svelte" export { default as card } from "./Card.svelte" -export { default as dataform } from "./DataForm.svelte" -export { default as dataformwide } from "./DataFormWide.svelte" -export { default as datepicker } from "./DatePicker.svelte" export { default as text } from "./Text.svelte" export { default as login } from "./Login.svelte" export { default as navigation } from "./Navigation.svelte" @@ -23,7 +29,7 @@ export { default as image } from "./Image.svelte" export { default as embed } from "./Embed.svelte" export { default as cardhorizontal } from "./CardHorizontal.svelte" export { default as cardstat } from "./CardStat.svelte" -export { default as newrow } from "./NewRow.svelte" export { default as icon } from "./Icon.svelte" export { default as search } from "./Search.svelte" export * from "./charts" +export * from "./forms" diff --git a/packages/standard-components/src/spectrum-icons.js b/packages/standard-components/src/spectrum-icons.js new file mode 100644 index 0000000000..932838339b --- /dev/null +++ b/packages/standard-components/src/spectrum-icons.js @@ -0,0 +1,32 @@ +import "@spectrum-css/icon/dist/index-vars.css" +import SpectrumUIIcons from "@spectrum-css/icon/dist/spectrum-css-icons.svg" +import SpectrumWorkflowIcons from "@adobe/spectrum-css-workflow-icons/dist/spectrum-icons.svg" + +export const loadSpectrumIcons = () => { + loadIconSet("Spectrum UI Icons", SpectrumUIIcons) + loadIconSet("Spectrum Workflow Icons", SpectrumWorkflowIcons) +} + +const loadIconSet = (name, markup) => { + // Parse the SVG + const parser = new DOMParser() + try { + const doc = parser.parseFromString(markup, "image/svg+xml") + const svg = doc.firstChild + + // Check a real SVG was parsed + if (svg && svg.tagName === "svg") { + // Hide the element + svg.style.display = "none" + + // Insert it into the head + document.head.insertBefore(svg, null) + } else { + throw "Invalid tag type for SVG definition" + } + } catch (err) { + // Swallow error, but icons won't work + console.error(err) + console.error(`Failed to parse ${name}. Icons won't work.`) + } +} diff --git a/packages/standard-components/yarn.lock b/packages/standard-components/yarn.lock index 3ea980fc71..8b0ca17cdc 100644 --- a/packages/standard-components/yarn.lock +++ b/packages/standard-components/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adobe/spectrum-css-workflow-icons@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@adobe/spectrum-css-workflow-icons/-/spectrum-css-workflow-icons-1.1.0.tgz#79e97f86130e1a30b84c8524cebff93a600dbb8a" + integrity sha512-07ec4Pfr+W5II4a36nto3jShBZTbpe39lB977ULYN436UTQsnAdYupezBwFd7eEvOMUawgKfqnxyR2oPxp1SMQ== + "@babel/code-frame@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" @@ -39,10 +44,10 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@budibase/bbui@^1.55.1": - version "1.56.2" - resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.56.2.tgz#bb8f7d9b9b5ed06a22df877fbe028780d7602471" - integrity sha512-cWYkT1FNwNGTjisxtC5/MlQ1zeu7MYbMJsD6UyCEW3Ku6JIQZ6jyOkV6HKrmNND8VzVfddEGpzR37q+NoDpDFQ== +"@budibase/bbui@^1.58.5": + version "1.58.5" + resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.5.tgz#c9ce712941760825c7774a1de77594e989db4561" + integrity sha512-0j1I7BetJ2GzB1BXKyvvlkuFphLmADJh2U/Ihubwxx5qUDY8REoVzLgAB4c24zt0CGVTF9VMmOoMLd0zD0QwdQ== dependencies: markdown-it "^12.0.2" quill "^1.3.7" @@ -127,6 +132,73 @@ estree-walker "^1.0.1" picomatch "^2.2.2" +"@spectrum-css/actionbutton@^1.0.0-beta.1": + version "1.0.0-beta.1" + resolved "https://registry.yarnpkg.com/@spectrum-css/actionbutton/-/actionbutton-1.0.0-beta.1.tgz#a6684cac108d4a9daefe0be6df8201d3c369a0d6" + integrity sha512-QbrPMTkbkmh+dEBP66TFXmF5z3qSde+BnLR5hnlo2XMvKvnblX2VJStEbQ+hTKuSZXCRFADXyXD5o0NOYDTByQ== + +"@spectrum-css/button@^3.0.0-beta.6": + version "3.0.0-beta.6" + resolved "https://registry.yarnpkg.com/@spectrum-css/button/-/button-3.0.0-beta.6.tgz#007919d3e7a6692e506dc9addcd46aee6b203b1a" + integrity sha512-ZoJxezt5Pc006RR7SMG7PfC0VAdWqaGDpd21N8SEykGuz/KmNulqGW8RiSZQGMVX/jk5ZCAthPrH8cI/qtKbMg== + +"@spectrum-css/checkbox@^3.0.0-beta.6": + version "3.0.0-beta.6" + resolved "https://registry.yarnpkg.com/@spectrum-css/checkbox/-/checkbox-3.0.0-beta.6.tgz#338c4e58c4570ac8023f7332794fcb45f5ae9374" + integrity sha512-Z0Mwu7yn2b+QcZaBqMpKhliTQiF8T/cRyKgTyaIACtJ0FAK5NBJ4h/X6SWW3iXtoUWCH4+p/Hdtq1iQHAFi1qQ== + +"@spectrum-css/fieldlabel@^3.0.0-beta.7": + version "3.0.0-beta.7" + resolved "https://registry.yarnpkg.com/@spectrum-css/fieldlabel/-/fieldlabel-3.0.0-beta.7.tgz#f37797565e21b3609b8fbc2dafcea8ea41ffa114" + integrity sha512-0pseiPghqlOdALsRtidveWyt2YjfSXTZWDlSkcne/J0/QXBJOQH/7Qfy7TmROQZYRB2LqH1VzmE1zbvGwr5Aog== + +"@spectrum-css/icon@^3.0.0-beta.2": + version "3.0.0-beta.2" + resolved "https://registry.yarnpkg.com/@spectrum-css/icon/-/icon-3.0.0-beta.2.tgz#2dd7258ded74501b56e5fc42d0b6f0a3f4936aeb" + integrity sha512-BEHJ68YIXSwsNAqTdq/FrS4A+jtbKzqYrsGKXdDf93ql+fHWYXRCh1EVYGHx/1696mY73DhM4snMpKGIFtXGFA== + +"@spectrum-css/inputgroup@^3.0.0-beta.7": + version "3.0.0-beta.7" + resolved "https://registry.yarnpkg.com/@spectrum-css/inputgroup/-/inputgroup-3.0.0-beta.7.tgz#9829812e349bf973fb8835f0586bf013c8c38d23" + integrity sha512-pZDpYhtTKZUVG31Rtx7imdwK2ohLyVuTEsl+mj2yDKn+2TOwYRxr6LdbfNhFN4xd0GtSqapKYfbgKBWYpIyiSw== + +"@spectrum-css/menu@^3.0.0-beta.5": + version "3.0.0-beta.5" + resolved "https://registry.yarnpkg.com/@spectrum-css/menu/-/menu-3.0.0-beta.5.tgz#99d5ea7f6760b7a89d5d732f4e91b98dd3f82d74" + integrity sha512-jvPD5GbNdX31rdFBLxCG7KoUVGeeNYLzNXDpiGZsWme/djVTwitljgNe7bhVwCVlXZE7H20Ti/YrdafnE154Rw== + +"@spectrum-css/page@^3.0.0-beta.0": + version "3.0.0-beta.0" + resolved "https://registry.yarnpkg.com/@spectrum-css/page/-/page-3.0.0-beta.0.tgz#885ea41b44861c5dc3aac904536f9e93c9109b58" + integrity sha512-+OD+l3aLisykxJnHfLkdkxMS1Uj1vKGYpKil7W0r5lSWU44eHyRgb8ZK5Vri1+sUO5SSf/CTybeVwtXME9wMLA== + dependencies: + "@spectrum-css/vars" "^3.0.0-beta.2" + +"@spectrum-css/picker@^1.0.0-beta.3": + version "1.0.0-beta.3" + resolved "https://registry.yarnpkg.com/@spectrum-css/picker/-/picker-1.0.0-beta.3.tgz#476593597b5a9e0105397e4e39350869cf6e7965" + integrity sha512-jHzFnS5Frd3JSwZ6B8ymH/sVnNqAUBo9p93Zax4VHTUDsPTtTkvxj/Vxo4POmrJEL9v3qUB2Yk13rD2BSfEzLQ== + +"@spectrum-css/popover@^3.0.0-beta.6": + version "3.0.0-beta.6" + resolved "https://registry.yarnpkg.com/@spectrum-css/popover/-/popover-3.0.0-beta.6.tgz#787611f020e091234e6ba7e946b0dbd0ed1a2fa2" + integrity sha512-dUJlwxoNpB6jOR0g/ywH2cPoUz2FVsL6xPfkm6BSsLp9ejhYy0/OFF4w0Q32Fu9qJDbWJ9qaoOlPpt7IjQ+/GQ== + +"@spectrum-css/stepper@^3.0.0-beta.7": + version "3.0.0-beta.7" + resolved "https://registry.yarnpkg.com/@spectrum-css/stepper/-/stepper-3.0.0-beta.7.tgz#fc78435ce878c5e233af13e43ed2c3e8671a2bbc" + integrity sha512-TQL2OBcdEgbHBwehMGgqMuWdKZZQPGcBRV5FlF0TUdOT58lEqFAO43Gajqvyte1P23lNmnX8KuMwkRfQdn0RzA== + +"@spectrum-css/textfield@^3.0.0-beta.6": + version "3.0.0-beta.6" + resolved "https://registry.yarnpkg.com/@spectrum-css/textfield/-/textfield-3.0.0-beta.6.tgz#30c044ceb403d6ea82d8046fb8f767f7fe455da6" + integrity sha512-U7P8C3Xx8h5X+r+dZu1qbxceIxBn7ZSmMvJyC7MPSPcU3EwdzCUepERNGX7NrQdcX91XSNlPUOF7hZUognBwhQ== + +"@spectrum-css/vars@^3.0.0-beta.2": + version "3.0.0-beta.2" + resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-3.0.0-beta.2.tgz#f0b3a2db44aa57b1a82e47ab392c716a3056a157" + integrity sha512-HpcRDUkSjKVWUi7+jf6zp33YszXs3qFljaaNVTVOf0m0mqjWWXHxgLrvYlFFlHp5ITbNXds5Cb7EgiXCKmVIpA== + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -979,6 +1051,11 @@ esprima@^4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== +estree-walker@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.2.1.tgz#bdafe8095383d8414d5dc2ecf4c9173b6db9412e" + integrity sha1-va/oCVOD2EFNXcLs9MkXO225QS4= + estree-walker@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" @@ -1562,6 +1639,11 @@ loader-utils@^1.1.0: emojis-list "^3.0.0" json5 "^1.0.1" +loadicons@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/loadicons/-/loadicons-1.0.0.tgz#79fd9b08ef2933988c94068cbd246ef3f21cbd04" + integrity sha512-KSywiudfuOK5sTdhNMM8hwRpMxZ5TbQlU4ZijMxUFwRW7jpxUmb9YJoLIzDn7+xuxeLzCZWBmLJS2JDjDWCpsw== + local-access@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/local-access/-/local-access-1.1.0.tgz#e007c76ba2ca83d5877ba1a125fc8dfe23ba4798" @@ -1667,7 +1749,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@^3.0.4: +minimatch@^3.0.2, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -2461,6 +2543,13 @@ rollup-plugin-svelte@^6.1.1: rollup-pluginutils "^2.8.2" sourcemap-codec "^1.4.8" +rollup-plugin-svg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-svg/-/rollup-plugin-svg-2.0.0.tgz#ce11b55e915d5b2190328c4e6632bd6b4fe12ee9" + integrity sha512-DmE7dSQHo1SC5L2uH2qul3Mjyd5oV6U1aVVkyvTLX/mUsRink7f1b1zaIm+32GEBA6EHu8H/JJi3DdWqM53ySQ== + dependencies: + rollup-pluginutils "^1.3.1" + rollup-plugin-terser@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" @@ -2471,6 +2560,14 @@ rollup-plugin-terser@^7.0.2: serialize-javascript "^4.0.0" terser "^5.0.0" +rollup-pluginutils@^1.3.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz#1e156e778f94b7255bfa1b3d0178be8f5c552408" + integrity sha1-HhVud4+UtyVb+hs9AXi+j1xVJAg= + dependencies: + estree-walker "^0.2.1" + minimatch "^3.0.2" + rollup-pluginutils@^2.8.2: version "2.8.2" resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e"