Add initial layouts panel and add explicit panels for theme and navigation tabs

This commit is contained in:
Andrew Kingston 2022-05-11 12:47:24 +01:00
parent 876cb4784d
commit 88018aff4e
13 changed files with 391 additions and 326 deletions

View File

@ -1,7 +1,7 @@
import { getFrontendStore } from "./store/frontend" import { getFrontendStore } from "./store/frontend"
import { getAutomationStore } from "./store/automation" import { getAutomationStore } from "./store/automation"
import { getThemeStore } from "./store/theme" import { getThemeStore } from "./store/theme"
import { derived, writable } from "svelte/store" import { derived } from "svelte/store"
import { LAYOUT_NAMES } from "../constants" import { LAYOUT_NAMES } from "../constants"
import { findComponent, findComponentPath } from "./componentUtils" import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
@ -14,6 +14,20 @@ export const selectedScreen = derived(store, $store => {
return $store.screens.find(screen => screen._id === $store.selectedScreenId) return $store.screens.find(screen => screen._id === $store.selectedScreenId)
}) })
export const selectedLayout = derived(store, $store => {
return $store.layouts?.find(layout => layout._id === $store.selectedLayoutId)
})
export const selectedComponent = derived(
[store, selectedScreen],
([$store, $selectedScreen]) => {
if (!$selectedScreen || !$store.selectedComponentId) {
return null
}
return findComponent($selectedScreen?.props, $store.selectedComponentId)
}
)
export const sortedScreens = derived(store, $store => { export const sortedScreens = derived(store, $store => {
return $store.screens.slice().sort((a, b) => { return $store.screens.slice().sort((a, b) => {
// Sort by role first // Sort by role first
@ -43,16 +57,6 @@ export const sortedScreens = derived(store, $store => {
}) })
}) })
export const selectedComponent = derived(
[store, selectedScreen],
([$store, $selectedScreen]) => {
if (!$selectedScreen || !$store.selectedComponentId) {
return null
}
return findComponent($selectedScreen?.props, $store.selectedComponentId)
}
)
export const selectedComponentPath = derived( export const selectedComponentPath = derived(
[store, selectedScreen], [store, selectedScreen],
([$store, $selectedScreen]) => { ([$store, $selectedScreen]) => {

View File

@ -43,7 +43,6 @@ const INITIAL_FRONTEND_STATE = {
continueIfAction: false, continueIfAction: false,
}, },
currentFrontEndType: "none", currentFrontEndType: "none",
selectedLayoutId: "",
errors: [], errors: [],
hasAppPackage: false, hasAppPackage: false,
libraries: null, libraries: null,
@ -57,6 +56,7 @@ const INITIAL_FRONTEND_STATE = {
// URL params // URL params
selectedScreenId: null, selectedScreenId: null,
selectedComponentId: null, selectedComponentId: null,
selectedLayoutId: null,
} }
export const getFrontendStore = () => { export const getFrontendStore = () => {

View File

@ -1,7 +1,12 @@
<script> <script>
import { get } from "svelte/store" import { get } from "svelte/store"
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import { store, selectedScreen, currentAsset } from "builderStore" import {
store,
selectedScreen,
selectedLayout,
currentAsset,
} from "builderStore"
import iframeTemplate from "./iframeTemplate" import iframeTemplate from "./iframeTemplate"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { import {
@ -40,8 +45,15 @@
// Extract data to pass to the iframe // Extract data to pass to the iframe
$: { $: {
screen = $selectedScreen screen = $selectedScreen
layout = $store.layouts.find(layout => layout._id === screen?.layoutId)
// If viewing legacy layouts, always show the custom layout
if ($isActive("./layouts")) {
layout = $selectedLayout
} else {
layout = $store.layouts.find(layout => layout._id === screen?.layoutId)
}
} }
// Don't show selected components unless on the components tab // Don't show selected components unless on the components tab
$: selectedComponentId = $isActive("./components") $: selectedComponentId = $isActive("./components")
? $store.selectedComponentId ? $store.selectedComponentId
@ -59,7 +71,10 @@
navigation: $store.navigation, navigation: $store.navigation,
isBudibaseEvent: true, isBudibaseEvent: true,
} }
// Refresh the preview when required
$: json = JSON.stringify(previewData) $: json = JSON.stringify(previewData)
$: refreshContent(json)
// Update the iframe with the builder info to render the correct preview // Update the iframe with the builder info to render the correct preview
const refreshContent = message => { const refreshContent = message => {
@ -68,10 +83,7 @@
} }
} }
// Refresh the preview when required const receiveMessage = message => {
$: refreshContent(json)
function receiveMessage(message) {
const handlers = { const handlers = {
[MessageTypes.READY]: () => { [MessageTypes.READY]: () => {
// Initialise the app when mounted // Initialise the app when mounted
@ -97,46 +109,6 @@
messageHandler(message) messageHandler(message)
} }
onMount(() => {
window.addEventListener("message", receiveMessage)
if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB
iframe.contentWindow.addEventListener(
"ready",
() => {
receiveMessage({ data: { type: MessageTypes.READY } })
},
{ once: true }
)
iframe.contentWindow.addEventListener(
"error",
event => {
receiveMessage({
data: { type: MessageTypes.ERROR, error: event.detail },
})
},
{ once: true }
)
// Add listener for events sent by client library in preview
iframe.contentWindow.addEventListener("bb-event", handleBudibaseEvent)
}
})
// Remove all iframe event listeners on component destroy
onDestroy(() => {
window.removeEventListener("message", receiveMessage)
if (iframe.contentWindow) {
if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB
iframe.contentWindow.removeEventListener(
"bb-event",
handleBudibaseEvent
)
}
}
})
const handleBudibaseEvent = async event => { const handleBudibaseEvent = async event => {
const { type, data } = event.data || event.detail const { type, data } = event.data || event.detail
if (!type) { if (!type) {
@ -212,6 +184,46 @@
const cancelDeleteComponent = () => { const cancelDeleteComponent = () => {
idToDelete = null idToDelete = null
} }
onMount(() => {
window.addEventListener("message", receiveMessage)
if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB
iframe.contentWindow.addEventListener(
"ready",
() => {
receiveMessage({ data: { type: MessageTypes.READY } })
},
{ once: true }
)
iframe.contentWindow.addEventListener(
"error",
event => {
receiveMessage({
data: { type: MessageTypes.ERROR, error: event.detail },
})
},
{ once: true }
)
// Add listener for events sent by client library in preview
iframe.contentWindow.addEventListener("bb-event", handleBudibaseEvent)
}
})
// Remove all iframe event listeners on component destroy
onDestroy(() => {
window.removeEventListener("message", receiveMessage)
if (iframe.contentWindow) {
if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB
iframe.contentWindow.removeEventListener(
"bb-event",
handleBudibaseEvent
)
}
}
})
</script> </script>
<div class="component-container"> <div class="component-container">

View File

@ -2,21 +2,11 @@
import { store } from "builderStore" import { store } from "builderStore"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
ActionMenu,
MenuItem,
Icon,
Modal,
ModalContent,
Input,
} from "@budibase/bbui"
import { cloneDeep } from "lodash/fp"
export let layout export let layout
let confirmDeleteDialog let confirmDeleteDialog
let editLayoutNameModal
let name = layout.name
const deleteLayout = async () => { const deleteLayout = async () => {
try { try {
@ -26,24 +16,12 @@
notifications.error("Error deleting layout") notifications.error("Error deleting layout")
} }
} }
const saveLayout = async () => {
try {
const layoutToSave = cloneDeep(layout)
layoutToSave.name = name
await store.actions.layouts.save(layoutToSave)
notifications.success("Layout saved successfully")
} catch (err) {
notifications.error("Error saving layout")
}
}
</script> </script>
<ActionMenu> <ActionMenu>
<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 icon="Edit" on:click={editLayoutNameModal.show}>Edit</MenuItem>
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem> <MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
</ActionMenu> </ActionMenu>
@ -55,17 +33,6 @@
onOk={deleteLayout} onOk={deleteLayout}
/> />
<Modal bind:this={editLayoutNameModal}>
<ModalContent
title="Edit Layout Name"
confirmText="Save"
onConfirm={saveLayout}
disabled={!name}
>
<Input thin type="text" label="Name" bind:value={name} />
</ModalContent>
</Modal>
<style> <style>
.icon { .icon {
display: grid; display: grid;

View File

@ -0,0 +1,34 @@
<script>
import NavigationPanel from "components/design/navigation/NavigationPanel.svelte"
import { Banner, Layout } from "@budibase/bbui"
import NavItem from "components/common/NavItem.svelte"
import { store } from "builderStore"
import LayoutDropdownMenu from "./LayoutDropdownMenu.svelte"
</script>
<NavigationPanel title="Layouts">
<Layout paddingX="L" paddingY="XL" gap="S">
<Banner
type="warning"
showCloseButton={false}
extraButtonText="View details"
extraButtonAction={() => {}}
>
Custom layouts are being deprecated. They will be removed from June 1st.
</Banner>
</Layout>
{#each $store.layouts as layout (layout._id)}
<NavItem
icon="Experience"
indentLevel={0}
selected={$store.selectedLayoutId === layout._id}
text={layout.name}
on:click={() => ($store.selectedLayoutId = layout._id)}
>
<LayoutDropdownMenu {layout} />
</NavItem>
{/each}
</NavigationPanel>
<!-- -->
<!-- color={RoleUtils.getRoleColour(screen.routing.roleId)}-->

View File

@ -0,0 +1,5 @@
<script>
import SettingsPanel from "components/design/settings/SettingsPanel.svelte"
</script>
<

View File

@ -0,0 +1,20 @@
<script>
import { syncURLToState } from "helpers/urlStateSync"
import { store } from "builderStore"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
// Keep URL and state in sync for selected component ID
const stopSyncing = syncURLToState({
urlParam: "layoutId",
stateKey: "selectedLayoutId",
validate: id => $store.layouts?.some(layout => layout._id === id),
fallbackUrl: "../",
store,
routify,
})
onDestroy(stopSyncing)
</script>
<slot />

View File

@ -0,0 +1,5 @@
<script>
import LayoutNavigationPanel from "./_components/LayoutNavigationPanel.svelte"
</script>
<LayoutNavigationPanel />

View File

@ -1,5 +1,13 @@
<script> <script>
import NavigationPanel from "components/design/navigation/NavigationPanel.svelte" import { store } from "builderStore"
import { onMount } from "svelte"
import { redirect } from "@roxi/routify"
onMount(() => {
if ($store.layouts?.length) {
$redirect(`./${$store.layouts[0]._id}`)
}
})
</script> </script>
<NavigationPanel title="Layouts" /> You don't have any layouts

View File

@ -0,0 +1,118 @@
<script>
import NavigationPanel from "components/design/navigation/NavigationPanel.svelte"
import {
Body,
Layout,
Label,
ActionGroup,
ActionButton,
Checkbox,
Select,
ColorPicker,
Input,
notifications,
} from "@budibase/bbui"
import NavigationLinksEditor from "./NavigationLinksEditor.svelte"
import { store } from "builderStore"
import { DefaultAppTheme } from "constants"
const update = async (key, value) => {
try {
let navigation = $store.navigation
navigation[key] = value
await store.actions.navigation.save(navigation)
} catch (error) {
notifications.error("Error updating navigation settings")
}
}
</script>
<NavigationPanel title="Navigation">
<Layout paddingX="L" paddingY="XL" gap="S">
<Body size="S">
Your navigation is configured for all the screens within your app
</Body>
<Body size="S">
You can hide and show your navigation for each screen in the screen
settings
</Body>
<NavigationLinksEditor />
<Layout noPadding gap="XS">
<Label>Position</Label>
<ActionGroup quiet>
<ActionButton
selected={$store.navigation.navigation === "Top"}
quiet={$store.navigation.navigation !== "Top"}
icon="PaddingTop"
on:click={() => update("navigation", "Top")}
/>
<ActionButton
selected={$store.navigation.navigation === "Left"}
quiet={$store.navigation.navigation !== "Left"}
icon="PaddingLeft"
on:click={() => update("navigation", "Left")}
/>
</ActionGroup>
</Layout>
{#if $store.navigation.navigation === "Top"}
<Checkbox
text="Sticky header"
value={$store.navigation.sticky}
on:change={e => update("sticky", e.detail)}
/>
<Select
label="Width"
options={["Max", "Large", "Medium", "Small"]}
plaveholder={null}
value={$store.navigation.navWidth}
on:change={e => update("navWidth", e.detail)}
/>
{/if}
<Layout noPadding gap="XS">
<Checkbox
text="Logo"
value={!$store.navigation.hideLogo}
on:change={e => update("hideLogo", !e.detail)}
/>
{#if !$store.navigation.hideLogo}
<Input
value={$store.navigation.logoUrl}
on:change={e => update("logoUrl", e.detail)}
placeholder="Add logo URL"
updateOnChange={false}
/>
{/if}
</Layout>
<Layout noPadding gap="XS">
<Checkbox
text="Title"
value={!$store.navigation.hideTitle}
on:change={e => update("hideTitle", !e.detail)}
/>
{#if !$store.navigation.hideTitle}
<Input
value={$store.navigation.title}
on:change={e => update("title", e.detail)}
placeholder="Add title"
updateOnChange={false}
/>
{/if}
</Layout>
<Layout noPadding gap="XS">
<Label>Background color</Label>
<ColorPicker
spectrumTheme={$store.theme}
value={$store.navigation.navBackground || DefaultAppTheme.navBackground}
on:change={e => update("navBackground", e.detail)}
/>
</Layout>
<Layout noPadding gap="XS">
<Label>Text color</Label>
<ColorPicker
spectrumTheme={$store.theme}
value={$store.navigation.navTextColor || DefaultAppTheme.navTextColor}
on:change={e => update("navTextColor", e.detail)}
/>
</Layout>
</Layout>
</NavigationPanel>

View File

@ -1,118 +1,5 @@
<script> <script>
import NavigationPanel from "components/design/navigation/NavigationPanel.svelte" import NavigationSettingsPanel from "./_components/NavigationSettingsPanel.svelte"
import {
Body,
Layout,
Label,
ActionGroup,
ActionButton,
Checkbox,
Select,
ColorPicker,
Input,
notifications,
} from "@budibase/bbui"
import NavigationLinksEditor from "./_components/NavigationLinksEditor.svelte"
import { store } from "builderStore"
import { DefaultAppTheme } from "constants"
const update = async (key, value) => {
try {
let navigation = $store.navigation
navigation[key] = value
await store.actions.navigation.save(navigation)
} catch (error) {
notifications.error("Error updating navigation settings")
}
}
</script> </script>
<NavigationPanel title="Navigation"> <NavigationSettingsPanel />
<Layout paddingX="L" paddingY="XL" gap="S">
<Body size="S">
Your navigation is configured for all the screens within your app
</Body>
<Body size="S">
You can hide and show your navigation for each screen in the screen
settings
</Body>
<NavigationLinksEditor />
<Layout noPadding gap="XS">
<Label>Position</Label>
<ActionGroup quiet>
<ActionButton
selected={$store.navigation.navigation === "Top"}
quiet={$store.navigation.navigation !== "Top"}
icon="PaddingTop"
on:click={() => update("navigation", "Top")}
/>
<ActionButton
selected={$store.navigation.navigation === "Left"}
quiet={$store.navigation.navigation !== "Left"}
icon="PaddingLeft"
on:click={() => update("navigation", "Left")}
/>
</ActionGroup>
</Layout>
{#if $store.navigation.navigation === "Top"}
<Checkbox
text="Sticky header"
value={$store.navigation.sticky}
on:change={e => update("sticky", e.detail)}
/>
<Select
label="Width"
options={["Max", "Large", "Medium", "Small"]}
plaveholder={null}
value={$store.navigation.navWidth}
on:change={e => update("navWidth", e.detail)}
/>
{/if}
<Layout noPadding gap="XS">
<Checkbox
text="Logo"
value={!$store.navigation.hideLogo}
on:change={e => update("hideLogo", !e.detail)}
/>
{#if !$store.navigation.hideLogo}
<Input
value={$store.navigation.logoUrl}
on:change={e => update("logoUrl", e.detail)}
placeholder="Add logo URL"
updateOnChange={false}
/>
{/if}
</Layout>
<Layout noPadding gap="XS">
<Checkbox
text="Title"
value={!$store.navigation.hideTitle}
on:change={e => update("hideTitle", !e.detail)}
/>
{#if !$store.navigation.hideTitle}
<Input
value={$store.navigation.title}
on:change={e => update("title", e.detail)}
placeholder="Add title"
updateOnChange={false}
/>
{/if}
</Layout>
<Layout noPadding gap="XS">
<Label>Background color</Label>
<ColorPicker
spectrumTheme={$store.theme}
value={$store.navigation.navBackground || DefaultAppTheme.navBackground}
on:change={e => update("navBackground", e.detail)}
/>
</Layout>
<Layout noPadding gap="XS">
<Label>Text color</Label>
<ColorPicker
spectrumTheme={$store.theme}
value={$store.navigation.navTextColor || DefaultAppTheme.navTextColor}
on:change={e => update("navTextColor", e.detail)}
/>
</Layout>
</Layout>
</NavigationPanel>

View File

@ -0,0 +1,120 @@
<script>
import NavigationPanel from "components/design/navigation/NavigationPanel.svelte"
import {
Body,
Layout,
Label,
ColorPicker,
Button,
notifications,
} from "@budibase/bbui"
import { store } from "builderStore"
import { get } from "svelte/store"
import { DefaultAppTheme } from "constants"
import AppThemeSelect from "./AppThemeSelect.svelte"
const ButtonBorderRadiusOptions = [
{
label: "Square",
value: "0",
},
{
label: "Soft edge",
value: "4px",
},
{
label: "Curved",
value: "8px",
},
{
label: "Round",
value: "16px",
},
]
$: customTheme = $store.customTheme || {}
const update = async (property, value) => {
try {
store.actions.customTheme.save({
...get(store).customTheme,
[property]: value,
})
} catch (error) {
notifications.error("Error updating custom theme")
}
}
</script>
<NavigationPanel title="Theme">
<Layout paddingX="L" paddingY="XL" gap="S">
<Body size="S">
Your theme is set across all the screens within your app
</Body>
<Layout noPadding gap="XS">
<Label>Theme</Label>
<AppThemeSelect />
</Layout>
<Layout noPadding gap="XS">
<Label>Buttons</Label>
<div class="buttons">
{#each ButtonBorderRadiusOptions as option}
<div
class:active={customTheme.buttonBorderRadius === option.value}
style={`--radius: ${option.value}`}
>
<Button
secondary
on:click={() => update("buttonBorderRadius", option.value)}
>
{option.label}
</Button>
</div>
{/each}
</div>
</Layout>
<Layout noPadding gap="XS">
<Label>Accent color</Label>
<ColorPicker
spectrumTheme={$store.theme}
value={customTheme.primaryColor || DefaultAppTheme.primaryColor}
on:change={e => update("primaryColor", e.detail)}
/>
</Layout>
<Layout noPadding gap="XS">
<Label>Accent color (hover)</Label>
<ColorPicker
spectrumTheme={$store.theme}
value={customTheme.primaryColorHover ||
DefaultAppTheme.primaryColorHover}
on:change={e => update("primaryColorHover", e.detail)}
/>
</Layout>
</Layout>
</NavigationPanel>
<style>
.buttons {
display: grid;
grid-template-columns: 100px 100px;
gap: var(--spacing-m);
}
.buttons > div {
display: contents;
}
.buttons > div :global(.spectrum-Button) {
border-radius: var(--radius) !important;
border-width: 1px;
border-color: var(--spectrum-global-color-gray-400);
font-weight: 600;
}
.buttons > div:hover :global(.spectrum-Button) {
background: var(--spectrum-global-color-gray-700);
border-color: var(--spectrum-global-color-gray-700);
}
.buttons > div.active :global(.spectrum-Button) {
background: var(--spectrum-global-color-gray-200);
color: var(--spectrum-global-color-gray-800);
border-color: var(--spectrum-global-color-gray-600);
}
</style>

View File

@ -1,120 +1,5 @@
<script> <script>
import NavigationPanel from "components/design/navigation/NavigationPanel.svelte" import ThemeSettingsPanel from "./_components/ThemeSettingsPanel.svelte"
import {
Body,
Layout,
Label,
ColorPicker,
Button,
notifications,
} from "@budibase/bbui"
import { store } from "builderStore"
import { get } from "svelte/store"
import { DefaultAppTheme } from "constants"
import AppThemeSelect from "./_components/AppThemeSelect.svelte"
const ButtonBorderRadiusOptions = [
{
label: "Square",
value: "0",
},
{
label: "Soft edge",
value: "4px",
},
{
label: "Curved",
value: "8px",
},
{
label: "Round",
value: "16px",
},
]
$: customTheme = $store.customTheme || {}
const update = async (property, value) => {
try {
store.actions.customTheme.save({
...get(store).customTheme,
[property]: value,
})
} catch (error) {
notifications.error("Error updating custom theme")
}
}
</script> </script>
<NavigationPanel title="Theme"> <ThemeSettingsPanel />
<Layout paddingX="L" paddingY="XL" gap="S">
<Body size="S">
Your theme is set across all the screens within your app
</Body>
<Layout noPadding gap="XS">
<Label>Theme</Label>
<AppThemeSelect />
</Layout>
<Layout noPadding gap="XS">
<Label>Buttons</Label>
<div class="buttons">
{#each ButtonBorderRadiusOptions as option}
<div
class:active={customTheme.buttonBorderRadius === option.value}
style={`--radius: ${option.value}`}
>
<Button
secondary
on:click={() => update("buttonBorderRadius", option.value)}
>
{option.label}
</Button>
</div>
{/each}
</div>
</Layout>
<Layout noPadding gap="XS">
<Label>Accent color</Label>
<ColorPicker
spectrumTheme={$store.theme}
value={customTheme.primaryColor || DefaultAppTheme.primaryColor}
on:change={e => update("primaryColor", e.detail)}
/>
</Layout>
<Layout noPadding gap="XS">
<Label>Accent color (hover)</Label>
<ColorPicker
spectrumTheme={$store.theme}
value={customTheme.primaryColorHover ||
DefaultAppTheme.primaryColorHover}
on:change={e => update("primaryColorHover", e.detail)}
/>
</Layout>
</Layout>
</NavigationPanel>
<style>
.buttons {
display: grid;
grid-template-columns: 100px 100px;
gap: var(--spacing-m);
}
.buttons > div {
display: contents;
}
.buttons > div :global(.spectrum-Button) {
border-radius: var(--radius) !important;
border-width: 1px;
border-color: var(--spectrum-global-color-gray-400);
font-weight: 600;
}
.buttons > div:hover :global(.spectrum-Button) {
background: var(--spectrum-global-color-gray-700);
border-color: var(--spectrum-global-color-gray-700);
}
.buttons > div.active :global(.spectrum-Button) {
background: var(--spectrum-global-color-gray-200);
color: var(--spectrum-global-color-gray-800);
border-color: var(--spectrum-global-color-gray-600);
}
</style>