Rewrite screen wizard to make modals reusable and fix some edge case URL bugs

This commit is contained in:
Andrew Kingston 2022-03-02 19:10:18 +00:00
parent 18d7176ab7
commit d89f5f74e5
6 changed files with 143 additions and 162 deletions

View File

@ -10,17 +10,18 @@ const allTemplates = tables => [
] ]
// Allows us to apply common behaviour to all create() functions // Allows us to apply common behaviour to all create() functions
const createTemplateOverride = (frontendState, create) => () => { const createTemplateOverride = (frontendState, template) => () => {
const screen = create() const screen = template.create()
screen.name = screen.props._id screen.name = screen.props._id
screen.routing.route = screen.routing.route.toLowerCase() screen.routing.route = screen.routing.route.toLowerCase()
screen.template = template.id
return screen return screen
} }
export default (frontendState, tables) => { export default (frontendState, tables) => {
const enrichTemplate = template => ({ const enrichTemplate = template => ({
...template, ...template,
create: createTemplateOverride(frontendState, template.create), create: createTemplateOverride(frontendState, template),
}) })
const fromScratch = enrichTemplate(createFromScratchScreen) const fromScratch = enrichTemplate(createFromScratchScreen)

View File

@ -73,13 +73,11 @@
} }
const storeComponentForCopy = (cut = false) => { const storeComponentForCopy = (cut = false) => {
// lives in store - also used by drag drop
store.actions.components.copy(component, cut) store.actions.components.copy(component, cut)
} }
const pasteComponent = mode => { const pasteComponent = mode => {
try { try {
// lives in store - also used by drag drop
store.actions.components.paste(component, mode) store.actions.components.paste(component, mode)
} catch (error) { } catch (error) {
notifications.error("Error saving component") notifications.error("Error saving component")

View File

@ -10,6 +10,8 @@
$: screen = $allScreens.find(screen => screen._id === screenId) $: screen = $allScreens.find(screen => screen._id === screenId)
const duplicateScreen = () => {}
const deleteScreen = async () => { const deleteScreen = async () => {
try { try {
await store.actions.screens.delete(screen) await store.actions.screens.delete(screen)
@ -25,6 +27,9 @@
<div slot="control" class="icon"> <div slot="control" class="icon">
<Icon size="S" hoverable name="MoreSmallList" /> <Icon size="S" hoverable name="MoreSmallList" />
</div> </div>
<MenuItem noClose icon="Duplicate" on:click={duplicateScreen}>
Duplicate
</MenuItem>
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem> <MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
</ActionMenu> </ActionMenu>

View File

@ -10,39 +10,19 @@
ProgressCircle, ProgressCircle,
} from "@budibase/bbui" } from "@budibase/bbui"
import getTemplates from "builderStore/store/screenTemplates" import getTemplates from "builderStore/store/screenTemplates"
import { onDestroy } from "svelte"
import { createEventDispatcher } from "svelte" export let onConfirm
export let onCancel
export let chooseModal
export let save
export let showProgressCircle = false export let showProgressCircle = false
let selectedScreens = []
const blankScreen = "createFromScratch" const blankScreen = "createFromScratch"
const dispatch = createEventDispatcher()
function setScreens() { let selectedScreens = []
dispatch("save", { let templates = getTemplates($store, $tables.list)
screens: selectedScreens,
})
}
$: blankSelected = selectedScreens?.length === 1 $: blankSelected = selectedScreens?.length === 1
$: autoSelected = selectedScreens?.length > 0 && !blankSelected $: autoSelected = selectedScreens?.length > 0 && !blankSelected
let templates = getTemplates($store, $tables.list)
const confirm = async () => {
if (autoSelected) {
setScreens()
await save()
} else {
setScreens()
chooseModal(1)
}
}
const toggleScreenSelection = table => { const toggleScreenSelection = table => {
if (selectedScreens.find(s => s.table === table.name)) { if (selectedScreens.find(s => s.table === table.name)) {
selectedScreens = selectedScreens.filter( selectedScreens = selectedScreens.filter(
@ -56,25 +36,25 @@
} }
} }
onDestroy(() => { const confirmScreenSelection = async () => {
selectedScreens = [] await onConfirm(selectedScreens)
}) }
</script> </script>
<div> <div>
<ModalContent <ModalContent
title="Add screens" title="Add screens"
confirmText="Add Screens" confirmText="Add screens"
cancelText="Cancel" cancelText="Cancel"
onConfirm={() => confirm()} onConfirm={confirmScreenSelection}
{onCancel}
disabled={!selectedScreens.length} disabled={!selectedScreens.length}
size="L" size="L"
> >
<Body size="S" <Body size="S">
>Please select the screens you would like to add to your application. Please select the screens you would like to add to your application.
Autogenerated screens come with CRUD functionality.</Body Autogenerated screens come with CRUD functionality.
> </Body>
<Layout noPadding gap="S"> <Layout noPadding gap="S">
<Detail size="S">Blank screen</Detail> <Detail size="S">Blank screen</Detail>
<div <div

View File

@ -2,58 +2,59 @@
import { ModalContent, Input, ProgressCircle } from "@budibase/bbui" import { ModalContent, Input, ProgressCircle } from "@budibase/bbui"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { selectedAccessRole, allScreens } from "builderStore" import { selectedAccessRole, allScreens } from "builderStore"
import { onDestroy } from "svelte" import { get } from "svelte/store"
export let screenName export let onConfirm
export let url export let onCancel
export let chooseModal
export let save
export let showProgressCircle = false export let showProgressCircle = false
let screenName
let screenUrl
let routeError let routeError
let roleId = $selectedAccessRole || "BASIC"
const routeChanged = event => { const routeChanged = event => {
if (!event.detail.startsWith("/")) { if (!event.detail.startsWith("/")) {
url = "/" + event.detail screenUrl = "/" + event.detail
} }
url = sanitizeUrl(url) screenUrl = sanitizeUrl(screenUrl)
if (routeExists(screenUrl)) {
if (routeExists(url, roleId)) {
routeError = "This URL is already taken for this access role" routeError = "This URL is already taken for this access role"
} else { } else {
routeError = "" routeError = null
} }
} }
const routeExists = (url, roleId) => { const routeExists = url => {
return $allScreens.some( const roleId = get(selectedAccessRole) || "BASIC"
return get(allScreens).some(
screen => screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() && screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === roleId screen.routing.roleId === roleId
) )
} }
onDestroy(() => { const confirmScreenDetails = async () => {
screenName = "" await onConfirm({
url = "" screenName,
}) screenUrl,
})
}
</script> </script>
<ModalContent <ModalContent
size="M" size="M"
title={"Enter details"} title={"Enter details"}
confirmText={"Continue"} confirmText={"Continue"}
onCancel={() => chooseModal(0)} onConfirm={confirmScreenDetails}
onConfirm={() => save()} {onCancel}
cancelText={"Back"} cancelText={"Back"}
disabled={!screenName || !url || routeError} disabled={!screenName || !screenUrl || routeError}
> >
<Input label="Name" bind:value={screenName} /> <Input label="Name" bind:value={screenName} />
<Input <Input
label="URL" label="URL"
error={routeError} error={routeError}
bind:value={url} bind:value={screenUrl}
on:change={routeChanged} on:change={routeChanged}
/> />
<div slot="footer"> <div slot="footer">

View File

@ -3,141 +3,137 @@
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte" import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { Modal, notifications } from "@budibase/bbui" import { Modal, notifications } from "@budibase/bbui"
import { store, selectedAccessRole, allScreens } from "builderStore" import { store, selectedAccessRole } from "builderStore"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
import { get } from "svelte/store"
let newScreenModal let pendingScreen
let navigationSelectionModal
let screenDetailsModal
let screenName = ""
let url = ""
let selectedScreens = []
let showProgressCircle = false let showProgressCircle = false
let routeError
let createdScreens = []
$: roleId = $selectedAccessRole || "BASIC" // Modal refs
let newScreenModal
let screenDetailsModal
const createScreens = async () => { // External handler to show the screen wizard
for (let screen of selectedScreens) { export const showModal = () => {
let test = screen.create() newScreenModal.show()
createdScreens.push(test)
analytics.captureEvent(Events.SCREEN.CREATED, {
template: screen.id || screen.name,
})
}
}
const save = async () => { // Reset state when showing modal again
showProgressCircle = true pendingScreen = null
try {
await createScreens()
for (let screen of createdScreens) {
await saveScreens(screen)
}
await store.actions.routing.fetch()
selectedScreens = []
createdScreens = []
screenName = ""
url = ""
} catch (error) {
notifications.error("Error creating screens")
}
showProgressCircle = false showProgressCircle = false
} }
const saveScreens = async draftScreen => { // Creates an array of screens, checking and sanitising their URLs
let existingScreenCount = $store.screens.filter( const createScreens = async screens => {
s => s.props._instanceName == draftScreen.props._instanceName if (!screens?.length) {
).length return
if (existingScreenCount > 0) {
let oldUrlArr = draftScreen.routing.route.split("/")
oldUrlArr[1] = `${oldUrlArr[1]}-${existingScreenCount + 1}`
draftScreen.routing.route = oldUrlArr.join("/")
} }
showProgressCircle = true
let route = url ? sanitizeUrl(`${url}`) : draftScreen.routing.route try {
if (draftScreen) { for (let screen of screens) {
if (!route) { // Check we aren't clashing with an existing URL
routeError = "URL is required" if (hasExistingUrl(screen.routing.route)) {
} else { let suffix = 2
if (routeExists(route, roleId)) { let candidateUrl = makeCandidateUrl(screen, suffix)
routeError = "This URL is already taken for this access role" while (hasExistingUrl(candidateUrl)) {
} else { candidateUrl = makeCandidateUrl(screen, ++suffix)
routeError = "" }
screen.routing.route = candidateUrl
} }
}
if (routeError) return false // Sanitise URL
screen.routing.route = sanitizeUrl(screen.routing.route)
if (screenName) { // Use the currently selected role
draftScreen.props._instanceName = screenName screen.routing.roleId = get(selectedAccessRole) || "BASIC"
}
draftScreen.routing.route = route // Create the screen
draftScreen.routing.roleId = roleId await store.actions.screens.save(screen)
await store.actions.screens.save(draftScreen) // Analytics
if (draftScreen.props._instanceName.endsWith("List")) { if (screen.template) {
try { analytics.captureEvent(Events.SCREEN.CREATED, {
template: screen.template,
})
}
// Add link in layout for list screens
if (screen.props._instanceName.endsWith("List")) {
await store.actions.components.links.save( await store.actions.components.links.save(
draftScreen.routing.route, screen.routing.route,
draftScreen.routing.route.split("/")[1] screen.routing.route.split("/")[1]
) )
} catch (error) {
notifications.error("Error creating link to screen")
} }
} }
// Refresh routing info since we have new screens
await store.actions.routing.fetch()
} catch (error) {
console.log(error)
notifications.error("Error creating screens")
}
showProgressCircle = false
}
// Checks if any screens exist in the store with the given route and
// currently selected role
const hasExistingUrl = url => {
const roleId = get(selectedAccessRole) || "BASIC"
const screens = get(store).screens.filter(s => s.routing.roleId === roleId)
return !!screens.find(s => s.routing?.route === url)
}
// Constructs a candidate URL for a new screen, suffixing the base of the
// screen's URL with a given suffix.
// e.g. "/sales/:id" => "/sales-1/:id"
const makeCandidateUrl = (screen, suffix) => {
let url = screen.routing?.route || ""
if (url.startsWith("/")) {
url = url.slice(1)
}
if (!url.includes("/")) {
return `/${url}-${suffix}`
} else {
const split = url.split("/")
return `/${split[0]}-${suffix}/${split.join("/")}`
} }
} }
const routeExists = (route, roleId) => { // Handler for NewScreenModal
return $allScreens.some( const confirmScreenSelection = async templates => {
screen => // Handle template selection
screen.routing.route.toLowerCase() === route.toLowerCase() && if (templates?.length > 1) {
screen.routing.roleId === roleId // Autoscreens, so create immediately
) const screens = templates.map(template => template.create())
} await createScreens(screens)
} else {
export const showModal = () => { // Empty screen, so proceed to the next modal
newScreenModal.show() pendingScreen = templates[0].create()
}
const setScreens = evt => {
selectedScreens = evt.detail.screens
}
const chooseModal = index => {
/*
0 = newScreenModal
1 = screenDetailsModal
2 = navigationSelectionModal
*/
if (index === 0) {
newScreenModal.show()
} else if (index === 1) {
screenDetailsModal.show() screenDetailsModal.show()
} else if (index === 2) {
navigationSelectionModal.show()
} }
} }
// Handler for ScreenDetailsModal
const confirmScreenDetails = async ({ screenName, screenUrl }) => {
if (!pendingScreen) {
return
}
pendingScreen.props._instanceName = screenName
pendingScreen.routing.route = screenUrl
await createScreens([pendingScreen])
}
</script> </script>
<Modal bind:this={newScreenModal}> <Modal bind:this={newScreenModal}>
<NewScreenModal <NewScreenModal onConfirm={confirmScreenSelection} {showProgressCircle} />
on:save={setScreens}
{showProgressCircle}
{save}
{chooseModal}
/>
</Modal> </Modal>
<Modal bind:this={screenDetailsModal}> <Modal bind:this={screenDetailsModal}>
<ScreenDetailsModal <ScreenDetailsModal
bind:screenName
bind:url
{showProgressCircle} {showProgressCircle}
{save} onConfirm={confirmScreenDetails}
{chooseModal} onCancel={() => newScreenModal.show()}
/> />
</Modal> </Modal>