diff --git a/charts/budibase/templates/alb-ingress.yaml b/charts/budibase/templates/alb-ingress.yaml index 388bcf1d3e..c128b70843 100644 --- a/charts/budibase/templates/alb-ingress.yaml +++ b/charts/budibase/templates/alb-ingress.yaml @@ -14,6 +14,9 @@ metadata: alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]' alb.ingress.kubernetes.io/certificate-arn: {{ .Values.ingress.certificateArn }} {{- end }} + {{- if .Values.ingress.securityGroups }} + alb.ingress.kubernetes.io/security-groups: {{ .Values.ingress.securityGroups }} + {{- end }} spec: rules: - http: diff --git a/lerna.json b/lerna.json index 8db64ebdca..5f433c0b1f 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.2.12-alpha.71", + "version": "2.3.0", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index b058b13001..94b1541499 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.2.12-alpha.71", + "version": "2.3.0", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -23,7 +23,7 @@ }, "dependencies": { "@budibase/nano": "10.1.1", - "@budibase/types": "2.2.12-alpha.71", + "@budibase/types": "^2.3.0", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index a85e970c43..5456a1fb3a 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.2.12-alpha.71", + "version": "2.3.0", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "2.2.12-alpha.71", + "@budibase/string-templates": "^2.3.0", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index 10aae67ec6..2c89a538a3 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -76,13 +76,6 @@ } // If time only set date component to 2000-01-01 if (timeOnly) { - // Classic flackpickr causing issues. - // When selecting a value for the first time for a "time only" field, - // the time is always offset by 1 hour for some reason (regardless of time - // zone) so we need to correct it. - if (!value && newValue) { - newValue = new Date(dates[0].getTime() + 60 * 60 * 1000).toISOString() - } newValue = `2000-01-01T${newValue.split("T")[1]}` } @@ -113,7 +106,7 @@ const clearDateOnBackspace = event => { if (["Backspace", "Clear", "Delete"].includes(event.key)) { - dispatch("change", null) + dispatch("change", "") flatpickr.close() } } diff --git a/packages/bbui/src/Form/Core/RadioGroup.svelte b/packages/bbui/src/Form/Core/RadioGroup.svelte index a3952a9759..f7afc10bbc 100644 --- a/packages/bbui/src/Form/Core/RadioGroup.svelte +++ b/packages/bbui/src/Form/Core/RadioGroup.svelte @@ -11,14 +11,31 @@ export let getOptionLabel = option => option export let getOptionValue = option => option export let getOptionTitle = option => option + export let sort = false const dispatch = createEventDispatcher() const onChange = e => dispatch("change", e.target.value) + + const getSortedOptions = (options, getLabel, sort) => { + if (!options?.length || !Array.isArray(options)) { + return [] + } + if (!sort) { + return options + } + return [...options].sort((a, b) => { + const labelA = getLabel(a) + const labelB = getLabel(b) + return labelA > labelB ? 1 : -1 + }) + } + + $: parsedOptions = getSortedOptions(options, getOptionLabel, sort)
- {#if options && Array.isArray(options)} - {#each options as option} + {#if parsedOptions && Array.isArray(parsedOptions)} + {#each parsedOptions as option}
{ + if (e.detail?.tableId) { + const tableSchema = getSchemaForTable(e.detail.tableId, { + searchableSchema: true, + }).schema + if (isTestModal) { + testData.schema = tableSchema + } else { + block.inputs.schema = tableSchema + } + } try { if (isTestModal) { // Special case for webhook, as it requires a body, but the schema already brings back the body's contents @@ -321,9 +332,17 @@ onChange(e, key)} + meta={inputData["meta"] || {}} + on:change={e => { + if (e.detail?.key) { + onChange(e, e.detail.key) + } else { + onChange(e, key) + } + }} {bindings} {isTestModal} + {isUpdateRow} /> {:else if value.customType === "webhookUrl"} import { tables } from "stores/backend" - import { Select } from "@budibase/bbui" + import { Select, Checkbox } from "@budibase/bbui" import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" import { createEventDispatcher } from "svelte" @@ -10,9 +10,11 @@ const dispatch = createEventDispatcher() export let value + export let meta export let bindings export let block export let isTestModal + export let isUpdateRow $: parsedBindings = bindings.map(binding => { let clone = Object.assign({}, binding) @@ -97,6 +99,17 @@ dispatch("change", value) } + const onChangeSetting = (e, field) => { + let fields = {} + fields[field] = { + clearRelationships: e.detail, + } + dispatch("change", { + key: "meta", + fields, + }) + } + // Ensure any nullish tableId values get set to empty string so // that the select works $: if (value?.tableId == null) value = { tableId: "" } @@ -124,21 +137,33 @@ {onChange} /> {:else} - onChange(e, field, schema.type)} - label={field} - type="string" - bindings={parsedBindings} - fillWidth={true} - allowJS={true} - updateOnChange={false} - /> +
+ onChange(e, field, schema.type)} + label={field} + type="string" + bindings={parsedBindings} + fillWidth={true} + allowJS={true} + updateOnChange={false} + /> + {#if isUpdateRow && schema.type === "link"} +
+ onChangeSetting(e, field)} + /> +
+ {/if} +
{/if} {/if} {/if} @@ -155,4 +180,12 @@ .schema-fields :global(label) { text-transform: capitalize; } + .checkbox-field { + padding-bottom: var(--spacing-s); + padding-left: 1px; + padding-top: var(--spacing-s); + } + .checkbox-field :global(label) { + text-transform: none; + } diff --git a/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte b/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte index cb80072694..b5173682d0 100644 --- a/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte @@ -58,7 +58,7 @@ entries = entries.filter(f => f.name !== originalName) } value = entries.reduce((newVals, current) => { - newVals[current.name] = current.type + newVals[current.name.trim()] = current.type return newVals }, {}) dispatch("change", value) diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 4ed77d55b2..3e5549fcf5 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -12,7 +12,7 @@ Modal, notifications, } from "@budibase/bbui" - import { createEventDispatcher, onMount } from "svelte" + import { createEventDispatcher } from "svelte" import { cloneDeep } from "lodash/fp" import { tables, datasources } from "stores/backend" import { TableNames, UNEDITABLE_USER_FIELDS } from "constants" @@ -48,7 +48,22 @@ const { hide } = getContext(Context.Modal) let fieldDefinitions = cloneDeep(FIELDS) - export let field = { + export let field + + let originalName + let linkEditDisabled + let primaryDisplay + let indexes = [...($tables.selected.indexes || [])] + let isCreating + + let table = $tables.selected + let confirmDeleteDialog + let deletion + let savingColumn + let deleteColName + let jsonSchemaModal + + let editableColumn = { type: "string", constraints: fieldDefinitions.STRING.constraints, @@ -56,48 +71,80 @@ fieldName: $tables.selected.name, } - let originalName = field.name - const linkEditDisabled = originalName != null - let primaryDisplay = - $tables.selected.primaryDisplay == null || - $tables.selected.primaryDisplay === field.name - let isCreating = originalName == null + $: if (primaryDisplay) { + editableColumn.constraints.presence = { allowEmpty: false } + } - let table = $tables.selected - let indexes = [...($tables.selected.indexes || [])] - let confirmDeleteDialog - let deletion - let deleteColName - let jsonSchemaModal + $: if (field && !savingColumn) { + editableColumn = cloneDeep(field) + originalName = editableColumn.name ? editableColumn.name + "" : null + linkEditDisabled = originalName != null + isCreating = originalName == null + primaryDisplay = + $tables.selected.primaryDisplay == null || + $tables.selected.primaryDisplay === editableColumn.name + } - $: checkConstraints(field) - $: required = !!field?.constraints?.presence || primaryDisplay + $: checkConstraints(editableColumn) + $: required = !!editableColumn?.constraints?.presence || primaryDisplay $: uneditable = $tables.selected?._id === TableNames.USERS && - UNEDITABLE_USER_FIELDS.includes(field.name) + UNEDITABLE_USER_FIELDS.includes(editableColumn.name) $: invalid = - !field.name || - (field.type === LINK_TYPE && !field.tableId) || + !editableColumn?.name || + (editableColumn?.type === LINK_TYPE && !editableColumn?.tableId) || Object.keys(errors).length !== 0 - $: errors = checkErrors(field) + $: errors = checkErrors(editableColumn) $: datasource = $datasources.list.find( source => source._id === table?.sourceId ) + const getTableAutoColumnTypes = table => { + return Object.keys(table?.schema).reduce((acc, key) => { + let fieldSchema = table?.schema[key] + if (fieldSchema.autocolumn) { + acc.push(fieldSchema.subtype) + } + return acc + }, []) + } + + let autoColumnInfo = getAutoColumnInformation() + + $: tableAutoColumnsTypes = getTableAutoColumnTypes($tables?.selected) + $: availableAutoColumns = Object.keys(autoColumnInfo).reduce((acc, key) => { + if (!tableAutoColumnsTypes.includes(key)) { + acc[key] = autoColumnInfo[key] + } + return acc + }, {}) + + $: availableAutoColumnKeys = availableAutoColumns + ? Object.keys(availableAutoColumns) + : [] + + $: autoColumnOptions = editableColumn.autocolumn + ? autoColumnInfo + : availableAutoColumns + // used to select what different options can be displayed for column type $: canBeSearched = - field.type !== LINK_TYPE && - field.type !== JSON_TYPE && - field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY && - field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY && - field.type !== FORMULA_TYPE + editableColumn?.type !== LINK_TYPE && + editableColumn?.type !== JSON_TYPE && + editableColumn?.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY && + editableColumn?.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY && + editableColumn?.type !== FORMULA_TYPE $: canBeDisplay = - field.type !== LINK_TYPE && - field.type !== AUTO_TYPE && - field.type !== JSON_TYPE + editableColumn?.type !== LINK_TYPE && + editableColumn?.type !== AUTO_TYPE && + editableColumn?.type !== JSON_TYPE && + !editableColumn.autocolumn $: canBeRequired = - field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_TYPE - $: relationshipOptions = getRelationshipOptions(field) + editableColumn?.type !== LINK_TYPE && + !uneditable && + editableColumn?.type !== AUTO_TYPE && + !editableColumn.autocolumn + $: relationshipOptions = getRelationshipOptions(editableColumn) $: external = table.type === "external" // in the case of internal tables the sourceId will just be undefined $: tableOptions = $tables.list.filter( @@ -108,76 +155,90 @@ ) $: typeEnabled = !originalName || - (originalName && SWITCHABLE_TYPES.indexOf(field.type) !== -1) + (originalName && + SWITCHABLE_TYPES.indexOf(editableColumn.type) !== -1 && + !editableColumn?.autocolumn) async function saveColumn() { - if (field.type === AUTO_TYPE) { - field = buildAutoColumn($tables.selected.name, field.name, field.subtype) + savingColumn = true + if (errors?.length) { + return } - if (field.type !== LINK_TYPE) { - delete field.fieldName + + let saveColumn = cloneDeep(editableColumn) + + if (saveColumn.type === AUTO_TYPE) { + saveColumn = buildAutoColumn( + $tables.selected.name, + saveColumn.name, + saveColumn.subtype + ) + } + if (saveColumn.type !== LINK_TYPE) { + delete saveColumn.fieldName } try { await tables.saveField({ originalName, - field, + field: saveColumn, primaryDisplay, indexes, }) dispatch("updatecolumns") } catch (err) { - notifications.error("Error saving column") + console.log(err) + notifications.error(`Error saving column: ${err.message}`) } } function cancelEdit() { - field.name = originalName + editableColumn.name = originalName } function deleteColumn() { try { - field.name = deleteColName - if (field.name === $tables.selected.primaryDisplay) { + editableColumn.name = deleteColName + if (editableColumn.name === $tables.selected.primaryDisplay) { notifications.error("You cannot delete the display column") } else { - tables.deleteField(field) - notifications.success(`Column ${field.name} deleted.`) + tables.deleteField(editableColumn) + notifications.success(`Column ${editableColumn.name} deleted.`) confirmDeleteDialog.hide() hide() deletion = false dispatch("updatecolumns") } } catch (error) { - notifications.error("Error deleting column") + notifications.error(`Error deleting column: ${error.message}`) } } function handleTypeChange(event) { // remove any extra fields that may not be related to this type - delete field.autocolumn - delete field.subtype - delete field.tableId - delete field.relationshipType - delete field.formulaType + delete editableColumn.autocolumn + delete editableColumn.subtype + delete editableColumn.tableId + delete editableColumn.relationshipType + delete editableColumn.formulaType // Add in defaults and initial definition const definition = fieldDefinitions[event.detail?.toUpperCase()] if (definition?.constraints) { - field.constraints = definition.constraints + editableColumn.constraints = definition.constraints } // Default relationships many to many - if (field.type === LINK_TYPE) { - field.relationshipType = RelationshipTypes.MANY_TO_MANY + if (editableColumn.type === LINK_TYPE) { + editableColumn.relationshipType = RelationshipTypes.MANY_TO_MANY } - if (field.type === FORMULA_TYPE) { - field.formulaType = "dynamic" + if (editableColumn.type === FORMULA_TYPE) { + editableColumn.formulaType = "dynamic" } } function onChangeRequired(e) { const req = e.detail - field.constraints.presence = req ? { allowEmpty: false } : false + editableColumn.constraints.presence = req ? { allowEmpty: false } : false required = req } @@ -185,17 +246,17 @@ const isPrimary = e.detail // primary display is always required if (isPrimary) { - field.constraints.presence = { allowEmpty: false } + editableColumn.constraints.presence = { allowEmpty: false } } } function onChangePrimaryIndex(e) { - indexes = e.detail ? [field.name] : [] + indexes = e.detail ? [editableColumn.name] : [] } function onChangeSecondaryIndex(e) { if (e.detail) { - indexes[1] = field.name + indexes[1] = editableColumn.name } else { indexes = indexes.slice(0, 1) } @@ -246,11 +307,14 @@ } function getAllowedTypes() { - if (originalName && ALLOWABLE_STRING_TYPES.indexOf(field.type) !== -1) { + if ( + originalName && + ALLOWABLE_STRING_TYPES.indexOf(editableColumn.type) !== -1 + ) { return ALLOWABLE_STRING_OPTIONS } else if ( originalName && - ALLOWABLE_NUMBER_TYPES.indexOf(field.type) !== -1 + ALLOWABLE_NUMBER_TYPES.indexOf(editableColumn.type) !== -1 ) { return ALLOWABLE_NUMBER_OPTIONS } else if (!external) { @@ -275,6 +339,9 @@ } function checkConstraints(fieldToCheck) { + if (!fieldToCheck) { + return + } // most types need this, just make sure its always present if (fieldToCheck && !fieldToCheck.constraints) { fieldToCheck.constraints = {} @@ -296,10 +363,16 @@ } function checkErrors(fieldInfo) { + if (!editableColumn) { + return {} + } function inUse(tbl, column, ogName = null) { - return Object.keys(tbl?.schema || {}).some( - key => key !== ogName && key === column - ) + const parsedColumn = column ? column.toLowerCase().trim() : column + + return Object.keys(tbl?.schema || {}).some(key => { + let lowerKey = key.toLowerCase() + return lowerKey !== ogName?.toLowerCase() && lowerKey === parsedColumn + }) } const newError = {} if (!external && fieldInfo.name?.startsWith("_")) { @@ -313,6 +386,11 @@ } else if (inUse($tables.selected, fieldInfo.name, originalName)) { newError.name = `Column name already in use.` } + + if (fieldInfo.type == "auto" && !fieldInfo.subtype) { + newError.subtype = `Auto Column requires a type` + } + if (fieldInfo.fieldName && fieldInfo.tableId) { const relatedTable = $tables.list.find( tbl => tbl._id === fieldInfo.tableId @@ -323,12 +401,6 @@ } return newError } - - onMount(() => { - if (primaryDisplay) { - field.constraints.presence = { allowEmpty: false } - } - }) - {:else if field.type === "options"} + {:else if editableColumn.type === "options"} - {:else if field.type === "longform"} + {:else if editableColumn.type === "longform"}
- {:else if field.type === "array"} + {:else if editableColumn.type === "array"} - {:else if field.type === "datetime"} + {:else if editableColumn.type === "datetime" && !editableColumn.autocolumn} + - {#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"}
- +
{/if} - {:else if field.type === "number"} + {:else if editableColumn.type === "number" && !editableColumn.autocolumn} - {:else if field.type === "link"} + {:else if editableColumn.type === "link"} - {:else if field.type === FORMULA_TYPE} + {:else if editableColumn.type === FORMULA_TYPE} {#if !table.sql} (field.subtype = e.detail)} - options={Object.entries(getAutoColumnInformation())} - getOptionLabel={option => option[1].name} - getOptionValue={option => option[0]} - /> - {:else if field.type === JSON_TYPE} + {:else if editableColumn.type === JSON_TYPE} {/if} + {#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn} +