Initial commit form screen flow and tour refactor

This commit is contained in:
Dean 2024-02-14 12:11:24 +00:00
parent 6a4ae1105b
commit e9e5281e82
34 changed files with 478 additions and 146 deletions

View File

@ -1131,7 +1131,7 @@ export const getAllStateVariables = () => {
"@budibase/standard-components/multistepformblockstep" "@budibase/standard-components/multistepformblockstep"
) )
steps.forEach(step => { steps?.forEach(step => {
parseComponentSettings(stepDefinition, step) parseComponentSettings(stepDefinition, step)
}) })
}) })

View File

@ -75,7 +75,7 @@ const INITIAL_FRONTEND_STATE = {
theme: "", theme: "",
customTheme: {}, customTheme: {},
previewDevice: "desktop", previewDevice: "desktop",
highlightedSettingKey: null, highlightedSetting: null,
propertyFocus: null, propertyFocus: null,
builderSidePanel: false, builderSidePanel: false,
hasLock: true, hasLock: true,
@ -1460,10 +1460,10 @@ export const getFrontendStore = () => {
}, },
}, },
settings: { settings: {
highlight: key => { highlight: (key, type) => {
store.update(state => ({ store.update(state => ({
...state, ...state,
highlightedSettingKey: key, highlightedSetting: { key, type: type || "info" },
})) }))
}, },
propertyFocus: key => { propertyFocus: key => {

View File

@ -0,0 +1,43 @@
import { Screen } from "./utils/Screen"
import { Component } from "./utils/Component"
import sanitizeUrl from "./utils/sanitizeUrl"
export const FORM_TEMPLATE = "FORM_TEMPLATE"
export const formUrl = datasource => sanitizeUrl(`/${datasource.label}-form`)
// Mode not really necessary
export default function (datasources, config) {
if (!Array.isArray(datasources)) {
return []
}
return datasources.map(datasource => {
return {
name: `${datasource.label} - Form`,
create: () => createScreen(datasource, config),
id: FORM_TEMPLATE,
resourceId: datasource.resourceId,
}
})
}
const generateMultistepFormBlock = (dataSource, { actionType } = {}) => {
const multistepFormBlock = new Component(
"@budibase/standard-components/multistepformblock"
)
multistepFormBlock
.customProps({
actionType,
dataSource,
steps: [{}],
})
.instanceName(`${dataSource.label} - Multistep Form block`)
return multistepFormBlock
}
const createScreen = (datasource, config) => {
return new Screen()
.route(formUrl(datasource))
.instanceName(`${datasource.label} - Form`)
.addChild(generateMultistepFormBlock(datasource, config))
.json()
}

View File

@ -1,7 +1,11 @@
import rowListScreen from "./rowListScreen" import rowListScreen from "./rowListScreen"
import createFromScratchScreen from "./createFromScratchScreen" import createFromScratchScreen from "./createFromScratchScreen"
import formScreen from "./formScreen"
const allTemplates = datasources => [...rowListScreen(datasources)] const allTemplates = datasources => [
...rowListScreen(datasources),
...formScreen(datasources),
]
// Allows us to apply common behaviour to all create() functions // Allows us to apply common behaviour to all create() functions
const createTemplateOverride = template => () => { const createTemplateOverride = template => () => {
@ -19,6 +23,7 @@ export default datasources => {
}) })
const fromScratch = enrichTemplate(createFromScratchScreen) const fromScratch = enrichTemplate(createFromScratchScreen)
const tableTemplates = allTemplates(datasources).map(enrichTemplate) const tableTemplates = allTemplates(datasources).map(enrichTemplate)
return [ return [
fromScratch, fromScratch,
...tableTemplates.sort((templateA, templateB) => { ...tableTemplates.sort((templateA, templateB) => {

View File

@ -156,9 +156,10 @@
</div> </div>
{/if} {/if}
<TourWrap <TourWrap
tourStepKey={$store.onboarding stepKeys={[
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT} TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
]}
> >
<div class="app-action-button users"> <div class="app-action-button users">
<div class="app-action" id="builder-app-users-button"> <div class="app-action" id="builder-app-users-button">
@ -204,7 +205,7 @@
<div bind:this={appActionPopoverAnchor}> <div bind:this={appActionPopoverAnchor}>
<div class="app-action"> <div class="app-action">
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} /> <Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}> <TourWrap stepKeys={[TOUR_STEP_KEYS.BUILDER_APP_PUBLISH]}>
<span class="publish-open" id="builder-app-publish-button"> <span class="publish-open" id="builder-app-publish-button">
Publish Publish
<Icon <Icon

View File

@ -1,5 +1,5 @@
<script> <script>
import EditComponentPopover from "../EditComponentPopover.svelte" import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { runtimeToReadableBinding } from "builderStore/dataBinding" import { runtimeToReadableBinding } from "builderStore/dataBinding"
import { isJSBinding } from "@budibase/string-templates" import { isJSBinding } from "@budibase/string-templates"

View File

@ -3,7 +3,8 @@
import { store } from "builderStore" import { store } from "builderStore"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { createEventDispatcher, getContext } from "svelte" import { createEventDispatcher, getContext } from "svelte"
import ComponentSettingsSection from "../../../../pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte" import { customPositionHandler } from "."
import ComponentSettingsSection from "../../../../../pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
export let anchor export let anchor
export let componentInstance export let componentInstance
@ -59,25 +60,6 @@
dispatch("change", nestedComponentInstance) dispatch("change", nestedComponentInstance)
} }
const customPositionHandler = (anchorBounds, eleBounds, cfg) => {
let { left, top } = cfg
let percentageOffset = 30
// left-outside
left = anchorBounds.left - eleBounds.width - 18
// shift up from the anchor, if space allows
let offsetPos = Math.floor(eleBounds.height / 100) * percentageOffset
let defaultTop = anchorBounds.top - offsetPos
if (window.innerHeight - defaultTop < eleBounds.height) {
top = window.innerHeight - eleBounds.height - 5
} else {
top = anchorBounds.top - offsetPos
}
return { ...cfg, left, top }
}
</script> </script>
<Icon <Icon

View File

@ -0,0 +1,18 @@
export const customPositionHandler = (anchorBounds, eleBounds, cfg) => {
let { left, top } = cfg
let percentageOffset = 30
// left-outside
left = anchorBounds.left - eleBounds.width - 18
// shift up from the anchor, if space allows
let offsetPos = Math.floor(eleBounds.height / 100) * percentageOffset
let defaultTop = anchorBounds.top - offsetPos
if (window.innerHeight - defaultTop < eleBounds.height) {
top = window.innerHeight - eleBounds.height - 5
} else {
top = anchorBounds.top - offsetPos
}
return { ...cfg, left, top }
}

View File

@ -1,5 +1,5 @@
<script> <script>
import EditComponentPopover from "../EditComponentPopover.svelte" import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
import { Toggle, Icon } from "@budibase/bbui" import { Toggle, Icon } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"

View File

@ -52,8 +52,8 @@
_id: Helpers.uuid(), _id: Helpers.uuid(),
_component: componentType, _component: componentType,
_instanceName: `Step ${currentStep + 1}`, _instanceName: `Step ${currentStep + 1}`,
title: stepSettings.title ?? defaults.title, title: stepSettings.title ?? defaults?.title,
buttons: stepSettings.buttons || defaults.buttons, buttons: stepSettings.buttons || defaults?.buttons,
fields: stepSettings.fields, fields: stepSettings.fields,
desc: stepSettings.desc, desc: stepSettings.desc,

View File

@ -1,5 +1,5 @@
<script> <script>
import EditComponentPopover from "../EditComponentPopover.svelte" import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
import { FieldTypeToComponentMap } from "../FieldConfiguration/utils" import { FieldTypeToComponentMap } from "../FieldConfiguration/utils"
import { Toggle, Icon } from "@budibase/bbui" import { Toggle, Icon } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"

View File

@ -1,5 +1,5 @@
<script> <script>
import EditComponentPopover from "../EditComponentPopover.svelte" import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { setContext } from "svelte" import { setContext } from "svelte"
import { writable } from "svelte/store" import { writable } from "svelte/store"

View File

@ -20,7 +20,7 @@
export let bindings = [] export let bindings = []
export let componentBindings = [] export let componentBindings = []
export let nested = false export let nested = false
export let highlighted = false export let highlighted
export let propertyFocus = false export let propertyFocus = false
export let info = null export let info = null
export let disableBindings = false export let disableBindings = false
@ -75,12 +75,17 @@
store.actions.settings.highlight(null) store.actions.settings.highlight(null)
} }
}) })
let highlight
$: if (!Array.isArray(value)) {
highlight = highlighted?.type ? `highlighted-${highlighted?.type}` : ""
}
</script> </script>
<div <div
class="property-control" id={`${key}-prop-control-wrap`}
class={`property-control ${highlight}`}
class:wide={!label || labelHidden || wide === true} class:wide={!label || labelHidden || wide === true}
class:highlighted={highlighted && nullishValue} class:highlighted={highlighted && !Array.isArray(value)}
class:property-focus={propertyFocus} class:property-focus={propertyFocus}
> >
{#if label && !labelHidden} {#if label && !labelHidden}
@ -115,6 +120,16 @@
</div> </div>
<style> <style>
.property-control.highlighted.highlighted-info {
border-color: var(--spectrum-semantic-informative-color-background);
}
.property-control.highlighted.highlighted-error {
border-color: var(--spectrum-global-color-static-red-600);
}
.property-control.highlighted.highlighted-warning {
border-color: var(--spectrum-global-color-static-orange-700);
}
.property-control { .property-control {
position: relative; position: relative;
display: grid; display: grid;
@ -132,6 +147,10 @@
.property-control.highlighted { .property-control.highlighted {
background: var(--spectrum-global-color-gray-300); background: var(--spectrum-global-color-gray-300);
border-color: var(--spectrum-global-color-static-red-600); border-color: var(--spectrum-global-color-static-red-600);
margin-top: -3.5px;
margin-bottom: -3.5px;
padding-bottom: 3.5px;
padding-top: 3.5px;
} }
.property-control.property-focus :global(input) { .property-control.property-focus :global(input) {

View File

@ -100,6 +100,7 @@
maxWidth={300} maxWidth={300}
dismissible={false} dismissible={false}
offset={15} offset={15}
handlePostionUpdate={tourStep?.positionHandler}
> >
<div class="tour-content"> <div class="tour-content">
<Layout noPadding gap="M"> <Layout noPadding gap="M">
@ -120,7 +121,7 @@
</Body> </Body>
<div class="tour-footer"> <div class="tour-footer">
<div class="tour-navigation"> <div class="tour-navigation">
{#if typeof tourOnSkip === "function"} {#if typeof tourOnSkip === "function" && !lastStep}
<Link <Link
secondary secondary
quiet quiet

View File

@ -1,30 +1,35 @@
<script> <script>
import { tourHandler } from "./tourHandler" import { tourHandler } from "./tourHandler"
import { TOURS } from "./tours" import { TOURSBYSTEP } from "./tours"
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import { store } from "builderStore" import { store } from "builderStore"
export let tourStepKey export let stepKeys = []
let currentTourStep
let ready = false let ready = false
let registered = false
let handler let handler
let registered = []
const registerTourNode = (tourKey, stepKey) => { const registerTourNode = (tourKey, stepKey) => {
if (ready && !registered && tourKey) { const step = TOURSBYSTEP[stepKey]
currentTourStep = TOURS[tourKey].steps.find(step => step.id === stepKey) if (
if (!currentTourStep) { ready &&
return step &&
} !registered.includes(stepKey) &&
const elem = document.querySelector(currentTourStep.query) step?.tour === tourKey
) {
const elem = document.querySelector(step.query)
handler = tourHandler(elem, stepKey) handler = tourHandler(elem, stepKey)
registered = true registered.push(stepKey)
} }
} }
$: tourKeyWatch = $store.tourKey $: tourKeyWatch = $store.tourKey
$: registerTourNode(tourKeyWatch, tourStepKey, ready) $: if (tourKeyWatch || stepKeys || ready) {
stepKeys.forEach(tourStepKey => {
registerTourNode(tourKeyWatch, tourStepKey)
})
}
onMount(() => { onMount(() => {
ready = true ready = true

View File

@ -0,0 +1,17 @@
<div>
You can use bindings to set the Row ID on your form.
<p>
This will allow you to pull the correct information into your form and allow
you to update!
</p>
<a href="https://docs.budibase.com/docs/form-block" target="_blank">
How to pass a row ID using bindings
</a>
</div>
<style>
a {
color: inherit;
text-decoration: underline;
}
</style>

View File

@ -1,3 +1,4 @@
export { default as OnboardingData } from "./OnboardingData.svelte" export { default as OnboardingData } from "./OnboardingData.svelte"
export { default as OnboardingDesign } from "./OnboardingDesign.svelte" export { default as OnboardingDesign } from "./OnboardingDesign.svelte"
export { default as OnboardingPublish } from "./OnboardingPublish.svelte" export { default as OnboardingPublish } from "./OnboardingPublish.svelte"
export { default as NewViewUpdateFormRowId } from "./NewViewUpdateFormRowId.svelte"

View File

@ -2,8 +2,14 @@ import { get } from "svelte/store"
import { store } from "builderStore" import { store } from "builderStore"
import { auth } from "stores/portal" import { auth } from "stores/portal"
import analytics from "analytics" import analytics from "analytics"
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps" import {
OnboardingData,
OnboardingDesign,
OnboardingPublish,
NewViewUpdateFormRowId,
} from "./steps"
import { API } from "api" import { API } from "api"
import { customPositionHandler } from "components/design/settings/controls/EditComponentPopover"
const ONBOARDING_EVENT_PREFIX = "onboarding" const ONBOARDING_EVENT_PREFIX = "onboarding"
@ -14,11 +20,26 @@ export const TOUR_STEP_KEYS = {
BUILDER_USER_MANAGEMENT: "builder-user-management", BUILDER_USER_MANAGEMENT: "builder-user-management",
BUILDER_AUTOMATION_SECTION: "builder-automation-section", BUILDER_AUTOMATION_SECTION: "builder-automation-section",
FEATURE_USER_MANAGEMENT: "feature-user-management", FEATURE_USER_MANAGEMENT: "feature-user-management",
BUILDER_FORM_CREATE_STEPS: "builder-form-create-steps",
BUILDER_FORM_VIEW_UPDATE_STEPS: "builder-form-view-update-steps",
BUILDER_FORM_ROW_ID: "builder-form-row-id",
} }
export const TOUR_KEYS = { export const TOUR_KEYS = {
TOUR_BUILDER_ONBOARDING: "builder-onboarding", TOUR_BUILDER_ONBOARDING: "builder-onboarding",
FEATURE_ONBOARDING: "feature-onboarding", FEATURE_ONBOARDING: "feature-onboarding",
BUILDER_FORM_CREATE: "builder-form-create",
BUILDER_FORM_VIEW_UPDATE: "builder-form-view-update",
}
const resetTourState = () => {
store.update(state => ({
...state,
tourNodes: undefined,
tourKey: undefined,
tourKeyStep: undefined,
onboarding: false,
}))
} }
const endUserOnboarding = async ({ skipped = false } = {}) => { const endUserOnboarding = async ({ skipped = false } = {}) => {
@ -37,13 +58,7 @@ const endUserOnboarding = async ({ skipped = false } = {}) => {
// Update the cached user // Update the cached user
await auth.getSelf() await auth.getSelf()
store.update(state => ({ resetTourState()
...state,
tourNodes: undefined,
tourKey: undefined,
tourKeyStep: undefined,
onboarding: false,
}))
} catch (e) { } catch (e) {
console.error("Onboarding failed", e) console.error("Onboarding failed", e)
return false return false
@ -52,9 +67,28 @@ const endUserOnboarding = async ({ skipped = false } = {}) => {
} }
} }
const tourEvent = eventKey => { const endTour = async ({ key, skipped = false } = {}) => {
const { tours = {} } = get(auth).user
tours[key] = new Date().toISOString()
await API.updateSelf({
tours,
})
if (skipped) {
tourEvent(key, skipped)
}
// Update the cached user
await auth.getSelf()
resetTourState()
}
const tourEvent = (eventKey, skipped) => {
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, { analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
eventSource: EventSource.PORTAL, eventSource: EventSource.PORTAL,
skipped,
}) })
} }
@ -135,7 +169,74 @@ const getTours = () => {
}, },
], ],
}, },
[TOUR_KEYS.BUILDER_FORM_CREATE]: {
steps: [
{
id: TOUR_STEP_KEYS.BUILDER_FORM_CREATE_STEPS,
title: "Add multiple steps",
body: `When faced with a sizable form, consider implementing a multi-step
approach to enhance user experience. Breaking the form into multiple steps
can significantly improve usability by making the process more digestible for your users.`,
query: "#steps-prop-control-wrap",
onComplete: () => {
store.actions.settings.highlight()
endTour({ key: TOUR_KEYS.BUILDER_FORM_CREATE })
},
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_CREATE_STEPS)
store.actions.settings.highlight("steps", "info")
},
positionHandler: customPositionHandler,
align: "left-outside",
},
],
},
[TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE]: {
steps: [
{
id: TOUR_STEP_KEYS.BUILDER_FORM_ROW_ID,
title: "Add row ID to update a row",
layout: NewViewUpdateFormRowId,
query: "#rowId-prop-control-wrap",
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_ROW_ID)
store.actions.settings.highlight("rowId", "info")
},
positionHandler: customPositionHandler,
align: "left-outside",
},
{
id: TOUR_STEP_KEYS.BUILDER_FORM_VIEW_UPDATE_STEPS,
title: "Add multiple steps",
body: `When faced with a sizable form, consider implementing a multi-step
approach to enhance user experience. Breaking the form into multiple steps
can significantly improve usability by making the process more digestible for your users.`,
query: "#steps-prop-control-wrap",
onComplete: () => {
store.actions.settings.highlight()
endTour({ key: TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE })
},
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_VIEW_UPDATE_STEPS)
store.actions.settings.highlight("steps", "info")
},
positionHandler: customPositionHandler,
align: "left-outside",
},
],
onSkip: async () => {
store.actions.settings.highlight()
endTour({ key: TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE, skipped: true })
},
},
} }
} }
export const TOURS = getTours() export const TOURS = getTours()
export const TOURSBYSTEP = Object.keys(TOURS).reduce((acc, tour) => {
TOURS[tour].steps.forEach(element => {
acc[element.id] = element
acc[element.id]["tour"] = tour
})
return acc
}, {})

View File

@ -151,7 +151,7 @@
</span> </span>
<Tabs {selected} size="M"> <Tabs {selected} size="M">
{#each $layout.children as { path, title }} {#each $layout.children as { path, title }}
<TourWrap tourStepKey={`builder-${title}-section`}> <TourWrap stepKeys={[`builder-${title}-section`]}>
<Tab <Tab
quiet quiet
selected={$isActive(path)} selected={$isActive(path)}

View File

@ -1,12 +1,18 @@
<script> <script>
import Panel from "components/design/Panel.svelte" import Panel from "components/design/Panel.svelte"
import { store, selectedComponent, selectedScreen } from "builderStore" import { store, selectedComponent, selectedScreen } from "builderStore"
import { auth } from "stores/portal"
import { getComponentName } from "builderStore/componentUtils" import { getComponentName } from "builderStore/componentUtils"
import ComponentSettingsSection from "./ComponentSettingsSection.svelte" import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
import DesignSection from "./DesignSection.svelte" import DesignSection from "./DesignSection.svelte"
import CustomStylesSection from "./CustomStylesSection.svelte" import CustomStylesSection from "./CustomStylesSection.svelte"
import ConditionalUISection from "./ConditionalUISection.svelte" import ConditionalUISection from "./ConditionalUISection.svelte"
import { notifications, ActionButton } from "@budibase/bbui" import { notifications, ActionButton } from "@budibase/bbui"
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
import {
TOUR_STEP_KEYS,
TOUR_KEYS,
} from "components/portal/onboarding/tours.js"
import { import {
getBindableProperties, getBindableProperties,
@ -14,6 +20,12 @@
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { capitalise } from "helpers" import { capitalise } from "helpers"
const {
BUILDER_FORM_CREATE_STEPS,
BUILDER_FORM_VIEW_UPDATE_STEPS,
BUILDER_FORM_ROW_ID,
} = TOUR_STEP_KEYS
const onUpdateName = async value => { const onUpdateName = async value => {
try { try {
await store.actions.components.updateSetting("_instanceName", value) await store.actions.components.updateSetting("_instanceName", value)
@ -43,7 +55,6 @@
$: id = $selectedComponent?._id $: id = $selectedComponent?._id
$: id, (section = tabs[0]) $: id, (section = tabs[0])
$: componentName = getComponentName(componentInstance) $: componentName = getComponentName(componentInstance)
</script> </script>
@ -89,13 +100,21 @@
</div> </div>
</span> </span>
{#if section == "settings"} {#if section == "settings"}
<ComponentSettingsSection <TourWrap
{componentInstance} stepKeys={[
{componentDefinition} BUILDER_FORM_CREATE_STEPS,
{bindings} BUILDER_FORM_VIEW_UPDATE_STEPS,
{componentBindings} BUILDER_FORM_ROW_ID,
{isScreen} ]}
/> >
<ComponentSettingsSection
{componentInstance}
{componentDefinition}
{bindings}
{componentBindings}
{isScreen}
/>
</TourWrap>
{/if} {/if}
{#if section == "styles"} {#if section == "styles"}
<DesignSection <DesignSection

View File

@ -177,7 +177,9 @@
defaultValue={setting.defaultValue} defaultValue={setting.defaultValue}
nested={setting.nested} nested={setting.nested}
onChange={val => updateSetting(setting, val)} onChange={val => updateSetting(setting, val)}
highlighted={$store.highlightedSettingKey === setting.key} highlighted={$store.highlightedSetting?.key === setting.key
? $store.highlightedSetting
: null}
propertyFocus={$store.propertyFocus === setting.key} propertyFocus={$store.propertyFocus === setting.key}
info={setting.info} info={setting.info}
disableBindings={setting.disableBindings} disableBindings={setting.disableBindings}

View File

@ -161,7 +161,7 @@
} else if (type === "request-add-component") { } else if (type === "request-add-component") {
toggleAddComponent() toggleAddComponent()
} else if (type === "highlight-setting") { } else if (type === "highlight-setting") {
store.actions.settings.highlight(data.setting) store.actions.settings.highlight(data.setting, "error")
// Also scroll setting into view // Also scroll setting into view
const selector = `#${data.setting}-prop-control` const selector = `#${data.setting}-prop-control`

View File

@ -4,14 +4,18 @@
import ScreenRoleModal from "./ScreenRoleModal.svelte" import ScreenRoleModal from "./ScreenRoleModal.svelte"
import FormTypeModal from "./FormTypeModal.svelte" import FormTypeModal from "./FormTypeModal.svelte"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import rowListScreen from "builderStore/store/screenTemplates/rowListScreen"
import formScreen from "builderStore/store/screenTemplates/formScreen"
import { Modal, notifications } from "@budibase/bbui" import { Modal, notifications } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import { get } from "svelte/store" import { get } from "svelte/store"
import getTemplates from "builderStore/store/screenTemplates" import getTemplates from "builderStore/store/screenTemplates"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { auth } from "stores/portal"
import { Roles } from "constants/backend" import { Roles } from "constants/backend"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
let mode let mode
let pendingScreen let pendingScreen
@ -25,7 +29,8 @@
// Cache variables for workflow // Cache variables for workflow
let screenAccessRole = Roles.BASIC let screenAccessRole = Roles.BASIC
let selectedTemplates = null let templates = null
let screens = null
let selectedDatasources = null let selectedDatasources = null
let blankScreenUrl = null let blankScreenUrl = null
@ -40,6 +45,7 @@
try { try {
let screenId let screenId
let createdScreens = []
for (let screen of screens) { for (let screen of screens) {
// Check we aren't clashing with an existing URL // Check we aren't clashing with an existing URL
@ -62,21 +68,19 @@
screen.routing.roleId = screenAccessRole screen.routing.roleId = screenAccessRole
// Create the screen // Create the screen
// const response = await store.actions.screens.save(screen) const response = await store.actions.screens.save(screen)
// screenId = response._id screenId = response._id
createdScreens.push(response)
// Add link in layout. We only ever actually create 1 screen now, even // Add link in layout. We only ever actually create 1 screen now, even
// for autoscreens, so it's always safe to do this. // for autoscreens, so it's always safe to do this.
// await store.actions.links.save( await store.actions.links.save(
// screen.routing.route, screen.routing.route,
// capitalise(screen.routing.route.split("/")[1]) capitalise(screen.routing.route.split("/")[1])
// ) )
console.log(screen)
} }
// Go to new screen return createdScreens
//$goto(`./${screenId}`)
//store.actions.screens.select(screenId)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
notifications.error("Error creating screens") notifications.error("Error creating screens")
@ -110,7 +114,8 @@
// Handler for NewScreenModal // Handler for NewScreenModal
export const show = newMode => { export const show = newMode => {
mode = newMode mode = newMode
// selectedTemplates = null templates = null
screens = null
selectedDatasources = null selectedDatasources = null
blankScreenUrl = null blankScreenUrl = null
screenMode = mode screenMode = mode
@ -135,26 +140,24 @@
// Handler for DatasourceModal confirmation, move to screen access select // Handler for DatasourceModal confirmation, move to screen access select
const confirmScreenDatasources = async ({ datasources }) => { const confirmScreenDatasources = async ({ datasources }) => {
selectedDatasources = datasources selectedDatasources = datasources
console.log("confirmScreenDatasources ", datasources) if (screenMode === "form") {
screenAccessRoleModal.show() formTypeModal.show()
} else {
screenAccessRoleModal.show()
}
} }
// Handler for Datasource Screen Creation // Handler for Datasource Screen Creation
const completeDatasourceScreenCreation = async () => { const completeDatasourceScreenCreation = async () => {
const screens = selectedTemplates.map(template => { templates = rowListScreen(selectedDatasources)
const screens = templates.map(template => {
let screenTemplate = template.create() let screenTemplate = template.create()
screenTemplate.autoTableId = template.resourceId screenTemplate.autoTableId = template.resourceId
return screenTemplate return screenTemplate
}) })
console.log("selectedTemplates ", selectedTemplates) const createdScreens = await createScreens({ screens, screenAccessRole })
/* loadNewScreen(createdScreens)
id : "ROW_LIST_TEMPLATE"
name : "Employees - List"
resourceId : "ta_bb_employee"
*/
await createScreens({ screens, screenAccessRole })
} }
const confirmScreenBlank = async ({ screenUrl }) => { const confirmScreenBlank = async ({ screenUrl }) => {
@ -171,7 +174,55 @@
return return
} }
pendingScreen.routing.route = screenUrl pendingScreen.routing.route = screenUrl
await createScreens({ screens: [pendingScreen], screenAccessRole }) const createdScreens = await createScreens({
screens: [pendingScreen],
screenAccessRole,
})
loadNewScreen(createdScreens)
}
const onConfirmFormType = () => {
screenAccessRoleModal.show()
}
const loadNewScreen = createdScreens => {
const lastScreen = createdScreens.slice(-1)
// Go to new screen
$goto(`./${lastScreen._id}`)
store.actions.screens.select(lastScreen._id)
}
const confirmFormScreenCreation = async () => {
templates = formScreen(selectedDatasources, { actionType: formType })
screens = templates.map(template => {
let screenTemplate = template.create()
return screenTemplate
})
const createdScreens = await createScreens({ screens, screenAccessRole })
const lastScreen = createdScreens?.slice(-1)?.pop()
const mainComponent = lastScreen?.props?._children?.[0]._id
if (formType === "Update" || formType === "Create") {
const associatedTour =
formType === "Update"
? TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE
: TOUR_KEYS.BUILDER_FORM_CREATE
const tourRequired = !$auth?.user?.tours?.[associatedTour]
if (tourRequired) {
store.update(state => ({
...state,
tourStepKey: null,
tourNodes: null,
tourKey: associatedTour,
}))
}
}
// Go to new screen
$goto(`./${lastScreen._id}/${mainComponent}`)
store.actions.screens.select(lastScreen._id)
} }
// Submit screen config for creation. // Submit screen config for creation.
@ -181,6 +232,8 @@
screenUrl: blankScreenUrl, screenUrl: blankScreenUrl,
screenAccessRole, screenAccessRole,
}) })
} else if (screenMode === "form") {
confirmFormScreenCreation()
} else { } else {
completeDatasourceScreenCreation() completeDatasourceScreenCreation()
} }
@ -193,30 +246,16 @@
datasourceModal.show() datasourceModal.show()
} }
} }
window.test = () => {
formTypeModal.show()
}
</script> </script>
<!--
returns templates, should return selected resources for use elsewhere
-->
<Modal bind:this={datasourceModal} autoFocus={false}> <Modal bind:this={datasourceModal} autoFocus={false}>
<DatasourceModal <DatasourceModal {mode} onConfirm={confirmScreenDatasources} />
{mode}
onConfirm={confirmScreenDatasources}
initialScreens={!selectedTemplates ? [] : [...selectedTemplates]}
/>
</Modal> </Modal>
<Modal bind:this={screenAccessRoleModal}> <Modal bind:this={screenAccessRoleModal}>
<ScreenRoleModal <ScreenRoleModal
onConfirm={() => { onConfirm={() => {
if (screenMode === "form") { confirmScreenCreation()
formTypeModal.show()
} else {
confirmScreenCreation()
}
}} }}
bind:screenAccessRole bind:screenAccessRole
onCancel={roleSelectBack} onCancel={roleSelectBack}
@ -232,24 +271,14 @@
/> />
</Modal> </Modal>
<Modal <Modal bind:this={formTypeModal}>
bind:this={formTypeModal}
on:hide={() => {
console.log("hide")
//formType = null
}}
>
<FormTypeModal <FormTypeModal
onConfirm={() => { onConfirm={onConfirmFormType}
console.log("test confirm")
}}
onCancel={() => { onCancel={() => {
console.log("cancel")
formTypeModal.hide() formTypeModal.hide()
screenAccessRoleModal.show() datasourceModal.show()
}} }}
on:select={e => { on:select={e => {
console.log("form type selection ", e.detail)
formType = e.detail formType = e.detail
}} }}
type={formType} type={formType}

View File

@ -4,37 +4,33 @@
import ICONS from "components/backend/DatasourceNavigator/icons" import ICONS from "components/backend/DatasourceNavigator/icons"
import { IntegrationNames } from "constants" import { IntegrationNames } from "constants"
import { onMount } from "svelte" import { onMount } from "svelte"
import rowListScreen from "builderStore/store/screenTemplates/rowListScreen"
import DatasourceTemplateRow from "./DatasourceTemplateRow.svelte" import DatasourceTemplateRow from "./DatasourceTemplateRow.svelte"
export let mode
export let onCancel export let onCancel
export let onConfirm export let onConfirm
export let initialScreens = []
let selectedScreens = [...initialScreens] let selectedSources = []
$: filteredSources = $datasources.list?.filter(datasource => { $: filteredSources = $datasources.list?.filter(datasource => {
return datasource.source !== IntegrationNames.REST && datasource["entities"] return datasource.source !== IntegrationNames.REST && datasource["entities"]
}) })
const toggleSelection = datasource => { const toggleSelection = datasource => {
const { resourceId } = datasource const exists = selectedSources.find(
if (selectedScreens.find(s => s.resourceId === resourceId)) { d => d.resourceId === datasource.resourceId
selectedScreens = selectedScreens.filter( )
screen => screen.resourceId !== resourceId if (exists) {
selectedSources = selectedSources.filter(
d => d.resourceId === datasource.resourceId
) )
} else { } else {
selectedScreens = [ selectedSources = [...selectedSources, datasource]
...selectedScreens,
rowListScreen([datasource], mode)[0],
]
} }
} }
const confirmDatasourceSelection = async () => { const confirmDatasourceSelection = async () => {
await onConfirm({ await onConfirm({
templates: selectedScreens, datasources: selectedSources,
}) })
} }
@ -54,7 +50,7 @@
cancelText="Back" cancelText="Back"
onConfirm={confirmDatasourceSelection} onConfirm={confirmDatasourceSelection}
{onCancel} {onCancel}
disabled={!selectedScreens.length} disabled={!selectedSources.length}
size="L" size="L"
> >
<Body size="S"> <Body size="S">
@ -85,8 +81,8 @@
resourceId: table._id, resourceId: table._id,
type: "table", type: "table",
}} }}
{@const selected = selectedScreens.find( {@const selected = selectedSources.find(
screen => screen.resourceId === tableDS.resourceId datasource => datasource.resourceId === tableDS.resourceId
)} )}
<DatasourceTemplateRow <DatasourceTemplateRow
on:click={() => toggleSelection(tableDS)} on:click={() => toggleSelection(tableDS)}
@ -103,7 +99,7 @@
tableId: view.tableId, tableId: view.tableId,
type: "viewV2", type: "viewV2",
}} }}
{@const selected = selectedScreens.find( {@const selected = selectedSources.find(
x => x.resourceId === viewDS.resourceId x => x.resourceId === viewDS.resourceId
)} )}
<DatasourceTemplateRow <DatasourceTemplateRow

View File

@ -0,0 +1,78 @@
<script>
import { ModalContent, Layout, Body, Label } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
export let onCancel = () => {}
export let onConfirm = () => {}
export let type
const dispatch = createEventDispatcher()
</script>
<span>
<ModalContent
title="Select form type"
confirmText="Done"
cancelText="Back"
{onConfirm}
{onCancel}
disabled={!type}
size="L"
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<Layout noPadding gap="S">
<div
class="form-type"
class:selected={type === "Create"}
on:click={() => {
dispatch("select", "Create")
}}
>
<Body noPadding>Create a new row</Body>
<Body size="S">For capturing and storing new data from your users</Body>
</div>
<div
class="form-type"
class:selected={type === "Update"}
on:click={() => {
dispatch("select", "Update")
}}
>
<Body noPadding>Update an existing row</Body>
<Body size="S">For viewing and updating existing data</Body>
</div>
<div
class="form-type"
class:selected={type === "View"}
on:click={() => {
dispatch("select", "View")
}}
>
<Body noPadding>View an existing row</Body>
<Body size="S">For a read only view of your data</Body>
</div>
</Layout>
</ModalContent>
</span>
<style>
.form-type {
cursor: pointer;
gap: var(--spacing-s);
padding: var(--spacing-m) var(--spacing-xl);
/* padding: 10px 16px technically correct*/
background: var(--spectrum-alias-background-color-secondary);
transition: 0.3s all;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
display: flex;
flex-direction: column;
}
.selected,
.form-type:hover {
background: var(--spectrum-alias-background-color-tertiary);
}
.form-type :global(p:nth-child(2)) {
color: var(--grey-6);
}
</style>

View File

@ -10,6 +10,7 @@
export let onCancel export let onCancel
export let screenUrl export let screenUrl
export let screenAccessRole export let screenAccessRole
export let confirmText = "Done"
let error let error
@ -41,7 +42,7 @@
<ModalContent <ModalContent
title="Access" title="Access"
confirmText="Done" {confirmText}
cancelText="Back" cancelText="Back"
{onConfirm} {onConfirm}
{onCancel} {onCancel}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,9 +1,10 @@
<script> <script>
import { Body } from "@budibase/bbui" import { Body } from "@budibase/bbui"
import CreationPage from "components/common/CreationPage.svelte" import CreationPage from "components/common/CreationPage.svelte"
import blankImage from "./blank.png" import blankImage from "./images/blank.png"
import tableImage from "./table.png" import tableImage from "./images/table.png"
import gridImage from "./grid.png" import gridImage from "./images/grid.png"
import formImage from "./images/form.png" //optimized example
import CreateScreenModal from "./CreateScreenModal.svelte" import CreateScreenModal from "./CreateScreenModal.svelte"
import { store } from "builderStore" import { store } from "builderStore"
@ -54,6 +55,16 @@
<Body size="XS">View and manipulate rows on a grid</Body> <Body size="XS">View and manipulate rows on a grid</Body>
</div> </div>
</div> </div>
<div class="card" on:click={() => createScreenModal.show("form")}>
<div class="image">
<img alt="" src={formImage} />
</div>
<div class="text">
<Body size="S">Form</Body>
<Body size="XS">Capture data from your users</Body>
</div>
</div>
</div> </div>
</CreationPage> </CreationPage>
</div> </div>

View File

@ -18,6 +18,7 @@ export interface UpdateSelfRequest {
password?: string password?: string
forceResetPassword?: boolean forceResetPassword?: boolean
onboardedAt?: string onboardedAt?: string
tours?: Record<string, Date>
} }
export interface UpdateSelfResponse { export interface UpdateSelfResponse {

View File

@ -55,6 +55,7 @@ export interface User extends Document {
dayPassRecordedAt?: string dayPassRecordedAt?: string
userGroups?: string[] userGroups?: string[]
onboardedAt?: string onboardedAt?: string
tours?: Record<string, Date>
scimInfo?: { isSync: true } & Record<string, any> scimInfo?: { isSync: true } & Record<string, any>
ssoId?: string ssoId?: string
} }

View File

@ -26,6 +26,7 @@ export const buildSelfSaveValidation = () => {
firstName: OPTIONAL_STRING, firstName: OPTIONAL_STRING,
lastName: OPTIONAL_STRING, lastName: OPTIONAL_STRING,
onboardedAt: Joi.string().optional(), onboardedAt: Joi.string().optional(),
tours: Joi.object().optional(),
} }
return auth.joiValidator.body(Joi.object(schema).required().unknown(false)) return auth.joiValidator.body(Joi.object(schema).required().unknown(false))
} }