New Left Panel for Design
This commit is contained in:
parent
e3cfdd537a
commit
6886a312c5
|
@ -61,6 +61,12 @@ export const selectedLayout = derived(store, $store => {
|
|||
export const selectedComponent = derived(
|
||||
[store, selectedScreen],
|
||||
([$store, $selectedScreen]) => {
|
||||
if (
|
||||
$selectedScreen &&
|
||||
["navigation", "screen"].includes($store.selectedComponentId)
|
||||
) {
|
||||
return findComponent($selectedScreen?.props, $selectedScreen?.props._id)
|
||||
}
|
||||
if (!$selectedScreen || !$store.selectedComponentId) {
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -769,9 +769,13 @@ export const getFrontendStore = () => {
|
|||
else {
|
||||
await store.actions.screens.patch(screen => {
|
||||
// Find the selected component
|
||||
let selectedComponentId = state.selectedComponentId
|
||||
if (["navigation", "screen"].includes(selectedComponentId)) {
|
||||
selectedComponentId = screen?.props._id
|
||||
}
|
||||
const currentComponent = findComponent(
|
||||
screen.props,
|
||||
state.selectedComponentId
|
||||
selectedComponentId
|
||||
)
|
||||
if (!currentComponent) {
|
||||
return false
|
||||
|
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
@ -0,0 +1,105 @@
|
|||
<script>
|
||||
import { Body } from "@budibase/bbui"
|
||||
import CreationPage from "components/common/CreationPage.svelte"
|
||||
import blankImage from "./blank.png"
|
||||
import tableImage from "./table.png"
|
||||
import CreateScreenModal from "./CreateScreenModal.svelte"
|
||||
import { store } from "builderStore"
|
||||
|
||||
export let onClose = null
|
||||
|
||||
let createScreenModal
|
||||
|
||||
$: hasScreens = $store.screens?.length
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
<CreationPage
|
||||
showClose={!!onClose}
|
||||
{onClose}
|
||||
heading={hasScreens ? "Create new screen" : "Create your first screen"}
|
||||
>
|
||||
<div class="subHeading">
|
||||
<Body>Start from scratch or create screens from your data</Body>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card" on:click={() => createScreenModal.show("blank")}>
|
||||
<div class="image">
|
||||
<img alt="" src={blankImage} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Blank screen</Body>
|
||||
<Body size="XS">Add an empty blank screen</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" on:click={() => createScreenModal.show("table")}>
|
||||
<div class="image">
|
||||
<img alt="" src={tableImage} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Table</Body>
|
||||
<Body size="XS">View, edit and delete rows on a table</Body>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CreationPage>
|
||||
</div>
|
||||
|
||||
<CreateScreenModal bind:this={createScreenModal} />
|
||||
|
||||
<style>
|
||||
.page {
|
||||
padding: 28px 40px 40px 40px;
|
||||
}
|
||||
|
||||
.subHeading :global(p) {
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 36px;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
max-width: 235px;
|
||||
transition: filter 150ms;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
filter: brightness(1.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image {
|
||||
border-radius: 4px 4px 0 0;
|
||||
width: 100%;
|
||||
max-height: 127px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text {
|
||||
border: 1px solid var(--grey-4);
|
||||
border-radius: 0 0 4px 4px;
|
||||
padding: 8px 16px 13px 16px;
|
||||
}
|
||||
|
||||
.text :global(p:nth-child(1)) {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.text :global(p:nth-child(2)) {
|
||||
color: var(--grey-6);
|
||||
}
|
||||
</style>
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$redirect("../screens")
|
||||
</script>
|
|
@ -1,11 +1,8 @@
|
|||
<script>
|
||||
import { IconSideNav, IconSideNavItem } from "@budibase/bbui"
|
||||
import * as routify from "@roxi/routify"
|
||||
import AppPanel from "./_components/AppPanel.svelte"
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import { store, selectedScreen } from "builderStore"
|
||||
import { store } from "builderStore"
|
||||
import { onDestroy } from "svelte"
|
||||
const { isActive, goto } = routify
|
||||
|
||||
$: screenId = $store.selectedScreenId
|
||||
$: store.actions.websocket.selectResource(screenId)
|
||||
|
@ -23,83 +20,4 @@
|
|||
onDestroy(stopSyncing)
|
||||
</script>
|
||||
|
||||
<div class="design">
|
||||
<div class="icon-nav">
|
||||
<IconSideNav>
|
||||
<IconSideNavItem
|
||||
icon="WebPage"
|
||||
tooltip="Screens"
|
||||
active={$isActive("./screens")}
|
||||
on:click={() => $goto("./screens")}
|
||||
/>
|
||||
<IconSideNavItem
|
||||
icon="ViewList"
|
||||
tooltip="Components"
|
||||
active={$isActive("./components")}
|
||||
on:click={() => $goto("./components")}
|
||||
/>
|
||||
<IconSideNavItem
|
||||
icon="Brush"
|
||||
tooltip="Theme"
|
||||
active={$isActive("./theme")}
|
||||
on:click={() => $goto("./theme")}
|
||||
/>
|
||||
<IconSideNavItem
|
||||
icon="Link"
|
||||
tooltip="Navigation"
|
||||
active={$isActive("./navigation")}
|
||||
on:click={() => $goto("./navigation")}
|
||||
/>
|
||||
{#if $store.layouts?.length}
|
||||
<IconSideNavItem
|
||||
icon="Experience"
|
||||
tooltip="Layouts"
|
||||
active={$isActive("./layouts")}
|
||||
on:click={() => $goto("./layouts")}
|
||||
/>
|
||||
{/if}
|
||||
</IconSideNav>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
{#if $selectedScreen}
|
||||
<slot />
|
||||
<AppPanel />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.design {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
height: 0;
|
||||
}
|
||||
.icon-nav {
|
||||
background: var(--background);
|
||||
border-right: var(--border-light);
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
/*
|
||||
This is hacky, yes, but it's the only way to prevent routify from
|
||||
remounting the iframe on route changes.
|
||||
*/
|
||||
.content :global(> *:last-child) {
|
||||
order: 1;
|
||||
}
|
||||
.content :global(> *:first-child) {
|
||||
order: 0;
|
||||
}
|
||||
.content :global(> *:nth-child(2)) {
|
||||
order: 2;
|
||||
}
|
||||
</style>
|
||||
<slot />
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
<script>
|
||||
import DevicePreviewSelect from "./DevicePreviewSelect.svelte"
|
||||
import AppPreview from "./AppPreview.svelte"
|
||||
import { store, sortedScreens, screenHistoryStore } from "builderStore"
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { store, screenHistoryStore } from "builderStore"
|
||||
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
|
||||
import { isActive } from "@roxi/routify"
|
||||
</script>
|
||||
|
@ -11,22 +9,11 @@
|
|||
<div class="app-panel">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<Select
|
||||
placeholder={null}
|
||||
options={$sortedScreens}
|
||||
getOptionLabel={x => x.routing.route}
|
||||
getOptionValue={x => x._id}
|
||||
getOptionColour={x => RoleUtils.getRoleColour(x.routing.roleId)}
|
||||
value={$store.selectedScreenId}
|
||||
on:change={e => store.actions.screens.select(e.detail)}
|
||||
quiet
|
||||
autoWidth
|
||||
/>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
{#if $isActive("./screens") || $isActive("./components")}
|
||||
<UndoRedoControl store={screenHistoryStore} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="header-right">
|
||||
{#if $store.clientFeatures.devicePreview}
|
||||
<DevicePreviewSelect />
|
||||
{/if}
|
||||
|
@ -47,37 +34,24 @@
|
|||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-m);
|
||||
padding: var(--spacing-l) var(--spacing-xl);
|
||||
padding: 9px var(--spacing-m);
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-l);
|
||||
margin: 0 2px;
|
||||
z-index: 1;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.header-left :global(div) {
|
||||
border-right: none;
|
||||
}
|
||||
.header-left,
|
||||
.header-right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.header-left {
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
}
|
||||
.header-left :global(> *) {
|
||||
max-width: 100%;
|
||||
}
|
||||
.header-left :global(.spectrum-Picker) {
|
||||
font-weight: 600;
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
}
|
|
@ -1,13 +1,7 @@
|
|||
<script>
|
||||
import { get } from "svelte/store"
|
||||
import { onMount, onDestroy } from "svelte"
|
||||
import {
|
||||
store,
|
||||
selectedComponent,
|
||||
selectedScreen,
|
||||
selectedLayout,
|
||||
currentAsset,
|
||||
} from "builderStore"
|
||||
import { store, selectedScreen, currentAsset } from "builderStore"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import {
|
||||
ProgressCircle,
|
||||
|
@ -20,12 +14,10 @@
|
|||
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
|
||||
import { findComponent, findComponentPath } from "builderStore/componentUtils"
|
||||
import { isActive, goto } from "@roxi/routify"
|
||||
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
|
||||
|
||||
let iframe
|
||||
let layout
|
||||
let screen
|
||||
let selectedComponentId
|
||||
let confirmDeleteDialog
|
||||
let idToDelete
|
||||
let loading = true
|
||||
|
@ -39,36 +31,11 @@
|
|||
BUDIBASE: "type",
|
||||
}
|
||||
|
||||
const placeholderScreen = new Screen()
|
||||
.name("Screen Placeholder")
|
||||
.route("/")
|
||||
.component("@budibase/standard-components/screenslot")
|
||||
.instanceName("Content Placeholder")
|
||||
.normalStyle({ flex: "1 1 auto" })
|
||||
.json()
|
||||
|
||||
// Extract data to pass to the iframe
|
||||
$: {
|
||||
// If viewing legacy layouts, always show the custom layout
|
||||
if ($isActive("./layouts")) {
|
||||
screen = placeholderScreen
|
||||
layout = $selectedLayout
|
||||
} else {
|
||||
screen = $selectedScreen
|
||||
layout = $store.layouts.find(layout => layout._id === screen?.layoutId)
|
||||
}
|
||||
}
|
||||
$: screen = $selectedScreen
|
||||
|
||||
// Determine selected component ID
|
||||
$: {
|
||||
if ($isActive("./components")) {
|
||||
selectedComponentId = $store.selectedComponentId
|
||||
} else if ($isActive("./navigation")) {
|
||||
selectedComponentId = "navigation"
|
||||
} else {
|
||||
selectedComponentId = null
|
||||
}
|
||||
}
|
||||
$: selectedComponentId = $store.selectedComponentId
|
||||
|
||||
$: previewData = {
|
||||
appId: $store.appId,
|
||||
|
@ -98,9 +65,7 @@
|
|||
$: refreshContent(json)
|
||||
|
||||
// Determine if the add component menu is active
|
||||
$: isAddingComponent = $isActive(
|
||||
`./components/${$selectedComponent?._id}/new`
|
||||
)
|
||||
$: isAddingComponent = $isActive(`./new`)
|
||||
|
||||
// Register handler to send custom to the preview
|
||||
$: sendPreviewEvent = (name, payload) => {
|
||||
|
@ -152,9 +117,6 @@
|
|||
error = event.error || "An unknown error occurred"
|
||||
} else if (type === "select-component" && data.id) {
|
||||
$store.selectedComponentId = data.id
|
||||
if (!$isActive("./components")) {
|
||||
$goto("./components")
|
||||
}
|
||||
} else if (type === "update-prop") {
|
||||
await store.actions.components.updateSetting(data.prop, data.value)
|
||||
} else if (type === "update-styles") {
|
||||
|
@ -195,9 +157,7 @@
|
|||
await store.actions.components.paste(destination, data.mode)
|
||||
}
|
||||
} else if (type === "click-nav") {
|
||||
if (!$isActive("./navigation")) {
|
||||
$goto("./navigation")
|
||||
}
|
||||
$store.selectedComponentId = "navigation"
|
||||
} else if (type === "request-add-component") {
|
||||
toggleAddComponent()
|
||||
} else if (type === "highlight-setting") {
|
||||
|
@ -248,10 +208,9 @@
|
|||
|
||||
const toggleAddComponent = () => {
|
||||
if (isAddingComponent) {
|
||||
$goto(`../${$selectedScreen._id}/components/${$selectedComponent?._id}`)
|
||||
$goto(`../`)
|
||||
} else {
|
||||
const id = $selectedComponent?._id || $selectedScreen?.props?._id
|
||||
$goto(`../${$selectedScreen._id}/components/${id}/new`)
|
||||
$goto(`./new`)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@
|
|||
import { goto, isActive } from "@roxi/routify"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { isBuilderInputFocused } from "helpers"
|
||||
|
||||
let confirmDeleteDialog
|
||||
let confirmEjectDialog
|
||||
|
@ -55,6 +54,9 @@
|
|||
},
|
||||
["Escape"]: () => {
|
||||
if ($isActive("./new")) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
$goto("./")
|
||||
}
|
||||
},
|
||||
|
@ -85,10 +87,13 @@
|
|||
const handler = keyHandlers[key]
|
||||
if (!handler) {
|
||||
return false
|
||||
} else if (event) {
|
||||
}
|
||||
|
||||
if (event && key !== "Escape") {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
return await handler(component)
|
||||
} catch (error) {
|
||||
notifications.error(error || "Error handling key press")
|
||||
|
@ -101,7 +106,13 @@
|
|||
return
|
||||
}
|
||||
// Ignore events when typing
|
||||
if (isBuilderInputFocused(e)) {
|
||||
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||
const inCodeEditor =
|
||||
document.activeElement?.classList?.contains("cm-content")
|
||||
if (
|
||||
(inCodeEditor || ["input", "textarea"].indexOf(activeTag) !== -1) &&
|
||||
e.key !== "Escape"
|
||||
) {
|
||||
return
|
||||
}
|
||||
// Key events are always for the selected component
|
|
@ -9,7 +9,7 @@
|
|||
if (!bounds) {
|
||||
return
|
||||
}
|
||||
const sidebarWidth = 259
|
||||
const sidebarWidth = 310
|
||||
const navItemHeight = 32
|
||||
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
|
||||
let scrollBounds = scrollRef.getBoundingClientRect()
|
||||
|
@ -64,6 +64,7 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
on:scroll
|
||||
bind:this={scrollRef}
|
||||
on:drop={onDrop}
|
||||
ondragover="return false"
|
||||
|
@ -74,9 +75,23 @@
|
|||
|
||||
<style>
|
||||
div {
|
||||
padding: var(--spacing-xl) 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
div::-webkit-scrollbar-track {
|
||||
background: var(--background-alt);
|
||||
}
|
||||
|
||||
div::-webkit-scrollbar {
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
div::-webkit-scrollbar-thumb {
|
||||
background-color: var(--grey-3);
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--background-alt);
|
||||
border-width: 5px 5px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,156 @@
|
|||
<script>
|
||||
import { notifications, Icon, Body } from "@budibase/bbui"
|
||||
import { isActive, goto } from "@roxi/routify"
|
||||
import { store, selectedScreen, userSelectedResourceMap } from "builderStore"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import ComponentTree from "./ComponentTree.svelte"
|
||||
import { dndStore } from "./dndStore.js"
|
||||
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
||||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||
import { DropPosition } from "./dndStore"
|
||||
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
|
||||
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
|
||||
|
||||
let scrolling = false
|
||||
|
||||
const toNewComponentRoute = () => {
|
||||
if ($isActive("./new")) {
|
||||
return
|
||||
} else {
|
||||
$goto(`./new`)
|
||||
}
|
||||
}
|
||||
|
||||
const onDrop = async () => {
|
||||
try {
|
||||
await dndStore.actions.drop()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error saving component")
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = e => {
|
||||
if (e.target.scrollTop === 0) {
|
||||
scrolling = false
|
||||
} else {
|
||||
scrolling = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="components">
|
||||
<div class="header" class:headerScrolling={scrolling}>
|
||||
<Body size="S">Components</Body>
|
||||
<div on:click={toNewComponentRoute} class="addButton">
|
||||
<Icon name="Add" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-panel">
|
||||
<ComponentScrollWrapper on:scroll={handleScroll}>
|
||||
<ul>
|
||||
<li>
|
||||
<NavItem
|
||||
text="Screen"
|
||||
indentLevel={0}
|
||||
selected={$store.selectedComponentId === "screen"}
|
||||
opened
|
||||
scrollable
|
||||
icon="WebPage"
|
||||
on:drop={onDrop}
|
||||
on:click={() => {
|
||||
$store.selectedComponentId = "screen"
|
||||
}}
|
||||
id={`component-screen`}
|
||||
selectedBy={$userSelectedResourceMap["screen"]}
|
||||
>
|
||||
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
||||
</NavItem>
|
||||
<NavItem
|
||||
text="Navigation"
|
||||
indentLevel={0}
|
||||
selected={$store.selectedComponentId === "navigation"}
|
||||
opened
|
||||
scrollable
|
||||
icon={$selectedScreen.showNavigation
|
||||
? "Visibility"
|
||||
: "VisibilityOff"}
|
||||
on:drop={onDrop}
|
||||
on:click={() => {
|
||||
$store.selectedComponentId = "navigation"
|
||||
}}
|
||||
id={`component-nav`}
|
||||
selectedBy={$userSelectedResourceMap["navigation"]}
|
||||
/>
|
||||
<ComponentTree
|
||||
level={0}
|
||||
components={$selectedScreen?.props._children}
|
||||
/>
|
||||
|
||||
<!-- Show drop indicators for the target and the parent -->
|
||||
{#if $dndStore.dragging && $dndStore.valid}
|
||||
<DNDPositionIndicator
|
||||
component={$dndStore.target}
|
||||
position={$dndStore.dropPosition}
|
||||
/>
|
||||
{#if $dndStore.dropPosition !== DropPosition.INSIDE}
|
||||
<DNDPositionIndicator
|
||||
component={$dndStore.targetParent}
|
||||
position={DropPosition.INSIDE}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</li>
|
||||
</ul>
|
||||
</ComponentScrollWrapper>
|
||||
</div>
|
||||
<ComponentKeyHandler />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.components {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 15px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: border-bottom 300ms;
|
||||
}
|
||||
|
||||
.headerScrolling {
|
||||
border-bottom: 2px solid var(--grey-2);
|
||||
}
|
||||
|
||||
.components :global(.nav-item) {
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
margin-left: auto;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.list-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
ul,
|
||||
li {
|
||||
min-width: max-content;
|
||||
}
|
||||
</style>
|
|
@ -26,7 +26,7 @@
|
|||
<StatusLight square {color} />
|
||||
{#if showTooltip}
|
||||
<div class="tooltip">
|
||||
<Tooltip textWrapping text={tooltip} direction="left" />
|
||||
<Tooltip textWrapping text={tooltip} direction="right" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -38,13 +38,11 @@
|
|||
.tooltip {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 8px);
|
||||
transform: translateX(-100%) translateY(-50%);
|
||||
bottom: -5px;
|
||||
left: 13px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
width: 200px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tooltip :global(.spectrum-Tooltip) {
|
|
@ -0,0 +1,233 @@
|
|||
<script>
|
||||
import { Icon, Layout, Body } from "@budibase/bbui"
|
||||
import { store, sortedScreens, userSelectedResourceMap } from "builderStore"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import RoleIndicator from "./RoleIndicator.svelte"
|
||||
import DropdownMenu from "./DropdownMenu.svelte"
|
||||
import NewScreen from "components/design/NewScreen/index.svelte"
|
||||
|
||||
let newScreen = false
|
||||
let search = false
|
||||
let searchValue = ""
|
||||
let searchInput
|
||||
let screensContainer
|
||||
let scrolling = false
|
||||
|
||||
$: filteredScreens = getFilteredScreens($sortedScreens, searchValue)
|
||||
|
||||
const openSearch = () => {
|
||||
search = true
|
||||
searchInput.focus()
|
||||
screensContainer.scroll({ top: 0, behavior: "smooth" })
|
||||
}
|
||||
|
||||
const closeSearch = () => {
|
||||
search = false
|
||||
searchValue = ""
|
||||
}
|
||||
|
||||
const getFilteredScreens = (screens, search) => {
|
||||
return screens.filter(screen => {
|
||||
const searchMatch = !search || screen.routing.route.includes(search)
|
||||
return searchMatch
|
||||
})
|
||||
}
|
||||
|
||||
const handleAddButton = () => {
|
||||
if (search) {
|
||||
closeSearch()
|
||||
} else {
|
||||
newScreen = true
|
||||
}
|
||||
}
|
||||
|
||||
const onKeyDown = e => {
|
||||
if (e.key === "Escape") {
|
||||
if (newScreen) {
|
||||
newScreen = false
|
||||
} else {
|
||||
closeSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = e => {
|
||||
if (e.target.scrollTop === 0) {
|
||||
scrolling = false
|
||||
} else {
|
||||
scrolling = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
<div class="screens" class:screenSearch={search}>
|
||||
<div class="header" class:headerScrolling={scrolling}>
|
||||
<input
|
||||
readonly={!search}
|
||||
bind:value={searchValue}
|
||||
bind:this={searchInput}
|
||||
class="input"
|
||||
placeholder="Search for screens"
|
||||
/>
|
||||
<div class="title" class:hide={search}>
|
||||
<Body size="S">Screens</Body>
|
||||
</div>
|
||||
<div on:click={openSearch} class="searchButton" class:hide={search}>
|
||||
<Icon name="Search" />
|
||||
</div>
|
||||
<div
|
||||
on:click={handleAddButton}
|
||||
class="addButton"
|
||||
class:closeButton={search}
|
||||
>
|
||||
<Icon name="Add" />
|
||||
</div>
|
||||
</div>
|
||||
<div on:scroll={handleScroll} bind:this={screensContainer} class="content">
|
||||
{#each filteredScreens as screen (screen._id)}
|
||||
<NavItem
|
||||
icon={screen.routing.homeScreen ? "Home" : null}
|
||||
indentLevel={0}
|
||||
selected={$store.selectedScreenId === screen._id}
|
||||
text={screen.routing.route}
|
||||
on:click={() => store.actions.screens.select(screen._id)}
|
||||
rightAlignIcon
|
||||
showTooltip
|
||||
selectedBy={$userSelectedResourceMap[screen._id]}
|
||||
>
|
||||
<DropdownMenu screenId={screen._id} />
|
||||
<RoleIndicator slot="icon" roleId={screen.routing.roleId} />
|
||||
</NavItem>
|
||||
{/each}
|
||||
</div>
|
||||
{#if !filteredScreens?.length}
|
||||
<Layout paddingY="" paddingX="L">
|
||||
<Body size="S">
|
||||
There aren't any screens matching the current filters
|
||||
</Body>
|
||||
</Layout>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="newScreen" class:newScreenVisible={newScreen}>
|
||||
<NewScreen onClose={() => (newScreen = false)} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.newScreen {
|
||||
width: 100vw;
|
||||
height: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
background-color: var(--background);
|
||||
z-index: 2;
|
||||
transition: height 350ms ease-in-out;
|
||||
}
|
||||
|
||||
.newScreenVisible {
|
||||
height: calc(100vh - 58px);
|
||||
}
|
||||
.screens {
|
||||
height: 196px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: height 300ms;
|
||||
}
|
||||
|
||||
.screenSearch {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
height: 51px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 12px 0 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: border-bottom 300ms;
|
||||
}
|
||||
|
||||
.headerScrolling {
|
||||
border-bottom: 2px solid var(--grey-2);
|
||||
}
|
||||
|
||||
.input {
|
||||
position: absolute;
|
||||
padding-left: 12px;
|
||||
color: var(--ink);
|
||||
background-color: var(--background-alt);
|
||||
border: none;
|
||||
font-size: var(--spectrum-alias-font-size-default);
|
||||
width: 260px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-left: 12px;
|
||||
flex: 1;
|
||||
opacity: 1;
|
||||
transition: opacity 300ms;
|
||||
background-color: var(--background-alt);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-y: scroll;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.content::-webkit-scrollbar-track {
|
||||
background: var(--background-alt);
|
||||
}
|
||||
|
||||
.content::-webkit-scrollbar {
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.content::-webkit-scrollbar-thumb {
|
||||
background-color: var(--grey-3);
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--background-alt);
|
||||
border-width: 5px 5px;
|
||||
}
|
||||
|
||||
.screens :global(.nav-item) {
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
opacity: 1;
|
||||
transition: opacity 300ms;
|
||||
}
|
||||
|
||||
.hide {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
transition: transform 300ms;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,24 @@
|
|||
<script>
|
||||
import Screens from "./Screens/index.svelte"
|
||||
import Components from "./Components/index.svelte"
|
||||
</script>
|
||||
|
||||
<div class="panel">
|
||||
<Screens />
|
||||
<div class="divider" />
|
||||
<Components />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.panel {
|
||||
width: 310px;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--grey-2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-bottom: 1px solid var(--grey-2);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,260 @@
|
|||
<script>
|
||||
import { get } from "svelte/store"
|
||||
import {
|
||||
Detail,
|
||||
Toggle,
|
||||
Body,
|
||||
Icon,
|
||||
Button,
|
||||
ColorPicker,
|
||||
Input,
|
||||
Label,
|
||||
ActionGroup,
|
||||
ActionButton,
|
||||
Checkbox,
|
||||
notifications,
|
||||
Select,
|
||||
} from "@budibase/bbui"
|
||||
import { selectedScreen, store } from "builderStore"
|
||||
import { DefaultAppTheme } from "constants"
|
||||
|
||||
const updateShowNavigation = async e => {
|
||||
await store.actions.screens.updateSetting(
|
||||
get(selectedScreen),
|
||||
"showNavigation",
|
||||
e.detail
|
||||
)
|
||||
}
|
||||
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>
|
||||
|
||||
<div class="panel">
|
||||
<div class="header">
|
||||
<div class="icon">
|
||||
<Icon
|
||||
name={$selectedScreen.showNavigation ? "Visibility" : "VisibilityOff"}
|
||||
/>
|
||||
</div>
|
||||
<Body>Navigation</Body>
|
||||
</div>
|
||||
<div class="divider" />
|
||||
<div class="generalSection">
|
||||
<div class="subheading">
|
||||
<Detail>General</Detail>
|
||||
</div>
|
||||
<div class="toggle">
|
||||
<Toggle
|
||||
on:change={updateShowNavigation}
|
||||
value={$selectedScreen.showNavigation}
|
||||
/>
|
||||
<Body size="S">Show nav on this screen</Body>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider" />
|
||||
<div class="customizeSection">
|
||||
<div class="subheading">
|
||||
<Detail>Customize</Detail>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="infoHeader">
|
||||
<Icon name="InfoOutline" />
|
||||
<Body size="S">These settings apply to all screens</Body>
|
||||
</div>
|
||||
</div>
|
||||
<div class="configureLinks">
|
||||
<Button cta>Configure Links</Button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="label">
|
||||
<Label size="M">Position</Label>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
{#if $store.navigation.navigation === "Top"}
|
||||
<div class="label">
|
||||
<Label size="M">Sticky header</Label>
|
||||
</div>
|
||||
<Checkbox
|
||||
value={$store.navigation.sticky}
|
||||
on:change={e => update("sticky", e.detail)}
|
||||
/>
|
||||
<div class="label">
|
||||
<Label size="M">Width</Label>
|
||||
</div>
|
||||
<Select
|
||||
options={["Max", "Large", "Medium", "Small"]}
|
||||
plaveholder={null}
|
||||
value={$store.navigation.navWidth}
|
||||
on:change={e => update("navWidth", e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
<div class="label">
|
||||
<Label size="M">Show logo</Label>
|
||||
</div>
|
||||
<Checkbox
|
||||
value={!$store.navigation.hideLogo}
|
||||
on:change={e => update("hideLogo", !e.detail)}
|
||||
/>
|
||||
{#if !$store.navigation.hideLogo}
|
||||
<div class="label">
|
||||
<Label size="M">Logo URL</Label>
|
||||
</div>
|
||||
<Input
|
||||
value={$store.navigation.logoUrl}
|
||||
on:change={e => update("logoUrl", e.detail)}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{/if}
|
||||
<div class="label">
|
||||
<Label size="M">Show title</Label>
|
||||
</div>
|
||||
<Checkbox
|
||||
value={!$store.navigation.hideTitle}
|
||||
on:change={e => update("hideTitle", !e.detail)}
|
||||
/>
|
||||
{#if !$store.navigation.hideTitle}
|
||||
<div class="label">
|
||||
<Label size="M">Title</Label>
|
||||
</div>
|
||||
<Input
|
||||
value={$store.navigation.title}
|
||||
on:change={e => update("title", e.detail)}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{/if}
|
||||
<div class="label">
|
||||
<Label>Background</Label>
|
||||
</div>
|
||||
<ColorPicker
|
||||
spectrumTheme={$store.theme}
|
||||
value={$store.navigation.navBackground || DefaultAppTheme.navBackground}
|
||||
on:change={e => update("navBackground", e.detail)}
|
||||
/>
|
||||
<div class="label">
|
||||
<Label>Text</Label>
|
||||
</div>
|
||||
<ColorPicker
|
||||
spectrumTheme={$store.theme}
|
||||
value={$store.navigation.navTextColor || DefaultAppTheme.navTextColor}
|
||||
on:change={e => update("navTextColor", e.detail)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.panel {
|
||||
overflow: hidden;
|
||||
width: 310px;
|
||||
background: var(--background);
|
||||
border-left: 2px solid var(--grey-2);
|
||||
}
|
||||
|
||||
.generalSection {
|
||||
padding: 13px 13px 25px;
|
||||
}
|
||||
|
||||
.customizeSection {
|
||||
padding: 13px 13px 25px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 16px 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header :global(p) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.subheading {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subheading :global(p) {
|
||||
color: var(--grey-6);
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--grey-6);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.icon :global(svg) {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 1px solid var(--grey-3);
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 90px 1fr;
|
||||
align-items: start;
|
||||
transition: background 130ms ease-out, border-color 130ms ease-out;
|
||||
border-left: 4px solid transparent;
|
||||
margin: 0 calc(-1 * var(--spacing-xl));
|
||||
padding: 0 var(--spacing-xl) 0 calc(var(--spacing-xl) - 4px);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 16px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: var(--background-alt);
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.infoHeader {
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.infoHeader :global(svg) {
|
||||
margin-right: 5px;
|
||||
color: var(--grey-6);
|
||||
}
|
||||
|
||||
.infoHeader :global(p) {
|
||||
color: var(--grey-7);
|
||||
}
|
||||
|
||||
.configureLinks :global(button) {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -1,90 +0,0 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import ComponentTree from "./ComponentTree.svelte"
|
||||
import { dndStore } from "./dndStore.js"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { store, selectedScreen, userSelectedResourceMap } from "builderStore"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
||||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||
import { DropPosition } from "./dndStore"
|
||||
import { notifications, Button } from "@budibase/bbui"
|
||||
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
|
||||
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
|
||||
|
||||
const onDrop = async () => {
|
||||
try {
|
||||
await dndStore.actions.drop()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error saving component")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel title="Components" showExpandIcon borderRight>
|
||||
<div class="add-component">
|
||||
<Button on:click={() => $goto("./new")} cta>Add component</Button>
|
||||
</div>
|
||||
<ComponentScrollWrapper>
|
||||
<ul>
|
||||
<li>
|
||||
<NavItem
|
||||
text="Screen"
|
||||
indentLevel={0}
|
||||
selected={$store.selectedComponentId === $selectedScreen?.props._id}
|
||||
opened
|
||||
scrollable
|
||||
icon="WebPage"
|
||||
on:drop={onDrop}
|
||||
on:click={() => {
|
||||
$store.selectedComponentId = $selectedScreen?.props._id
|
||||
}}
|
||||
id={`component-${$selectedScreen?.props._id}`}
|
||||
selectedBy={$userSelectedResourceMap[$selectedScreen?.props._id]}
|
||||
>
|
||||
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
||||
</NavItem>
|
||||
<ComponentTree
|
||||
level={0}
|
||||
components={$selectedScreen?.props._children}
|
||||
/>
|
||||
|
||||
<!-- Show drop indicators for the target and the parent -->
|
||||
{#if $dndStore.dragging && $dndStore.valid}
|
||||
<DNDPositionIndicator
|
||||
component={$dndStore.target}
|
||||
position={$dndStore.dropPosition}
|
||||
/>
|
||||
{#if $dndStore.dropPosition !== DropPosition.INSIDE}
|
||||
<DNDPositionIndicator
|
||||
component={$dndStore.targetParent}
|
||||
position={DropPosition.INSIDE}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</li>
|
||||
</ul>
|
||||
</ComponentScrollWrapper>
|
||||
</Panel>
|
||||
<ComponentKeyHandler />
|
||||
|
||||
<style>
|
||||
.add-component {
|
||||
padding: var(--spacing-xl) var(--spacing-l);
|
||||
padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
ul,
|
||||
li {
|
||||
min-width: max-content;
|
||||
}
|
||||
</style>
|
|
@ -1,14 +1,19 @@
|
|||
<script>
|
||||
import LeftPanel from "./_components/LeftPanel/index.svelte"
|
||||
import AppPanel from "./_components/AppPanel.svelte"
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import { store, selectedScreen } from "builderStore"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { onDestroy } from "svelte"
|
||||
import { findComponent } from "builderStore/componentUtils"
|
||||
import ComponentListPanel from "./_components/navigation/ComponentListPanel.svelte"
|
||||
import ComponentSettingsPanel from "./_components/settings/ComponentSettingsPanel.svelte"
|
||||
import ComponentSettingsPanel from "./_components/Component/ComponentSettingsPanel.svelte"
|
||||
import NavigationPanel from "./_components/Navigation/index.svelte"
|
||||
import ScreenSettingsPanel from "./_components/Screen/SettingsPanel.svelte"
|
||||
|
||||
$: componentId = $store.selectedComponentId
|
||||
$: store.actions.websocket.selectResource(componentId)
|
||||
$: params = routify.params
|
||||
$: routeComponentId = $params.componentId
|
||||
|
||||
const cleanUrl = url => {
|
||||
// Strip trailing slashes
|
||||
|
@ -22,11 +27,18 @@
|
|||
return { url }
|
||||
}
|
||||
|
||||
const validate = id => {
|
||||
if (id === "screen") return true
|
||||
if (id === "navigation") return true
|
||||
|
||||
return !!findComponent($selectedScreen.props, id)
|
||||
}
|
||||
|
||||
// Keep URL and state in sync for selected component ID
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "componentId",
|
||||
stateKey: "selectedComponentId",
|
||||
validate: id => !!findComponent($selectedScreen.props, id),
|
||||
validate,
|
||||
fallbackUrl: "../",
|
||||
store,
|
||||
routify,
|
||||
|
@ -36,6 +48,38 @@
|
|||
onDestroy(stopSyncing)
|
||||
</script>
|
||||
|
||||
<ComponentListPanel />
|
||||
<ComponentSettingsPanel />
|
||||
<slot />
|
||||
<div class="design">
|
||||
<div class="content">
|
||||
{#if $selectedScreen}
|
||||
<LeftPanel />
|
||||
<AppPanel />
|
||||
{#if routeComponentId === "screen"}
|
||||
<ScreenSettingsPanel />
|
||||
{:else if routeComponentId === "navigation"}
|
||||
<NavigationPanel />
|
||||
{:else}
|
||||
<ComponentSettingsPanel />
|
||||
{/if}
|
||||
<slot />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.design {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,18 +1,8 @@
|
|||
<script>
|
||||
import { selectedScreen, selectedComponent } from "builderStore"
|
||||
import { onMount } from "svelte"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
onMount(() => {
|
||||
if ($selectedComponent) {
|
||||
// Navigate to the selected component if one exists
|
||||
$redirect(`./${$selectedComponent._id}`)
|
||||
} else if ($selectedScreen) {
|
||||
// Otherwise the screen slot if a screen exists
|
||||
$redirect(`./${$selectedScreen.props._id}`)
|
||||
} else {
|
||||
// Otherwise go up so we can select a new valid screen
|
||||
$redirect("../")
|
||||
}
|
||||
$redirect(`./screen`)
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$redirect("./screens")
|
||||
onMount(() => {
|
||||
$redirect("./components/screen")
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
|
||||
|
||||
export let layout
|
||||
|
||||
let confirmDeleteDialog
|
||||
|
||||
const deleteLayout = async () => {
|
||||
try {
|
||||
await store.actions.layouts.delete(layout)
|
||||
notifications.success("Layout deleted successfully")
|
||||
} catch (err) {
|
||||
notifications.error(err?.message || "Error deleting layout")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionMenu>
|
||||
<div slot="control" class="icon">
|
||||
<Icon size="S" hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
||||
</ActionMenu>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
title="Confirm Deletion"
|
||||
body={"Are you sure you wish to delete this layout?"}
|
||||
okText="Delete layout"
|
||||
onOk={deleteLayout}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
|
@ -1,29 +0,0 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import { store } from "builderStore"
|
||||
import LayoutDropdownMenu from "./LayoutDropdownMenu.svelte"
|
||||
</script>
|
||||
|
||||
<Panel title="Layouts" borderRight>
|
||||
<div class="layouts">
|
||||
{#each $store.layouts as layout (layout._id)}
|
||||
<NavItem
|
||||
icon="Experience"
|
||||
indentLevel={0}
|
||||
selected={$store.selectedLayoutId === layout._id}
|
||||
text={layout.name}
|
||||
on:click={() => store.actions.layouts.select(layout._id)}
|
||||
>
|
||||
<LayoutDropdownMenu {layout} />
|
||||
</NavItem>
|
||||
{/each}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<style>
|
||||
.layouts {
|
||||
margin-top: var(--spacing-xl);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
|
@ -1,53 +0,0 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { store, selectedLayout } from "builderStore"
|
||||
import { Layout, Body, Button, Banner, notifications } from "@budibase/bbui"
|
||||
import { Component } from "builderStore/store/screenTemplates/utils/Component"
|
||||
|
||||
const copyLayout = () => {
|
||||
// Build an outer container component to put layout contents inside
|
||||
let container = new Component("@budibase/standard-components/container")
|
||||
.instanceName($selectedLayout.name)
|
||||
.customProps({
|
||||
gap: "M",
|
||||
direction: "column",
|
||||
hAlign: "stretch",
|
||||
vAlign: "top",
|
||||
size: "shrink",
|
||||
})
|
||||
.json()
|
||||
|
||||
// Attach layout components
|
||||
container._children = $selectedLayout.props._children
|
||||
|
||||
// Replace the screenslot component with a container. This is better than
|
||||
// simply removing it as it still shows its position.
|
||||
container = JSON.parse(
|
||||
JSON.stringify(container).replace(
|
||||
"@budibase/standard-components/screenslot",
|
||||
"@budibase/standard-components/container"
|
||||
)
|
||||
)
|
||||
|
||||
// Copy new component structure
|
||||
store.actions.components.copy(container)
|
||||
notifications.success("Components copied successfully")
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel title={$selectedLayout?.name} icon="Experience" borderLeft wide>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Banner type="warning" showCloseButton={false}>
|
||||
Custom layouts are being deprecated. They will be removed in a future
|
||||
release.
|
||||
</Banner>
|
||||
<Body size="S">
|
||||
You can save the content of this layout by pressing the button below.
|
||||
</Body>
|
||||
<Body size="S">
|
||||
This will copy all components inside your layout, which you can then paste
|
||||
into a screen.
|
||||
</Body>
|
||||
<Button cta on:click={copyLayout}>Copy components</Button>
|
||||
</Layout>
|
||||
</Panel>
|
|
@ -1,20 +0,0 @@
|
|||
<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 />
|
|
@ -1,7 +0,0 @@
|
|||
<script>
|
||||
import LayoutListPanel from "./_components/LayoutListPanel.svelte"
|
||||
import LayoutSettingsPanel from "./_components/LayoutSettingsPanel.svelte"
|
||||
</script>
|
||||
|
||||
<LayoutListPanel />
|
||||
<LayoutSettingsPanel />
|
|
@ -1,12 +0,0 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$: {
|
||||
if (!$store.layouts?.length) {
|
||||
$redirect("../")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -1,12 +0,0 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import { onMount } from "svelte"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
onMount(() => {
|
||||
if ($store.layouts?.length) {
|
||||
$redirect(`./${$store.layouts[0]._id}`)
|
||||
}
|
||||
// The redirection when no layouts exist is handled by the routify layout
|
||||
})
|
||||
</script>
|
|
@ -1,176 +0,0 @@
|
|||
<script>
|
||||
import Pane from "components/design/Pane.svelte"
|
||||
import {
|
||||
ColorPicker,
|
||||
Input,
|
||||
Label,
|
||||
ActionGroup,
|
||||
ActionButton,
|
||||
Checkbox,
|
||||
notifications,
|
||||
Icon,
|
||||
Body,
|
||||
Button,
|
||||
Select,
|
||||
} from "@budibase/bbui"
|
||||
import { DefaultAppTheme } from "constants"
|
||||
import { store } from "builderStore"
|
||||
|
||||
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>
|
||||
|
||||
<Pane title="Customize">
|
||||
<div class="info">
|
||||
<div class="infoHeader">
|
||||
<Icon name="InfoOutline" />
|
||||
<Body size="S">CHANGES WILL APPLY TO ALL SCREENS</Body>
|
||||
</div>
|
||||
<Body>
|
||||
Your navigation is configured for all the screens within your app.
|
||||
</Body>
|
||||
</div>
|
||||
<div class="configureLinks">
|
||||
<Button cta>Configure Links</Button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="label">
|
||||
<Label size="M">Position</Label>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
{#if $store.navigation.navigation === "Top"}
|
||||
<div class="label">
|
||||
<Label size="M">Sticky header</Label>
|
||||
</div>
|
||||
<Checkbox
|
||||
value={$store.navigation.sticky}
|
||||
on:change={e => update("sticky", e.detail)}
|
||||
/>
|
||||
<div class="label">
|
||||
<Label size="M">Width</Label>
|
||||
</div>
|
||||
<Select
|
||||
options={["Max", "Large", "Medium", "Small"]}
|
||||
plaveholder={null}
|
||||
value={$store.navigation.navWidth}
|
||||
on:change={e => update("navWidth", e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
<div class="label">
|
||||
<Label size="M">Show logo</Label>
|
||||
</div>
|
||||
<Checkbox
|
||||
value={!$store.navigation.hideLogo}
|
||||
on:change={e => update("hideLogo", !e.detail)}
|
||||
/>
|
||||
{#if !$store.navigation.hideLogo}
|
||||
<div class="label">
|
||||
<Label size="M">Logo URL</Label>
|
||||
</div>
|
||||
<Input
|
||||
value={$store.navigation.logoUrl}
|
||||
on:change={e => update("logoUrl", e.detail)}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{/if}
|
||||
<div class="label">
|
||||
<Label size="M">Show title</Label>
|
||||
</div>
|
||||
<Checkbox
|
||||
value={!$store.navigation.hideTitle}
|
||||
on:change={e => update("hideTitle", !e.detail)}
|
||||
/>
|
||||
{#if !$store.navigation.hideTitle}
|
||||
<div class="label">
|
||||
<Label size="M">Title</Label>
|
||||
</div>
|
||||
<Input
|
||||
value={$store.navigation.title}
|
||||
on:change={e => update("title", e.detail)}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{/if}
|
||||
<div class="label">
|
||||
<Label>Background</Label>
|
||||
</div>
|
||||
<ColorPicker
|
||||
spectrumTheme={$store.theme}
|
||||
value={$store.navigation.navBackground || DefaultAppTheme.navBackground}
|
||||
on:change={e => update("navBackground", e.detail)}
|
||||
/>
|
||||
<div class="label">
|
||||
<Label>Text</Label>
|
||||
</div>
|
||||
<ColorPicker
|
||||
spectrumTheme={$store.theme}
|
||||
value={$store.navigation.navTextColor || DefaultAppTheme.navTextColor}
|
||||
on:change={e => update("navTextColor", e.detail)}
|
||||
/>
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
<style>
|
||||
.controls {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 90px 1fr;
|
||||
align-items: start;
|
||||
transition: background 130ms ease-out, border-color 130ms ease-out;
|
||||
border-left: 4px solid transparent;
|
||||
margin: 0 calc(-1 * var(--spacing-xl));
|
||||
padding: 0 var(--spacing-xl) 0 calc(var(--spacing-xl) - 4px);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 16px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: var(--background-alt);
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.infoHeader {
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.infoHeader :global(svg) {
|
||||
margin-right: 5px;
|
||||
color: var(--grey-6);
|
||||
}
|
||||
|
||||
.infoHeader :global(p) {
|
||||
color: var(--grey-6);
|
||||
}
|
||||
|
||||
.configureLinks :global(button) {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -1,31 +0,0 @@
|
|||
<script>
|
||||
import Pane from "components/design/Pane.svelte"
|
||||
import { get } from "svelte/store"
|
||||
import { Toggle, Body } from "@budibase/bbui"
|
||||
import { selectedScreen, store } from "builderStore"
|
||||
|
||||
const updateShowNavigation = async e => {
|
||||
await store.actions.screens.updateSetting(
|
||||
get(selectedScreen),
|
||||
"showNavigation",
|
||||
e.detail
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Pane title="Settings">
|
||||
<div class="toggle">
|
||||
<Toggle
|
||||
on:change={updateShowNavigation}
|
||||
value={$selectedScreen.showNavigation}
|
||||
/>
|
||||
<Body size="S">Show nav on this screen</Body>
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
<style>
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -1,10 +0,0 @@
|
|||
<script>
|
||||
import RightPanel from "components/design/RightPanel.svelte"
|
||||
import CustomizePane from "./CustomizePane.svelte"
|
||||
import SettingsPane from "./SettingsPane.svelte"
|
||||
</script>
|
||||
|
||||
<RightPanel title="Screen" icon="WebPage">
|
||||
<SettingsPane />
|
||||
<CustomizePane />
|
||||
</RightPanel>
|
|
@ -1,130 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
DrawerContent,
|
||||
Layout,
|
||||
Input,
|
||||
Combobox,
|
||||
} from "@budibase/bbui"
|
||||
import { flip } from "svelte/animate"
|
||||
import { dndzone } from "svelte-dnd-action"
|
||||
import { generate } from "shortid"
|
||||
import { store } from "builderStore"
|
||||
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
||||
|
||||
export let links = []
|
||||
|
||||
const flipDurationMs = 150
|
||||
let dragDisabled = true
|
||||
|
||||
$: links.forEach(link => {
|
||||
if (!link.id) {
|
||||
link.id = generate()
|
||||
}
|
||||
})
|
||||
$: urlOptions = $store.screens
|
||||
.map(screen => screen.routing?.route)
|
||||
.filter(x => x != null)
|
||||
|
||||
const addLink = () => {
|
||||
links = [...links, {}]
|
||||
}
|
||||
|
||||
const removeLink = id => {
|
||||
links = links.filter(link => link.id !== id)
|
||||
}
|
||||
|
||||
const updateLinks = e => {
|
||||
links = e.detail.items
|
||||
}
|
||||
|
||||
const handleFinalize = e => {
|
||||
updateLinks(e)
|
||||
dragDisabled = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<DrawerContent>
|
||||
<div class="container">
|
||||
<Layout noPadding gap="S">
|
||||
{#if links?.length}
|
||||
<div
|
||||
class="links"
|
||||
use:dndzone={{
|
||||
items: links,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: { outline: "none" },
|
||||
dragDisabled,
|
||||
}}
|
||||
on:finalize={handleFinalize}
|
||||
on:consider={updateLinks}
|
||||
>
|
||||
{#each links as link (link.id)}
|
||||
<div class="link" animate:flip={{ duration: flipDurationMs }}>
|
||||
<div
|
||||
class="handle"
|
||||
aria-label="drag-handle"
|
||||
style={dragDisabled ? "cursor: grab" : "cursor: grabbing"}
|
||||
on:mousedown={() => (dragDisabled = false)}
|
||||
>
|
||||
<Icon name="DragHandle" size="XL" />
|
||||
</div>
|
||||
<Input bind:value={link.text} placeholder="Text" />
|
||||
<Combobox
|
||||
bind:value={link.url}
|
||||
placeholder="URL"
|
||||
options={urlOptions}
|
||||
/>
|
||||
<RoleSelect bind:value={link.roleId} placeholder="Minimum role" />
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => removeLink(link.id)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<Button secondary icon="Add" on:click={addLink}>Add Link</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.link {
|
||||
gap: var(--spacing-l);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius-s);
|
||||
transition: background-color ease-in-out 130ms;
|
||||
}
|
||||
.link:hover {
|
||||
background-color: var(--spectrum-global-color-gray-100);
|
||||
}
|
||||
.link > :global(.spectrum-Form-item) {
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
}
|
||||
.handle {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
|
@ -1,34 +0,0 @@
|
|||
<script>
|
||||
import { Button, Drawer } from "@budibase/bbui"
|
||||
import NavigationLinksDrawer from "./NavigationLinksDrawer.svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { store } from "builderStore"
|
||||
|
||||
let drawer
|
||||
let links
|
||||
|
||||
const openDrawer = () => {
|
||||
links = cloneDeep($store.navigation.links || [])
|
||||
drawer.show()
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
let navigation = $store.navigation
|
||||
navigation.links = links
|
||||
await store.actions.navigation.save(navigation)
|
||||
drawer.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button cta on:click={openDrawer}>Configure links</Button>
|
||||
<Drawer
|
||||
bind:this={drawer}
|
||||
title={"Navigation Links"}
|
||||
width="calc(100% - 334px)"
|
||||
>
|
||||
<svelte:fragment slot="description">
|
||||
Configure the links in your navigation bar.
|
||||
</svelte:fragment>
|
||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||
<NavigationLinksDrawer slot="body" bind:links />
|
||||
</Drawer>
|
|
@ -1,110 +0,0 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import {
|
||||
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>
|
||||
|
||||
<Panel title="Navigation" borderRight>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<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>
|
||||
</Panel>
|
|
@ -1,7 +0,0 @@
|
|||
<script>
|
||||
import NavigationSettingsPanel from "./_components/NavigationSettingsPanel.svelte"
|
||||
import NavigationInfoPanel from "./_components/NavigationInfoPanel/index.svelte"
|
||||
</script>
|
||||
|
||||
<NavigationSettingsPanel />
|
||||
<NavigationInfoPanel />
|
|
@ -1,75 +0,0 @@
|
|||
<script>
|
||||
import { Search, Layout, Select, Body, Button } from "@budibase/bbui"
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { roles } from "stores/backend"
|
||||
import { store, sortedScreens, userSelectedResourceMap } from "builderStore"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
||||
import RoleIndicator from "./RoleIndicator.svelte"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
||||
let searchString
|
||||
let accessRole = "all"
|
||||
|
||||
$: filteredScreens = getFilteredScreens(
|
||||
$sortedScreens,
|
||||
searchString,
|
||||
accessRole
|
||||
)
|
||||
|
||||
const getFilteredScreens = (screens, search, role) => {
|
||||
return screens.filter(screen => {
|
||||
const searchMatch = !search || screen.routing.route.includes(search)
|
||||
const roleMatch =
|
||||
!role || role === "all" || screen.routing.roleId === role
|
||||
return searchMatch && roleMatch
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel title="Screens" borderRight>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Button on:click={() => $goto("../../new")} cta>Add screen</Button>
|
||||
<Search
|
||||
placeholder="Search"
|
||||
value={searchString}
|
||||
on:change={e => (searchString = e.detail)}
|
||||
/>
|
||||
<Select
|
||||
bind:value={accessRole}
|
||||
placeholder={null}
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionValue={role => role._id}
|
||||
getOptionColour={role => {
|
||||
if (role?._id === "all") {
|
||||
return null
|
||||
}
|
||||
return RoleUtils.getRoleColour(role._id)
|
||||
}}
|
||||
options={[{ name: "All screens", _id: "all" }, ...$roles]}
|
||||
/>
|
||||
</Layout>
|
||||
{#each filteredScreens as screen (screen._id)}
|
||||
<NavItem
|
||||
icon={screen.routing.homeScreen ? "Home" : null}
|
||||
indentLevel={0}
|
||||
selected={$store.selectedScreenId === screen._id}
|
||||
text={screen.routing.route}
|
||||
on:click={() => store.actions.screens.select(screen._id)}
|
||||
rightAlignIcon
|
||||
showTooltip
|
||||
selectedBy={$userSelectedResourceMap[screen._id]}
|
||||
>
|
||||
<ScreenDropdownMenu screenId={screen._id} />
|
||||
<RoleIndicator slot="right" roleId={screen.routing.roleId} />
|
||||
</NavItem>
|
||||
{/each}
|
||||
{#if !filteredScreens?.length}
|
||||
<Layout paddingY="" paddingX="L">
|
||||
<Body size="S">
|
||||
There aren't any screens matching the current filters
|
||||
</Body>
|
||||
</Layout>
|
||||
{/if}
|
||||
</Panel>
|
|
@ -1,12 +0,0 @@
|
|||
<script>
|
||||
import { selectedScreen } from "builderStore"
|
||||
import ScreenListPanel from "./_components/ScreenListPanel.svelte"
|
||||
import ScreenSettingsPanel from "./_components/ScreenSettingsPanel.svelte"
|
||||
</script>
|
||||
|
||||
<ScreenListPanel />
|
||||
{#if $selectedScreen}
|
||||
{#key $selectedScreen._id}
|
||||
<ScreenSettingsPanel />
|
||||
{/key}
|
||||
{/if}
|
|
@ -1,64 +0,0 @@
|
|||
<script>
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
const onChangeTheme = async theme => {
|
||||
try {
|
||||
await store.actions.theme.save(`spectrum--${theme}`)
|
||||
} catch (error) {
|
||||
notifications.error("Error updating theme")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#each Constants.Themes as theme}
|
||||
<div
|
||||
class="theme"
|
||||
class:selected={`spectrum--${theme.class}` === $store.theme}
|
||||
on:click={() => onChangeTheme(theme.class)}
|
||||
>
|
||||
<div
|
||||
style="background: {theme.preview}"
|
||||
class="color spectrum--{theme.class}"
|
||||
/>
|
||||
{theme.name}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
.color {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50px;
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.theme {
|
||||
border-radius: 4px;
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
transition: background 130ms ease-out;
|
||||
font-weight: 600;
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
.theme:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.theme.selected,
|
||||
.theme:hover {
|
||||
background: var(--spectrum-global-color-gray-50);
|
||||
}
|
||||
</style>
|
|
@ -1,38 +0,0 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { Slider, Button } from "@budibase/bbui"
|
||||
|
||||
export let customTheme
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const options = ["0", "4px", "8px", "16px"]
|
||||
|
||||
$: index = options.indexOf(customTheme.buttonBorderRadius) ?? 2
|
||||
|
||||
const onChange = async e => {
|
||||
dispatch("change", options[e.detail])
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<Slider min={0} max={3} step={1} value={index} on:change={onChange} />
|
||||
<div class="button" style="--radius: {customTheme.buttonBorderRadius};">
|
||||
<Button primary>Button</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.container :global(.spectrum-Form-item) {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.button :global(.spectrum-Button) {
|
||||
border-radius: var(--radius) !important;
|
||||
}
|
||||
</style>
|
|
@ -1,12 +0,0 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { Body, Layout } from "@budibase/bbui"
|
||||
</script>
|
||||
|
||||
<Panel borderLeft title="Theme" icon="InfoOutline" wide>
|
||||
<Layout paddingX="L" paddingY="XL">
|
||||
<Body size="S">
|
||||
Your theme is set across all the screens within your app.
|
||||
</Body>
|
||||
</Layout>
|
||||
</Panel>
|
|
@ -1,55 +0,0 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { Layout, Label, ColorPicker, notifications } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { get } from "svelte/store"
|
||||
import { DefaultAppTheme } from "constants"
|
||||
import AppThemeSelect from "./AppThemeSelect.svelte"
|
||||
import ButtonRoundnessSelect from "./ButtonRoundnessSelect.svelte"
|
||||
|
||||
$: 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>
|
||||
|
||||
<Panel title="Theme" borderRight>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Layout noPadding gap="XS">
|
||||
<Label>Theme</Label>
|
||||
<AppThemeSelect />
|
||||
</Layout>
|
||||
<Layout noPadding gap="XS">
|
||||
<Label>Button roundness</Label>
|
||||
<ButtonRoundnessSelect
|
||||
{customTheme}
|
||||
on:change={e => update("buttonBorderRadius", e.detail)}
|
||||
/>
|
||||
</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>
|
||||
</Panel>
|
|
@ -1,7 +0,0 @@
|
|||
<script>
|
||||
import ThemeSettingsPanel from "./_components/ThemeSettingsPanel.svelte"
|
||||
import ThemeInfoPanel from "./_components/ThemeInfoPanel.svelte"
|
||||
</script>
|
||||
|
||||
<ThemeSettingsPanel />
|
||||
<ThemeInfoPanel />
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
$: {
|
||||
if ($frontendStore.screens.length > 0) {
|
||||
$redirect(`./${$frontendStore.screens[0]._id}`)
|
||||
$redirect(`./${$frontendStore.screens[0]._id}/components/screen`)
|
||||
} else {
|
||||
$redirect("./new")
|
||||
}
|
||||
|
|
|
@ -1,104 +1,5 @@
|
|||
<script>
|
||||
import { Body } from "@budibase/bbui"
|
||||
import CreationPage from "components/common/CreationPage.svelte"
|
||||
import blankImage from "./blank.png"
|
||||
import tableImage from "./table.png"
|
||||
import CreateScreenModal from "./_components/CreateScreenModal.svelte"
|
||||
import { store } from "builderStore"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
let createScreenModal
|
||||
|
||||
$: hasScreens = $store.screens?.length
|
||||
import NewScreen from "components/design/NewScreen/index.svelte"
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
<CreationPage
|
||||
showClose={$store.screens.length > 0}
|
||||
onClose={() => $goto(`./${$store.screens[0]._id}`)}
|
||||
heading={hasScreens ? "Create new screen" : "Create your first screen"}
|
||||
>
|
||||
<div class="subHeading">
|
||||
<Body>Start from scratch or create screens from your data</Body>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card" on:click={() => createScreenModal.show("blank")}>
|
||||
<div class="image">
|
||||
<img alt="" src={blankImage} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Blank screen</Body>
|
||||
<Body size="XS">Add an empty blank screen</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" on:click={() => createScreenModal.show("table")}>
|
||||
<div class="image">
|
||||
<img alt="" src={tableImage} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Table</Body>
|
||||
<Body size="XS">View, edit and delete rows on a table</Body>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CreationPage>
|
||||
</div>
|
||||
|
||||
<CreateScreenModal bind:this={createScreenModal} />
|
||||
|
||||
<style>
|
||||
.page {
|
||||
padding: 28px 40px 40px 40px;
|
||||
}
|
||||
|
||||
.subHeading :global(p) {
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 36px;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
max-width: 235px;
|
||||
transition: filter 150ms;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
filter: brightness(1.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image {
|
||||
border-radius: 4px 4px 0 0;
|
||||
width: 100%;
|
||||
max-height: 127px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text {
|
||||
border: 1px solid var(--grey-4);
|
||||
border-radius: 0 0 4px 4px;
|
||||
padding: 8px 16px 13px 16px;
|
||||
}
|
||||
|
||||
.text :global(p:nth-child(1)) {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.text :global(p:nth-child(2)) {
|
||||
color: var(--grey-6);
|
||||
}
|
||||
</style>
|
||||
<NewScreen />
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
export let instance = {}
|
||||
export let isLayout = false
|
||||
export let isScreen = false
|
||||
export let isRoot = false
|
||||
export let isBlock = false
|
||||
|
||||
// Get parent contexts
|
||||
|
@ -104,7 +104,7 @@
|
|||
// Extract component instance info
|
||||
$: children = instance._children || []
|
||||
$: id = instance._id
|
||||
$: name = isScreen ? "Screen" : instance._instanceName
|
||||
$: name = isRoot ? "Screen" : instance._instanceName
|
||||
$: icon = definition?.icon
|
||||
|
||||
// Determine if the component is selected or is part of the critical path
|
||||
|
@ -133,13 +133,13 @@
|
|||
$: builderInteractive =
|
||||
$builderStore.inBuilder && insideScreenslot && !isBlock && !instance.static
|
||||
$: devToolsInteractive = $devToolsStore.allowSelection && !isBlock
|
||||
$: interactive = builderInteractive || devToolsInteractive
|
||||
$: interactive = !isRoot && (builderInteractive || devToolsInteractive)
|
||||
$: editing = editable && selected && $builderStore.editMode
|
||||
$: draggable =
|
||||
!inDragPath &&
|
||||
interactive &&
|
||||
!isLayout &&
|
||||
!isScreen &&
|
||||
!isRoot &&
|
||||
definition?.draggable !== false
|
||||
$: droppable = interactive
|
||||
$: builderHidden =
|
||||
|
@ -538,7 +538,7 @@
|
|||
<svelte:self instance={child} />
|
||||
{/each}
|
||||
{:else if emptyState}
|
||||
{#if isScreen}
|
||||
{#if isRoot}
|
||||
<ScreenPlaceholder />
|
||||
{:else}
|
||||
<EmptyPlaceholder />
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
{#if $routeStore.routerLoaded}
|
||||
{#key screenDefinition?._id}
|
||||
<Provider key="url" data={params}>
|
||||
<Component isScreen instance={screenDefinition} />
|
||||
<Component isRoot instance={screenDefinition} />
|
||||
</Provider>
|
||||
{/key}
|
||||
{/if}
|
||||
|
|
|
@ -146,21 +146,24 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="layout layout--{typeClass}"
|
||||
class="component screen layout layout--{typeClass}"
|
||||
use:styleable={$component.styles}
|
||||
class:desktop={!mobile}
|
||||
class:mobile={!!mobile}
|
||||
data-id="screen"
|
||||
data-name="Screen"
|
||||
data-icon="WebPage"
|
||||
>
|
||||
<div class="layout-body">
|
||||
<div class="screen-dom screen-wrapper layout-body">
|
||||
{#if typeClass !== "none"}
|
||||
<div
|
||||
class="interactive component navigation"
|
||||
data-id="navigation"
|
||||
data-name="Navigation"
|
||||
data-icon="Link"
|
||||
data-icon="Visibility"
|
||||
>
|
||||
<div
|
||||
class="nav-wrapper"
|
||||
class="nav-wrapper navigation-dom"
|
||||
class:sticky
|
||||
class:hidden={$routeStore.queryParams?.peek}
|
||||
class:clickable={$builderStore.inBuilder}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
}
|
||||
}
|
||||
$: settings = getBarSettings(definition)
|
||||
$: isScreen = id === $builderStore.screen?.props?._id
|
||||
$: isRoot = id === $builderStore.screen?.props?._id
|
||||
|
||||
const getBarSettings = definition => {
|
||||
let allSettings = []
|
||||
|
@ -160,11 +160,11 @@
|
|||
{:else if setting.type === "color"}
|
||||
<SettingsColorPicker prop={setting.key} />
|
||||
{/if}
|
||||
{#if setting.barSeparator !== false && (settings.length != idx + 1 || !isScreen)}
|
||||
{#if setting.barSeparator !== false && (settings.length != idx + 1 || !isRoot)}
|
||||
<div class="divider" />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if !isScreen}
|
||||
{#if !isRoot}
|
||||
<SettingsButton
|
||||
icon="Duplicate"
|
||||
on:click={() => {
|
||||
|
|
Loading…
Reference in New Issue