mirror of https://github.com/Budibase/budibase.git
284 changed files with 5098 additions and 5064 deletions
@ -0,0 +1,62 @@ |
|||||
|
name: Deploy Budibase Single Container Image to DockerHub |
||||
|
on: |
||||
|
push: |
||||
|
branches: |
||||
|
- "omnibus-action" |
||||
|
- "develop" |
||||
|
- "master" |
||||
|
- "main" |
||||
|
env: |
||||
|
BASE_BRANCH: ${{ github.event.pull_request.base.ref}} |
||||
|
BRANCH: ${{ github.event.pull_request.head.ref }} |
||||
|
CI: true |
||||
|
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }} |
||||
|
REGISTRY_URL: registry.hub.docker.com |
||||
|
jobs: |
||||
|
build: |
||||
|
name: "build" |
||||
|
runs-on: ubuntu-latest |
||||
|
strategy: |
||||
|
matrix: |
||||
|
node-version: [14.x] |
||||
|
steps: |
||||
|
- name: "Checkout" |
||||
|
uses: actions/checkout@v2 |
||||
|
- name: Use Node.js ${{ matrix.node-version }} |
||||
|
uses: actions/setup-node@v1 |
||||
|
with: |
||||
|
node-version: ${{ matrix.node-version }} |
||||
|
- name: Setup QEMU |
||||
|
uses: docker/setup-qemu-action@v1 |
||||
|
- name: Setup Docker Buildx |
||||
|
id: buildx |
||||
|
uses: docker/setup-buildx-action@v1 |
||||
|
- name: Install Pro |
||||
|
run: yarn install:pro $BRANCH $BASE_BRANCH |
||||
|
- name: Run Yarn |
||||
|
run: yarn |
||||
|
- name: Run Yarn Bootstrap |
||||
|
run: yarn bootstrap |
||||
|
- name: Runt Yarn Lint |
||||
|
run: yarn lint |
||||
|
- name: Run Yarn Build |
||||
|
run: yarn build |
||||
|
- name: Login to Docker Hub |
||||
|
uses: docker/login-action@v2 |
||||
|
with: |
||||
|
username: ${{ secrets.DOCKER_USERNAME }} |
||||
|
password: ${{ secrets.DOCKER_API_KEY }} |
||||
|
- name: Get the latest release version |
||||
|
id: version |
||||
|
run: | |
||||
|
release_version=$(cat lerna.json | jq -r '.version') |
||||
|
echo $release_version |
||||
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV |
||||
|
- name: Tag and release Budibase service docker image |
||||
|
uses: docker/build-push-action@v2 |
||||
|
with: |
||||
|
context: . |
||||
|
push: true |
||||
|
platforms: linux/amd64,linux/arm64 |
||||
|
tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }} |
||||
|
file: ./hosting/single/Dockerfile |
||||
@ -0,0 +1 @@ |
|||||
|
network-timeout 100000 |
||||
@ -0,0 +1,5 @@ |
|||||
|
; CouchDB Configuration Settings |
||||
|
|
||||
|
[couchdb] |
||||
|
database_dir = /data/couch/dbs |
||||
|
view_index_dir = /data/couch/views |
||||
@ -1,4 +1,4 @@ |
|||||
#!/bin/bash |
#!/bin/bash |
||||
id=$(docker run -t -d -p 80:80 budibase:latest) |
id=$(docker run -t -d -p 8080:80 budibase:latest) |
||||
docker exec -it $id bash |
docker exec -it $id bash |
||||
docker kill $id |
docker kill $id |
||||
|
|||||
@ -0,0 +1,86 @@ |
|||||
|
<script> |
||||
|
import "@spectrum-css/slider/dist/index-vars.css" |
||||
|
import { createEventDispatcher } from "svelte" |
||||
|
|
||||
|
export let value = false |
||||
|
export let id = null |
||||
|
export let disabled = false |
||||
|
export let min = 0 |
||||
|
export let max = 100 |
||||
|
export let step = 1 |
||||
|
|
||||
|
const dispatch = createEventDispatcher() |
||||
|
const onChange = event => { |
||||
|
dispatch("change", event.target.value) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div> |
||||
|
<input |
||||
|
type="range" |
||||
|
{min} |
||||
|
{max} |
||||
|
{step} |
||||
|
{value} |
||||
|
{disabled} |
||||
|
{id} |
||||
|
on:change={onChange} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
div { |
||||
|
display: grid; |
||||
|
place-items: center; |
||||
|
} |
||||
|
|
||||
|
input { |
||||
|
width: 100%; |
||||
|
padding: 0; |
||||
|
margin: 0; |
||||
|
-webkit-appearance: none; |
||||
|
background: transparent; |
||||
|
} |
||||
|
input::-webkit-slider-thumb { |
||||
|
-webkit-appearance: none; |
||||
|
} |
||||
|
input:focus { |
||||
|
outline: none; |
||||
|
} |
||||
|
|
||||
|
input[type="range"]::-webkit-slider-thumb { |
||||
|
-webkit-appearance: none; |
||||
|
border: 2px solid var(--spectrum-global-color-gray-700); |
||||
|
height: 16px; |
||||
|
width: 16px; |
||||
|
border-radius: 50%; |
||||
|
background: var(--background); |
||||
|
cursor: pointer; |
||||
|
transition: background 130ms ease-out; |
||||
|
margin-top: -7px; |
||||
|
} |
||||
|
input[type="range"]::-moz-range-thumb { |
||||
|
border: 2px solid var(--spectrum-global-color-gray-700); |
||||
|
height: 12px; |
||||
|
width: 12px; |
||||
|
border-radius: 50%; |
||||
|
background: var(--background); |
||||
|
cursor: pointer; |
||||
|
transition: background 130ms ease-out; |
||||
|
} |
||||
|
|
||||
|
input[type="range"]::-webkit-slider-runnable-track { |
||||
|
width: 100%; |
||||
|
height: 2px; |
||||
|
cursor: pointer; |
||||
|
background: var(--spectrum-global-color-gray-300); |
||||
|
border-radius: 2px; |
||||
|
} |
||||
|
input[type="range"]::-moz-range-track { |
||||
|
width: 100%; |
||||
|
height: 2px; |
||||
|
cursor: pointer; |
||||
|
background: var(--spectrum-global-color-gray-300); |
||||
|
border-radius: 2px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,24 @@ |
|||||
|
<script> |
||||
|
import Field from "./Field.svelte" |
||||
|
import Slider from "./Core/Slider.svelte" |
||||
|
import { createEventDispatcher } from "svelte" |
||||
|
|
||||
|
export let value = null |
||||
|
export let label = null |
||||
|
export let labelPosition = "above" |
||||
|
export let min = 0 |
||||
|
export let max = 100 |
||||
|
export let step = 1 |
||||
|
export let disabled = false |
||||
|
export let error = null |
||||
|
|
||||
|
const dispatch = createEventDispatcher() |
||||
|
const onChange = e => { |
||||
|
value = e.detail |
||||
|
dispatch("change", e.detail) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<Field {label} {labelPosition} {error}> |
||||
|
<Slider {disabled} {value} {min} {max} {step} on:change={onChange} /> |
||||
|
</Field> |
||||
@ -0,0 +1,14 @@ |
|||||
|
<div class="icon-side-nav"> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.icon-side-nav { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: flex-start; |
||||
|
align-items: center; |
||||
|
padding: var(--spacing-s); |
||||
|
gap: var(--spacing-xs); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,56 @@ |
|||||
|
<script> |
||||
|
import Icon from "../Icon/Icon.svelte" |
||||
|
import Tooltip from "../Tooltip/Tooltip.svelte" |
||||
|
import { fade } from "svelte/transition" |
||||
|
|
||||
|
export let icon |
||||
|
export let active = false |
||||
|
export let tooltip |
||||
|
|
||||
|
let showTooltip = false |
||||
|
</script> |
||||
|
|
||||
|
<div |
||||
|
class="icon-side-nav-item" |
||||
|
class:active |
||||
|
on:mouseover={() => (showTooltip = true)} |
||||
|
on:focus={() => (showTooltip = true)} |
||||
|
on:mouseleave={() => (showTooltip = false)} |
||||
|
on:click |
||||
|
> |
||||
|
<Icon name={icon} hoverable /> |
||||
|
{#if tooltip && showTooltip} |
||||
|
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}> |
||||
|
<Tooltip textWrapping direction="right" text={tooltip} /> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.icon-side-nav-item { |
||||
|
width: 36px; |
||||
|
height: 36px; |
||||
|
display: grid; |
||||
|
place-items: center; |
||||
|
border-radius: 4px; |
||||
|
position: relative; |
||||
|
cursor: pointer; |
||||
|
transition: background 130ms ease-out; |
||||
|
} |
||||
|
.icon-side-nav-item:hover :global(svg), |
||||
|
.active :global(svg) { |
||||
|
color: var(--spectrum-global-color-gray-900); |
||||
|
} |
||||
|
.active { |
||||
|
background: var(--spectrum-global-color-gray-300); |
||||
|
} |
||||
|
.tooltip { |
||||
|
position: absolute; |
||||
|
pointer-events: none; |
||||
|
left: calc(100% - 4px); |
||||
|
top: 50%; |
||||
|
white-space: nowrap; |
||||
|
transform: translateY(-50%); |
||||
|
z-index: 1; |
||||
|
} |
||||
|
</style> |
||||
@ -1,65 +1,77 @@ |
|||||
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 { FrontendTypes, 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" |
||||
|
|
||||
export const store = getFrontendStore() |
export const store = getFrontendStore() |
||||
export const automationStore = getAutomationStore() |
export const automationStore = getAutomationStore() |
||||
export const themeStore = getThemeStore() |
export const themeStore = getThemeStore() |
||||
|
|
||||
export const currentAsset = derived(store, $store => { |
export const selectedScreen = derived(store, $store => { |
||||
const type = $store.currentFrontEndType |
return $store.screens.find(screen => screen._id === $store.selectedScreenId) |
||||
if (type === FrontendTypes.SCREEN) { |
}) |
||||
return $store.screens.find(screen => screen._id === $store.selectedScreenId) |
|
||||
} else if (type === FrontendTypes.LAYOUT) { |
export const selectedLayout = derived(store, $store => { |
||||
return $store.layouts.find(layout => layout._id === $store.selectedLayoutId) |
return $store.layouts?.find(layout => layout._id === $store.selectedLayoutId) |
||||
} |
|
||||
return null |
|
||||
}) |
}) |
||||
|
|
||||
export const selectedComponent = derived( |
export const selectedComponent = derived( |
||||
[store, currentAsset], |
[store, selectedScreen], |
||||
([$store, $currentAsset]) => { |
([$store, $selectedScreen]) => { |
||||
if (!$currentAsset || !$store.selectedComponentId) { |
if (!$selectedScreen || !$store.selectedComponentId) { |
||||
return null |
return null |
||||
} |
} |
||||
return findComponent($currentAsset?.props, $store.selectedComponentId) |
return findComponent($selectedScreen?.props, $store.selectedComponentId) |
||||
} |
} |
||||
) |
) |
||||
|
|
||||
|
export const sortedScreens = derived(store, $store => { |
||||
|
return $store.screens.slice().sort((a, b) => { |
||||
|
// Sort by role first
|
||||
|
const roleA = RoleUtils.getRolePriority(a.routing.roleId) |
||||
|
const roleB = RoleUtils.getRolePriority(b.routing.roleId) |
||||
|
if (roleA !== roleB) { |
||||
|
return roleA > roleB ? -1 : 1 |
||||
|
} |
||||
|
// Then put home screens first
|
||||
|
const homeA = !!a.routing.homeScreen |
||||
|
const homeB = !!b.routing.homeScreen |
||||
|
if (homeA !== homeB) { |
||||
|
return homeA ? -1 : 1 |
||||
|
} |
||||
|
// Then sort alphabetically by each URL param
|
||||
|
const aParams = a.routing.route.split("/") |
||||
|
const bParams = b.routing.route.split("/") |
||||
|
let minParams = Math.min(aParams.length, bParams.length) |
||||
|
for (let i = 0; i < minParams; i++) { |
||||
|
if (aParams[i] === bParams[i]) { |
||||
|
continue |
||||
|
} |
||||
|
return aParams[i] < bParams[i] ? -1 : 1 |
||||
|
} |
||||
|
// Then sort by the fewest amount of URL params
|
||||
|
return aParams.length < bParams.length ? -1 : 1 |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
export const selectedComponentPath = derived( |
export const selectedComponentPath = derived( |
||||
[store, currentAsset], |
[store, selectedScreen], |
||||
([$store, $currentAsset]) => { |
([$store, $selectedScreen]) => { |
||||
return findComponentPath( |
return findComponentPath( |
||||
$currentAsset?.props, |
$selectedScreen?.props, |
||||
$store.selectedComponentId |
$store.selectedComponentId |
||||
).map(component => component._id) |
).map(component => component._id) |
||||
} |
} |
||||
) |
) |
||||
|
|
||||
export const currentAssetId = derived(store, $store => { |
|
||||
return $store.currentFrontEndType === FrontendTypes.SCREEN |
|
||||
? $store.selectedScreenId |
|
||||
: $store.selectedLayoutId |
|
||||
}) |
|
||||
|
|
||||
export const currentAssetName = derived(currentAsset, $currentAsset => { |
|
||||
return $currentAsset?.name |
|
||||
}) |
|
||||
|
|
||||
// leave this as before for consistency
|
|
||||
export const allScreens = derived(store, $store => { |
|
||||
return $store.screens |
|
||||
}) |
|
||||
|
|
||||
export const mainLayout = derived(store, $store => { |
export const mainLayout = derived(store, $store => { |
||||
return $store.layouts?.find( |
return $store.layouts?.find( |
||||
layout => layout._id === LAYOUT_NAMES.MASTER.PRIVATE |
layout => layout._id === LAYOUT_NAMES.MASTER.PRIVATE |
||||
) |
) |
||||
}) |
}) |
||||
|
|
||||
export const selectedAccessRole = writable("BASIC") |
// For compatibility
|
||||
|
export const currentAsset = selectedScreen |
||||
export const screenSearchString = writable(null) |
|
||||
|
|||||
@ -1,54 +0,0 @@ |
|||||
<script> |
|
||||
import { notifications, Select } from "@budibase/bbui" |
|
||||
import { store } from "builderStore" |
|
||||
import { get } from "svelte/store" |
|
||||
|
|
||||
const themeOptions = [ |
|
||||
{ |
|
||||
label: "Lightest", |
|
||||
value: "spectrum--lightest", |
|
||||
}, |
|
||||
{ |
|
||||
label: "Light", |
|
||||
value: "spectrum--light", |
|
||||
}, |
|
||||
{ |
|
||||
label: "Dark", |
|
||||
value: "spectrum--dark", |
|
||||
}, |
|
||||
{ |
|
||||
label: "Darkest", |
|
||||
value: "spectrum--darkest", |
|
||||
}, |
|
||||
] |
|
||||
|
|
||||
const onChangeTheme = async theme => { |
|
||||
try { |
|
||||
await store.actions.theme.save(theme) |
|
||||
await store.actions.customTheme.save({ |
|
||||
...get(store).customTheme, |
|
||||
navBackground: |
|
||||
theme === "spectrum--light" |
|
||||
? "var(--spectrum-global-color-gray-50)" |
|
||||
: "var(--spectrum-global-color-gray-100)", |
|
||||
}) |
|
||||
} catch (error) { |
|
||||
notifications.error("Error updating theme") |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div> |
|
||||
<Select |
|
||||
value={$store.theme} |
|
||||
options={themeOptions} |
|
||||
placeholder={null} |
|
||||
on:change={e => onChangeTheme(e.detail)} |
|
||||
/> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
div { |
|
||||
width: 100px; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,114 +0,0 @@ |
|||||
<script> |
|
||||
import { |
|
||||
ActionMenu, |
|
||||
ActionButton, |
|
||||
MenuItem, |
|
||||
Icon, |
|
||||
notifications, |
|
||||
} from "@budibase/bbui" |
|
||||
import { store, currentAssetName, selectedComponent } from "builderStore" |
|
||||
import structure from "./componentStructure.json" |
|
||||
|
|
||||
$: enrichedStructure = enrichStructure(structure, $store.components) |
|
||||
|
|
||||
const isChildAllowed = ({ name }, selectedComponent) => { |
|
||||
const currentComponent = store.actions.components.getDefinition( |
|
||||
selectedComponent?._component |
|
||||
) |
|
||||
return currentComponent?.illegalChildren?.includes(name.toLowerCase()) |
|
||||
} |
|
||||
|
|
||||
const enrichStructure = (structure, definitions) => { |
|
||||
let enrichedStructure = [] |
|
||||
structure.forEach(item => { |
|
||||
if (typeof item === "string") { |
|
||||
const def = definitions[`@budibase/standard-components/${item}`] |
|
||||
if (def) { |
|
||||
enrichedStructure.push({ |
|
||||
...def, |
|
||||
isCategory: false, |
|
||||
}) |
|
||||
} |
|
||||
} else { |
|
||||
enrichedStructure.push({ |
|
||||
...item, |
|
||||
isCategory: true, |
|
||||
children: enrichStructure(item.children || [], definitions), |
|
||||
}) |
|
||||
} |
|
||||
}) |
|
||||
return enrichedStructure |
|
||||
} |
|
||||
|
|
||||
const onItemChosen = async item => { |
|
||||
if (!item.isCategory) { |
|
||||
try { |
|
||||
await store.actions.components.create(item.component) |
|
||||
} catch (error) { |
|
||||
notifications.error("Error creating component") |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div class="components"> |
|
||||
{#each enrichedStructure as item} |
|
||||
<ActionMenu disabled={!item.isCategory}> |
|
||||
<ActionButton |
|
||||
icon={item.icon} |
|
||||
disabled={!item.isCategory && isChildAllowed(item, $selectedComponent)} |
|
||||
quiet |
|
||||
size="S" |
|
||||
slot="control" |
|
||||
dataCy={`category-${item.name}`} |
|
||||
on:click={() => onItemChosen(item)} |
|
||||
> |
|
||||
<div class="buttonContent"> |
|
||||
{item.name} |
|
||||
{#if item.isCategory} |
|
||||
<Icon size="S" name="ChevronDown" /> |
|
||||
{/if} |
|
||||
</div> |
|
||||
</ActionButton> |
|
||||
{#each item.children || [] as item} |
|
||||
{#if !item.showOnAsset || item.showOnAsset.includes($currentAssetName)} |
|
||||
<MenuItem |
|
||||
dataCy={`component-${item.name}`} |
|
||||
icon={item.icon} |
|
||||
on:click={() => onItemChosen(item)} |
|
||||
disabled={isChildAllowed(item, $selectedComponent)} |
|
||||
> |
|
||||
{item.name} |
|
||||
</MenuItem> |
|
||||
{/if} |
|
||||
{/each} |
|
||||
</ActionMenu> |
|
||||
{/each} |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.components { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: flex-start; |
|
||||
align-items: center; |
|
||||
flex-wrap: wrap; |
|
||||
} |
|
||||
.components :global(> *) { |
|
||||
height: 32px; |
|
||||
display: grid; |
|
||||
place-items: center; |
|
||||
} |
|
||||
|
|
||||
.buttonContent { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: flex-start; |
|
||||
align-items: flex-end; |
|
||||
} |
|
||||
.buttonContent :global(svg) { |
|
||||
margin-left: 2px !important; |
|
||||
margin-right: 0 !important; |
|
||||
margin-bottom: -1px; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,156 +0,0 @@ |
|||||
<script> |
|
||||
import { get } from "svelte/store" |
|
||||
import { |
|
||||
ActionButton, |
|
||||
Modal, |
|
||||
ModalContent, |
|
||||
Layout, |
|
||||
ColorPicker, |
|
||||
Label, |
|
||||
Select, |
|
||||
Button, |
|
||||
notifications, |
|
||||
} from "@budibase/bbui" |
|
||||
import { store } from "builderStore" |
|
||||
import AppThemeSelect from "./AppThemeSelect.svelte" |
|
||||
|
|
||||
let modal |
|
||||
|
|
||||
const defaultTheme = { |
|
||||
primaryColor: "var(--spectrum-global-color-blue-600)", |
|
||||
primaryColorHover: "var(--spectrum-global-color-blue-500)", |
|
||||
buttonBorderRadius: "16px", |
|
||||
navBackground: "var(--spectrum-global-color-gray-50)", |
|
||||
navTextColor: "var(--spectrum-global-color-gray-800)", |
|
||||
} |
|
||||
|
|
||||
const buttonBorderRadiusOptions = [ |
|
||||
{ |
|
||||
label: "None", |
|
||||
value: "0", |
|
||||
}, |
|
||||
{ |
|
||||
label: "Small", |
|
||||
value: "4px", |
|
||||
}, |
|
||||
{ |
|
||||
label: "Medium", |
|
||||
value: "8px", |
|
||||
}, |
|
||||
{ |
|
||||
label: "Large", |
|
||||
value: "16px", |
|
||||
}, |
|
||||
] |
|
||||
|
|
||||
const updateProperty = property => { |
|
||||
return async e => { |
|
||||
try { |
|
||||
store.actions.customTheme.save({ |
|
||||
...get(store).customTheme, |
|
||||
[property]: e.detail, |
|
||||
}) |
|
||||
} catch (error) { |
|
||||
notifications.error("Error updating custom theme") |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const resetTheme = () => { |
|
||||
try { |
|
||||
const theme = get(store).theme |
|
||||
store.actions.customTheme.save({ |
|
||||
...defaultTheme, |
|
||||
navBackground: |
|
||||
theme === "spectrum--light" |
|
||||
? "var(--spectrum-global-color-gray-50)" |
|
||||
: "var(--spectrum-global-color-gray-100)", |
|
||||
}) |
|
||||
} catch (error) { |
|
||||
notifications.error("Error saving custom theme") |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div class="container"> |
|
||||
<ActionButton icon="Brush" on:click={modal.show}>Theme</ActionButton> |
|
||||
</div> |
|
||||
<Modal bind:this={modal}> |
|
||||
<ModalContent |
|
||||
showConfirmButton={false} |
|
||||
cancelText="View changes" |
|
||||
showCloseIcon={false} |
|
||||
title="Theme settings" |
|
||||
> |
|
||||
<Layout noPadding gap="S"> |
|
||||
<div class="setting"> |
|
||||
<Label size="L">Theme</Label> |
|
||||
<AppThemeSelect /> |
|
||||
</div> |
|
||||
<div class="setting"> |
|
||||
<Label size="L">Button roundness</Label> |
|
||||
<div class="select-wrapper"> |
|
||||
<Select |
|
||||
placeholder={null} |
|
||||
value={$store.customTheme?.buttonBorderRadius || |
|
||||
defaultTheme.buttonBorderRadius} |
|
||||
on:change={updateProperty("buttonBorderRadius")} |
|
||||
options={buttonBorderRadiusOptions} |
|
||||
/> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="setting"> |
|
||||
<Label size="L">Accent color</Label> |
|
||||
<ColorPicker |
|
||||
spectrumTheme={$store.theme} |
|
||||
value={$store.customTheme?.primaryColor || defaultTheme.primaryColor} |
|
||||
on:change={updateProperty("primaryColor")} |
|
||||
/> |
|
||||
</div> |
|
||||
<div class="setting"> |
|
||||
<Label size="L">Accent color (hover)</Label> |
|
||||
<ColorPicker |
|
||||
spectrumTheme={$store.theme} |
|
||||
value={$store.customTheme?.primaryColorHover || |
|
||||
defaultTheme.primaryColorHover} |
|
||||
on:change={updateProperty("primaryColorHover")} |
|
||||
/> |
|
||||
</div> |
|
||||
<div class="setting"> |
|
||||
<Label size="L">Navigation bar background color</Label> |
|
||||
<ColorPicker |
|
||||
spectrumTheme={$store.theme} |
|
||||
value={$store.customTheme?.navBackground || |
|
||||
defaultTheme.navBackground} |
|
||||
on:change={updateProperty("navBackground")} |
|
||||
/> |
|
||||
</div> |
|
||||
<div class="setting"> |
|
||||
<Label size="L">Navigation bar text color</Label> |
|
||||
<ColorPicker |
|
||||
spectrumTheme={$store.theme} |
|
||||
value={$store.customTheme?.navTextColor || defaultTheme.navTextColor} |
|
||||
on:change={updateProperty("navTextColor")} |
|
||||
/> |
|
||||
</div> |
|
||||
</Layout> |
|
||||
<div slot="footer"> |
|
||||
<Button secondary quiet on:click={resetTheme}>Reset</Button> |
|
||||
</div> |
|
||||
</ModalContent> |
|
||||
</Modal> |
|
||||
|
|
||||
<style> |
|
||||
.container { |
|
||||
padding-right: 8px; |
|
||||
} |
|
||||
.setting { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: space-between; |
|
||||
align-items: center; |
|
||||
} |
|
||||
.select-wrapper { |
|
||||
width: 100px; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1 +0,0 @@ |
|||||
export { default } from "./CurrentItemPreview.svelte" |
|
||||
@ -1,157 +0,0 @@ |
|||||
<script> |
|
||||
import { store } from "builderStore" |
|
||||
import { DropEffect, DropPosition } from "./dragDropStore" |
|
||||
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte" |
|
||||
import NavItem from "components/common/NavItem.svelte" |
|
||||
import { capitalise } from "helpers" |
|
||||
import { notifications } from "@budibase/bbui" |
|
||||
import { selectedComponentPath } from "builderStore" |
|
||||
|
|
||||
export let components = [] |
|
||||
export let currentComponent |
|
||||
export let onSelect = () => {} |
|
||||
export let level = 0 |
|
||||
export let dragDropStore |
|
||||
|
|
||||
let closedNodes = {} |
|
||||
|
|
||||
const selectComponent = component => { |
|
||||
store.actions.components.select(component) |
|
||||
} |
|
||||
|
|
||||
const dragstart = component => e => { |
|
||||
e.dataTransfer.dropEffect = DropEffect.MOVE |
|
||||
dragDropStore.actions.dragstart(component) |
|
||||
} |
|
||||
|
|
||||
const dragover = (component, index) => e => { |
|
||||
const definition = store.actions.components.getDefinition( |
|
||||
component._component |
|
||||
) |
|
||||
const canHaveChildrenButIsEmpty = |
|
||||
definition?.hasChildren && !component._children?.length |
|
||||
|
|
||||
e.dataTransfer.dropEffect = DropEffect.COPY |
|
||||
|
|
||||
// how far down the mouse pointer is on the drop target |
|
||||
const mousePosition = e.offsetY / e.currentTarget.offsetHeight |
|
||||
|
|
||||
dragDropStore.actions.dragover({ |
|
||||
component, |
|
||||
index, |
|
||||
canHaveChildrenButIsEmpty, |
|
||||
mousePosition, |
|
||||
}) |
|
||||
|
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
const getComponentText = component => { |
|
||||
if (component._instanceName) { |
|
||||
return component._instanceName |
|
||||
} |
|
||||
const type = |
|
||||
component._component.replace("@budibase/standard-components/", "") || |
|
||||
"component" |
|
||||
return capitalise(type) |
|
||||
} |
|
||||
|
|
||||
function toggleNodeOpen(componentId) { |
|
||||
if (closedNodes[componentId]) { |
|
||||
delete closedNodes[componentId] |
|
||||
} else { |
|
||||
closedNodes[componentId] = true |
|
||||
} |
|
||||
closedNodes = closedNodes |
|
||||
} |
|
||||
|
|
||||
const onDrop = async () => { |
|
||||
try { |
|
||||
await dragDropStore.actions.drop() |
|
||||
} catch (error) { |
|
||||
notifications.error("Error saving component") |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const isOpen = (component, selectedComponentPath, closedNodes) => { |
|
||||
if (!component?._children?.length) { |
|
||||
return false |
|
||||
} |
|
||||
if (selectedComponentPath.includes(component._id)) { |
|
||||
return true |
|
||||
} |
|
||||
return !closedNodes[component._id] |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<ul> |
|
||||
{#each components || [] as component, index (component._id)} |
|
||||
<li on:click|stopPropagation={() => selectComponent(component)}> |
|
||||
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE} |
|
||||
<div |
|
||||
on:drop={onDrop} |
|
||||
ondragover="return false" |
|
||||
ondragenter="return false" |
|
||||
class="drop-item" |
|
||||
style="margin-left: {(level + 1) * 16}px" |
|
||||
/> |
|
||||
{/if} |
|
||||
|
|
||||
<NavItem |
|
||||
draggable |
|
||||
on:dragend={dragDropStore.actions.reset} |
|
||||
on:dragstart={dragstart(component)} |
|
||||
on:dragover={dragover(component, index)} |
|
||||
on:iconClick={() => toggleNodeOpen(component._id)} |
|
||||
on:drop={onDrop} |
|
||||
text={getComponentText(component)} |
|
||||
withArrow |
|
||||
indentLevel={level + 1} |
|
||||
selected={$store.selectedComponentId === component._id} |
|
||||
opened={isOpen(component, $selectedComponentPath, closedNodes)} |
|
||||
> |
|
||||
<ComponentDropdownMenu {component} /> |
|
||||
</NavItem> |
|
||||
|
|
||||
{#if isOpen(component, $selectedComponentPath, closedNodes)} |
|
||||
<svelte:self |
|
||||
components={component._children} |
|
||||
{currentComponent} |
|
||||
{onSelect} |
|
||||
{dragDropStore} |
|
||||
level={level + 1} |
|
||||
/> |
|
||||
{/if} |
|
||||
|
|
||||
{#if $dragDropStore?.targetComponent === component && ($dragDropStore.dropPosition === DropPosition.INSIDE || $dragDropStore.dropPosition === DropPosition.BELOW)} |
|
||||
<div |
|
||||
on:drop={onDrop} |
|
||||
ondragover="return false" |
|
||||
ondragenter="return false" |
|
||||
class="drop-item" |
|
||||
style="margin-left: {(level + |
|
||||
($dragDropStore.dropPosition === DropPosition.INSIDE ? 3 : 1)) * |
|
||||
16}px" |
|
||||
/> |
|
||||
{/if} |
|
||||
</li> |
|
||||
{/each} |
|
||||
</ul> |
|
||||
|
|
||||
<style> |
|
||||
ul { |
|
||||
list-style: none; |
|
||||
padding-left: 0; |
|
||||
margin: 0; |
|
||||
} |
|
||||
ul, |
|
||||
li { |
|
||||
min-width: max-content; |
|
||||
} |
|
||||
|
|
||||
.drop-item { |
|
||||
border-radius: var(--border-radius-m); |
|
||||
height: 32px; |
|
||||
background: var(--grey-3); |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,74 +0,0 @@ |
|||||
<script> |
|
||||
import { store } from "builderStore" |
|
||||
import { notifications } from "@budibase/bbui" |
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
|
||||
import { |
|
||||
ActionMenu, |
|
||||
MenuItem, |
|
||||
Icon, |
|
||||
Modal, |
|
||||
ModalContent, |
|
||||
Input, |
|
||||
} from "@budibase/bbui" |
|
||||
import { cloneDeep } from "lodash/fp" |
|
||||
|
|
||||
export let layout |
|
||||
|
|
||||
let confirmDeleteDialog |
|
||||
let editLayoutNameModal |
|
||||
let name = layout.name |
|
||||
|
|
||||
const deleteLayout = async () => { |
|
||||
try { |
|
||||
await store.actions.layouts.delete(layout) |
|
||||
notifications.success("Layout deleted successfully") |
|
||||
} catch (err) { |
|
||||
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> |
|
||||
|
|
||||
<ActionMenu> |
|
||||
<div slot="control" class="icon"> |
|
||||
<Icon size="S" hoverable name="MoreSmallList" /> |
|
||||
</div> |
|
||||
<MenuItem icon="Edit" on:click={editLayoutNameModal.show}>Edit</MenuItem> |
|
||||
<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} |
|
||||
/> |
|
||||
|
|
||||
<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> |
|
||||
.icon { |
|
||||
display: grid; |
|
||||
place-items: center; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,82 +0,0 @@ |
|||||
<script> |
|
||||
import { goto } from "@roxi/routify" |
|
||||
import { store } from "builderStore" |
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
|
||||
import { |
|
||||
ActionMenu, |
|
||||
MenuItem, |
|
||||
Icon, |
|
||||
Layout, |
|
||||
notifications, |
|
||||
} from "@budibase/bbui" |
|
||||
import { get } from "svelte/store" |
|
||||
|
|
||||
export let path |
|
||||
export let screens |
|
||||
|
|
||||
let confirmDeleteDialog |
|
||||
|
|
||||
const deleteScreens = async () => { |
|
||||
if (!screens?.length) { |
|
||||
return |
|
||||
} |
|
||||
try { |
|
||||
for (let { id } of screens) { |
|
||||
// We have to fetch the screen to be deleted immediately before deleting |
|
||||
// as otherwise we're very likely to 409 |
|
||||
const screen = get(store).screens.find(screen => screen._id === id) |
|
||||
if (!screen) { |
|
||||
continue |
|
||||
} |
|
||||
await store.actions.screens.delete(screen) |
|
||||
} |
|
||||
notifications.success("Screens deleted successfully") |
|
||||
$goto("../") |
|
||||
} catch (error) { |
|
||||
notifications.error("Error deleting screens") |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<ActionMenu> |
|
||||
<div slot="control" class="icon"> |
|
||||
<Icon size="S" hoverable name="MoreSmallList" /> |
|
||||
</div> |
|
||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}> |
|
||||
Delete all screens |
|
||||
</MenuItem> |
|
||||
</ActionMenu> |
|
||||
|
|
||||
<ConfirmDialog |
|
||||
bind:this={confirmDeleteDialog} |
|
||||
title="Confirm Deletion" |
|
||||
okText="Delete screens" |
|
||||
onOk={deleteScreens} |
|
||||
> |
|
||||
<Layout noPadding gap="S"> |
|
||||
<div> |
|
||||
Are you sure you want to delete all screens under the <b>{path}</b> route? |
|
||||
</div> |
|
||||
<div>The following screens will be deleted:</div> |
|
||||
<div class="to-delete"> |
|
||||
{#each screens as screen} |
|
||||
<div>{screen.route}</div> |
|
||||
{/each} |
|
||||
</div> |
|
||||
</Layout> |
|
||||
</ConfirmDialog> |
|
||||
|
|
||||
<style> |
|
||||
.to-delete { |
|
||||
font-weight: bold; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: flex-start; |
|
||||
align-items: flex-start; |
|
||||
padding-left: var(--spacing-xl); |
|
||||
} |
|
||||
.icon { |
|
||||
display: grid; |
|
||||
place-items: center; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,106 +0,0 @@ |
|||||
<script> |
|
||||
import { |
|
||||
store, |
|
||||
selectedComponent, |
|
||||
currentAsset, |
|
||||
screenSearchString, |
|
||||
} from "builderStore" |
|
||||
import instantiateStore from "./dragDropStore" |
|
||||
import ComponentTree from "./ComponentTree.svelte" |
|
||||
import NavItem from "components/common/NavItem.svelte" |
|
||||
import PathDropdownMenu from "./PathDropdownMenu.svelte" |
|
||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte" |
|
||||
import { get } from "svelte/store" |
|
||||
|
|
||||
const ROUTE_NAME_MAP = { |
|
||||
"/": { |
|
||||
BASIC: "Home", |
|
||||
PUBLIC: "Home", |
|
||||
ADMIN: "Home", |
|
||||
POWER: "Home", |
|
||||
}, |
|
||||
} |
|
||||
|
|
||||
const dragDropStore = instantiateStore() |
|
||||
|
|
||||
export let route |
|
||||
export let path |
|
||||
export let indent |
|
||||
export let border |
|
||||
|
|
||||
let routeManuallyOpened = false |
|
||||
|
|
||||
$: selectedScreen = $currentAsset |
|
||||
$: allScreens = getAllScreens(route) |
|
||||
$: filteredScreens = getFilteredScreens(allScreens, $screenSearchString) |
|
||||
$: hasSearchMatch = $screenSearchString && filteredScreens.length > 0 |
|
||||
$: noSearchMatch = $screenSearchString && !filteredScreens.length |
|
||||
$: routeSelected = |
|
||||
route.subpaths[selectedScreen?.routing?.route] !== undefined |
|
||||
$: routeOpened = routeManuallyOpened || routeSelected || hasSearchMatch |
|
||||
|
|
||||
const changeScreen = screenId => { |
|
||||
store.actions.screens.select(screenId) |
|
||||
} |
|
||||
|
|
||||
const getAllScreens = route => { |
|
||||
let screens = [] |
|
||||
Object.entries(route.subpaths).forEach(([route, subpath]) => { |
|
||||
Object.entries(subpath.screens).forEach(([role, id]) => { |
|
||||
screens.push({ id, route, role }) |
|
||||
}) |
|
||||
}) |
|
||||
return screens |
|
||||
} |
|
||||
|
|
||||
const getFilteredScreens = (screens, searchString) => { |
|
||||
return screens.filter( |
|
||||
screen => !searchString || screen.route.includes(searchString) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
const toggleManuallyOpened = () => { |
|
||||
if (get(screenSearchString)) { |
|
||||
return |
|
||||
} |
|
||||
routeManuallyOpened = !routeManuallyOpened |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
{#if !noSearchMatch} |
|
||||
<NavItem |
|
||||
icon="FolderOutline" |
|
||||
text={path} |
|
||||
on:click={toggleManuallyOpened} |
|
||||
opened={routeOpened} |
|
||||
{border} |
|
||||
withArrow={route.subpaths} |
|
||||
> |
|
||||
<PathDropdownMenu screens={allScreens} {path} /> |
|
||||
</NavItem> |
|
||||
|
|
||||
{#if routeOpened} |
|
||||
{#each filteredScreens as screen (screen.id)} |
|
||||
<NavItem |
|
||||
icon="WebPage" |
|
||||
indentLevel={indent || 1} |
|
||||
selected={$store.selectedScreenId === screen.id && |
|
||||
$store.currentView === "detail"} |
|
||||
opened={$store.selectedScreenId === screen.id} |
|
||||
text={ROUTE_NAME_MAP[screen.route]?.[screen.role] || screen.route} |
|
||||
withArrow={route.subpaths} |
|
||||
on:click={() => changeScreen(screen.id)} |
|
||||
> |
|
||||
<ScreenDropdownMenu screenId={screen.id} /> |
|
||||
</NavItem> |
|
||||
{#if selectedScreen?._id === screen.id} |
|
||||
<ComponentTree |
|
||||
level={1} |
|
||||
components={selectedScreen.props._children} |
|
||||
currentComponent={$selectedComponent} |
|
||||
{dragDropStore} |
|
||||
/> |
|
||||
{/if} |
|
||||
{/each} |
|
||||
{/if} |
|
||||
{/if} |
|
||||
@ -1,104 +0,0 @@ |
|||||
import { writable, get } from "svelte/store" |
|
||||
import { store as frontendStore } from "builderStore" |
|
||||
import { findComponentPath } from "builderStore/componentUtils" |
|
||||
|
|
||||
export const DropEffect = { |
|
||||
MOVE: "move", |
|
||||
COPY: "copy", |
|
||||
} |
|
||||
|
|
||||
export const DropPosition = { |
|
||||
ABOVE: "above", |
|
||||
BELOW: "below", |
|
||||
INSIDE: "inside", |
|
||||
} |
|
||||
|
|
||||
export default function () { |
|
||||
const store = writable({}) |
|
||||
|
|
||||
store.actions = { |
|
||||
dragstart: component => { |
|
||||
store.update(state => { |
|
||||
state.dragged = component |
|
||||
return state |
|
||||
}) |
|
||||
}, |
|
||||
dragover: ({ |
|
||||
component, |
|
||||
index, |
|
||||
canHaveChildrenButIsEmpty, |
|
||||
mousePosition, |
|
||||
}) => { |
|
||||
store.update(state => { |
|
||||
state.targetComponent = component |
|
||||
// only allow dropping inside when container is empty
|
|
||||
// if container has children, drag over them
|
|
||||
|
|
||||
if (canHaveChildrenButIsEmpty && index === 0) { |
|
||||
// hovered above center of target
|
|
||||
if (mousePosition < 0.4) { |
|
||||
state.dropPosition = DropPosition.ABOVE |
|
||||
} |
|
||||
|
|
||||
// hovered around bottom of target
|
|
||||
if (mousePosition > 0.8) { |
|
||||
state.dropPosition = DropPosition.BELOW |
|
||||
} |
|
||||
|
|
||||
// hovered in center of target
|
|
||||
if (mousePosition > 0.4 && mousePosition < 0.8) { |
|
||||
state.dropPosition = DropPosition.INSIDE |
|
||||
} |
|
||||
return state |
|
||||
} |
|
||||
|
|
||||
// bottom half
|
|
||||
if (mousePosition > 0.5) { |
|
||||
state.dropPosition = DropPosition.BELOW |
|
||||
} else { |
|
||||
state.dropPosition = canHaveChildrenButIsEmpty |
|
||||
? DropPosition.INSIDE |
|
||||
: DropPosition.ABOVE |
|
||||
} |
|
||||
|
|
||||
return state |
|
||||
}) |
|
||||
}, |
|
||||
reset: () => { |
|
||||
store.update(state => { |
|
||||
state.dropPosition = "" |
|
||||
state.targetComponent = null |
|
||||
state.dragged = null |
|
||||
return state |
|
||||
}) |
|
||||
}, |
|
||||
drop: async () => { |
|
||||
const state = get(store) |
|
||||
|
|
||||
// Stop if the target and source are the same
|
|
||||
if (state.targetComponent === state.dragged) { |
|
||||
return |
|
||||
} |
|
||||
// Stop if the target or source are null
|
|
||||
if (!state.targetComponent || !state.dragged) { |
|
||||
return |
|
||||
} |
|
||||
// Stop if the target is a child of source
|
|
||||
const path = findComponentPath(state.dragged, state.targetComponent._id) |
|
||||
const ids = path.map(component => component._id) |
|
||||
if (ids.includes(state.targetComponent._id)) { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// Cut and paste the component
|
|
||||
frontendStore.actions.components.copy(state.dragged, true) |
|
||||
await frontendStore.actions.components.paste( |
|
||||
state.targetComponent, |
|
||||
state.dropPosition |
|
||||
) |
|
||||
store.actions.reset() |
|
||||
}, |
|
||||
} |
|
||||
|
|
||||
return store |
|
||||
} |
|
||||
@ -1,83 +0,0 @@ |
|||||
<script> |
|
||||
import { store, selectedAccessRole } from "builderStore" |
|
||||
import PathTree from "./PathTree.svelte" |
|
||||
|
|
||||
let routes = {} |
|
||||
let paths = [] |
|
||||
|
|
||||
$: allRoutes = $store.routes |
|
||||
$: selectedScreenId = $store.selectedScreenId |
|
||||
$: updatePaths(allRoutes, $selectedAccessRole, selectedScreenId) |
|
||||
|
|
||||
const updatePaths = (allRoutes, selectedRoleId, selectedScreenId) => { |
|
||||
const sortedPaths = Object.keys(allRoutes || {}).sort() |
|
||||
|
|
||||
let found = false |
|
||||
let firstValidScreenId |
|
||||
let filteredRoutes = {} |
|
||||
let screenRoleId |
|
||||
|
|
||||
// Filter all routes down to only those which match the current role |
|
||||
sortedPaths.forEach(path => { |
|
||||
const config = allRoutes[path] |
|
||||
Object.entries(config.subpaths).forEach(([subpath, pathConfig]) => { |
|
||||
Object.entries(pathConfig.screens).forEach(([roleId, screenId]) => { |
|
||||
if (screenId === selectedScreenId) { |
|
||||
screenRoleId = roleId |
|
||||
found = roleId === selectedRoleId |
|
||||
} |
|
||||
if (roleId === selectedRoleId) { |
|
||||
if (!firstValidScreenId) { |
|
||||
firstValidScreenId = screenId |
|
||||
} |
|
||||
if (!filteredRoutes[path]) { |
|
||||
filteredRoutes[path] = { subpaths: {} } |
|
||||
} |
|
||||
filteredRoutes[path].subpaths[subpath] = { |
|
||||
screens: { |
|
||||
[selectedRoleId]: screenId, |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
routes = { ...filteredRoutes } |
|
||||
paths = Object.keys(routes || {}).sort() |
|
||||
|
|
||||
// Select the correct role for the current screen ID |
|
||||
if (!found && screenRoleId) { |
|
||||
selectedAccessRole.set(screenRoleId) |
|
||||
if (screenRoleId !== selectedRoleId) { |
|
||||
updatePaths(allRoutes, screenRoleId, selectedScreenId) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// If the selected screen isn't in this filtered list, select the first one |
|
||||
else if (!found && firstValidScreenId) { |
|
||||
store.actions.screens.select(firstValidScreenId) |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div class="root" class:has-screens={!!paths?.length}> |
|
||||
{#each paths as path, idx (path)} |
|
||||
<PathTree border={idx > 0} {path} route={routes[path]} /> |
|
||||
{/each} |
|
||||
{#if !paths.length} |
|
||||
<div class="empty"> |
|
||||
There aren't any screens configured with this access role. |
|
||||
</div> |
|
||||
{/if} |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.root.has-screens { |
|
||||
min-width: max-content; |
|
||||
} |
|
||||
div.empty { |
|
||||
font-size: var(--font-size-s); |
|
||||
color: var(--grey-5); |
|
||||
padding: var(--spacing-xs) var(--spacing-xl); |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,229 +0,0 @@ |
|||||
<script> |
|
||||
import { onMount, setContext } from "svelte" |
|
||||
import { goto, params } from "@roxi/routify" |
|
||||
import { |
|
||||
store, |
|
||||
allScreens, |
|
||||
selectedAccessRole, |
|
||||
screenSearchString, |
|
||||
} from "builderStore" |
|
||||
import { roles } from "stores/backend" |
|
||||
import ComponentNavigationTree from "components/design/NavigationPanel/ComponentNavigationTree/index.svelte" |
|
||||
import Layout from "components/design/NavigationPanel/Layout.svelte" |
|
||||
import NewLayoutModal from "components/design/NavigationPanel/NewLayoutModal.svelte" |
|
||||
import { |
|
||||
Icon, |
|
||||
Modal, |
|
||||
Select, |
|
||||
Search, |
|
||||
Tabs, |
|
||||
Tab, |
|
||||
Layout as BBUILayout, |
|
||||
notifications, |
|
||||
} from "@budibase/bbui" |
|
||||
|
|
||||
export let showModal |
|
||||
|
|
||||
let scrollRef |
|
||||
|
|
||||
const scrollTo = bounds => { |
|
||||
if (!bounds) { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
const sidebarWidth = 259 |
|
||||
const navItemHeight = 32 |
|
||||
const { scrollLeft, scrollTop, offsetHeight } = scrollRef |
|
||||
|
|
||||
let scrollBounds = scrollRef.getBoundingClientRect() |
|
||||
let newOffsets = {} |
|
||||
|
|
||||
// Calculate left offset |
|
||||
const offsetX = bounds.left + bounds.width + scrollLeft + 20 |
|
||||
if (offsetX > sidebarWidth) { |
|
||||
newOffsets.left = offsetX - sidebarWidth |
|
||||
} else { |
|
||||
newOffsets.left = 0 |
|
||||
} |
|
||||
if (newOffsets.left === scrollLeft) { |
|
||||
delete newOffsets.left |
|
||||
} |
|
||||
|
|
||||
// Calculate top offset |
|
||||
const offsetY = bounds.top - scrollBounds?.top + scrollTop |
|
||||
if (offsetY > scrollTop + offsetHeight - 2 * navItemHeight) { |
|
||||
newOffsets.top = offsetY - offsetHeight + 2 * navItemHeight |
|
||||
} else if (offsetY < scrollTop + navItemHeight) { |
|
||||
newOffsets.top = offsetY - navItemHeight |
|
||||
} else { |
|
||||
delete newOffsets.top |
|
||||
} |
|
||||
|
|
||||
// Skip if offset is unchanged |
|
||||
if (newOffsets.left == null && newOffsets.top == null) { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// Smoothly scroll to the offset |
|
||||
scrollRef.scroll({ |
|
||||
...newOffsets, |
|
||||
behavior: "smooth", |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
setContext("scroll", { |
|
||||
scrollTo, |
|
||||
}) |
|
||||
|
|
||||
const tabs = [ |
|
||||
{ |
|
||||
title: "Screens", |
|
||||
key: "screen", |
|
||||
}, |
|
||||
{ |
|
||||
title: "Layouts", |
|
||||
key: "layout", |
|
||||
}, |
|
||||
] |
|
||||
let newLayoutModal |
|
||||
$: selected = tabs.find(t => t.key === $params.assetType)?.title || "Screens" |
|
||||
|
|
||||
const navigate = ({ detail }) => { |
|
||||
const { key } = tabs.find(t => t.title === detail) |
|
||||
$goto(`../${key}`) |
|
||||
} |
|
||||
|
|
||||
const updateAccessRole = event => { |
|
||||
const role = event.detail |
|
||||
|
|
||||
// Select a valid screen with this new role - otherwise we'll not be |
|
||||
// able to change role at all because ComponentNavigationTree will kick us |
|
||||
// back the current role again because the same screen ID is still selected |
|
||||
const firstValidScreenId = $allScreens.find( |
|
||||
screen => screen.routing.roleId === role |
|
||||
)?._id |
|
||||
if (firstValidScreenId) { |
|
||||
store.actions.screens.select(firstValidScreenId) |
|
||||
} |
|
||||
|
|
||||
// Otherwise clear the selected screen ID so that the first new valid screen |
|
||||
// can be selected by ComponentNavigationTree |
|
||||
else { |
|
||||
store.update(state => { |
|
||||
state.selectedScreenId = null |
|
||||
return state |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
selectedAccessRole.set(role) |
|
||||
} |
|
||||
|
|
||||
onMount(async () => { |
|
||||
try { |
|
||||
await store.actions.routing.fetch() |
|
||||
} catch (error) { |
|
||||
notifications.error("Error fetching routes") |
|
||||
} |
|
||||
}) |
|
||||
</script> |
|
||||
|
|
||||
<div class="title"> |
|
||||
<Tabs {selected} on:select={navigate}> |
|
||||
<Tab title="Screens"> |
|
||||
<div class="tab-content-padding"> |
|
||||
<BBUILayout noPadding gap="XS"> |
|
||||
<Select |
|
||||
on:change={updateAccessRole} |
|
||||
value={$selectedAccessRole} |
|
||||
label="Filter by Access" |
|
||||
getOptionLabel={role => role.name} |
|
||||
getOptionValue={role => role._id} |
|
||||
options={$roles} |
|
||||
/> |
|
||||
<Search |
|
||||
placeholder="Enter a route to search" |
|
||||
label="Search Screens" |
|
||||
bind:value={$screenSearchString} |
|
||||
/> |
|
||||
</BBUILayout> |
|
||||
<div class="nav-items-container" bind:this={scrollRef}> |
|
||||
<ComponentNavigationTree /> |
|
||||
</div> |
|
||||
</div> |
|
||||
</Tab> |
|
||||
<Tab title="Layouts"> |
|
||||
<div class="tab-content-padding"> |
|
||||
<div |
|
||||
class="nav-items-container nav-items-container--layouts" |
|
||||
bind:this={scrollRef} |
|
||||
> |
|
||||
<div class="layouts-container"> |
|
||||
{#each $store.layouts as layout, idx (layout._id)} |
|
||||
<Layout {layout} border={idx > 0} /> |
|
||||
{/each} |
|
||||
</div> |
|
||||
</div> |
|
||||
<Modal bind:this={newLayoutModal}> |
|
||||
<NewLayoutModal /> |
|
||||
</Modal> |
|
||||
</div> |
|
||||
</Tab> |
|
||||
</Tabs> |
|
||||
<div class="add-button"> |
|
||||
<Icon |
|
||||
hoverable |
|
||||
name="AddCircle" |
|
||||
on:click={selected === "Layouts" ? newLayoutModal.show() : showModal()} |
|
||||
/> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.title { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: flex-start; |
|
||||
align-items: stretch; |
|
||||
position: relative; |
|
||||
flex: 1 1 auto; |
|
||||
} |
|
||||
.title :global(.spectrum-Tabs-content), |
|
||||
.title :global(.spectrum-Tabs-content > div), |
|
||||
.title :global(.spectrum-Tabs-content > div > div) { |
|
||||
height: 100%; |
|
||||
} |
|
||||
|
|
||||
.add-button { |
|
||||
position: absolute; |
|
||||
top: var(--spacing-l); |
|
||||
right: var(--spacing-xl); |
|
||||
} |
|
||||
|
|
||||
.tab-content-padding { |
|
||||
padding: 0 var(--spacing-xl); |
|
||||
height: 100%; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: flex-start; |
|
||||
align-items: stretch; |
|
||||
gap: var(--spacing-xl); |
|
||||
} |
|
||||
|
|
||||
.nav-items-container { |
|
||||
border-top: var(--border-light); |
|
||||
margin: 0 calc(-1 * var(--spacing-xl)); |
|
||||
padding: var(--spacing-m) 0; |
|
||||
flex: 1 1 auto; |
|
||||
overflow: auto; |
|
||||
height: 0; |
|
||||
position: relative; |
|
||||
} |
|
||||
.nav-items-container--layouts { |
|
||||
border-top: none; |
|
||||
margin-top: calc(-1 * var(--spectrum-global-dimension-static-size-150)); |
|
||||
} |
|
||||
|
|
||||
.layouts-container { |
|
||||
min-width: max-content; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,36 +0,0 @@ |
|||||
<script> |
|
||||
import ComponentTree from "./ComponentNavigationTree/ComponentTree.svelte" |
|
||||
import LayoutDropdownMenu from "./ComponentNavigationTree/LayoutDropdownMenu.svelte" |
|
||||
import initDragDropStore from "./ComponentNavigationTree/dragDropStore" |
|
||||
import NavItem from "components/common/NavItem.svelte" |
|
||||
import { store, selectedComponent } from "builderStore" |
|
||||
|
|
||||
export let layout |
|
||||
export let border |
|
||||
|
|
||||
const dragDropStore = initDragDropStore() |
|
||||
|
|
||||
const selectLayout = () => { |
|
||||
store.actions.layouts.select(layout._id) |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<NavItem |
|
||||
{border} |
|
||||
icon="ClassicGridView" |
|
||||
text={layout.name} |
|
||||
withArrow |
|
||||
selected={$store.selectedLayoutId === layout._id} |
|
||||
opened={$store.selectedLayoutId === layout._id} |
|
||||
on:click={selectLayout} |
|
||||
> |
|
||||
<LayoutDropdownMenu {layout} /> |
|
||||
</NavItem> |
|
||||
|
|
||||
{#if $store.selectedLayoutId === layout._id && layout.props?._children} |
|
||||
<ComponentTree |
|
||||
components={layout.props._children} |
|
||||
currentComponent={$selectedComponent} |
|
||||
{dragDropStore} |
|
||||
/> |
|
||||
{/if} |
|
||||
@ -1,93 +0,0 @@ |
|||||
<script> |
|
||||
import { ModalContent, Body, Detail } from "@budibase/bbui" |
|
||||
|
|
||||
export let selectedScreens |
|
||||
export let chooseModal |
|
||||
export let save |
|
||||
let selectedNav |
|
||||
let createdScreens = [] |
|
||||
$: blankSelected = selectedScreens.length === 1 |
|
||||
</script> |
|
||||
|
|
||||
<ModalContent |
|
||||
title="Select navigation" |
|
||||
cancelText="Back" |
|
||||
onCancel={() => (blankSelected ? chooseModal(1) : chooseModal(0))} |
|
||||
size="M" |
|
||||
onConfirm={() => { |
|
||||
save(createdScreens) |
|
||||
}} |
|
||||
disabled={!selectedNav} |
|
||||
> |
|
||||
<Body size="S" |
|
||||
>Please select your preferred layout for the new application:</Body |
|
||||
> |
|
||||
|
|
||||
<div class="wrapper"> |
|
||||
<div |
|
||||
data-cy="left-nav" |
|
||||
on:click={() => (selectedNav = "Left")} |
|
||||
class:unselected={selectedNav && selectedNav !== "Left"} |
|
||||
> |
|
||||
<div class="box"> |
|
||||
<div class="side-nav" /> |
|
||||
</div> |
|
||||
<div><Detail>Side Nav</Detail></div> |
|
||||
</div> |
|
||||
<div |
|
||||
on:click={() => (selectedNav = "Top")} |
|
||||
class:unselected={selectedNav && selectedNav !== "Top"} |
|
||||
> |
|
||||
<div class="box"> |
|
||||
<div class="top-nav" /> |
|
||||
</div> |
|
||||
<div><Detail>Top Nav</Detail></div> |
|
||||
</div> |
|
||||
<div |
|
||||
on:click={() => (selectedNav = "None")} |
|
||||
class:unselected={selectedNav && selectedNav !== "None"} |
|
||||
> |
|
||||
<div class="box" /> |
|
||||
<div><Detail>No Nav</Detail></div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</ModalContent> |
|
||||
|
|
||||
<style> |
|
||||
.side-nav { |
|
||||
float: left; |
|
||||
background: #d3d3d3 0% 0% no-repeat padding-box; |
|
||||
border-radius: 2px 0px 0px 2px; |
|
||||
height: 100%; |
|
||||
width: 10%; |
|
||||
} |
|
||||
|
|
||||
.top-nav { |
|
||||
background: #d3d3d3 0% 0% no-repeat padding-box; |
|
||||
vertical-align: top; |
|
||||
width: 100%; |
|
||||
height: 15%; |
|
||||
} |
|
||||
.box { |
|
||||
display: inline-block; |
|
||||
background: #eaeaea 0% 0% no-repeat padding-box; |
|
||||
border: 1px solid #d3d3d3; |
|
||||
border-radius: 2px; |
|
||||
opacity: 1; |
|
||||
width: 120px; |
|
||||
height: 70px; |
|
||||
margin-right: 20px; |
|
||||
} |
|
||||
|
|
||||
.wrapper { |
|
||||
display: flex; |
|
||||
padding-top: 4%; |
|
||||
list-style-type: none; |
|
||||
margin: 0; |
|
||||
padding: 0; |
|
||||
margin-right: 5%; |
|
||||
} |
|
||||
.unselected { |
|
||||
opacity: 0.3; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,20 +0,0 @@ |
|||||
<script> |
|
||||
import { notifications } from "@budibase/bbui" |
|
||||
import { store } from "builderStore" |
|
||||
import { Input, ModalContent } from "@budibase/bbui" |
|
||||
|
|
||||
let name = "" |
|
||||
|
|
||||
async function save() { |
|
||||
try { |
|
||||
await store.actions.layouts.save({ name }) |
|
||||
notifications.success(`Layout ${name} created successfully`) |
|
||||
} catch (error) { |
|
||||
notifications.error("Error creating layout") |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<ModalContent title="Create Layout" confirmText="Create" onConfirm={save}> |
|
||||
<Input thin label="Name" bind:value={name} /> |
|
||||
</ModalContent> |
|
||||
@ -0,0 +1,112 @@ |
|||||
|
<script> |
||||
|
import { Icon, Heading } from "@budibase/bbui" |
||||
|
|
||||
|
export let title |
||||
|
export let icon |
||||
|
export let showAddButton = false |
||||
|
export let showBackButton = false |
||||
|
export let showExpandIcon = false |
||||
|
export let onClickAddButton |
||||
|
export let onClickBackButton |
||||
|
export let borderLeft = false |
||||
|
export let borderRight = false |
||||
|
|
||||
|
let wide = false |
||||
|
</script> |
||||
|
|
||||
|
<div class="panel" class:wide class:borderLeft class:borderRight> |
||||
|
<div class="header"> |
||||
|
{#if showBackButton} |
||||
|
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} /> |
||||
|
{/if} |
||||
|
{#if icon} |
||||
|
<Icon name={icon} /> |
||||
|
{/if} |
||||
|
<div class="title"> |
||||
|
<Heading size="XXS">{title || ""}</Heading> |
||||
|
</div> |
||||
|
{#if showExpandIcon} |
||||
|
<Icon |
||||
|
name={wide ? "Minimize" : "Maximize"} |
||||
|
hoverable |
||||
|
on:click={() => (wide = !wide)} |
||||
|
/> |
||||
|
{/if} |
||||
|
{#if showAddButton} |
||||
|
<div class="add-button" on:click={onClickAddButton}> |
||||
|
<Icon name="Add" /> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
<div class="body"> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.panel { |
||||
|
width: 260px; |
||||
|
background: var(--background); |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: flex-start; |
||||
|
align-items: stretch; |
||||
|
transition: width 130ms ease-out; |
||||
|
} |
||||
|
.panel.borderLeft { |
||||
|
border-left: var(--border-light); |
||||
|
} |
||||
|
.panel.borderRight { |
||||
|
border-right: var(--border-light); |
||||
|
} |
||||
|
.panel.wide { |
||||
|
width: 420px; |
||||
|
} |
||||
|
.header { |
||||
|
flex: 0 0 48px; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
padding: 0 var(--spacing-l); |
||||
|
border-bottom: var(--border-light); |
||||
|
gap: var(--spacing-l); |
||||
|
} |
||||
|
.title { |
||||
|
flex: 1 1 auto; |
||||
|
width: 0; |
||||
|
} |
||||
|
.title :global(h1) { |
||||
|
overflow: hidden; |
||||
|
font-weight: 600; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
.add-button { |
||||
|
flex: 0 0 30px; |
||||
|
height: 30px; |
||||
|
display: grid; |
||||
|
place-items: center; |
||||
|
border-radius: 4px; |
||||
|
position: relative; |
||||
|
cursor: pointer; |
||||
|
background: var(--spectrum-semantic-cta-color-background-default); |
||||
|
transition: background var(--spectrum-global-animation-duration-100, 130ms) |
||||
|
ease-out; |
||||
|
} |
||||
|
.add-button:hover { |
||||
|
background: var(--spectrum-semantic-cta-color-background-hover); |
||||
|
} |
||||
|
.add-button :global(svg) { |
||||
|
fill: white; |
||||
|
} |
||||
|
.body { |
||||
|
flex: 1 1 auto; |
||||
|
overflow: auto; |
||||
|
overflow-x: hidden; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: flex-start; |
||||
|
align-items: stretch; |
||||
|
} |
||||
|
</style> |
||||
@ -1,63 +0,0 @@ |
|||||
<script> |
|
||||
import { store, selectedComponent, currentAsset } from "builderStore" |
|
||||
import { Tabs, Tab } from "@budibase/bbui" |
|
||||
import ScreenSettingsSection from "./ScreenSettingsSection.svelte" |
|
||||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte" |
|
||||
import DesignSection from "./DesignSection.svelte" |
|
||||
import CustomStylesSection from "./CustomStylesSection.svelte" |
|
||||
import ConditionalUISection from "./ConditionalUISection.svelte" |
|
||||
import { |
|
||||
getBindableProperties, |
|
||||
getComponentBindableProperties, |
|
||||
} from "builderStore/dataBinding" |
|
||||
|
|
||||
$: componentInstance = $selectedComponent |
|
||||
$: componentDefinition = store.actions.components.getDefinition( |
|
||||
$selectedComponent?._component |
|
||||
) |
|
||||
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId) |
|
||||
$: componentBindings = getComponentBindableProperties( |
|
||||
$currentAsset, |
|
||||
$store.selectedComponentId |
|
||||
) |
|
||||
</script> |
|
||||
|
|
||||
<Tabs selected="Settings" noPadding> |
|
||||
<Tab title="Settings"> |
|
||||
<div class="container"> |
|
||||
{#key componentInstance?._id} |
|
||||
<ScreenSettingsSection |
|
||||
{componentInstance} |
|
||||
{componentDefinition} |
|
||||
{bindings} |
|
||||
/> |
|
||||
<ComponentSettingsSection |
|
||||
{componentInstance} |
|
||||
{componentDefinition} |
|
||||
{bindings} |
|
||||
{componentBindings} |
|
||||
/> |
|
||||
<DesignSection {componentInstance} {componentDefinition} {bindings} /> |
|
||||
<CustomStylesSection |
|
||||
{componentInstance} |
|
||||
{componentDefinition} |
|
||||
{bindings} |
|
||||
/> |
|
||||
<ConditionalUISection |
|
||||
{componentInstance} |
|
||||
{componentDefinition} |
|
||||
{bindings} |
|
||||
/> |
|
||||
{/key} |
|
||||
</div> |
|
||||
</Tab> |
|
||||
</Tabs> |
|
||||
|
|
||||
<style> |
|
||||
.container { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: flex-start; |
|
||||
align-items: stretch; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,25 +0,0 @@ |
|||||
<script> |
|
||||
import { Button, ActionButton, Drawer } from "@budibase/bbui" |
|
||||
import { createEventDispatcher } from "svelte" |
|
||||
import NavigationDrawer from "./NavigationDrawer.svelte" |
|
||||
import { cloneDeep } from "lodash/fp" |
|
||||
|
|
||||
export let value = [] |
|
||||
let drawer |
|
||||
let links = cloneDeep(value || []) |
|
||||
|
|
||||
const dispatch = createEventDispatcher() |
|
||||
const save = () => { |
|
||||
dispatch("change", links) |
|
||||
drawer.hide() |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<ActionButton on:click={drawer.show}>Configure links</ActionButton> |
|
||||
<Drawer bind:this={drawer} title={"Navigation Links"}> |
|
||||
<svelte:fragment slot="description"> |
|
||||
Configure the links in your navigation bar. |
|
||||
</svelte:fragment> |
|
||||
<Button cta slot="buttons" on:click={save}>Save</Button> |
|
||||
<NavigationDrawer slot="body" bind:links /> |
|
||||
</Drawer> |
|
||||
@ -1,68 +0,0 @@ |
|||||
import { Checkbox, Select, Stepper } from "@budibase/bbui" |
|
||||
import DataSourceSelect from "./DataSourceSelect.svelte" |
|
||||
import S3DataSourceSelect from "./S3DataSourceSelect.svelte" |
|
||||
import DataProviderSelect from "./DataProviderSelect.svelte" |
|
||||
import ButtonActionEditor from "./ButtonActionEditor/ButtonActionEditor.svelte" |
|
||||
import TableSelect from "./TableSelect.svelte" |
|
||||
import ColorPicker from "./ColorPicker.svelte" |
|
||||
import { IconSelect } from "./IconSelect" |
|
||||
import FieldSelect from "./FieldSelect.svelte" |
|
||||
import MultiFieldSelect from "./MultiFieldSelect.svelte" |
|
||||
import SearchFieldSelect from "./SearchFieldSelect.svelte" |
|
||||
import SchemaSelect from "./SchemaSelect.svelte" |
|
||||
import SectionSelect from "./SectionSelect.svelte" |
|
||||
import NavigationEditor from "./NavigationEditor/NavigationEditor.svelte" |
|
||||
import FilterEditor from "./FilterEditor/FilterEditor.svelte" |
|
||||
import URLSelect from "./URLSelect.svelte" |
|
||||
import OptionsEditor from "./OptionsEditor/OptionsEditor.svelte" |
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte" |
|
||||
import ValidationEditor from "./ValidationEditor/ValidationEditor.svelte" |
|
||||
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte" |
|
||||
import ColumnEditor from "./ColumnEditor/ColumnEditor.svelte" |
|
||||
|
|
||||
const componentMap = { |
|
||||
text: DrawerBindableCombobox, |
|
||||
select: Select, |
|
||||
dataSource: DataSourceSelect, |
|
||||
"dataSource/s3": S3DataSourceSelect, |
|
||||
dataProvider: DataProviderSelect, |
|
||||
boolean: Checkbox, |
|
||||
number: Stepper, |
|
||||
event: ButtonActionEditor, |
|
||||
table: TableSelect, |
|
||||
color: ColorPicker, |
|
||||
icon: IconSelect, |
|
||||
field: FieldSelect, |
|
||||
multifield: MultiFieldSelect, |
|
||||
searchfield: SearchFieldSelect, |
|
||||
options: OptionsEditor, |
|
||||
schema: SchemaSelect, |
|
||||
section: SectionSelect, |
|
||||
navigation: NavigationEditor, |
|
||||
filter: FilterEditor, |
|
||||
url: URLSelect, |
|
||||
columns: ColumnEditor, |
|
||||
"field/string": FormFieldSelect, |
|
||||
"field/number": FormFieldSelect, |
|
||||
"field/options": FormFieldSelect, |
|
||||
"field/boolean": FormFieldSelect, |
|
||||
"field/longform": FormFieldSelect, |
|
||||
"field/datetime": FormFieldSelect, |
|
||||
"field/attachment": FormFieldSelect, |
|
||||
"field/link": FormFieldSelect, |
|
||||
"field/array": FormFieldSelect, |
|
||||
"field/json": FormFieldSelect, |
|
||||
// Some validation types are the same as others, so not all types are
|
|
||||
// explicitly listed here. e.g. options uses string validation
|
|
||||
"validation/string": ValidationEditor, |
|
||||
"validation/array": ValidationEditor, |
|
||||
"validation/number": ValidationEditor, |
|
||||
"validation/boolean": ValidationEditor, |
|
||||
"validation/datetime": ValidationEditor, |
|
||||
"validation/attachment": ValidationEditor, |
|
||||
"validation/link": ValidationEditor, |
|
||||
} |
|
||||
|
|
||||
export const getComponentForSettingType = type => { |
|
||||
return componentMap[type] |
|
||||
} |
|
||||
@ -1,125 +0,0 @@ |
|||||
<script> |
|
||||
import { get } from "svelte/store" |
|
||||
import { get as deepGet, setWith } from "lodash" |
|
||||
import { Input, DetailSummary, notifications } from "@budibase/bbui" |
|
||||
import PropertyControl from "./PropertyControls/PropertyControl.svelte" |
|
||||
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte" |
|
||||
import RoleSelect from "./PropertyControls/RoleSelect.svelte" |
|
||||
import { currentAsset, store } from "builderStore" |
|
||||
import { FrontendTypes } from "constants" |
|
||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" |
|
||||
import { allScreens, selectedAccessRole } from "builderStore" |
|
||||
|
|
||||
export let componentInstance |
|
||||
export let bindings |
|
||||
|
|
||||
let errors = {} |
|
||||
|
|
||||
const routeTaken = url => { |
|
||||
const roleId = get(selectedAccessRole) || "BASIC" |
|
||||
return get(allScreens).some( |
|
||||
screen => |
|
||||
screen.routing.route.toLowerCase() === url.toLowerCase() && |
|
||||
screen.routing.roleId === roleId |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
const roleTaken = roleId => { |
|
||||
const url = get(currentAsset)?.routing.route |
|
||||
return get(allScreens).some( |
|
||||
screen => |
|
||||
screen.routing.route.toLowerCase() === url.toLowerCase() && |
|
||||
screen.routing.roleId === roleId |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
const setAssetProps = (name, value, parser, validate) => { |
|
||||
if (parser) { |
|
||||
value = parser(value) |
|
||||
} |
|
||||
if (validate) { |
|
||||
const error = validate(value) |
|
||||
errors = { |
|
||||
...errors, |
|
||||
[name]: error, |
|
||||
} |
|
||||
if (error) { |
|
||||
return |
|
||||
} |
|
||||
} else { |
|
||||
errors = { |
|
||||
...errors, |
|
||||
[name]: null, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const selectedAsset = get(currentAsset) |
|
||||
store.update(state => { |
|
||||
if ( |
|
||||
name === "_instanceName" && |
|
||||
state.currentFrontEndType === FrontendTypes.SCREEN |
|
||||
) { |
|
||||
selectedAsset.props._instanceName = value |
|
||||
} else { |
|
||||
setWith(selectedAsset, name.split("."), value, Object) |
|
||||
} |
|
||||
return state |
|
||||
}) |
|
||||
|
|
||||
try { |
|
||||
store.actions.preview.saveSelected() |
|
||||
} catch (error) { |
|
||||
notifications.error("Error saving settings") |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const screenSettings = [ |
|
||||
{ |
|
||||
key: "routing.route", |
|
||||
label: "Route", |
|
||||
control: Input, |
|
||||
parser: val => { |
|
||||
if (!val.startsWith("/")) { |
|
||||
val = "/" + val |
|
||||
} |
|
||||
return sanitizeUrl(val) |
|
||||
}, |
|
||||
validate: val => { |
|
||||
const exisingValue = get(currentAsset)?.routing.route |
|
||||
if (val !== exisingValue && routeTaken(val)) { |
|
||||
return "That URL is already in use for this role" |
|
||||
} |
|
||||
return null |
|
||||
}, |
|
||||
}, |
|
||||
{ |
|
||||
key: "routing.roleId", |
|
||||
label: "Access", |
|
||||
control: RoleSelect, |
|
||||
validate: val => { |
|
||||
const exisingValue = get(currentAsset)?.routing.roleId |
|
||||
if (val !== exisingValue && roleTaken(val)) { |
|
||||
return "That role is already in use for this URL" |
|
||||
} |
|
||||
return null |
|
||||
}, |
|
||||
}, |
|
||||
{ key: "layoutId", label: "Layout", control: LayoutSelect }, |
|
||||
] |
|
||||
</script> |
|
||||
|
|
||||
{#if $store.currentView !== "component" && $currentAsset && $store.currentFrontEndType === FrontendTypes.SCREEN} |
|
||||
<DetailSummary name="Screen" collapsible={false}> |
|
||||
{#each screenSettings as def (`${componentInstance._id}-${def.key}`)} |
|
||||
<PropertyControl |
|
||||
control={def.control} |
|
||||
label={def.label} |
|
||||
key={def.key} |
|
||||
value={deepGet($currentAsset, def.key)} |
|
||||
onChange={val => setAssetProps(def.key, val, def.parser, def.validate)} |
|
||||
{bindings} |
|
||||
props={{ error: errors[def.key] }} |
|
||||
/> |
|
||||
{/each} |
|
||||
</DetailSummary> |
|
||||
{/if} |
|
||||
@ -0,0 +1,77 @@ |
|||||
|
import { Checkbox, Select, Stepper } from "@budibase/bbui" |
||||
|
import DataSourceSelect from "./controls/DataSourceSelect.svelte" |
||||
|
import S3DataSourceSelect from "./controls/S3DataSourceSelect.svelte" |
||||
|
import DataProviderSelect from "./controls/DataProviderSelect.svelte" |
||||
|
import ButtonActionEditor from "./controls/ButtonActionEditor/ButtonActionEditor.svelte" |
||||
|
import TableSelect from "./controls/TableSelect.svelte" |
||||
|
import ColorPicker from "./controls/ColorPicker.svelte" |
||||
|
import { IconSelect } from "./controls/IconSelect" |
||||
|
import FieldSelect from "./controls/FieldSelect.svelte" |
||||
|
import MultiFieldSelect from "./controls/MultiFieldSelect.svelte" |
||||
|
import SearchFieldSelect from "./controls/SearchFieldSelect.svelte" |
||||
|
import SchemaSelect from "./controls/SchemaSelect.svelte" |
||||
|
import SectionSelect from "./controls/SectionSelect.svelte" |
||||
|
import FilterEditor from "./controls/FilterEditor/FilterEditor.svelte" |
||||
|
import URLSelect from "./controls/URLSelect.svelte" |
||||
|
import OptionsEditor from "./controls/OptionsEditor/OptionsEditor.svelte" |
||||
|
import FormFieldSelect from "./controls/FormFieldSelect.svelte" |
||||
|
import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelte" |
||||
|
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte" |
||||
|
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte" |
||||
|
import BarButtonList from "./controls/BarButtonList.svelte" |
||||
|
|
||||
|
const componentMap = { |
||||
|
text: DrawerBindableCombobox, |
||||
|
select: Select, |
||||
|
dataSource: DataSourceSelect, |
||||
|
"dataSource/s3": S3DataSourceSelect, |
||||
|
dataProvider: DataProviderSelect, |
||||
|
boolean: Checkbox, |
||||
|
number: Stepper, |
||||
|
event: ButtonActionEditor, |
||||
|
table: TableSelect, |
||||
|
color: ColorPicker, |
||||
|
icon: IconSelect, |
||||
|
field: FieldSelect, |
||||
|
multifield: MultiFieldSelect, |
||||
|
searchfield: SearchFieldSelect, |
||||
|
options: OptionsEditor, |
||||
|
schema: SchemaSelect, |
||||
|
section: SectionSelect, |
||||
|
filter: FilterEditor, |
||||
|
url: URLSelect, |
||||
|
columns: ColumnEditor, |
||||
|
"field/string": FormFieldSelect, |
||||
|
"field/number": FormFieldSelect, |
||||
|
"field/options": FormFieldSelect, |
||||
|
"field/boolean": FormFieldSelect, |
||||
|
"field/longform": FormFieldSelect, |
||||
|
"field/datetime": FormFieldSelect, |
||||
|
"field/attachment": FormFieldSelect, |
||||
|
"field/link": FormFieldSelect, |
||||
|
"field/array": FormFieldSelect, |
||||
|
"field/json": FormFieldSelect, |
||||
|
// Some validation types are the same as others, so not all types are
|
||||
|
// explicitly listed here. e.g. options uses string validation
|
||||
|
"validation/string": ValidationEditor, |
||||
|
"validation/array": ValidationEditor, |
||||
|
"validation/number": ValidationEditor, |
||||
|
"validation/boolean": ValidationEditor, |
||||
|
"validation/datetime": ValidationEditor, |
||||
|
"validation/attachment": ValidationEditor, |
||||
|
"validation/link": ValidationEditor, |
||||
|
} |
||||
|
|
||||
|
export const getComponentForSetting = setting => { |
||||
|
const { type, showInBar, barStyle } = setting || {} |
||||
|
if (!type) { |
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
// We can show a clone of the bar settings for certain select settings
|
||||
|
if (showInBar && type === "select" && barStyle === "buttons") { |
||||
|
return BarButtonList |
||||
|
} |
||||
|
|
||||
|
return componentMap[type] |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
<script> |
||||
|
import { ActionButton, ActionGroup } from "@budibase/bbui" |
||||
|
|
||||
|
export let value |
||||
|
export let onChange |
||||
|
export let options |
||||
|
</script> |
||||
|
|
||||
|
<ActionGroup> |
||||
|
{#each options as option} |
||||
|
<ActionButton |
||||
|
icon={option.barIcon} |
||||
|
quiet |
||||
|
on:click={() => onChange(option.value)} |
||||
|
selected={option.value === value} |
||||
|
/> |
||||
|
{/each} |
||||
|
</ActionGroup> |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue