Initial commit form screen flow and tour refactor
|
@ -1131,7 +1131,7 @@ export const getAllStateVariables = () => {
|
|||
"@budibase/standard-components/multistepformblockstep"
|
||||
)
|
||||
|
||||
steps.forEach(step => {
|
||||
steps?.forEach(step => {
|
||||
parseComponentSettings(stepDefinition, step)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -75,7 +75,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
theme: "",
|
||||
customTheme: {},
|
||||
previewDevice: "desktop",
|
||||
highlightedSettingKey: null,
|
||||
highlightedSetting: null,
|
||||
propertyFocus: null,
|
||||
builderSidePanel: false,
|
||||
hasLock: true,
|
||||
|
@ -1460,10 +1460,10 @@ export const getFrontendStore = () => {
|
|||
},
|
||||
},
|
||||
settings: {
|
||||
highlight: key => {
|
||||
highlight: (key, type) => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
highlightedSettingKey: key,
|
||||
highlightedSetting: { key, type: type || "info" },
|
||||
}))
|
||||
},
|
||||
propertyFocus: key => {
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
import rowListScreen from "./rowListScreen"
|
||||
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
|
||||
const createTemplateOverride = template => () => {
|
||||
|
@ -19,6 +23,7 @@ export default datasources => {
|
|||
})
|
||||
const fromScratch = enrichTemplate(createFromScratchScreen)
|
||||
const tableTemplates = allTemplates(datasources).map(enrichTemplate)
|
||||
|
||||
return [
|
||||
fromScratch,
|
||||
...tableTemplates.sort((templateA, templateB) => {
|
||||
|
|
|
@ -156,9 +156,10 @@
|
|||
</div>
|
||||
{/if}
|
||||
<TourWrap
|
||||
tourStepKey={$store.onboarding
|
||||
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
||||
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
||||
stepKeys={[
|
||||
TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
|
||||
TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
|
||||
]}
|
||||
>
|
||||
<div class="app-action-button users">
|
||||
<div class="app-action" id="builder-app-users-button">
|
||||
|
@ -204,7 +205,7 @@
|
|||
<div bind:this={appActionPopoverAnchor}>
|
||||
<div class="app-action">
|
||||
<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">
|
||||
Publish
|
||||
<Icon
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { runtimeToReadableBinding } from "builderStore/dataBinding"
|
||||
import { isJSBinding } from "@budibase/string-templates"
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
import { store } from "builderStore"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
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 componentInstance
|
||||
|
@ -59,25 +60,6 @@
|
|||
|
||||
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>
|
||||
|
||||
<Icon
|
|
@ -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 }
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
||||
import { Toggle, Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
|
|
@ -52,8 +52,8 @@
|
|||
_id: Helpers.uuid(),
|
||||
_component: componentType,
|
||||
_instanceName: `Step ${currentStep + 1}`,
|
||||
title: stepSettings.title ?? defaults.title,
|
||||
buttons: stepSettings.buttons || defaults.buttons,
|
||||
title: stepSettings.title ?? defaults?.title,
|
||||
buttons: stepSettings.buttons || defaults?.buttons,
|
||||
fields: stepSettings.fields,
|
||||
desc: stepSettings.desc,
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
||||
import { FieldTypeToComponentMap } from "../FieldConfiguration/utils"
|
||||
import { Toggle, Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { setContext } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
export let bindings = []
|
||||
export let componentBindings = []
|
||||
export let nested = false
|
||||
export let highlighted = false
|
||||
export let highlighted
|
||||
export let propertyFocus = false
|
||||
export let info = null
|
||||
export let disableBindings = false
|
||||
|
@ -75,12 +75,17 @@
|
|||
store.actions.settings.highlight(null)
|
||||
}
|
||||
})
|
||||
let highlight
|
||||
$: if (!Array.isArray(value)) {
|
||||
highlight = highlighted?.type ? `highlighted-${highlighted?.type}` : ""
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="property-control"
|
||||
id={`${key}-prop-control-wrap`}
|
||||
class={`property-control ${highlight}`}
|
||||
class:wide={!label || labelHidden || wide === true}
|
||||
class:highlighted={highlighted && nullishValue}
|
||||
class:highlighted={highlighted && !Array.isArray(value)}
|
||||
class:property-focus={propertyFocus}
|
||||
>
|
||||
{#if label && !labelHidden}
|
||||
|
@ -115,6 +120,16 @@
|
|||
</div>
|
||||
|
||||
<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 {
|
||||
position: relative;
|
||||
display: grid;
|
||||
|
@ -132,6 +147,10 @@
|
|||
.property-control.highlighted {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
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) {
|
||||
|
|
|
@ -100,6 +100,7 @@
|
|||
maxWidth={300}
|
||||
dismissible={false}
|
||||
offset={15}
|
||||
handlePostionUpdate={tourStep?.positionHandler}
|
||||
>
|
||||
<div class="tour-content">
|
||||
<Layout noPadding gap="M">
|
||||
|
@ -120,7 +121,7 @@
|
|||
</Body>
|
||||
<div class="tour-footer">
|
||||
<div class="tour-navigation">
|
||||
{#if typeof tourOnSkip === "function"}
|
||||
{#if typeof tourOnSkip === "function" && !lastStep}
|
||||
<Link
|
||||
secondary
|
||||
quiet
|
||||
|
|
|
@ -1,30 +1,35 @@
|
|||
<script>
|
||||
import { tourHandler } from "./tourHandler"
|
||||
import { TOURS } from "./tours"
|
||||
import { TOURSBYSTEP } from "./tours"
|
||||
import { onMount, onDestroy } from "svelte"
|
||||
import { store } from "builderStore"
|
||||
|
||||
export let tourStepKey
|
||||
export let stepKeys = []
|
||||
|
||||
let currentTourStep
|
||||
let ready = false
|
||||
let registered = false
|
||||
let handler
|
||||
let registered = []
|
||||
|
||||
const registerTourNode = (tourKey, stepKey) => {
|
||||
if (ready && !registered && tourKey) {
|
||||
currentTourStep = TOURS[tourKey].steps.find(step => step.id === stepKey)
|
||||
if (!currentTourStep) {
|
||||
return
|
||||
}
|
||||
const elem = document.querySelector(currentTourStep.query)
|
||||
const step = TOURSBYSTEP[stepKey]
|
||||
if (
|
||||
ready &&
|
||||
step &&
|
||||
!registered.includes(stepKey) &&
|
||||
step?.tour === tourKey
|
||||
) {
|
||||
const elem = document.querySelector(step.query)
|
||||
handler = tourHandler(elem, stepKey)
|
||||
registered = true
|
||||
registered.push(stepKey)
|
||||
}
|
||||
}
|
||||
|
||||
$: tourKeyWatch = $store.tourKey
|
||||
$: registerTourNode(tourKeyWatch, tourStepKey, ready)
|
||||
$: if (tourKeyWatch || stepKeys || ready) {
|
||||
stepKeys.forEach(tourStepKey => {
|
||||
registerTourNode(tourKeyWatch, tourStepKey)
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
ready = true
|
||||
|
|
|
@ -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>
|
|
@ -1,3 +1,4 @@
|
|||
export { default as OnboardingData } from "./OnboardingData.svelte"
|
||||
export { default as OnboardingDesign } from "./OnboardingDesign.svelte"
|
||||
export { default as OnboardingPublish } from "./OnboardingPublish.svelte"
|
||||
export { default as NewViewUpdateFormRowId } from "./NewViewUpdateFormRowId.svelte"
|
||||
|
|
|
@ -2,8 +2,14 @@ import { get } from "svelte/store"
|
|||
import { store } from "builderStore"
|
||||
import { auth } from "stores/portal"
|
||||
import analytics from "analytics"
|
||||
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps"
|
||||
import {
|
||||
OnboardingData,
|
||||
OnboardingDesign,
|
||||
OnboardingPublish,
|
||||
NewViewUpdateFormRowId,
|
||||
} from "./steps"
|
||||
import { API } from "api"
|
||||
import { customPositionHandler } from "components/design/settings/controls/EditComponentPopover"
|
||||
|
||||
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
||||
|
||||
|
@ -14,11 +20,26 @@ export const TOUR_STEP_KEYS = {
|
|||
BUILDER_USER_MANAGEMENT: "builder-user-management",
|
||||
BUILDER_AUTOMATION_SECTION: "builder-automation-section",
|
||||
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 = {
|
||||
TOUR_BUILDER_ONBOARDING: "builder-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 } = {}) => {
|
||||
|
@ -37,13 +58,7 @@ const endUserOnboarding = async ({ skipped = false } = {}) => {
|
|||
// Update the cached user
|
||||
await auth.getSelf()
|
||||
|
||||
store.update(state => ({
|
||||
...state,
|
||||
tourNodes: undefined,
|
||||
tourKey: undefined,
|
||||
tourKeyStep: undefined,
|
||||
onboarding: false,
|
||||
}))
|
||||
resetTourState()
|
||||
} catch (e) {
|
||||
console.error("Onboarding failed", e)
|
||||
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}`, {
|
||||
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 TOURSBYSTEP = Object.keys(TOURS).reduce((acc, tour) => {
|
||||
TOURS[tour].steps.forEach(element => {
|
||||
acc[element.id] = element
|
||||
acc[element.id]["tour"] = tour
|
||||
})
|
||||
return acc
|
||||
}, {})
|
||||
|
|
|
@ -151,7 +151,7 @@
|
|||
</span>
|
||||
<Tabs {selected} size="M">
|
||||
{#each $layout.children as { path, title }}
|
||||
<TourWrap tourStepKey={`builder-${title}-section`}>
|
||||
<TourWrap stepKeys={[`builder-${title}-section`]}>
|
||||
<Tab
|
||||
quiet
|
||||
selected={$isActive(path)}
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { store, selectedComponent, selectedScreen } from "builderStore"
|
||||
import { auth } from "stores/portal"
|
||||
import { getComponentName } from "builderStore/componentUtils"
|
||||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
||||
import DesignSection from "./DesignSection.svelte"
|
||||
import CustomStylesSection from "./CustomStylesSection.svelte"
|
||||
import ConditionalUISection from "./ConditionalUISection.svelte"
|
||||
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 {
|
||||
getBindableProperties,
|
||||
|
@ -14,6 +20,12 @@
|
|||
} from "builderStore/dataBinding"
|
||||
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 => {
|
||||
try {
|
||||
await store.actions.components.updateSetting("_instanceName", value)
|
||||
|
@ -43,7 +55,6 @@
|
|||
|
||||
$: id = $selectedComponent?._id
|
||||
$: id, (section = tabs[0])
|
||||
|
||||
$: componentName = getComponentName(componentInstance)
|
||||
</script>
|
||||
|
||||
|
@ -89,13 +100,21 @@
|
|||
</div>
|
||||
</span>
|
||||
{#if section == "settings"}
|
||||
<ComponentSettingsSection
|
||||
{componentInstance}
|
||||
{componentDefinition}
|
||||
{bindings}
|
||||
{componentBindings}
|
||||
{isScreen}
|
||||
/>
|
||||
<TourWrap
|
||||
stepKeys={[
|
||||
BUILDER_FORM_CREATE_STEPS,
|
||||
BUILDER_FORM_VIEW_UPDATE_STEPS,
|
||||
BUILDER_FORM_ROW_ID,
|
||||
]}
|
||||
>
|
||||
<ComponentSettingsSection
|
||||
{componentInstance}
|
||||
{componentDefinition}
|
||||
{bindings}
|
||||
{componentBindings}
|
||||
{isScreen}
|
||||
/>
|
||||
</TourWrap>
|
||||
{/if}
|
||||
{#if section == "styles"}
|
||||
<DesignSection
|
||||
|
|
|
@ -177,7 +177,9 @@
|
|||
defaultValue={setting.defaultValue}
|
||||
nested={setting.nested}
|
||||
onChange={val => updateSetting(setting, val)}
|
||||
highlighted={$store.highlightedSettingKey === setting.key}
|
||||
highlighted={$store.highlightedSetting?.key === setting.key
|
||||
? $store.highlightedSetting
|
||||
: null}
|
||||
propertyFocus={$store.propertyFocus === setting.key}
|
||||
info={setting.info}
|
||||
disableBindings={setting.disableBindings}
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
} else if (type === "request-add-component") {
|
||||
toggleAddComponent()
|
||||
} else if (type === "highlight-setting") {
|
||||
store.actions.settings.highlight(data.setting)
|
||||
store.actions.settings.highlight(data.setting, "error")
|
||||
|
||||
// Also scroll setting into view
|
||||
const selector = `#${data.setting}-prop-control`
|
||||
|
|
|
@ -4,14 +4,18 @@
|
|||
import ScreenRoleModal from "./ScreenRoleModal.svelte"
|
||||
import FormTypeModal from "./FormTypeModal.svelte"
|
||||
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 { store } from "builderStore"
|
||||
import { get } from "svelte/store"
|
||||
import getTemplates from "builderStore/store/screenTemplates"
|
||||
import { tables } from "stores/backend"
|
||||
import { auth } from "stores/portal"
|
||||
import { Roles } from "constants/backend"
|
||||
import { capitalise } from "helpers"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||
|
||||
let mode
|
||||
let pendingScreen
|
||||
|
@ -25,7 +29,8 @@
|
|||
// Cache variables for workflow
|
||||
let screenAccessRole = Roles.BASIC
|
||||
|
||||
let selectedTemplates = null
|
||||
let templates = null
|
||||
let screens = null
|
||||
|
||||
let selectedDatasources = null
|
||||
let blankScreenUrl = null
|
||||
|
@ -40,6 +45,7 @@
|
|||
|
||||
try {
|
||||
let screenId
|
||||
let createdScreens = []
|
||||
|
||||
for (let screen of screens) {
|
||||
// Check we aren't clashing with an existing URL
|
||||
|
@ -62,21 +68,19 @@
|
|||
screen.routing.roleId = screenAccessRole
|
||||
|
||||
// Create the screen
|
||||
// const response = await store.actions.screens.save(screen)
|
||||
// screenId = response._id
|
||||
const response = await store.actions.screens.save(screen)
|
||||
screenId = response._id
|
||||
createdScreens.push(response)
|
||||
|
||||
// Add link in layout. We only ever actually create 1 screen now, even
|
||||
// for autoscreens, so it's always safe to do this.
|
||||
// await store.actions.links.save(
|
||||
// screen.routing.route,
|
||||
// capitalise(screen.routing.route.split("/")[1])
|
||||
// )
|
||||
console.log(screen)
|
||||
await store.actions.links.save(
|
||||
screen.routing.route,
|
||||
capitalise(screen.routing.route.split("/")[1])
|
||||
)
|
||||
}
|
||||
|
||||
// Go to new screen
|
||||
//$goto(`./${screenId}`)
|
||||
//store.actions.screens.select(screenId)
|
||||
return createdScreens
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error creating screens")
|
||||
|
@ -110,7 +114,8 @@
|
|||
// Handler for NewScreenModal
|
||||
export const show = newMode => {
|
||||
mode = newMode
|
||||
// selectedTemplates = null
|
||||
templates = null
|
||||
screens = null
|
||||
selectedDatasources = null
|
||||
blankScreenUrl = null
|
||||
screenMode = mode
|
||||
|
@ -135,26 +140,24 @@
|
|||
// Handler for DatasourceModal confirmation, move to screen access select
|
||||
const confirmScreenDatasources = async ({ datasources }) => {
|
||||
selectedDatasources = datasources
|
||||
console.log("confirmScreenDatasources ", datasources)
|
||||
screenAccessRoleModal.show()
|
||||
if (screenMode === "form") {
|
||||
formTypeModal.show()
|
||||
} else {
|
||||
screenAccessRoleModal.show()
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for Datasource Screen Creation
|
||||
const completeDatasourceScreenCreation = async () => {
|
||||
const screens = selectedTemplates.map(template => {
|
||||
templates = rowListScreen(selectedDatasources)
|
||||
|
||||
const screens = templates.map(template => {
|
||||
let screenTemplate = template.create()
|
||||
screenTemplate.autoTableId = template.resourceId
|
||||
return screenTemplate
|
||||
})
|
||||
console.log("selectedTemplates ", selectedTemplates)
|
||||
/*
|
||||
|
||||
id : "ROW_LIST_TEMPLATE"
|
||||
name : "Employees - List"
|
||||
resourceId : "ta_bb_employee"
|
||||
|
||||
*/
|
||||
await createScreens({ screens, screenAccessRole })
|
||||
const createdScreens = await createScreens({ screens, screenAccessRole })
|
||||
loadNewScreen(createdScreens)
|
||||
}
|
||||
|
||||
const confirmScreenBlank = async ({ screenUrl }) => {
|
||||
|
@ -171,7 +174,55 @@
|
|||
return
|
||||
}
|
||||
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.
|
||||
|
@ -181,6 +232,8 @@
|
|||
screenUrl: blankScreenUrl,
|
||||
screenAccessRole,
|
||||
})
|
||||
} else if (screenMode === "form") {
|
||||
confirmFormScreenCreation()
|
||||
} else {
|
||||
completeDatasourceScreenCreation()
|
||||
}
|
||||
|
@ -193,30 +246,16 @@
|
|||
datasourceModal.show()
|
||||
}
|
||||
}
|
||||
window.test = () => {
|
||||
formTypeModal.show()
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--
|
||||
returns templates, should return selected resources for use elsewhere
|
||||
-->
|
||||
<Modal bind:this={datasourceModal} autoFocus={false}>
|
||||
<DatasourceModal
|
||||
{mode}
|
||||
onConfirm={confirmScreenDatasources}
|
||||
initialScreens={!selectedTemplates ? [] : [...selectedTemplates]}
|
||||
/>
|
||||
<DatasourceModal {mode} onConfirm={confirmScreenDatasources} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={screenAccessRoleModal}>
|
||||
<ScreenRoleModal
|
||||
onConfirm={() => {
|
||||
if (screenMode === "form") {
|
||||
formTypeModal.show()
|
||||
} else {
|
||||
confirmScreenCreation()
|
||||
}
|
||||
confirmScreenCreation()
|
||||
}}
|
||||
bind:screenAccessRole
|
||||
onCancel={roleSelectBack}
|
||||
|
@ -232,24 +271,14 @@
|
|||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
bind:this={formTypeModal}
|
||||
on:hide={() => {
|
||||
console.log("hide")
|
||||
//formType = null
|
||||
}}
|
||||
>
|
||||
<Modal bind:this={formTypeModal}>
|
||||
<FormTypeModal
|
||||
onConfirm={() => {
|
||||
console.log("test confirm")
|
||||
}}
|
||||
onConfirm={onConfirmFormType}
|
||||
onCancel={() => {
|
||||
console.log("cancel")
|
||||
formTypeModal.hide()
|
||||
screenAccessRoleModal.show()
|
||||
datasourceModal.show()
|
||||
}}
|
||||
on:select={e => {
|
||||
console.log("form type selection ", e.detail)
|
||||
formType = e.detail
|
||||
}}
|
||||
type={formType}
|
||||
|
|
|
@ -4,37 +4,33 @@
|
|||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||
import { IntegrationNames } from "constants"
|
||||
import { onMount } from "svelte"
|
||||
import rowListScreen from "builderStore/store/screenTemplates/rowListScreen"
|
||||
import DatasourceTemplateRow from "./DatasourceTemplateRow.svelte"
|
||||
|
||||
export let mode
|
||||
export let onCancel
|
||||
export let onConfirm
|
||||
export let initialScreens = []
|
||||
|
||||
let selectedScreens = [...initialScreens]
|
||||
let selectedSources = []
|
||||
|
||||
$: filteredSources = $datasources.list?.filter(datasource => {
|
||||
return datasource.source !== IntegrationNames.REST && datasource["entities"]
|
||||
})
|
||||
|
||||
const toggleSelection = datasource => {
|
||||
const { resourceId } = datasource
|
||||
if (selectedScreens.find(s => s.resourceId === resourceId)) {
|
||||
selectedScreens = selectedScreens.filter(
|
||||
screen => screen.resourceId !== resourceId
|
||||
const exists = selectedSources.find(
|
||||
d => d.resourceId === datasource.resourceId
|
||||
)
|
||||
if (exists) {
|
||||
selectedSources = selectedSources.filter(
|
||||
d => d.resourceId === datasource.resourceId
|
||||
)
|
||||
} else {
|
||||
selectedScreens = [
|
||||
...selectedScreens,
|
||||
rowListScreen([datasource], mode)[0],
|
||||
]
|
||||
selectedSources = [...selectedSources, datasource]
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDatasourceSelection = async () => {
|
||||
await onConfirm({
|
||||
templates: selectedScreens,
|
||||
datasources: selectedSources,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -54,7 +50,7 @@
|
|||
cancelText="Back"
|
||||
onConfirm={confirmDatasourceSelection}
|
||||
{onCancel}
|
||||
disabled={!selectedScreens.length}
|
||||
disabled={!selectedSources.length}
|
||||
size="L"
|
||||
>
|
||||
<Body size="S">
|
||||
|
@ -85,8 +81,8 @@
|
|||
resourceId: table._id,
|
||||
type: "table",
|
||||
}}
|
||||
{@const selected = selectedScreens.find(
|
||||
screen => screen.resourceId === tableDS.resourceId
|
||||
{@const selected = selectedSources.find(
|
||||
datasource => datasource.resourceId === tableDS.resourceId
|
||||
)}
|
||||
<DatasourceTemplateRow
|
||||
on:click={() => toggleSelection(tableDS)}
|
||||
|
@ -103,7 +99,7 @@
|
|||
tableId: view.tableId,
|
||||
type: "viewV2",
|
||||
}}
|
||||
{@const selected = selectedScreens.find(
|
||||
{@const selected = selectedSources.find(
|
||||
x => x.resourceId === viewDS.resourceId
|
||||
)}
|
||||
<DatasourceTemplateRow
|
||||
|
|
|
@ -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>
|
|
@ -10,6 +10,7 @@
|
|||
export let onCancel
|
||||
export let screenUrl
|
||||
export let screenAccessRole
|
||||
export let confirmText = "Done"
|
||||
|
||||
let error
|
||||
|
||||
|
@ -41,7 +42,7 @@
|
|||
|
||||
<ModalContent
|
||||
title="Access"
|
||||
confirmText="Done"
|
||||
{confirmText}
|
||||
cancelText="Back"
|
||||
{onConfirm}
|
||||
{onCancel}
|
||||
|
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
@ -1,9 +1,10 @@
|
|||
<script>
|
||||
import { Body } from "@budibase/bbui"
|
||||
import CreationPage from "components/common/CreationPage.svelte"
|
||||
import blankImage from "./blank.png"
|
||||
import tableImage from "./table.png"
|
||||
import gridImage from "./grid.png"
|
||||
import blankImage from "./images/blank.png"
|
||||
import tableImage from "./images/table.png"
|
||||
import gridImage from "./images/grid.png"
|
||||
import formImage from "./images/form.png" //optimized example
|
||||
import CreateScreenModal from "./CreateScreenModal.svelte"
|
||||
import { store } from "builderStore"
|
||||
|
||||
|
@ -54,6 +55,16 @@
|
|||
<Body size="XS">View and manipulate rows on a grid</Body>
|
||||
</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>
|
||||
</CreationPage>
|
||||
</div>
|
||||
|
|
|
@ -18,6 +18,7 @@ export interface UpdateSelfRequest {
|
|||
password?: string
|
||||
forceResetPassword?: boolean
|
||||
onboardedAt?: string
|
||||
tours?: Record<string, Date>
|
||||
}
|
||||
|
||||
export interface UpdateSelfResponse {
|
||||
|
|
|
@ -55,6 +55,7 @@ export interface User extends Document {
|
|||
dayPassRecordedAt?: string
|
||||
userGroups?: string[]
|
||||
onboardedAt?: string
|
||||
tours?: Record<string, Date>
|
||||
scimInfo?: { isSync: true } & Record<string, any>
|
||||
ssoId?: string
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ export const buildSelfSaveValidation = () => {
|
|||
firstName: OPTIONAL_STRING,
|
||||
lastName: OPTIONAL_STRING,
|
||||
onboardedAt: Joi.string().optional(),
|
||||
tours: Joi.object().optional(),
|
||||
}
|
||||
return auth.joiValidator.body(Joi.object(schema).required().unknown(false))
|
||||
}
|
||||
|
|