diff --git a/packages/bbui/src/Form/Combobox.svelte b/packages/bbui/src/Form/Combobox.svelte
index 83927b05db..9b7cab1a08 100644
--- a/packages/bbui/src/Form/Combobox.svelte
+++ b/packages/bbui/src/Form/Combobox.svelte
@@ -13,6 +13,7 @@
export let options = []
export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value")
+ export let autofocus = false
const dispatch = createEventDispatcher()
const onChange = e => {
@@ -35,6 +36,7 @@
{options}
{placeholder}
{readonly}
+ {autofocus}
{getOptionLabel}
{getOptionValue}
on:change={onChange}
diff --git a/packages/bbui/src/Form/Core/Combobox.svelte b/packages/bbui/src/Form/Core/Combobox.svelte
index 6a6423cccf..1c590ea395 100644
--- a/packages/bbui/src/Form/Core/Combobox.svelte
+++ b/packages/bbui/src/Form/Core/Combobox.svelte
@@ -3,13 +3,14 @@
import "@spectrum-css/popover/dist/index-vars.css"
import "@spectrum-css/menu/dist/index-vars.css"
import { fly } from "svelte/transition"
- import { createEventDispatcher } from "svelte"
+ import { createEventDispatcher, getContext } from "svelte"
export let value = null
export let id = null
export let placeholder = "Choose an option or type"
export let disabled = false
export let readonly = false
+ export let autofocus = false
export let error = null
export let options = []
export let getOptionLabel = option => option
@@ -18,22 +19,12 @@
const dispatch = createEventDispatcher()
let open = false
let focus = false
- $: fieldText = getFieldText(value, options, placeholder)
+ let comboInput
- const getFieldText = (value, options, placeholder) => {
- // Always use placeholder if no value
- if (value == null || value === "") {
- return placeholder || "Choose an option or type"
- }
+ let builderFocus = getContext("field_focus")
- // Wait for options to load if there is a value but no options
- if (!options?.length) {
- return ""
- }
-
- // Render the label if the selected option is found, otherwise raw value
- const selected = options.find(option => getOptionValue(option) === value)
- return selected ? getOptionLabel(selected) : value
+ $: if (autofocus && comboInput) {
+ comboInput.focus()
}
const selectOption = value => {
@@ -66,10 +57,16 @@
class:is-focused={open || focus}
>
(focus = true)}
- on:blur={() => (focus = false)}
+ on:blur={() => {
+ if (builderFocus) {
+ builderFocus.clear()
+ }
+ focus = false
+ }}
on:change={onType}
value={value || ""}
placeholder={placeholder || ""}
diff --git a/packages/builder/cypress/integration/createComponents.spec.js b/packages/builder/cypress/integration/createComponents.spec.js
index e13439d9c6..3525e66fb4 100644
--- a/packages/builder/cypress/integration/createComponents.spec.js
+++ b/packages/builder/cypress/integration/createComponents.spec.js
@@ -1,9 +1,7 @@
-// TODO for now components are skipped, might not be good to keep doing this
-
import filterTests from "../support/filterTests"
filterTests(['all'], () => {
- xcontext("Create Components", () => {
+ context("Create Components", () => {
let headlineId
before(() => {
@@ -12,12 +10,13 @@ filterTests(['all'], () => {
cy.createTable("dog")
cy.addColumn("dog", "name", "Text")
cy.addColumn("dog", "age", "Number")
- cy.addColumn("dog", "type", "Options")
+ cy.addColumn("dog", "breed", "Options")
cy.navigateToFrontend()
+ cy.wait(1000) //allow the iframe some wiggle room
})
it("should add a container", () => {
- cy.addComponent(null, "Container").then(componentId => {
+ cy.addComponent("Layout", "Container").then(componentId => {
cy.getComponent(componentId).should("exist")
})
})
@@ -31,7 +30,6 @@ filterTests(['all'], () => {
it("should change the text of the headline", () => {
const text = "Lorem ipsum dolor sit amet."
- cy.get("[data-cy=Settings]").click()
cy.get("[data-cy=setting-text] input")
.type(text)
.blur()
@@ -39,32 +37,34 @@ filterTests(['all'], () => {
})
it("should change the size of the headline", () => {
- cy.get("[data-cy=Design]").click()
- cy.contains("Typography").click()
- cy.get("[data-cy=font-size-prop-control]").click()
- cy.contains("60px").click()
- cy.getComponent(headlineId).should("have.css", "font-size", "60px")
+ cy.get("[data-cy=setting-size]").scrollIntoView().click()
+ cy.get("[data-cy=setting-size]").within(() => {
+ cy.get(".spectrum-Form-item li.spectrum-Menu-item").contains("3XL").click()
+ })
+
+ cy.getComponent(headlineId).within(() => {
+ cy.get(".spectrum-Heading").should("have.css", "font-size", "60px")
+ })
})
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]")
- .contains("Choose option")
+ .contains("Custom")
.click()
cy.get(".dropdown")
.contains("dog")
.click()
cy.addComponent("Form", "Field Group").then(fieldGroupId => {
- cy.get("[data-cy=Settings]").click()
- cy.contains("Update Form Fields").click()
- cy.get(".modal")
- .get("button.primary")
+ cy.contains("Update form fields").click()
+ cy.get(".spectrum-Modal")
+ .get(".confirm-wrap .spectrum-Button")
.click()
+ cy.wait(500)
cy.getComponent(fieldGroupId).within(() => {
cy.contains("name").should("exist")
cy.contains("age").should("exist")
- cy.contains("type").should("exist")
+ cy.contains("breed").should("exist")
})
cy.getComponent(fieldGroupId)
.find("input")
@@ -81,17 +81,78 @@ filterTests(['all'], () => {
cy.get("[data-cy=setting-_instanceName] input")
.type(componentId)
.blur()
- cy.get(".ui-nav ul .nav-item.selected .ri-more-line").click({
+ cy.get(".nav-items-container .nav-item.selected .actions > div > .icon").click({
force: true,
})
- cy.get(".dropdown-container")
+ cy.get(".spectrum-Popover.is-open li")
.contains("Delete")
.click()
- cy.get(".modal")
+ cy.get(".spectrum-Modal button")
.contains("Delete Component")
- .click()
+ .click({
+ force: true,
+ })
cy.getComponent(componentId).should("not.exist")
})
})
+
+ it("should set focus to the field setting when fields are added to a form", () => {
+ cy.addComponent("Form", "Form").then(() => {
+ cy.get("[data-cy=setting-dataSource]")
+ .contains("Custom")
+ .click()
+ cy.get(".dropdown")
+ .contains("dog")
+ .click()
+
+ const componentTypeLabels = ["Text Field", "Number Field", "Password Field",
+ "Options Picker", "Checkbox", "Long Form Field", "Date Picker", "Attachment",
+ "JSON Field", "Multi-select Picker", "Relationship Picker"]
+
+ const refocusTest = (componentId) => {
+ let inputClasses
+
+ cy.getComponent(componentId)
+ .find(".showMe").should("exist").click({ force: true })
+
+ cy.get("[data-cy=setting-field] .spectrum-InputGroup")
+ .should("have.class", "is-focused").within(() => {
+ cy.get("input").should(($input) => {
+ expect($input).to.have.length(1)
+ inputClasses = Cypress.$($input).attr('class')
+ })
+ })
+
+ cy.focused().then(($focused) => {
+ const focusedClasses = Cypress.$($focused).attr('class')
+ expect(inputClasses).to.equal(focusedClasses)
+ })
+ }
+
+ const testFieldFocusOnCreate = (componentLabel) => {
+ let inputClasses
+
+ cy.addComponent("Form", componentLabel).then((componentId) => {
+
+ refocusTest(componentId)
+
+ cy.get("[data-cy=setting-field] .spectrum-InputGroup")
+ .should("have.class", "is-focused").within(() => {
+ cy.get("input").should(($input) => {
+ expect($input).to.have.length(1)
+ inputClasses = Cypress.$($input).attr('class')
+ })
+ })
+ })
+ cy.focused().then(($focused) => {
+ const focusedClasses = Cypress.$($focused).attr('class')
+ expect(inputClasses).to.equal(focusedClasses)
+ })
+ }
+
+ componentTypeLabels.forEach( testFieldFocusOnCreate )
+
+ })
+ })
})
})
diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js
index fe355441b9..3baa148ae8 100644
--- a/packages/builder/cypress/support/commands.js
+++ b/packages/builder/cypress/support/commands.js
@@ -334,9 +334,9 @@ Cypress.Commands.add("getComponent", componentId => {
.its("0.contentDocument")
.should("exist")
.its("body")
- .should("not.be.null")
+ .should("not.be.undefined")
.then(cy.wrap)
- .find(`[data-id=${componentId}]`)
+ .find(`[data-id='${componentId}']`)
})
Cypress.Commands.add("navigateToFrontend", () => {
diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js
index 3ffc890c7d..48e5982eae 100644
--- a/packages/builder/src/builderStore/store/frontend.js
+++ b/packages/builder/src/builderStore/store/frontend.js
@@ -380,6 +380,21 @@ export const getFrontendStore = () => {
const selected = get(selectedComponent)
const asset = get(currentAsset)
+ const formComponents = [
+ "stringfield",
+ "optionsfield",
+ "numberfield",
+ "datetimefield",
+ "booleanfield",
+ "passwordfield",
+ "longformfield",
+ "attachmentfield",
+ "jsonfield",
+ "relationshipfield",
+ "multifieldselect",
+ "s3upload",
+ ]
+
// Create new component
const componentInstance = store.actions.components.createInstance(
componentName,
@@ -417,11 +432,31 @@ export const getFrontendStore = () => {
}
parentComponent._children.push(componentInstance)
+ let isFormComponent = false
+ let componentPrefix = "@budibase/standard-components/"
+ if (parentComponent._component === componentPrefix + "form") {
+ const mappedComponentTypes = formComponents.map(cmp => {
+ return componentPrefix + cmp
+ })
+ if (mappedComponentTypes.indexOf(componentInstance._component) > -1) {
+ isFormComponent = true
+ }
+ }
+
// Save components and update UI
await store.actions.preview.saveSelected()
store.update(state => {
state.currentView = "component"
state.selectedComponentId = componentInstance._id
+
+ if (isFormComponent) {
+ //A field component added to a form.
+ state.builderFocus = {
+ key: "field",
+ target: state.selectedComponentId,
+ location: "component_settings",
+ }
+ }
return state
})
diff --git a/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte b/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
index 28bc50d15a..9bab224807 100644
--- a/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
+++ b/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
@@ -157,6 +157,14 @@
try {
if (type === "select-component" && data.id) {
store.actions.components.select({ _id: data.id })
+ //Clear focus
+ if(data.id !== $store.builderFocus?.target){
+ store.update(state => {
+ delete state.builderFocus
+ return state
+ })
+ }
+ //check if the builder-focus matches?
} else if (type === "update-prop") {
await store.actions.components.updateProp(data.prop, data.value)
} else if (type === "delete-component" && data.id) {
@@ -190,6 +198,12 @@
store.actions.components.copy(source, true)
await store.actions.components.paste(destination, data.mode)
}
+ } else if(type == "builder-focus") {
+ store.update(state => ({
+ ...state,
+ builderFocus :
+ { ...data }
+ }))
} else {
console.warn(`Client sent unknown event type: ${type}`)
}
diff --git a/packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte b/packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte
index a043cca619..a36ce52379 100644
--- a/packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte
+++ b/packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte
@@ -86,6 +86,14 @@
return true
}
+
+ const isFocused = setting => {
+ return (
+ componentInstance._id === $store.builderFocus?.target &&
+ setting.key === $store.builderFocus?.key &&
+ "component_settings" === $store.builderFocus?.location
+ )
+ }
{#each sections as section, idx (section.name)}
@@ -120,6 +128,7 @@
{componentBindings}
{componentInstance}
{componentDefinition}
+ autofocus={isFocused(setting)}
/>
{/if}
{/each}
diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte
index 1f08c56ff5..6fd904bb56 100644
--- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte
+++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte
@@ -4,12 +4,24 @@
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
- import { currentAsset } from "builderStore"
+ import { currentAsset, store } from "builderStore"
import { findClosestMatchingComponent } from "builderStore/componentUtils"
+ import { setContext } from "svelte"
+
+ setContext("field_focus", {
+ clear: () => {
+ store.update(state => {
+ delete state.builderFocus
+ return state
+ })
+ },
+ test: $store.builderFocus?.target,
+ })
export let componentInstance
export let value
export let type
+ export let autofocus = false
$: form = findClosestMatchingComponent(
$currentAsset?.props,
@@ -40,4 +52,4 @@
}
-