Rewrite screen wizard to make modals reusable and fix some edge case URL bugs
This commit is contained in:
parent
18d7176ab7
commit
d89f5f74e5
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue