Merge remote-tracking branch 'origin/feature/app-list-actions' into feature/app-favourites
This commit is contained in:
commit
f2dbeed6e8
|
@ -32,7 +32,7 @@ describe("docWritethrough", () => {
|
||||||
|
|
||||||
describe("patch", () => {
|
describe("patch", () => {
|
||||||
function generatePatchObject(fieldCount: number) {
|
function generatePatchObject(fieldCount: number) {
|
||||||
const keys = generator.unique(() => generator.word(), fieldCount)
|
const keys = generator.unique(() => generator.guid(), fieldCount)
|
||||||
return keys.reduce((acc, c) => {
|
return keys.reduce((acc, c) => {
|
||||||
acc[c] = generator.word()
|
acc[c] = generator.word()
|
||||||
return acc
|
return acc
|
||||||
|
|
|
@ -32,6 +32,13 @@ const handleClick = event => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore clicks for drawers, unless the handler is registered from a drawer
|
||||||
|
const sourceInDrawer = handler.anchor.closest(".drawer-container") != null
|
||||||
|
const clickInDrawer = event.target.closest(".drawer-container") != null
|
||||||
|
if (clickInDrawer && !sourceInDrawer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
handler.callback?.(event)
|
handler.callback?.(event)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ export default function positionDropdown(element, opts) {
|
||||||
align,
|
align,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
|
minWidth,
|
||||||
useAnchorWidth,
|
useAnchorWidth,
|
||||||
offset = 5,
|
offset = 5,
|
||||||
customUpdate,
|
customUpdate,
|
||||||
|
@ -28,7 +29,7 @@ export default function positionDropdown(element, opts) {
|
||||||
const elementBounds = element.getBoundingClientRect()
|
const elementBounds = element.getBoundingClientRect()
|
||||||
let styles = {
|
let styles = {
|
||||||
maxHeight: null,
|
maxHeight: null,
|
||||||
minWidth: null,
|
minWidth,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
left: null,
|
left: null,
|
||||||
top: null,
|
top: null,
|
||||||
|
@ -41,8 +42,13 @@ export default function positionDropdown(element, opts) {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Determine vertical styles
|
// Determine vertical styles
|
||||||
if (align === "right-outside") {
|
if (align === "right-outside" || align === "left-outside") {
|
||||||
styles.top = anchorBounds.top
|
styles.top =
|
||||||
|
anchorBounds.top + anchorBounds.height / 2 - elementBounds.height / 2
|
||||||
|
styles.maxHeight = maxHeight
|
||||||
|
if (styles.top + elementBounds.height > window.innerHeight) {
|
||||||
|
styles.top = window.innerHeight - elementBounds.height
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
window.innerHeight - anchorBounds.bottom <
|
window.innerHeight - anchorBounds.bottom <
|
||||||
(maxHeight || 100)
|
(maxHeight || 100)
|
||||||
|
|
|
@ -1,28 +1,109 @@
|
||||||
|
<script context="module">
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
|
||||||
|
// Observe this class name if possible in order to know how to size the
|
||||||
|
// drawer. If this doesn't exist we'll use a fixed size.
|
||||||
|
const drawerContainer = "drawer-container"
|
||||||
|
|
||||||
|
// Context level stores to keep drawers in sync
|
||||||
|
const openDrawers = writable([])
|
||||||
|
const modal = writable(false)
|
||||||
|
const resizable = writable(true)
|
||||||
|
const drawerLeft = writable(null)
|
||||||
|
const drawerWidth = writable(null)
|
||||||
|
|
||||||
|
// Resize observer to keep track of size changes
|
||||||
|
let observer
|
||||||
|
|
||||||
|
// Starts observing the target node to watching to size changes.
|
||||||
|
// Invoked when the first drawer of a chain is rendered.
|
||||||
|
const observe = () => {
|
||||||
|
const target = document.getElementsByClassName(drawerContainer)[0]
|
||||||
|
if (observer || !target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
observer = new ResizeObserver(entries => {
|
||||||
|
if (!entries?.[0]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const bounds = entries[0].target.getBoundingClientRect()
|
||||||
|
drawerLeft.set(bounds.left)
|
||||||
|
drawerWidth.set(bounds.width)
|
||||||
|
})
|
||||||
|
observer.observe(target)
|
||||||
|
|
||||||
|
// Manually measure once to ensure that we have dimensions for the initial
|
||||||
|
// paint
|
||||||
|
const bounds = target.getBoundingClientRect()
|
||||||
|
drawerLeft.set(bounds.left)
|
||||||
|
drawerWidth.set(bounds.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stops observing the target node.
|
||||||
|
// Invoked when the last drawer of a chain is removed.
|
||||||
|
const unobserve = () => {
|
||||||
|
if (get(openDrawers).length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
observer?.disconnect()
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
observer = null
|
||||||
|
modal.set(false)
|
||||||
|
resizable.set(true)
|
||||||
|
drawerLeft.set(null)
|
||||||
|
drawerWidth.set(null)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Portal from "svelte-portal"
|
import Portal from "svelte-portal"
|
||||||
import Button from "../Button/Button.svelte"
|
import Button from "../Button/Button.svelte"
|
||||||
import Body from "../Typography/Body.svelte"
|
import { setContext, createEventDispatcher, onDestroy } from "svelte"
|
||||||
import Heading from "../Typography/Heading.svelte"
|
|
||||||
import { setContext, createEventDispatcher } from "svelte"
|
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let fillWidth
|
export let forceModal = false
|
||||||
export let left = "314px"
|
|
||||||
export let width = "calc(100% - 626px)"
|
|
||||||
export let headless = false
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
const spacing = 11
|
||||||
|
|
||||||
let visible = false
|
let visible = false
|
||||||
let drawerId = generate()
|
let drawerId = generate()
|
||||||
|
|
||||||
|
$: depth = $openDrawers.length - $openDrawers.indexOf(drawerId) - 1
|
||||||
|
$: style = getStyle(depth, $drawerLeft, $drawerWidth, $modal)
|
||||||
|
|
||||||
|
const getStyle = (depth, left, width, modal) => {
|
||||||
|
let style = `
|
||||||
|
--scale-factor: ${getScaleFactor(depth)};
|
||||||
|
--spacing: ${spacing}px;
|
||||||
|
`
|
||||||
|
// Most modal styles are handled by class names
|
||||||
|
if (modal || left == null || width == null) {
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drawers observing another dom node need custom position styles
|
||||||
|
return `
|
||||||
|
${style}
|
||||||
|
left: ${left + spacing}px;
|
||||||
|
width: ${width - 2 * spacing}px;
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
export function show() {
|
export function show() {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (forceModal) {
|
||||||
|
modal.set(true)
|
||||||
|
resizable.set(false)
|
||||||
|
}
|
||||||
|
observe()
|
||||||
visible = true
|
visible = true
|
||||||
dispatch("drawerShow", drawerId)
|
dispatch("drawerShow", drawerId)
|
||||||
|
openDrawers.update(state => [...state, drawerId])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hide() {
|
export function hide() {
|
||||||
|
@ -31,12 +112,15 @@
|
||||||
}
|
}
|
||||||
visible = false
|
visible = false
|
||||||
dispatch("drawerHide", drawerId)
|
dispatch("drawerHide", drawerId)
|
||||||
|
openDrawers.update(state => state.filter(id => id !== drawerId))
|
||||||
|
unobserve()
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext("drawer-actions", {
|
setContext("drawer", {
|
||||||
hide,
|
hide,
|
||||||
show,
|
show,
|
||||||
headless,
|
modal,
|
||||||
|
resizable,
|
||||||
})
|
})
|
||||||
|
|
||||||
const easeInOutQuad = x => {
|
const easeInOutQuad = x => {
|
||||||
|
@ -45,66 +129,127 @@
|
||||||
|
|
||||||
// Use a custom svelte transition here because the built-in slide
|
// Use a custom svelte transition here because the built-in slide
|
||||||
// transition has a horrible overshoot
|
// transition has a horrible overshoot
|
||||||
const slide = () => {
|
const drawerSlide = () => {
|
||||||
return {
|
return {
|
||||||
duration: 360,
|
duration: 260,
|
||||||
css: t => {
|
css: t => {
|
||||||
const translation = 100 - Math.round(easeInOutQuad(t) * 100)
|
const f = easeInOutQuad(t)
|
||||||
return `transform: translateY(${translation}%);`
|
const yOffset = (1 - f) * 200
|
||||||
|
return `
|
||||||
|
transform: translateY(calc(${yOffset}px - 800px * (1 - var(--scale-factor))));
|
||||||
|
opacity: ${f};
|
||||||
|
`
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom fade transition because the default svelte one doesn't work any more
|
||||||
|
// with svelte 4
|
||||||
|
const drawerFade = () => {
|
||||||
|
return {
|
||||||
|
duration: 260,
|
||||||
|
css: t => {
|
||||||
|
return `opacity: ${easeInOutQuad(t)};`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getScaleFactor = depth => {
|
||||||
|
// Quadratic function approaching a limit of 1 as depth tends to infinity
|
||||||
|
const lim = 1 - 1 / (depth * depth + 1)
|
||||||
|
// Scale drawers between 1 and 0.9 as depth approaches infinity
|
||||||
|
return 1 - lim * 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (visible) {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<Portal>
|
<Portal target=".modal-container">
|
||||||
<section
|
<div class="drawer-container">
|
||||||
class:fillWidth
|
<div
|
||||||
class="drawer"
|
class="underlay"
|
||||||
class:headless
|
class:hidden={!$modal}
|
||||||
transition:slide|local
|
transition:drawerFade|local
|
||||||
style={`width: ${width}; left: ${left};`}
|
/>
|
||||||
>
|
<div
|
||||||
{#if !headless}
|
class="drawer"
|
||||||
|
class:stacked={depth > 0}
|
||||||
|
class:modal={$modal}
|
||||||
|
transition:drawerSlide|local
|
||||||
|
{style}
|
||||||
|
>
|
||||||
<header>
|
<header>
|
||||||
<div class="text">
|
<div class="text">{title || "Bindings"}</div>
|
||||||
<Heading size="XS">{title}</Heading>
|
|
||||||
<Body size="S">
|
|
||||||
<slot name="description" />
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button secondary quiet on:click={hide}>Cancel</Button>
|
<Button secondary quiet on:click={hide}>Cancel</Button>
|
||||||
<slot name="buttons" />
|
<slot name="buttons" />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{/if}
|
<slot name="body" />
|
||||||
<slot name="body" />
|
<div class="overlay" class:hidden={$modal || depth === 0} />
|
||||||
</section>
|
</div>
|
||||||
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.drawer.headless :global(.drawer-contents) {
|
|
||||||
height: calc(40vh + 75px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer {
|
.drawer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
left: 25vw;
|
||||||
|
width: 50vw;
|
||||||
|
bottom: var(--spacing);
|
||||||
|
height: 420px;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
border-top: var(--border-light);
|
border: var(--border-light);
|
||||||
z-index: 3;
|
z-index: 999;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: transform 260ms ease-out, bottom 260ms ease-out,
|
||||||
|
left 260ms ease-out, width 260ms ease-out, height 260ms ease-out;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.drawer.modal {
|
||||||
|
left: 15vw;
|
||||||
|
width: 70vw;
|
||||||
|
bottom: 15vh;
|
||||||
|
height: 70vh;
|
||||||
|
}
|
||||||
|
.drawer.stacked {
|
||||||
|
transform: translateY(calc(-1 * 1024px * (1 - var(--scale-factor))))
|
||||||
|
scale(var(--scale-factor));
|
||||||
}
|
}
|
||||||
|
|
||||||
.fillWidth {
|
.overlay,
|
||||||
left: 260px !important;
|
.underlay {
|
||||||
width: calc(100% - 260px) !important;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 999;
|
||||||
|
display: block;
|
||||||
|
transition: opacity 260ms ease-out;
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--background);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.underlay {
|
||||||
|
position: fixed;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.underlay.hidden,
|
||||||
|
.overlay.hidden {
|
||||||
|
opacity: 0 !important;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
|
@ -112,10 +257,9 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: var(--border-light);
|
border-bottom: var(--border-light);
|
||||||
padding: var(--spacing-l) var(--spacing-xl);
|
padding: var(--spacing-m) var(--spacing-xl);
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -123,7 +267,6 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
<div class="drawer-contents">
|
<script>
|
||||||
|
export let padding = true
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="drawer-contents" class:padding>
|
||||||
<div class:no-sidebar={!$$slots.sidebar} class="container">
|
<div class:no-sidebar={!$$slots.sidebar} class="container">
|
||||||
{#if $$slots.sidebar}
|
{#if $$slots.sidebar}
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
|
@ -13,8 +17,9 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.drawer-contents {
|
.drawer-contents {
|
||||||
height: 40vh;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 0;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -27,14 +32,22 @@
|
||||||
.sidebar {
|
.sidebar {
|
||||||
border-right: var(--border-light);
|
border-right: var(--border-light);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: var(--spacing-xl);
|
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
.padding .sidebar {
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
}
|
||||||
.sidebar::-webkit-scrollbar {
|
.sidebar::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.main {
|
.main {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.padding .main {
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl);
|
||||||
|
height: calc(100% - var(--spacing-xl) * 2);
|
||||||
}
|
}
|
||||||
.main :global(textarea) {
|
.main :global(textarea) {
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
export let inline = false
|
export let inline = false
|
||||||
export let disableCancel = false
|
export let disableCancel = false
|
||||||
export let autoFocus = true
|
export let autoFocus = true
|
||||||
|
export let zIndex = 999
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let visible = fixed || inline
|
let visible = fixed || inline
|
||||||
|
@ -101,7 +102,11 @@
|
||||||
<Portal target=".modal-container">
|
<Portal target=".modal-container">
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div class="spectrum-Underlay is-open" on:mousedown|self={cancel}>
|
<div
|
||||||
|
class="spectrum-Underlay is-open"
|
||||||
|
on:mousedown|self={cancel}
|
||||||
|
style="z-index:{zIndex || 999}"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="background"
|
class="background"
|
||||||
in:fade={{ duration: 200 }}
|
in:fade={{ duration: 200 }}
|
||||||
|
@ -132,7 +137,6 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 999;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
export let anchor
|
export let anchor
|
||||||
export let align = "right"
|
export let align = "right"
|
||||||
export let portalTarget
|
export let portalTarget
|
||||||
|
export let minWidth
|
||||||
export let maxWidth
|
export let maxWidth
|
||||||
export let maxHeight
|
export let maxHeight
|
||||||
export let open = false
|
export let open = false
|
||||||
|
@ -21,7 +22,6 @@
|
||||||
export let customHeight
|
export let customHeight
|
||||||
export let animate = true
|
export let animate = true
|
||||||
export let customZindex
|
export let customZindex
|
||||||
|
|
||||||
export let handlePostionUpdate
|
export let handlePostionUpdate
|
||||||
export let showPopover = true
|
export let showPopover = true
|
||||||
export let clickOutsideOverride = false
|
export let clickOutsideOverride = false
|
||||||
|
@ -87,6 +87,7 @@
|
||||||
align,
|
align,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
|
minWidth,
|
||||||
useAnchorWidth,
|
useAnchorWidth,
|
||||||
offset,
|
offset,
|
||||||
customUpdate: handlePostionUpdate,
|
customUpdate: handlePostionUpdate,
|
||||||
|
@ -102,6 +103,8 @@
|
||||||
role="presentation"
|
role="presentation"
|
||||||
style="height: {customHeight}; --customZindex: {customZindex};"
|
style="height: {customHeight}; --customZindex: {customZindex};"
|
||||||
transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }}
|
transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }}
|
||||||
|
on:mouseenter
|
||||||
|
on:mouseleave
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -66,10 +66,11 @@
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
"@zerodevx/svelte-json-view": "^1.0.7",
|
"@zerodevx/svelte-json-view": "^1.0.7",
|
||||||
"codemirror": "^5.59.0",
|
"codemirror": "^5.65.16",
|
||||||
"dayjs": "^1.10.8",
|
"dayjs": "^1.10.8",
|
||||||
"downloadjs": "1.4.7",
|
"downloadjs": "1.4.7",
|
||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
|
"json-format-highlight": "^1.0.4",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"posthog-js": "^1.36.0",
|
"posthog-js": "^1.36.0",
|
||||||
"remixicon": "2.5.0",
|
"remixicon": "2.5.0",
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
Drawer,
|
Drawer,
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
Icon,
|
|
||||||
Checkbox,
|
Checkbox,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
@ -31,7 +30,7 @@
|
||||||
import Editor from "components/integration/QueryEditor.svelte"
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||||
import BindingPicker from "components/common/bindings/BindingPicker.svelte"
|
import BindingSidePanel from "components/common/bindings/BindingSidePanel.svelte"
|
||||||
import { BindingHelpers } from "components/common/bindings/utils"
|
import { BindingHelpers } from "components/common/bindings/utils"
|
||||||
import {
|
import {
|
||||||
bindingsToCompletions,
|
bindingsToCompletions,
|
||||||
|
@ -52,11 +51,12 @@
|
||||||
export let testData
|
export let testData
|
||||||
export let schemaProperties
|
export let schemaProperties
|
||||||
export let isTestModal = false
|
export let isTestModal = false
|
||||||
|
|
||||||
let webhookModal
|
let webhookModal
|
||||||
let drawer
|
let drawer
|
||||||
let fillWidth = true
|
|
||||||
let inputData
|
let inputData
|
||||||
let insertAtPos, getCaretPosition
|
let insertAtPos, getCaretPosition
|
||||||
|
|
||||||
$: filters = lookForFilters(schemaProperties) || []
|
$: filters = lookForFilters(schemaProperties) || []
|
||||||
$: tempFilters = filters
|
$: tempFilters = filters
|
||||||
$: stepId = block.stepId
|
$: stepId = block.stepId
|
||||||
|
@ -80,7 +80,6 @@
|
||||||
})
|
})
|
||||||
$: editingJs = codeMode === EditorModes.JS
|
$: editingJs = codeMode === EditorModes.JS
|
||||||
$: requiredProperties = block.schema.inputs.required || []
|
$: requiredProperties = block.schema.inputs.required || []
|
||||||
|
|
||||||
$: stepCompletions =
|
$: stepCompletions =
|
||||||
codeMode === EditorModes.Handlebars
|
codeMode === EditorModes.Handlebars
|
||||||
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||||
|
@ -377,12 +376,13 @@
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
{#each schemaProperties as [key, value]}
|
{#each schemaProperties as [key, value]}
|
||||||
{#if canShowField(key, value)}
|
{#if canShowField(key, value)}
|
||||||
|
{@const label = getFieldLabel(key, value)}
|
||||||
<div class:block-field={shouldRenderField(value)}>
|
<div class:block-field={shouldRenderField(value)}>
|
||||||
{#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)}
|
{#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)}
|
||||||
<Label
|
<Label
|
||||||
tooltip={value.title === "Binding / Value"
|
tooltip={value.title === "Binding / Value"
|
||||||
? "If using the String input type, please use a comma or newline separated string"
|
? "If using the String input type, please use a comma or newline separated string"
|
||||||
: null}>{getFieldLabel(key, value)}</Label
|
: null}>{label}</Label
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<div class:field-width={shouldRenderField(value)}>
|
<div class:field-width={shouldRenderField(value)}>
|
||||||
|
@ -415,8 +415,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if value.type === "date"}
|
{:else if value.type === "date"}
|
||||||
<DrawerBindableSlot
|
<DrawerBindableSlot
|
||||||
fillWidth
|
title={value.title ?? label}
|
||||||
title={value.title}
|
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type={"date"}
|
type={"date"}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
|
@ -439,7 +438,7 @@
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "filters"}
|
{:else if value.customType === "filters"}
|
||||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||||
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
|
<Drawer bind:this={drawer} title="Filtering">
|
||||||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -450,7 +449,6 @@
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
datasource={{ type: "table", tableId }}
|
datasource={{ type: "table", tableId }}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
fillWidth
|
|
||||||
on:change={e => (tempFilters = e.detail)}
|
on:change={e => (tempFilters = e.detail)}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
@ -463,19 +461,17 @@
|
||||||
{:else if value.customType === "email"}
|
{:else if value.customType === "email"}
|
||||||
{#if isTestModal}
|
{#if isTestModal}
|
||||||
<ModalBindableInput
|
<ModalBindableInput
|
||||||
title={value.title}
|
title={value.title ?? label}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type="email"
|
type="email"
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
{bindings}
|
||||||
fillWidth
|
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
fillWidth
|
title={value.title ?? label}
|
||||||
title={value.title}
|
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type="email"
|
type="email"
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
|
@ -550,7 +546,7 @@
|
||||||
{:else if value.customType === "code"}
|
{:else if value.customType === "code"}
|
||||||
<CodeEditorModal>
|
<CodeEditorModal>
|
||||||
<div class:js-editor={editingJs}>
|
<div class:js-editor={editingJs}>
|
||||||
<div class:js-code={editingJs} style="width: 100%">
|
<div class:js-code={editingJs} style="width:100%;height:500px;">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
|
@ -563,24 +559,14 @@
|
||||||
autocompleteEnabled={codeMode !== EditorModes.JS}
|
autocompleteEnabled={codeMode !== EditorModes.JS}
|
||||||
bind:getCaretPosition
|
bind:getCaretPosition
|
||||||
bind:insertAtPos
|
bind:insertAtPos
|
||||||
height={500}
|
placeholder={codeMode === EditorModes.Handlebars
|
||||||
|
? "Add bindings by typing {{"
|
||||||
|
: null}
|
||||||
/>
|
/>
|
||||||
<div class="messaging">
|
|
||||||
{#if codeMode === EditorModes.Handlebars}
|
|
||||||
<Icon name="FlashOn" />
|
|
||||||
<div class="messaging-wrap">
|
|
||||||
<div>
|
|
||||||
Add available bindings by typing <strong>
|
|
||||||
}}
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{#if editingJs}
|
{#if editingJs}
|
||||||
<div class="js-binding-picker">
|
<div class="js-binding-picker">
|
||||||
<BindingPicker
|
<BindingSidePanel
|
||||||
{bindings}
|
{bindings}
|
||||||
allowHelpers={false}
|
allowHelpers={false}
|
||||||
addBinding={binding =>
|
addBinding={binding =>
|
||||||
|
@ -609,7 +595,7 @@
|
||||||
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
||||||
{#if isTestModal}
|
{#if isTestModal}
|
||||||
<ModalBindableInput
|
<ModalBindableInput
|
||||||
title={value.title}
|
title={value.title || label}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type={value.customType}
|
type={value.customType}
|
||||||
|
@ -620,8 +606,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<div class="test">
|
<div class="test">
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
fillWidth={true}
|
title={value.title ?? label}
|
||||||
title={value.title}
|
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type={value.customType}
|
type={value.customType}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
|
@ -654,11 +639,6 @@
|
||||||
width: 320px;
|
width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messaging {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.fields {
|
.fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -670,7 +650,6 @@
|
||||||
.block-field {
|
.block-field {
|
||||||
display: flex; /* Use Flexbox */
|
display: flex; /* Use Flexbox */
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
|
||||||
flex-direction: row; /* Arrange label and field side by side */
|
flex-direction: row; /* Arrange label and field side by side */
|
||||||
align-items: center; /* Align vertically in the center */
|
align-items: center; /* Align vertically in the center */
|
||||||
gap: 10px; /* Add some space between label and field */
|
gap: 10px; /* Add some space between label and field */
|
||||||
|
|
|
@ -57,7 +57,6 @@
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
type="string"
|
type="string"
|
||||||
{bindings}
|
{bindings}
|
||||||
fillWidth={true}
|
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -52,7 +52,6 @@
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
type="string"
|
type="string"
|
||||||
{bindings}
|
{bindings}
|
||||||
fillWidth={true}
|
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -129,8 +129,7 @@
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<DrawerBindableSlot
|
<DrawerBindableSlot
|
||||||
fillWidth
|
title={value.title || field}
|
||||||
title={value.title}
|
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type={schema.type}
|
type={schema.type}
|
||||||
{schema}
|
{schema}
|
||||||
|
|
|
@ -85,8 +85,8 @@
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
type="string"
|
type="string"
|
||||||
bindings={parsedBindings}
|
bindings={parsedBindings}
|
||||||
fillWidth={true}
|
|
||||||
allowJS={true}
|
allowJS={true}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
|
title={schema.name}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -36,6 +36,8 @@
|
||||||
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
||||||
import { FieldType, FieldSubtype, SourceName } from "@budibase/types"
|
import { FieldType, FieldSubtype, SourceName } from "@budibase/types"
|
||||||
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||||
|
import { RowUtils } from "@budibase/frontend-core"
|
||||||
|
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
||||||
|
|
||||||
const AUTO_TYPE = FIELDS.AUTO.type
|
const AUTO_TYPE = FIELDS.AUTO.type
|
||||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||||
|
@ -49,43 +51,21 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
|
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
|
||||||
const { dispatch: gridDispatch } = getContext("grid")
|
const { dispatch: gridDispatch, rows } = getContext("grid")
|
||||||
|
|
||||||
export let field
|
export let field
|
||||||
|
|
||||||
let mounted = false
|
let mounted = false
|
||||||
const fieldDefinitions = Object.values(FIELDS).reduce(
|
|
||||||
// Storing the fields by complex field id
|
|
||||||
(acc, field) => ({
|
|
||||||
...acc,
|
|
||||||
[makeFieldId(field.type, field.subtype)]: field,
|
|
||||||
}),
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
function makeFieldId(type, subtype, autocolumn) {
|
|
||||||
// don't make field IDs for auto types
|
|
||||||
if (type === AUTO_TYPE || autocolumn) {
|
|
||||||
return type.toUpperCase()
|
|
||||||
} else {
|
|
||||||
return `${type}${subtype || ""}`.toUpperCase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let originalName
|
let originalName
|
||||||
let linkEditDisabled
|
let linkEditDisabled
|
||||||
let primaryDisplay
|
let primaryDisplay
|
||||||
let indexes = [...($tables.selected.indexes || [])]
|
let indexes = [...($tables.selected.indexes || [])]
|
||||||
let isCreating = undefined
|
let isCreating = undefined
|
||||||
|
|
||||||
let relationshipPart1 = PrettyRelationshipDefinitions.Many
|
let relationshipPart1 = PrettyRelationshipDefinitions.Many
|
||||||
let relationshipPart2 = PrettyRelationshipDefinitions.One
|
let relationshipPart2 = PrettyRelationshipDefinitions.One
|
||||||
|
|
||||||
let relationshipTableIdPrimary = null
|
let relationshipTableIdPrimary = null
|
||||||
let relationshipTableIdSecondary = null
|
let relationshipTableIdSecondary = null
|
||||||
|
|
||||||
let table = $tables.selected
|
let table = $tables.selected
|
||||||
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
let savingColumn
|
let savingColumn
|
||||||
let deleteColName
|
let deleteColName
|
||||||
|
@ -99,11 +79,6 @@
|
||||||
}
|
}
|
||||||
let relationshipOpts1 = Object.values(PrettyRelationshipDefinitions)
|
let relationshipOpts1 = Object.values(PrettyRelationshipDefinitions)
|
||||||
let relationshipOpts2 = Object.values(PrettyRelationshipDefinitions)
|
let relationshipOpts2 = Object.values(PrettyRelationshipDefinitions)
|
||||||
|
|
||||||
$: if (primaryDisplay) {
|
|
||||||
editableColumn.constraints.presence = { allowEmpty: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
let relationshipMap = {
|
let relationshipMap = {
|
||||||
[RelationshipType.ONE_TO_MANY]: {
|
[RelationshipType.ONE_TO_MANY]: {
|
||||||
part1: PrettyRelationshipDefinitions.MANY,
|
part1: PrettyRelationshipDefinitions.MANY,
|
||||||
|
@ -118,7 +93,12 @@
|
||||||
part2: PrettyRelationshipDefinitions.MANY,
|
part2: PrettyRelationshipDefinitions.MANY,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
let autoColumnInfo = getAutoColumnInformation()
|
||||||
|
|
||||||
|
$: rowGoldenSample = RowUtils.generateGoldenSample($rows)
|
||||||
|
$: if (primaryDisplay) {
|
||||||
|
editableColumn.constraints.presence = { allowEmpty: false }
|
||||||
|
}
|
||||||
$: {
|
$: {
|
||||||
// this parses any changes the user has made when creating a new internal relationship
|
// this parses any changes the user has made when creating a new internal relationship
|
||||||
// into what we expect the schema to look like
|
// into what we expect the schema to look like
|
||||||
|
@ -148,6 +128,74 @@
|
||||||
editableColumn.tableId = relationshipTableIdSecondary
|
editableColumn.tableId = relationshipTableIdSecondary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$: initialiseField(field, savingColumn)
|
||||||
|
$: checkConstraints(editableColumn)
|
||||||
|
$: required = !!editableColumn?.constraints?.presence || primaryDisplay
|
||||||
|
$: uneditable =
|
||||||
|
$tables.selected?._id === TableNames.USERS &&
|
||||||
|
UNEDITABLE_USER_FIELDS.includes(editableColumn.name)
|
||||||
|
$: invalid =
|
||||||
|
!editableColumn?.name ||
|
||||||
|
(editableColumn?.type === LINK_TYPE && !editableColumn?.tableId) ||
|
||||||
|
Object.keys(errors).length !== 0
|
||||||
|
$: errors = checkErrors(editableColumn)
|
||||||
|
$: datasource = $datasources.list.find(
|
||||||
|
source => source._id === table?.sourceId
|
||||||
|
)
|
||||||
|
$: tableAutoColumnsTypes = getTableAutoColumnTypes($tables?.selected)
|
||||||
|
$: availableAutoColumns = Object.keys(autoColumnInfo).reduce((acc, key) => {
|
||||||
|
if (!tableAutoColumnsTypes.includes(key)) {
|
||||||
|
acc[key] = autoColumnInfo[key]
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
$: availableAutoColumnKeys = availableAutoColumns
|
||||||
|
? Object.keys(availableAutoColumns)
|
||||||
|
: []
|
||||||
|
$: autoColumnOptions = editableColumn.autocolumn
|
||||||
|
? autoColumnInfo
|
||||||
|
: availableAutoColumns
|
||||||
|
// used to select what different options can be displayed for column type
|
||||||
|
$: canBeDisplay =
|
||||||
|
editableColumn?.type !== LINK_TYPE &&
|
||||||
|
editableColumn?.type !== AUTO_TYPE &&
|
||||||
|
editableColumn?.type !== JSON_TYPE &&
|
||||||
|
!editableColumn.autocolumn
|
||||||
|
$: canBeRequired =
|
||||||
|
editableColumn?.type !== LINK_TYPE &&
|
||||||
|
!uneditable &&
|
||||||
|
editableColumn?.type !== AUTO_TYPE &&
|
||||||
|
!editableColumn.autocolumn
|
||||||
|
$: externalTable = table.sourceType === DB_TYPE_EXTERNAL
|
||||||
|
// in the case of internal tables the sourceId will just be undefined
|
||||||
|
$: tableOptions = $tables.list.filter(
|
||||||
|
opt =>
|
||||||
|
opt.sourceType === table.sourceType && table.sourceId === opt.sourceId
|
||||||
|
)
|
||||||
|
$: typeEnabled =
|
||||||
|
!originalName ||
|
||||||
|
(originalName &&
|
||||||
|
SWITCHABLE_TYPES.indexOf(editableColumn.type) !== -1 &&
|
||||||
|
!editableColumn?.autocolumn)
|
||||||
|
|
||||||
|
const fieldDefinitions = Object.values(FIELDS).reduce(
|
||||||
|
// Storing the fields by complex field id
|
||||||
|
(acc, field) => ({
|
||||||
|
...acc,
|
||||||
|
[makeFieldId(field.type, field.subtype)]: field,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
function makeFieldId(type, subtype, autocolumn) {
|
||||||
|
// don't make field IDs for auto types
|
||||||
|
if (type === AUTO_TYPE || autocolumn) {
|
||||||
|
return type.toUpperCase()
|
||||||
|
} else {
|
||||||
|
return `${type}${subtype || ""}`.toUpperCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const initialiseField = (field, savingColumn) => {
|
const initialiseField = (field, savingColumn) => {
|
||||||
isCreating = !field
|
isCreating = !field
|
||||||
if (field && !savingColumn) {
|
if (field && !savingColumn) {
|
||||||
|
@ -187,22 +235,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: initialiseField(field, savingColumn)
|
|
||||||
|
|
||||||
$: checkConstraints(editableColumn)
|
|
||||||
$: required = !!editableColumn?.constraints?.presence || primaryDisplay
|
|
||||||
$: uneditable =
|
|
||||||
$tables.selected?._id === TableNames.USERS &&
|
|
||||||
UNEDITABLE_USER_FIELDS.includes(editableColumn.name)
|
|
||||||
$: invalid =
|
|
||||||
!editableColumn?.name ||
|
|
||||||
(editableColumn?.type === LINK_TYPE && !editableColumn?.tableId) ||
|
|
||||||
Object.keys(errors).length !== 0
|
|
||||||
$: errors = checkErrors(editableColumn)
|
|
||||||
$: datasource = $datasources.list.find(
|
|
||||||
source => source._id === table?.sourceId
|
|
||||||
)
|
|
||||||
|
|
||||||
const getTableAutoColumnTypes = table => {
|
const getTableAutoColumnTypes = table => {
|
||||||
return Object.keys(table?.schema).reduce((acc, key) => {
|
return Object.keys(table?.schema).reduce((acc, key) => {
|
||||||
let fieldSchema = table?.schema[key]
|
let fieldSchema = table?.schema[key]
|
||||||
|
@ -213,47 +245,6 @@
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
let autoColumnInfo = getAutoColumnInformation()
|
|
||||||
|
|
||||||
$: tableAutoColumnsTypes = getTableAutoColumnTypes($tables?.selected)
|
|
||||||
$: availableAutoColumns = Object.keys(autoColumnInfo).reduce((acc, key) => {
|
|
||||||
if (!tableAutoColumnsTypes.includes(key)) {
|
|
||||||
acc[key] = autoColumnInfo[key]
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
$: availableAutoColumnKeys = availableAutoColumns
|
|
||||||
? Object.keys(availableAutoColumns)
|
|
||||||
: []
|
|
||||||
|
|
||||||
$: autoColumnOptions = editableColumn.autocolumn
|
|
||||||
? autoColumnInfo
|
|
||||||
: availableAutoColumns
|
|
||||||
|
|
||||||
// used to select what different options can be displayed for column type
|
|
||||||
$: canBeDisplay =
|
|
||||||
editableColumn?.type !== LINK_TYPE &&
|
|
||||||
editableColumn?.type !== AUTO_TYPE &&
|
|
||||||
editableColumn?.type !== JSON_TYPE &&
|
|
||||||
!editableColumn.autocolumn
|
|
||||||
$: canBeRequired =
|
|
||||||
editableColumn?.type !== LINK_TYPE &&
|
|
||||||
!uneditable &&
|
|
||||||
editableColumn?.type !== AUTO_TYPE &&
|
|
||||||
!editableColumn.autocolumn
|
|
||||||
$: externalTable = table.sourceType === DB_TYPE_EXTERNAL
|
|
||||||
// in the case of internal tables the sourceId will just be undefined
|
|
||||||
$: tableOptions = $tables.list.filter(
|
|
||||||
opt =>
|
|
||||||
opt.sourceType === table.sourceType && table.sourceId === opt.sourceId
|
|
||||||
)
|
|
||||||
$: typeEnabled =
|
|
||||||
!originalName ||
|
|
||||||
(originalName &&
|
|
||||||
SWITCHABLE_TYPES.indexOf(editableColumn.type) !== -1 &&
|
|
||||||
!editableColumn?.autocolumn)
|
|
||||||
|
|
||||||
async function saveColumn() {
|
async function saveColumn() {
|
||||||
savingColumn = true
|
savingColumn = true
|
||||||
if (errors?.length) {
|
if (errors?.length) {
|
||||||
|
@ -679,6 +670,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="input-length">
|
<div class="input-length">
|
||||||
<ModalBindableInput
|
<ModalBindableInput
|
||||||
|
panel={ServerBindingPanel}
|
||||||
title="Formula"
|
title="Formula"
|
||||||
value={editableColumn.formula}
|
value={editableColumn.formula}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
|
@ -689,6 +681,7 @@
|
||||||
}}
|
}}
|
||||||
bindings={getBindings({ table })}
|
bindings={getBindings({ table })}
|
||||||
allowJS
|
allowJS
|
||||||
|
context={rowGoldenSample}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -42,13 +42,11 @@
|
||||||
} from "@codemirror/commands"
|
} from "@codemirror/commands"
|
||||||
import { Compartment } from "@codemirror/state"
|
import { Compartment } from "@codemirror/state"
|
||||||
import { javascript } from "@codemirror/lang-javascript"
|
import { javascript } from "@codemirror/lang-javascript"
|
||||||
import { EditorModes, getDefaultTheme } from "./"
|
import { EditorModes } from "./"
|
||||||
import { themeStore } from "stores/portal"
|
import { themeStore } from "stores/portal"
|
||||||
|
|
||||||
export let label
|
export let label
|
||||||
export let completions = []
|
export let completions = []
|
||||||
export let height = 200
|
|
||||||
export let resize = "none"
|
|
||||||
export let mode = EditorModes.Handlebars
|
export let mode = EditorModes.Handlebars
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
|
@ -56,6 +54,8 @@
|
||||||
export let autofocus = false
|
export let autofocus = false
|
||||||
export let jsBindingWrapping = true
|
export let jsBindingWrapping = true
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
// Export a function to expose caret position
|
// Export a function to expose caret position
|
||||||
export const getCaretPosition = () => {
|
export const getCaretPosition = () => {
|
||||||
const selection_range = editor.state.selection.ranges[0]
|
const selection_range = editor.state.selection.ranges[0]
|
||||||
|
@ -107,8 +107,6 @@
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
// Theming!
|
// Theming!
|
||||||
let currentTheme = $themeStore?.theme
|
let currentTheme = $themeStore?.theme
|
||||||
let isDark = !currentTheme.includes("light")
|
let isDark = !currentTheme.includes("light")
|
||||||
|
@ -117,7 +115,7 @@
|
||||||
const indentWithTabCustom = {
|
const indentWithTabCustom = {
|
||||||
key: "Tab",
|
key: "Tab",
|
||||||
run: view => {
|
run: view => {
|
||||||
if (completionStatus(view.state) == "active") {
|
if (completionStatus(view.state) === "active") {
|
||||||
acceptCompletion(view)
|
acceptCompletion(view)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -131,7 +129,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildKeymap = () => {
|
const buildKeymap = () => {
|
||||||
const baseMap = [
|
return [
|
||||||
...closeBracketsKeymap,
|
...closeBracketsKeymap,
|
||||||
...defaultKeymap,
|
...defaultKeymap,
|
||||||
...historyKeymap,
|
...historyKeymap,
|
||||||
|
@ -139,7 +137,6 @@
|
||||||
...completionKeymap,
|
...completionKeymap,
|
||||||
indentWithTabCustom,
|
indentWithTabCustom,
|
||||||
]
|
]
|
||||||
return baseMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildBaseExtensions = () => {
|
const buildBaseExtensions = () => {
|
||||||
|
@ -154,6 +151,8 @@
|
||||||
syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }),
|
syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }),
|
||||||
highlightActiveLineGutter(),
|
highlightActiveLineGutter(),
|
||||||
highlightSpecialChars(),
|
highlightSpecialChars(),
|
||||||
|
lineNumbers(),
|
||||||
|
foldGutter(),
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.updateListener.of(v => {
|
EditorView.updateListener.of(v => {
|
||||||
const docStr = v.state.doc?.toString()
|
const docStr = v.state.doc?.toString()
|
||||||
|
@ -163,14 +162,7 @@
|
||||||
dispatch("change", docStr)
|
dispatch("change", docStr)
|
||||||
}),
|
}),
|
||||||
keymap.of(buildKeymap()),
|
keymap.of(buildKeymap()),
|
||||||
themeConfig.of([
|
themeConfig.of([...(isDark ? [oneDark] : [])]),
|
||||||
getDefaultTheme({
|
|
||||||
height: editorHeight,
|
|
||||||
resize,
|
|
||||||
dark: isDark,
|
|
||||||
}),
|
|
||||||
...(isDark ? [oneDark] : []),
|
|
||||||
]),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,11 +207,9 @@
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode.name == "javascript") {
|
if (mode.name === "javascript") {
|
||||||
complete.push(javascript())
|
complete.push(javascript())
|
||||||
complete.push(highlightWhitespace())
|
complete.push(highlightWhitespace())
|
||||||
complete.push(lineNumbers())
|
|
||||||
complete.push(foldGutter())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (placeholder) {
|
if (placeholder) {
|
||||||
|
@ -249,8 +239,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: editorHeight = typeof height === "number" ? `${height}px` : height
|
|
||||||
|
|
||||||
// Init when all elements are ready
|
// Init when all elements are ready
|
||||||
$: if (mounted && !isEditorInitialised) {
|
$: if (mounted && !isEditorInitialised) {
|
||||||
isEditorInitialised = true
|
isEditorInitialised = true
|
||||||
|
@ -265,14 +253,7 @@
|
||||||
|
|
||||||
// Issue theme compartment update
|
// Issue theme compartment update
|
||||||
editor.dispatch({
|
editor.dispatch({
|
||||||
effects: themeConfig.reconfigure([
|
effects: themeConfig.reconfigure([...(isDark ? [oneDark] : [])]),
|
||||||
getDefaultTheme({
|
|
||||||
height: editorHeight,
|
|
||||||
resize,
|
|
||||||
dark: isDark,
|
|
||||||
}),
|
|
||||||
...(isDark ? [oneDark] : []),
|
|
||||||
]),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -298,27 +279,194 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.code-editor.handlebars :global(.cm-content) {
|
/* Editor */
|
||||||
font-family: var(--font-sans);
|
.code-editor {
|
||||||
|
font-size: 12px;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
.code-editor :global(.cm-tooltip.cm-completionInfo) {
|
.code-editor :global(.cm-editor) {
|
||||||
padding: var(--spacing-m);
|
height: 100%;
|
||||||
|
background: var(--spectrum-global-color-gray-50) !important;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
.code-editor :global(.cm-tooltip-autocomplete > ul > li[aria-selected]) {
|
.code-editor :global(.cm-content) {
|
||||||
border-radius: var(
|
padding: 10px 0;
|
||||||
--spectrum-popover-border-radius,
|
}
|
||||||
var(--spectrum-alias-border-radius-regular)
|
.code-editor > div {
|
||||||
),
|
height: 100%;
|
||||||
var(
|
|
||||||
--spectrum-popover-border-radius,
|
|
||||||
var(--spectrum-alias-border-radius-regular)
|
|
||||||
),
|
|
||||||
0, 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Active line */
|
||||||
|
.code-editor :global(.cm-line) {
|
||||||
|
height: 16px;
|
||||||
|
padding: 0 var(--spacing-s);
|
||||||
|
color: var(--spectrum-alias-text-color);
|
||||||
|
}
|
||||||
|
.code-editor :global(.cm-activeLine) {
|
||||||
|
position: relative;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.code-editor :global(.cm-activeLine::before) {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 1px;
|
||||||
|
height: calc(100% - 2px);
|
||||||
|
width: 100%;
|
||||||
|
background: var(--spectrum-global-color-gray-100) !important;
|
||||||
|
z-index: -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code selection */
|
||||||
|
.code-editor :global(.cm-selectionBackground) {
|
||||||
|
background-color: var(--spectrum-global-color-blue-400) !important;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gutters */
|
||||||
|
.code-editor :global(.cm-gutterElement) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.code-editor :global(.cm-gutters) {
|
||||||
|
background-color: var(--spectrum-global-color-gray-75) !important;
|
||||||
|
color: var(--spectrum-global-color-gray-500);
|
||||||
|
}
|
||||||
|
.code-editor :global(.cm-activeLineGutter::before) {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 1px;
|
||||||
|
height: calc(100% - 2px);
|
||||||
|
width: 100%;
|
||||||
|
background: var(--spectrum-global-color-gray-200) !important;
|
||||||
|
z-index: -2;
|
||||||
|
}
|
||||||
|
.code-editor :global(.cm-activeLineGutter) {
|
||||||
|
color: var(--spectrum-global-color-gray-700);
|
||||||
|
background: transparent;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cursor color */
|
||||||
|
.code-editor :global(.cm-focused .cm-cursor) {
|
||||||
|
border-left-color: var(--spectrum-alias-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder */
|
||||||
|
.code-editor :global(.cm-placeholder) {
|
||||||
|
color: var(--spectrum-global-color-gray-700);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlight bindings */
|
||||||
|
.code-editor :global(.binding-wrap) {
|
||||||
|
color: var(--spectrum-global-color-blue-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Completion popover */
|
||||||
|
.code-editor :global(.cm-tooltip-autocomplete) {
|
||||||
|
background: var(--spectrum-global-color-gray-75);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
|
.code-editor :global(.cm-tooltip-autocomplete > ul) {
|
||||||
|
max-height: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Completion section header*/
|
||||||
|
.code-editor :global(.info-section) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
color: var(--spectrum-alias-text-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.code-editor :global(.info-section:not(:first-of-type)) {
|
||||||
|
border-top: 1px solid var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Completion item container */
|
||||||
|
.code-editor :global(.autocomplete-option) {
|
||||||
|
padding: var(--spacing-s) var(--spacing-m) !important;
|
||||||
|
padding-left: calc(16px + 2 * var(--spacing-m)) !important;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
color: var(--spectrum-alias-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlighted completion item */
|
||||||
|
.code-editor :global(.autocomplete-option[aria-selected]) {
|
||||||
|
background: var(--spectrum-global-color-blue-400);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.code-editor
|
||||||
|
:global(.autocomplete-option[aria-selected] .cm-completionDetail) {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Completion item label */
|
||||||
|
.code-editor :global(.cm-completionLabel) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Completion item type */
|
||||||
.code-editor :global(.autocomplete-option .cm-completionDetail) {
|
.code-editor :global(.autocomplete-option .cm-completionDetail) {
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
font-family: var(--font-mono);
|
||||||
|
color: var(--spectrum-global-color-gray-700);
|
||||||
|
font-style: normal;
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Live binding value / helper container */
|
||||||
|
.code-editor :global(.cm-completionInfo) {
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
border-radius: var(--border-radius-s);
|
border-radius: var(--border-radius-s);
|
||||||
padding: 4px 6px;
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wrapper around helpers */
|
||||||
|
.code-editor :global(.info-bubble) {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
color: var(--spectrum-global-color-gray-800);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Live binding value / helper value */
|
||||||
|
.code-editor :global(.binding__description) {
|
||||||
|
color: var(--spectrum-alias-text-color);
|
||||||
|
font-size: var(--font-size-m);
|
||||||
|
}
|
||||||
|
.code-editor :global(.binding__example) {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
white-space: pre;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 480px;
|
||||||
|
}
|
||||||
|
.code-editor :global(.binding__example) {
|
||||||
|
color: var(--spectrum-global-color-blue-700);
|
||||||
|
}
|
||||||
|
.code-editor :global(.binding__example span) {
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { EditorView } from "@codemirror/view"
|
|
||||||
import { getManifest } from "@budibase/string-templates"
|
import { getManifest } from "@budibase/string-templates"
|
||||||
import sanitizeHtml from "sanitize-html"
|
import sanitizeHtml from "sanitize-html"
|
||||||
import { groupBy } from "lodash"
|
import { groupBy } from "lodash"
|
||||||
|
@ -27,123 +26,33 @@ export const SECTIONS = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDefaultTheme = opts => {
|
|
||||||
const { height, resize, dark } = opts
|
|
||||||
return EditorView.theme(
|
|
||||||
{
|
|
||||||
"&.cm-focused .cm-cursor": {
|
|
||||||
borderLeftColor: "var(--spectrum-alias-text-color)",
|
|
||||||
},
|
|
||||||
"&": {
|
|
||||||
height: height ? `${height}` : "",
|
|
||||||
lineHeight: "1.3",
|
|
||||||
border:
|
|
||||||
"var(--spectrum-alias-border-size-thin) solid var(--spectrum-alias-border-color)",
|
|
||||||
borderRadius: "var(--border-radius-s)",
|
|
||||||
backgroundColor:
|
|
||||||
"var( --spectrum-textfield-m-background-color, var(--spectrum-global-color-gray-50) )",
|
|
||||||
resize: resize ? `${resize}` : "",
|
|
||||||
overflow: "hidden",
|
|
||||||
color: "var(--spectrum-alias-text-color)",
|
|
||||||
},
|
|
||||||
"& .cm-tooltip.cm-tooltip-autocomplete > ul": {
|
|
||||||
fontFamily:
|
|
||||||
"var(--spectrum-alias-body-text-font-family, var(--spectrum-global-font-family-base))",
|
|
||||||
maxHeight: "16em",
|
|
||||||
},
|
|
||||||
"& .cm-placeholder": {
|
|
||||||
color: "var(--spectrum-alias-text-color)",
|
|
||||||
fontStyle: "italic",
|
|
||||||
},
|
|
||||||
"&.cm-focused": {
|
|
||||||
outline: "none",
|
|
||||||
borderColor: "var(--spectrum-alias-border-color-mouse-focus)",
|
|
||||||
},
|
|
||||||
// AUTO COMPLETE
|
|
||||||
"& .cm-completionDetail": {
|
|
||||||
fontStyle: "unset",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
fontSize: "10px",
|
|
||||||
backgroundColor: "var(--spectrum-global-color-gray-100)",
|
|
||||||
color: "var(--spectrum-global-color-gray-600)",
|
|
||||||
},
|
|
||||||
"& .cm-completionLabel": {
|
|
||||||
marginLeft:
|
|
||||||
"calc(var(--spectrum-alias-workflow-icon-size-m) + var(--spacing-m))",
|
|
||||||
},
|
|
||||||
"& .info-bubble": {
|
|
||||||
fontSize: "var(--font-size-s)",
|
|
||||||
display: "grid",
|
|
||||||
gridGap: "var(--spacing-s)",
|
|
||||||
gridTemplateColumns: "1fr",
|
|
||||||
color: "var(--spectrum-global-color-gray-800)",
|
|
||||||
},
|
|
||||||
"& .cm-tooltip": {
|
|
||||||
marginLeft: "var(--spacing-s)",
|
|
||||||
border: "1px solid var(--spectrum-global-color-gray-300)",
|
|
||||||
borderRadius:
|
|
||||||
"var( --spectrum-popover-border-radius, var(--spectrum-alias-border-radius-regular) )",
|
|
||||||
backgroundColor: "var(--spectrum-global-color-gray-50)",
|
|
||||||
},
|
|
||||||
// Section header
|
|
||||||
"& .info-section": {
|
|
||||||
display: "flex",
|
|
||||||
padding: "var(--spacing-s)",
|
|
||||||
gap: "var(--spacing-m)",
|
|
||||||
borderBottom: "1px solid var(--spectrum-global-color-gray-200)",
|
|
||||||
color: "var(--spectrum-global-color-gray-800)",
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
"& .info-section .spectrum-Icon": {
|
|
||||||
color: "var(--spectrum-global-color-gray-600)",
|
|
||||||
},
|
|
||||||
// Autocomplete Option
|
|
||||||
"& .cm-tooltip.cm-tooltip-autocomplete .autocomplete-option": {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
fontSize: "var(--spectrum-alias-font-size-default)",
|
|
||||||
padding: "var(--spacing-s)",
|
|
||||||
color: "var(--spectrum-global-color-gray-800)",
|
|
||||||
},
|
|
||||||
"& .cm-tooltip-autocomplete ul li[aria-selected].autocomplete-option": {
|
|
||||||
backgroundColor: "var(--spectrum-global-color-gray-200)",
|
|
||||||
},
|
|
||||||
"& .binding-wrap": {
|
|
||||||
color: "var(--spectrum-global-color-blue-700)",
|
|
||||||
fontFamily: "monospace",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ dark }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const buildHelperInfoNode = (completion, helper) => {
|
export const buildHelperInfoNode = (completion, helper) => {
|
||||||
const ele = document.createElement("div")
|
const ele = document.createElement("div")
|
||||||
ele.classList.add("info-bubble")
|
ele.classList.add("info-bubble")
|
||||||
|
|
||||||
const exampleNodeHtml = helper.example
|
const exampleNodeHtml = helper.example
|
||||||
? `<div class="binding__example">${helper.example}</div>`
|
? `<div class="binding__example helper">${helper.example}</div>`
|
||||||
: ""
|
: ""
|
||||||
const descriptionMarkup = sanitizeHtml(helper.description, {
|
const descriptionMarkup = sanitizeHtml(helper.description, {
|
||||||
allowedTags: [],
|
allowedTags: [],
|
||||||
allowedAttributes: {},
|
allowedAttributes: {},
|
||||||
})
|
})
|
||||||
const descriptionNodeHtml = `<div class="binding__description">${descriptionMarkup}</div>`
|
const descriptionNodeHtml = `<div class="binding__description helper">${descriptionMarkup}</div>`
|
||||||
|
|
||||||
ele.innerHTML = `
|
ele.innerHTML = `
|
||||||
${exampleNodeHtml}
|
|
||||||
${descriptionNodeHtml}
|
${descriptionNodeHtml}
|
||||||
|
${exampleNodeHtml}
|
||||||
`
|
`
|
||||||
return ele
|
return ele
|
||||||
}
|
}
|
||||||
|
|
||||||
const toSpectrumIcon = name => {
|
const toSpectrumIcon = name => {
|
||||||
return `<svg
|
return `<svg
|
||||||
class="spectrum-Icon spectrum-Icon--sizeM"
|
class="spectrum-Icon spectrum-Icon--sizeS"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
aria-hidden="false"
|
aria-hidden="false"
|
||||||
aria-label="${name}-section-icon"
|
aria-label="${name}-section-icon"
|
||||||
|
style="color:var(--spectrum-global-color-gray-700)"
|
||||||
>
|
>
|
||||||
<use style="pointer-events: none;" xlink:href="#spectrum-icon-18-${name}" />
|
<use style="pointer-events: none;" xlink:href="#spectrum-icon-18-${name}" />
|
||||||
</svg>`
|
</svg>`
|
||||||
|
@ -152,7 +61,9 @@ const toSpectrumIcon = name => {
|
||||||
export const buildSectionHeader = (type, sectionName, icon, rank) => {
|
export const buildSectionHeader = (type, sectionName, icon, rank) => {
|
||||||
const ele = document.createElement("div")
|
const ele = document.createElement("div")
|
||||||
ele.classList.add("info-section")
|
ele.classList.add("info-section")
|
||||||
ele.classList.add(type)
|
if (type) {
|
||||||
|
ele.classList.add(type)
|
||||||
|
}
|
||||||
ele.innerHTML = `${toSpectrumIcon(icon)}<span>${sectionName}</span>`
|
ele.innerHTML = `${toSpectrumIcon(icon)}<span>${sectionName}</span>`
|
||||||
return {
|
return {
|
||||||
name: sectionName,
|
name: sectionName,
|
||||||
|
@ -174,7 +85,7 @@ export const helpersToCompletion = (helpers, mode) => {
|
||||||
},
|
},
|
||||||
type: "helper",
|
type: "helper",
|
||||||
section: helperSection,
|
section: helperSection,
|
||||||
detail: "FUNCTION",
|
detail: "Function",
|
||||||
apply: (view, completion, from, to) => {
|
apply: (view, completion, from, to) => {
|
||||||
insertBinding(view, from, to, key, mode)
|
insertBinding(view, from, to, key, mode)
|
||||||
},
|
},
|
||||||
|
@ -252,21 +163,12 @@ export const jsAutocomplete = baseCompletions => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildBindingInfoNode = (completion, binding) => {
|
export const buildBindingInfoNode = (completion, binding) => {
|
||||||
|
if (!binding.valueHTML || binding.value == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
const ele = document.createElement("div")
|
const ele = document.createElement("div")
|
||||||
ele.classList.add("info-bubble")
|
ele.classList.add("info-bubble")
|
||||||
|
ele.innerHTML = `<div class="binding__example">${binding.valueHTML}</div>`
|
||||||
const exampleNodeHtml = binding.readableBinding
|
|
||||||
? `<div class="binding__example">{{ ${binding.readableBinding} }}</div>`
|
|
||||||
: ""
|
|
||||||
|
|
||||||
const descriptionNodeHtml = binding.description
|
|
||||||
? `<div class="binding__description">${binding.description}</div>`
|
|
||||||
: ""
|
|
||||||
|
|
||||||
ele.innerHTML = `
|
|
||||||
${exampleNodeHtml}
|
|
||||||
${descriptionNodeHtml}
|
|
||||||
`
|
|
||||||
return ele
|
return ele
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,19 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
Tabs,
|
ActionButton,
|
||||||
Tab,
|
Icon,
|
||||||
|
Heading,
|
||||||
Body,
|
Body,
|
||||||
Button,
|
Button,
|
||||||
ActionButton,
|
|
||||||
Heading,
|
|
||||||
Icon,
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher, onMount, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
import {
|
import {
|
||||||
isValid,
|
|
||||||
decodeJSBinding,
|
decodeJSBinding,
|
||||||
encodeJSBinding,
|
encodeJSBinding,
|
||||||
convertToJS,
|
processStringSync,
|
||||||
} from "@budibase/string-templates"
|
} from "@budibase/string-templates"
|
||||||
import {
|
import { readableToRuntimeBinding } from "dataBinding"
|
||||||
readableToRuntimeBinding,
|
|
||||||
runtimeToReadableBinding,
|
|
||||||
} from "dataBinding"
|
|
||||||
|
|
||||||
import { admin } from "stores/portal"
|
|
||||||
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
||||||
import {
|
import {
|
||||||
getHelperCompletions,
|
getHelperCompletions,
|
||||||
|
@ -30,45 +22,118 @@
|
||||||
EditorModes,
|
EditorModes,
|
||||||
bindingsToCompletions,
|
bindingsToCompletions,
|
||||||
} from "../CodeEditor"
|
} from "../CodeEditor"
|
||||||
import BindingPicker from "./BindingPicker.svelte"
|
import BindingSidePanel from "./BindingSidePanel.svelte"
|
||||||
|
import EvaluationSidePanel from "./EvaluationSidePanel.svelte"
|
||||||
import { BindingHelpers } from "./utils"
|
import { BindingHelpers } from "./utils"
|
||||||
|
import formatHighlight from "json-format-highlight"
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let bindings
|
export let bindings
|
||||||
// jsValue/hbsValue are the state of the value that is being built
|
|
||||||
// within this binding panel - the value should not be updated until
|
|
||||||
// the binding panel is saved. This is the default value of the
|
|
||||||
// expression when the binding panel is opened, but shouldn't be updated.
|
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let valid
|
|
||||||
export let allowJS = false
|
export let allowJS = false
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
|
export let context = null
|
||||||
export let autofocusEditor = false
|
export let autofocusEditor = false
|
||||||
|
|
||||||
const drawerActions = getContext("drawer-actions")
|
const drawerContext = getContext("drawer")
|
||||||
const bindingDrawerActions = getContext("binding-drawer-actions")
|
const Modes = {
|
||||||
|
Text: "Text",
|
||||||
|
JavaScript: "JavaScript",
|
||||||
|
}
|
||||||
|
const SidePanels = {
|
||||||
|
Bindings: "FlashOn",
|
||||||
|
Evaluation: "Play",
|
||||||
|
}
|
||||||
|
|
||||||
|
let initialValueJS = value?.startsWith?.("{{ js ")
|
||||||
|
let mode = initialValueJS ? Modes.JavaScript : Modes.Text
|
||||||
|
let sidePanel = SidePanels.Bindings
|
||||||
let getCaretPosition
|
let getCaretPosition
|
||||||
let insertAtPos
|
let insertAtPos
|
||||||
let initialValueJS = typeof value === "string" && value?.startsWith("{{ js ")
|
|
||||||
let mode = initialValueJS ? "JavaScript" : "Text"
|
|
||||||
let jsValue = initialValueJS ? value : null
|
let jsValue = initialValueJS ? value : null
|
||||||
let hbsValue = initialValueJS ? null : value
|
let hbsValue = initialValueJS ? null : value
|
||||||
let sidebar = true
|
|
||||||
let targetMode = null
|
let targetMode = null
|
||||||
|
let expressionResult
|
||||||
|
let drawerIsModal
|
||||||
|
let evaluating = false
|
||||||
|
|
||||||
$: usingJS = mode === "JavaScript"
|
$: drawerContext?.modal.subscribe(val => (drawerIsModal = val))
|
||||||
|
$: editorTabs = allowJS ? [Modes.Text, Modes.JavaScript] : [Modes.Text]
|
||||||
|
$: sideTabs = context
|
||||||
|
? [SidePanels.Evaluation, SidePanels.Bindings]
|
||||||
|
: [SidePanels.Bindings]
|
||||||
|
$: enrichedBindings = enrichBindings(bindings, context)
|
||||||
|
$: usingJS = mode === Modes.JavaScript
|
||||||
$: editorMode =
|
$: editorMode =
|
||||||
mode === "JavaScript" ? EditorModes.JS : EditorModes.Handlebars
|
mode === Modes.JavaScript ? EditorModes.JS : EditorModes.Handlebars
|
||||||
$: bindingCompletions = bindingsToCompletions(bindings, editorMode)
|
$: editorValue = editorMode === EditorModes.JS ? jsValue : hbsValue
|
||||||
|
$: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode)
|
||||||
|
$: runtimeExpression = readableToRuntimeBinding(enrichedBindings, value)
|
||||||
|
$: requestUpdateEvaluation(runtimeExpression, context)
|
||||||
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
|
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
|
||||||
|
$: hbsCompletions = [
|
||||||
|
hbAutocomplete([
|
||||||
|
...bindingCompletions,
|
||||||
|
...getHelperCompletions(EditorModes.Handlebars),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
$: jsCompletions = [
|
||||||
|
jsAutocomplete([
|
||||||
|
...bindingCompletions,
|
||||||
|
...getHelperCompletions(EditorModes.JS),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
const debouncedUpdateEvaluation = Utils.debounce((expression, context) => {
|
||||||
|
expressionResult = processStringSync(expression || "", context)
|
||||||
|
evaluating = false
|
||||||
|
}, 260)
|
||||||
|
|
||||||
|
const requestUpdateEvaluation = (expression, context) => {
|
||||||
|
evaluating = true
|
||||||
|
debouncedUpdateEvaluation(expression, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBindingValue = (binding, context) => {
|
||||||
|
const js = `return $("${binding.runtimeBinding}")`
|
||||||
|
const hbs = encodeJSBinding(js)
|
||||||
|
const res = processStringSync(hbs, context)
|
||||||
|
return JSON.stringify(res, null, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlightJSON = json => {
|
||||||
|
return formatHighlight(json, {
|
||||||
|
keyColor: "#e06c75",
|
||||||
|
numberColor: "#e5c07b",
|
||||||
|
stringColor: "#98c379",
|
||||||
|
trueColor: "#d19a66",
|
||||||
|
falseColor: "#d19a66",
|
||||||
|
nullColor: "#c678dd",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrichBindings = (bindings, context) => {
|
||||||
|
return bindings.map(binding => {
|
||||||
|
if (!context) {
|
||||||
|
return binding
|
||||||
|
}
|
||||||
|
const value = getBindingValue(binding, context)
|
||||||
|
return {
|
||||||
|
...binding,
|
||||||
|
value,
|
||||||
|
valueHTML: highlightJSON(value),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const updateValue = val => {
|
const updateValue = val => {
|
||||||
valid = isValid(readableToRuntimeBinding(bindings, val))
|
const runtimeExpression = readableToRuntimeBinding(enrichedBindings, val)
|
||||||
if (valid) {
|
dispatch("change", val)
|
||||||
dispatch("change", val)
|
requestUpdateEvaluation(runtimeExpression, context)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSelectHelper = (helper, js) => {
|
const onSelectHelper = (helper, js) => {
|
||||||
|
@ -80,9 +145,27 @@
|
||||||
bindingHelpers.onSelectBinding(js ? jsValue : hbsValue, binding, { js })
|
bindingHelpers.onSelectBinding(js ? jsValue : hbsValue, binding, { js })
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChangeMode = e => {
|
const changeMode = newMode => {
|
||||||
mode = e.detail
|
if (targetMode || newMode === mode) {
|
||||||
updateValue(mode === "JavaScript" ? jsValue : hbsValue)
|
return
|
||||||
|
}
|
||||||
|
if (editorValue) {
|
||||||
|
targetMode = newMode
|
||||||
|
} else {
|
||||||
|
mode = newMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmChangeMode = () => {
|
||||||
|
jsValue = null
|
||||||
|
hbsValue = null
|
||||||
|
updateValue(null)
|
||||||
|
mode = targetMode
|
||||||
|
targetMode = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeSidePanel = newSidePanel => {
|
||||||
|
sidePanel = newSidePanel === sidePanel ? null : newSidePanel
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChangeHBSValue = e => {
|
const onChangeHBSValue = e => {
|
||||||
|
@ -94,375 +177,177 @@
|
||||||
jsValue = encodeJSBinding(e.detail)
|
jsValue = encodeJSBinding(e.detail)
|
||||||
updateValue(jsValue)
|
updateValue(jsValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
const switchMode = () => {
|
|
||||||
if (targetMode == "Text") {
|
|
||||||
jsValue = null
|
|
||||||
updateValue(jsValue)
|
|
||||||
} else {
|
|
||||||
hbsValue = null
|
|
||||||
updateValue(hbsValue)
|
|
||||||
}
|
|
||||||
mode = targetMode + ""
|
|
||||||
targetMode = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const convert = () => {
|
|
||||||
const runtime = readableToRuntimeBinding(bindings, hbsValue)
|
|
||||||
const runtimeJs = encodeJSBinding(convertToJS(runtime))
|
|
||||||
jsValue = runtimeToReadableBinding(bindings, runtimeJs)
|
|
||||||
hbsValue = null
|
|
||||||
mode = "JavaScript"
|
|
||||||
onSelectBinding("", { forceJS: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
valid = isValid(readableToRuntimeBinding(bindings, value))
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="binding-drawer">
|
<DrawerContent padding={false}>
|
||||||
<DrawerContent>
|
<div class="binding-panel">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<Tabs
|
<div class="tabs">
|
||||||
selected={mode}
|
<div class="editor-tabs">
|
||||||
on:select={onChangeMode}
|
{#each editorTabs as tab}
|
||||||
beforeSwitch={selectedMode => {
|
<ActionButton
|
||||||
if (selectedMode == mode) {
|
size="M"
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
//Get the current mode value
|
|
||||||
const editorValue = usingJS ? decodeJSBinding(jsValue) : hbsValue
|
|
||||||
|
|
||||||
if (editorValue) {
|
|
||||||
targetMode = selectedMode
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tab title="Text">
|
|
||||||
<div class="main-content" class:binding-panel={sidebar}>
|
|
||||||
<div class="editor">
|
|
||||||
<div class="overlay-wrap">
|
|
||||||
{#if targetMode}
|
|
||||||
<div class="mode-overlay">
|
|
||||||
<div class="prompt-body">
|
|
||||||
<Heading size="S">
|
|
||||||
{`Switch to ${targetMode}?`}
|
|
||||||
</Heading>
|
|
||||||
<Body>This will discard anything in your binding</Body>
|
|
||||||
<div class="switch-actions">
|
|
||||||
<Button
|
|
||||||
secondary
|
|
||||||
size="S"
|
|
||||||
on:click={() => {
|
|
||||||
targetMode = null
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
No - keep text
|
|
||||||
</Button>
|
|
||||||
<Button cta size="S" on:click={switchMode}>
|
|
||||||
Yes - discard text
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<CodeEditor
|
|
||||||
value={hbsValue}
|
|
||||||
on:change={onChangeHBSValue}
|
|
||||||
bind:getCaretPosition
|
|
||||||
bind:insertAtPos
|
|
||||||
completions={[
|
|
||||||
hbAutocomplete([
|
|
||||||
...bindingCompletions,
|
|
||||||
...getHelperCompletions(editorMode),
|
|
||||||
]),
|
|
||||||
]}
|
|
||||||
placeholder=""
|
|
||||||
height="100%"
|
|
||||||
autofocus={autofocusEditor}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="binding-footer">
|
|
||||||
<div class="messaging">
|
|
||||||
{#if !valid}
|
|
||||||
<div class="syntax-error">
|
|
||||||
Current Handlebars syntax is invalid, please check the
|
|
||||||
guide
|
|
||||||
<a href="https://handlebarsjs.com/guide/" target="_blank"
|
|
||||||
>here</a
|
|
||||||
>
|
|
||||||
for more details.
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<Icon name="FlashOn" />
|
|
||||||
<div class="messaging-wrap">
|
|
||||||
<div>
|
|
||||||
Add available bindings by typing {{ or use the
|
|
||||||
menu on the right
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
{#if $admin.isDev && allowJS}
|
|
||||||
<ActionButton
|
|
||||||
secondary
|
|
||||||
on:click={() => {
|
|
||||||
convert()
|
|
||||||
targetMode = null
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Convert To JS
|
|
||||||
</ActionButton>
|
|
||||||
{/if}
|
|
||||||
<ActionButton
|
|
||||||
secondary
|
|
||||||
icon={sidebar ? "RailRightClose" : "RailRightOpen"}
|
|
||||||
on:click={() => {
|
|
||||||
sidebar = !sidebar
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if sidebar}
|
|
||||||
<div class="binding-picker">
|
|
||||||
<BindingPicker
|
|
||||||
{bindings}
|
|
||||||
{allowHelpers}
|
|
||||||
addHelper={onSelectHelper}
|
|
||||||
addBinding={onSelectBinding}
|
|
||||||
mode={editorMode}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
{#if allowJS}
|
|
||||||
<Tab title="JavaScript">
|
|
||||||
<div class="main-content" class:binding-panel={sidebar}>
|
|
||||||
<div class="editor">
|
|
||||||
<div class="overlay-wrap">
|
|
||||||
{#if targetMode}
|
|
||||||
<div class="mode-overlay">
|
|
||||||
<div class="prompt-body">
|
|
||||||
<Heading size="S">
|
|
||||||
{`Switch to ${targetMode}?`}
|
|
||||||
</Heading>
|
|
||||||
<Body>This will discard anything in your binding</Body>
|
|
||||||
<div class="switch-actions">
|
|
||||||
<Button
|
|
||||||
secondary
|
|
||||||
size="S"
|
|
||||||
on:click={() => {
|
|
||||||
targetMode = null
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
No - keep javascript
|
|
||||||
</Button>
|
|
||||||
<Button cta size="S" on:click={switchMode}>
|
|
||||||
Yes - discard javascript
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<CodeEditor
|
|
||||||
value={decodeJSBinding(jsValue)}
|
|
||||||
on:change={onChangeJSValue}
|
|
||||||
completions={[
|
|
||||||
jsAutocomplete([
|
|
||||||
...bindingCompletions,
|
|
||||||
...getHelperCompletions(editorMode),
|
|
||||||
]),
|
|
||||||
]}
|
|
||||||
mode={EditorModes.JS}
|
|
||||||
bind:getCaretPosition
|
|
||||||
bind:insertAtPos
|
|
||||||
height="100%"
|
|
||||||
autofocus={autofocusEditor}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="binding-footer">
|
|
||||||
<div class="messaging">
|
|
||||||
<Icon name="FlashOn" />
|
|
||||||
<div class="messaging-wrap">
|
|
||||||
<div>
|
|
||||||
Add available bindings by typing $ or use the menu on
|
|
||||||
the right
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<ActionButton
|
|
||||||
secondary
|
|
||||||
icon={sidebar ? "RailRightClose" : "RailRightOpen"}
|
|
||||||
on:click={() => {
|
|
||||||
sidebar = !sidebar
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if sidebar}
|
|
||||||
<div class="binding-picker">
|
|
||||||
<BindingPicker
|
|
||||||
{bindings}
|
|
||||||
{allowHelpers}
|
|
||||||
addHelper={onSelectHelper}
|
|
||||||
addBinding={onSelectBinding}
|
|
||||||
mode={editorMode}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
{/if}
|
|
||||||
<div class="drawer-actions">
|
|
||||||
{#if typeof drawerActions?.hide === "function" && drawerActions?.headless}
|
|
||||||
<Button
|
|
||||||
secondary
|
|
||||||
quiet
|
quiet
|
||||||
on:click={() => {
|
selected={mode === tab}
|
||||||
drawerActions.hide()
|
on:click={() => changeMode(tab)}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Cancel
|
{capitalise(tab)}
|
||||||
</Button>
|
</ActionButton>
|
||||||
{/if}
|
{/each}
|
||||||
{#if typeof bindingDrawerActions?.save === "function" && drawerActions?.headless}
|
</div>
|
||||||
<Button
|
<div class="side-tabs">
|
||||||
cta
|
{#each sideTabs as tab}
|
||||||
disabled={!valid}
|
<ActionButton
|
||||||
on:click={() => {
|
size="M"
|
||||||
bindingDrawerActions.save()
|
quiet
|
||||||
}}
|
selected={sidePanel === tab}
|
||||||
|
on:click={() => changeSidePanel(tab)}
|
||||||
>
|
>
|
||||||
Save
|
<Icon name={tab} size="S" />
|
||||||
</Button>
|
</ActionButton>
|
||||||
|
{/each}
|
||||||
|
{#if drawerContext && get(drawerContext.resizable)}
|
||||||
|
<ActionButton
|
||||||
|
size="M"
|
||||||
|
quiet
|
||||||
|
selected={drawerIsModal}
|
||||||
|
on:click={() => drawerContext.modal.set(!drawerIsModal)}
|
||||||
|
>
|
||||||
|
<Icon name={drawerIsModal ? "Minimize" : "Maximize"} size="S" />
|
||||||
|
</ActionButton>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</div>
|
||||||
|
<div class="editor">
|
||||||
|
{#if mode === Modes.Text}
|
||||||
|
{#key hbsCompletions}
|
||||||
|
<CodeEditor
|
||||||
|
value={hbsValue}
|
||||||
|
on:change={onChangeHBSValue}
|
||||||
|
bind:getCaretPosition
|
||||||
|
bind:insertAtPos
|
||||||
|
completions={hbsCompletions}
|
||||||
|
autofocus={autofocusEditor}
|
||||||
|
placeholder="Add bindings by typing {{ or use the menu on the right"
|
||||||
|
jsBindingWrapping={false}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
{:else if mode === Modes.JavaScript}
|
||||||
|
{#key jsCompletions}
|
||||||
|
<CodeEditor
|
||||||
|
value={decodeJSBinding(jsValue)}
|
||||||
|
on:change={onChangeJSValue}
|
||||||
|
completions={jsCompletions}
|
||||||
|
mode={EditorModes.JS}
|
||||||
|
bind:getCaretPosition
|
||||||
|
bind:insertAtPos
|
||||||
|
autofocus={autofocusEditor}
|
||||||
|
placeholder="Add bindings by typing $ or use the menu on the right"
|
||||||
|
jsBindingWrapping
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
{/if}
|
||||||
|
{#if targetMode}
|
||||||
|
<div class="mode-overlay">
|
||||||
|
<div class="prompt-body">
|
||||||
|
<Heading size="S">
|
||||||
|
Switch to {targetMode}?
|
||||||
|
</Heading>
|
||||||
|
<Body>This will discard anything in your binding</Body>
|
||||||
|
<div class="switch-actions">
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
size="S"
|
||||||
|
on:click={() => {
|
||||||
|
targetMode = null
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No - keep {mode}
|
||||||
|
</Button>
|
||||||
|
<Button cta size="S" on:click={confirmChangeMode}>
|
||||||
|
Yes - discard {mode}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DrawerContent>
|
<div class="side" class:visible={!!sidePanel}>
|
||||||
</span>
|
{#if sidePanel === SidePanels.Bindings}
|
||||||
|
<BindingSidePanel
|
||||||
|
bindings={enrichedBindings}
|
||||||
|
{allowHelpers}
|
||||||
|
{context}
|
||||||
|
addHelper={onSelectHelper}
|
||||||
|
addBinding={onSelectBinding}
|
||||||
|
mode={editorMode}
|
||||||
|
/>
|
||||||
|
{:else if sidePanel === SidePanels.Evaluation}
|
||||||
|
<EvaluationSidePanel
|
||||||
|
{expressionResult}
|
||||||
|
{evaluating}
|
||||||
|
expression={editorValue}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.binding-drawer :global(.container > .main) {
|
.binding-panel {
|
||||||
overflow: hidden;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.binding-drawer :global(.container > .main > .main) {
|
|
||||||
overflow: hidden;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.binding-drawer :global(.spectrum-Tabs-content) {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.binding-drawer :global(.spectrum-Tabs-content > div),
|
|
||||||
.binding-drawer :global(.spectrum-Tabs-content > div > div),
|
|
||||||
.binding-drawer :global(.spectrum-Tabs-content .main-content) {
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
.binding-panel,
|
||||||
.binding-drawer .main-content {
|
.tabs {
|
||||||
grid-template-rows: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messaging {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.messaging-wrap {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.messaging-wrap > div {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.main :global(textarea) {
|
|
||||||
min-height: 202px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
padding: var(--spacing-s) var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main :global(.spectrum-Tabs div.drawer-actions) {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main :global(.spectrum-Tabs-content),
|
|
||||||
.main :global(.spectrum-Tabs-content .main-content) {
|
|
||||||
margin-top: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main :global(.spectrum-Tabs) {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.syntax-error {
|
|
||||||
color: var(--red);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.syntax-error a {
|
|
||||||
color: var(--red);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.binding-footer {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.main-content {
|
.main {
|
||||||
display: grid;
|
flex: 1 1 auto;
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-rows: 380px;
|
|
||||||
}
|
|
||||||
.main-content.binding-panel {
|
|
||||||
grid-template-columns: 1fr 320px;
|
|
||||||
}
|
|
||||||
.binding-picker {
|
|
||||||
border-left: 2px solid var(--border-light);
|
|
||||||
border-left: var(--border-light);
|
|
||||||
overflow: scroll;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.editor {
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-xl);
|
justify-content: flex-start;
|
||||||
overflow: hidden;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.overlay-wrap {
|
.side {
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 0 0 360px;
|
||||||
|
margin-right: -360px;
|
||||||
|
transition: margin-right 130ms ease-out;
|
||||||
|
}
|
||||||
|
.side.visible {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.tabs {
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
}
|
||||||
|
.editor-tabs,
|
||||||
|
.side-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.side-tabs :global(.icon) {
|
||||||
|
width: 16px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editor */
|
||||||
|
.editor {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Overlay */
|
||||||
.mode-overlay {
|
.mode-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -471,6 +356,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: var(
|
background-color: var(
|
||||||
|
@ -490,9 +376,4 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
.binding-drawer :global(.code-editor),
|
|
||||||
.binding-drawer :global(.code-editor > div) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,399 +0,0 @@
|
||||||
<script>
|
|
||||||
import groupBy from "lodash/fp/groupBy"
|
|
||||||
import { convertToJS } from "@budibase/string-templates"
|
|
||||||
import { Input, Layout, ActionButton, Icon, Popover } from "@budibase/bbui"
|
|
||||||
import { handlebarsCompletions } from "constants/completions"
|
|
||||||
|
|
||||||
export let addHelper
|
|
||||||
export let addBinding
|
|
||||||
export let bindings
|
|
||||||
export let mode
|
|
||||||
export let allowHelpers
|
|
||||||
export let noPaddingTop = false
|
|
||||||
|
|
||||||
let search = ""
|
|
||||||
let popover
|
|
||||||
let popoverAnchor
|
|
||||||
let hoverTarget
|
|
||||||
let helpers = handlebarsCompletions()
|
|
||||||
|
|
||||||
let selectedCategory
|
|
||||||
|
|
||||||
$: searchRgx = new RegExp(search, "ig")
|
|
||||||
|
|
||||||
// Icons
|
|
||||||
$: bindingIcons = bindings?.reduce((acc, ele) => {
|
|
||||||
if (ele.icon) {
|
|
||||||
acc[ele.category] = acc[ele.category] || ele.icon
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
$: categoryIcons = { ...bindingIcons, Helpers: "MagicWand" }
|
|
||||||
|
|
||||||
$: categories = Object.entries(groupBy("category", bindings))
|
|
||||||
$: categoryNames = getCategoryNames(categories)
|
|
||||||
|
|
||||||
$: filteredCategories = categories
|
|
||||||
.map(([name, categoryBindings]) => ({
|
|
||||||
name,
|
|
||||||
bindings: categoryBindings?.filter(binding => {
|
|
||||||
return !search || binding.readableBinding.match(searchRgx)
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
.filter(category => {
|
|
||||||
return (
|
|
||||||
category.bindings?.length > 0 &&
|
|
||||||
(!selectedCategory ? true : selectedCategory === category.name)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
$: filteredHelpers = helpers?.filter(helper => {
|
|
||||||
return (
|
|
||||||
(!search ||
|
|
||||||
helper.label.match(searchRgx) ||
|
|
||||||
helper.description.match(searchRgx)) &&
|
|
||||||
(mode.name !== "javascript" || helper.allowsJs)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const getHelperExample = (helper, js) => {
|
|
||||||
let example = helper.example || ""
|
|
||||||
if (js) {
|
|
||||||
example = convertToJS(example).split("\n")[0].split("= ")[1]
|
|
||||||
if (example === "null;") {
|
|
||||||
example = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return example || ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCategoryNames = categories => {
|
|
||||||
let names = [...categories.map(cat => cat[0])]
|
|
||||||
if (allowHelpers) {
|
|
||||||
names.push("Helpers")
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<span class="detailPopover">
|
|
||||||
<Popover
|
|
||||||
align="left-outside"
|
|
||||||
bind:this={popover}
|
|
||||||
anchor={popoverAnchor}
|
|
||||||
maxWidth={300}
|
|
||||||
maxHeight={300}
|
|
||||||
dismissible={false}
|
|
||||||
>
|
|
||||||
<Layout gap="S">
|
|
||||||
<div class="helper">
|
|
||||||
{#if hoverTarget.title}
|
|
||||||
<div class="helper__name">{hoverTarget.title}</div>
|
|
||||||
{/if}
|
|
||||||
{#if hoverTarget.description}
|
|
||||||
<div class="helper__description">
|
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
|
||||||
{@html hoverTarget.description}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if hoverTarget.example}
|
|
||||||
<pre class="helper__example">{hoverTarget.example}</pre>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</Popover>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions-->
|
|
||||||
<Layout noPadding gap="S">
|
|
||||||
{#if selectedCategory}
|
|
||||||
<div class="sub-section-back">
|
|
||||||
<ActionButton
|
|
||||||
secondary
|
|
||||||
icon={"ArrowLeft"}
|
|
||||||
on:click={() => {
|
|
||||||
selectedCategory = null
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !selectedCategory}
|
|
||||||
<div class="search">
|
|
||||||
<span class="search-input">
|
|
||||||
<Input
|
|
||||||
placeholder={"Search for bindings"}
|
|
||||||
autocomplete="off"
|
|
||||||
bind:value={search}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="search-input-icon"
|
|
||||||
on:click={() => {
|
|
||||||
search = null
|
|
||||||
}}
|
|
||||||
class:searching={search}
|
|
||||||
>
|
|
||||||
<Icon name={search ? "Close" : "Search"} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !selectedCategory && !search}
|
|
||||||
<ul class="category-list">
|
|
||||||
{#each categoryNames as categoryName}
|
|
||||||
<li
|
|
||||||
on:click={() => {
|
|
||||||
selectedCategory = categoryName
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name={categoryIcons[categoryName]} />
|
|
||||||
<span class="category-name">{categoryName} </span>
|
|
||||||
<span class="category-chevron"><Icon name="ChevronRight" /></span>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if selectedCategory || search}
|
|
||||||
{#each filteredCategories as category}
|
|
||||||
{#if category.bindings?.length}
|
|
||||||
<div class="sub-section">
|
|
||||||
<div class="cat-heading">
|
|
||||||
<Icon name={categoryIcons[category.name]} />{category.name}
|
|
||||||
</div>
|
|
||||||
<ul>
|
|
||||||
{#each category.bindings as binding}
|
|
||||||
<li
|
|
||||||
class="binding"
|
|
||||||
on:mouseenter={e => {
|
|
||||||
popoverAnchor = e.target
|
|
||||||
if (!binding.description) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hoverTarget = {
|
|
||||||
title: binding.display?.name || binding.fieldSchema?.name,
|
|
||||||
description: binding.description,
|
|
||||||
}
|
|
||||||
popover.show()
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
on:mouseleave={() => {
|
|
||||||
popover.hide()
|
|
||||||
popoverAnchor = null
|
|
||||||
hoverTarget = null
|
|
||||||
}}
|
|
||||||
on:focus={() => {}}
|
|
||||||
on:blur={() => {}}
|
|
||||||
on:click={() => addBinding(binding)}
|
|
||||||
>
|
|
||||||
<span class="binding__label">
|
|
||||||
{#if binding.display?.name}
|
|
||||||
{binding.display.name}
|
|
||||||
{:else if binding.fieldSchema?.name}
|
|
||||||
{binding.fieldSchema?.name}
|
|
||||||
{:else}
|
|
||||||
{binding.readableBinding}
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{#if binding.display?.type || binding.fieldSchema?.type}
|
|
||||||
<span class="binding__typeWrap">
|
|
||||||
<span class="binding__type">
|
|
||||||
{binding.display?.type || binding.fieldSchema?.type}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#if selectedCategory === "Helpers" || search}
|
|
||||||
{#if filteredHelpers?.length}
|
|
||||||
<div class="sub-section">
|
|
||||||
<div class="cat-heading">Helpers</div>
|
|
||||||
<ul class="helpers">
|
|
||||||
{#each filteredHelpers as helper}
|
|
||||||
<li
|
|
||||||
class="binding"
|
|
||||||
on:click={() => addHelper(helper, mode.name == "javascript")}
|
|
||||||
on:mouseenter={e => {
|
|
||||||
popoverAnchor = e.target
|
|
||||||
if (!helper.displayText && helper.description) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hoverTarget = {
|
|
||||||
title: helper.displayText,
|
|
||||||
description: helper.description,
|
|
||||||
example: getHelperExample(
|
|
||||||
helper,
|
|
||||||
mode.name == "javascript"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
popover.show()
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
on:mouseleave={() => {
|
|
||||||
popover.hide()
|
|
||||||
popoverAnchor = null
|
|
||||||
hoverTarget = null
|
|
||||||
}}
|
|
||||||
on:focus={() => {}}
|
|
||||||
on:blur={() => {}}
|
|
||||||
>
|
|
||||||
<span class="binding__label">{helper.displayText}</span>
|
|
||||||
<span class="binding__typeWrap">
|
|
||||||
<span class="binding__type">function</span>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.search :global(input) {
|
|
||||||
border: none;
|
|
||||||
border-radius: 0px;
|
|
||||||
background: none;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search {
|
|
||||||
padding: var(--spacing-m) var(--spacing-l);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-top: 0px;
|
|
||||||
border-bottom: var(--border-light);
|
|
||||||
border-left: 2px solid transparent;
|
|
||||||
border-right: 2px solid transparent;
|
|
||||||
margin-right: 1px;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background-color: var(--background);
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input-icon.searching {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.category-list {
|
|
||||||
padding: 0px var(--spacing-l);
|
|
||||||
padding-bottom: var(--spacing-l);
|
|
||||||
}
|
|
||||||
.sub-section {
|
|
||||||
padding: var(--spacing-l);
|
|
||||||
padding-top: 0px;
|
|
||||||
}
|
|
||||||
.sub-section-back {
|
|
||||||
padding: var(--spacing-l);
|
|
||||||
padding-top: var(--spacing-xl);
|
|
||||||
padding-bottom: 0px;
|
|
||||||
}
|
|
||||||
.cat-heading {
|
|
||||||
margin-bottom: var(--spacing-l);
|
|
||||||
}
|
|
||||||
ul.helpers li * {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
ul.category-list li {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
ul.category-list .category-name {
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
ul.category-list .category-chevron {
|
|
||||||
flex: 1;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
ul.category-list .category-chevron :global(div.icon),
|
|
||||||
.cat-heading :global(div.icon) {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
li.binding {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
li.binding .binding__typeWrap {
|
|
||||||
flex: 1;
|
|
||||||
text-align: right;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.drawer-actions) {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cat-heading {
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--spectrum-global-color-gray-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cat-heading {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
padding: var(--spacing-m);
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
|
||||||
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
|
||||||
border-color 130ms ease-in-out;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
li:not(:last-of-type) {
|
|
||||||
margin-bottom: var(--spacing-s);
|
|
||||||
}
|
|
||||||
li :global(*) {
|
|
||||||
transition: color 130ms ease-in-out;
|
|
||||||
}
|
|
||||||
li:hover {
|
|
||||||
color: var(--spectrum-global-color-gray-900);
|
|
||||||
background-color: var(--spectrum-global-color-gray-50);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.binding__label {
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.binding__type {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
|
||||||
border-radius: var(--border-radius-s);
|
|
||||||
padding: 2px 4px;
|
|
||||||
margin-left: 2px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,422 @@
|
||||||
|
<script>
|
||||||
|
import groupBy from "lodash/fp/groupBy"
|
||||||
|
import { convertToJS } from "@budibase/string-templates"
|
||||||
|
import { Input, Layout, Icon, Popover } from "@budibase/bbui"
|
||||||
|
import { handlebarsCompletions } from "constants/completions"
|
||||||
|
|
||||||
|
export let addHelper
|
||||||
|
export let addBinding
|
||||||
|
export let bindings
|
||||||
|
export let mode
|
||||||
|
export let allowHelpers
|
||||||
|
export let context = null
|
||||||
|
|
||||||
|
let search = ""
|
||||||
|
let popover
|
||||||
|
let popoverAnchor
|
||||||
|
let hoverTarget
|
||||||
|
let helpers = handlebarsCompletions()
|
||||||
|
let selectedCategory
|
||||||
|
let hideTimeout
|
||||||
|
|
||||||
|
$: bindingIcons = bindings?.reduce((acc, ele) => {
|
||||||
|
if (ele.icon) {
|
||||||
|
acc[ele.category] = acc[ele.category] || ele.icon
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
$: categoryIcons = { ...bindingIcons, Helpers: "MagicWand" }
|
||||||
|
$: categories = Object.entries(groupBy("category", bindings))
|
||||||
|
$: categoryNames = getCategoryNames(categories)
|
||||||
|
$: searchRgx = new RegExp(search, "ig")
|
||||||
|
$: filteredCategories = categories
|
||||||
|
.map(([name, categoryBindings]) => ({
|
||||||
|
name,
|
||||||
|
bindings: categoryBindings?.filter(binding => {
|
||||||
|
return !search || binding.readableBinding.match(searchRgx)
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
.filter(category => {
|
||||||
|
return (
|
||||||
|
category.bindings?.length > 0 &&
|
||||||
|
(!selectedCategory ? true : selectedCategory === category.name)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
$: filteredHelpers = helpers?.filter(helper => {
|
||||||
|
return (
|
||||||
|
(!search ||
|
||||||
|
helper.label.match(searchRgx) ||
|
||||||
|
helper.description.match(searchRgx)) &&
|
||||||
|
(mode.name !== "javascript" || helper.allowsJs)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getHelperExample = (helper, js) => {
|
||||||
|
let example = helper.example || ""
|
||||||
|
if (js) {
|
||||||
|
example = convertToJS(example).split("\n")[0].split("= ")[1]
|
||||||
|
if (example === "null;") {
|
||||||
|
example = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return example || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoryNames = categories => {
|
||||||
|
let names = [...categories.map(cat => cat[0])]
|
||||||
|
if (allowHelpers) {
|
||||||
|
names.push("Helpers")
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
const showBindingPopover = (binding, target) => {
|
||||||
|
if (!context || !binding.value || binding.value === "") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stopHidingPopover()
|
||||||
|
popoverAnchor = target
|
||||||
|
hoverTarget = {
|
||||||
|
helper: false,
|
||||||
|
code: binding.valueHTML,
|
||||||
|
}
|
||||||
|
popover.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const showHelperPopover = (helper, target) => {
|
||||||
|
stopHidingPopover()
|
||||||
|
if (!helper.displayText && helper.description) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
popoverAnchor = target
|
||||||
|
hoverTarget = {
|
||||||
|
helper: true,
|
||||||
|
description: helper.description,
|
||||||
|
code: getHelperExample(helper, mode.name === "javascript"),
|
||||||
|
}
|
||||||
|
popover.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const hidePopover = () => {
|
||||||
|
hideTimeout = setTimeout(() => {
|
||||||
|
popover.hide()
|
||||||
|
popoverAnchor = null
|
||||||
|
hoverTarget = null
|
||||||
|
hideTimeout = null
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopHidingPopover = () => {
|
||||||
|
if (hideTimeout) {
|
||||||
|
clearTimeout(hideTimeout)
|
||||||
|
hideTimeout = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
align="left-outside"
|
||||||
|
bind:this={popover}
|
||||||
|
anchor={popoverAnchor}
|
||||||
|
minWidth={0}
|
||||||
|
maxWidth={480}
|
||||||
|
maxHeight={480}
|
||||||
|
dismissible={false}
|
||||||
|
on:mouseenter={stopHidingPopover}
|
||||||
|
on:mouseleave={hidePopover}
|
||||||
|
>
|
||||||
|
<div class="binding-popover" class:helper={hoverTarget.helper}>
|
||||||
|
{#if hoverTarget.description}
|
||||||
|
<div>
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||||
|
{@html hoverTarget.description}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if hoverTarget.code}
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||||
|
<pre>{@html hoverTarget.code}</pre>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<div class="binding-side-panel">
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
{#if selectedCategory}
|
||||||
|
<div class="header">
|
||||||
|
<Icon
|
||||||
|
name="BackAndroid"
|
||||||
|
hoverable
|
||||||
|
size="S"
|
||||||
|
on:click={() => (selectedCategory = null)}
|
||||||
|
/>
|
||||||
|
{selectedCategory}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !selectedCategory}
|
||||||
|
<div class="header">
|
||||||
|
<span class="search-input">
|
||||||
|
<Input
|
||||||
|
placeholder={"Search for bindings"}
|
||||||
|
autocomplete="off"
|
||||||
|
bind:value={search}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="search-input-icon"
|
||||||
|
on:click={() => {
|
||||||
|
search = null
|
||||||
|
}}
|
||||||
|
class:searching={search}
|
||||||
|
>
|
||||||
|
<Icon size="S" name={search ? "Close" : "Search"} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !selectedCategory && !search}
|
||||||
|
<ul class="category-list">
|
||||||
|
{#each categoryNames as categoryName}
|
||||||
|
<li
|
||||||
|
on:click={() => {
|
||||||
|
selectedCategory = categoryName
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
size="S"
|
||||||
|
color="var(--spectrum-global-color-gray-700)"
|
||||||
|
name={categoryIcons[categoryName]}
|
||||||
|
/>
|
||||||
|
<span class="category-name">{categoryName} </span>
|
||||||
|
<span class="category-chevron"><Icon name="ChevronRight" /></span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedCategory || search}
|
||||||
|
{#each filteredCategories as category}
|
||||||
|
{#if category.bindings?.length}
|
||||||
|
<div class="sub-section">
|
||||||
|
{#if filteredCategories.length > 1}
|
||||||
|
<div class="cat-heading">
|
||||||
|
<Icon name={categoryIcons[category.name]} />{category.name}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<ul>
|
||||||
|
{#each category.bindings as binding}
|
||||||
|
<li
|
||||||
|
class="binding"
|
||||||
|
on:mouseenter={e => showBindingPopover(binding, e.target)}
|
||||||
|
on:mouseleave={hidePopover}
|
||||||
|
on:click={() => addBinding(binding)}
|
||||||
|
>
|
||||||
|
<span class="binding__label">
|
||||||
|
{#if binding.display?.name}
|
||||||
|
{binding.display.name}
|
||||||
|
{:else if binding.fieldSchema?.name}
|
||||||
|
{binding.fieldSchema?.name}
|
||||||
|
{:else}
|
||||||
|
{binding.readableBinding}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{#if binding.display?.type || binding.fieldSchema?.type}
|
||||||
|
<span class="binding__typeWrap">
|
||||||
|
<span class="binding__type">
|
||||||
|
{binding.display?.type || binding.fieldSchema?.type}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if selectedCategory === "Helpers" || search}
|
||||||
|
{#if filteredHelpers?.length}
|
||||||
|
<div class="sub-section">
|
||||||
|
<ul class="helpers">
|
||||||
|
{#each filteredHelpers as helper}
|
||||||
|
<li
|
||||||
|
class="binding"
|
||||||
|
on:mouseenter={e => showHelperPopover(helper, e.target)}
|
||||||
|
on:mouseleave={hidePopover}
|
||||||
|
on:click={() => addHelper(helper, mode.name === "javascript")}
|
||||||
|
>
|
||||||
|
<span class="binding__label">{helper.displayText}</span>
|
||||||
|
<span class="binding__typeWrap">
|
||||||
|
<span class="binding__type">function</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.binding-side-panel {
|
||||||
|
border-left: var(--border-light);
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
height: 53px;
|
||||||
|
padding: 0 var(--spacing-l);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
background: var(--background);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header :global(input) {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.search-input-icon.searching {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.category-list {
|
||||||
|
padding: 0 var(--spacing-l);
|
||||||
|
padding-bottom: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.sub-section {
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
ul.helpers li * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
ul.category-list li {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
ul.category-list :global(.spectrum-Icon) {
|
||||||
|
margin: -4px 0;
|
||||||
|
}
|
||||||
|
ul.category-list .category-name {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
ul.category-list .category-chevron {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
ul.category-list .category-chevron :global(div.icon),
|
||||||
|
.cat-heading :global(div.icon) {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
li.binding {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
li.binding .binding__typeWrap {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.drawer-actions) {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-heading {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--spectrum-global-color-gray-700);
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-heading {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
transition: background-color 130ms ease-out, color 130ms ease-out,
|
||||||
|
border-color 130ms ease-out;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
li:not(:last-of-type) {
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
li :global(*) {
|
||||||
|
transition: color 130ms ease-out;
|
||||||
|
}
|
||||||
|
li:hover {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding__label {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
.binding__type {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--spectrum-global-color-gray-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding-popover {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.binding-popover pre {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: pre;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.binding-popover.helper pre {
|
||||||
|
color: var(--spectrum-global-color-blue-700);
|
||||||
|
}
|
||||||
|
.binding-popover pre :global(span) {
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
}
|
||||||
|
.binding-popover :global(p) {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.binding-popover.helper :global(code) {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,8 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import BindingPanel from "./BindingPanel.svelte"
|
import BindingPanel from "./BindingPanel.svelte"
|
||||||
|
import { previewStore } from "stores/builder"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let valid
|
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let allowJS = false
|
export let allowJS = false
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
|
@ -20,11 +21,13 @@
|
||||||
type: null,
|
type: null,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(previewStore.requestComponentContext)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BindingPanel
|
<BindingPanel
|
||||||
bind:valid
|
|
||||||
bindings={enrichedBindings}
|
bindings={enrichedBindings}
|
||||||
|
context={$previewStore.selectedComponentContext}
|
||||||
{value}
|
{value}
|
||||||
{allowJS}
|
{allowJS}
|
||||||
{allowHelpers}
|
{allowHelpers}
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
let valid = true
|
|
||||||
|
|
||||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||||
$: tempValue = readableValue
|
$: tempValue = readableValue
|
||||||
|
@ -79,20 +78,13 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Drawer bind:this={bindingDrawer} {title} headless>
|
<Drawer bind:this={bindingDrawer} title={title ?? placeholder ?? "Bindings"}>
|
||||||
<svelte:fragment slot="description">
|
<Button cta slot="buttons" on:click={handleClose}>Save</Button>
|
||||||
Add the objects on the left to enrich your text.
|
|
||||||
</svelte:fragment>
|
|
||||||
|
|
||||||
<Button cta slot="buttons" on:click={handleClose} disabled={!valid}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={panel}
|
this={panel}
|
||||||
slot="body"
|
slot="body"
|
||||||
value={readableValue}
|
value={readableValue}
|
||||||
close={handleClose}
|
close={handleClose}
|
||||||
bind:valid
|
|
||||||
on:change={event => (tempValue = event.detail)}
|
on:change={event => (tempValue = event.detail)}
|
||||||
{bindings}
|
{bindings}
|
||||||
{allowJS}
|
{allowJS}
|
||||||
|
|
|
@ -13,21 +13,21 @@
|
||||||
export let panel = ClientBindingPanel
|
export let panel = ClientBindingPanel
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let title = "Bindings"
|
export let title
|
||||||
export let placeholder
|
export let placeholder
|
||||||
export let label
|
export let label
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let fillWidth
|
|
||||||
export let allowJS = true
|
export let allowJS = true
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let drawerLeft
|
|
||||||
export let key
|
export let key
|
||||||
export let disableBindings = false
|
export let disableBindings = false
|
||||||
|
export let forceModal = false
|
||||||
|
export let context = null
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
let valid = true
|
|
||||||
let currentVal = value
|
let currentVal = value
|
||||||
|
|
||||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||||
|
@ -88,27 +88,20 @@
|
||||||
<Drawer
|
<Drawer
|
||||||
on:drawerHide={onDrawerHide}
|
on:drawerHide={onDrawerHide}
|
||||||
on:drawerShow
|
on:drawerShow
|
||||||
{fillWidth}
|
|
||||||
bind:this={bindingDrawer}
|
bind:this={bindingDrawer}
|
||||||
{title}
|
title={title ?? placeholder ?? "Bindings"}
|
||||||
left={drawerLeft}
|
{forceModal}
|
||||||
headless
|
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="description">
|
<Button cta slot="buttons" on:click={saveBinding}>Save</Button>
|
||||||
Add the objects on the left to enrich your text.
|
|
||||||
</svelte:fragment>
|
|
||||||
<Button cta slot="buttons" disabled={!valid} on:click={saveBinding}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={panel}
|
this={panel}
|
||||||
slot="body"
|
slot="body"
|
||||||
bind:valid
|
|
||||||
value={readableValue}
|
value={readableValue}
|
||||||
on:change={event => (tempValue = event.detail)}
|
on:change={event => (tempValue = event.detail)}
|
||||||
{bindings}
|
{bindings}
|
||||||
{allowJS}
|
{allowJS}
|
||||||
{allowHelpers}
|
{allowHelpers}
|
||||||
|
{context}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
export let placeholder
|
export let placeholder
|
||||||
export let label
|
export let label
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let fillWidth
|
|
||||||
export let allowJS = true
|
export let allowJS = true
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
|
@ -26,7 +25,6 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
let valid = true
|
|
||||||
let currentVal = value
|
let currentVal = value
|
||||||
|
|
||||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||||
|
@ -173,22 +171,14 @@
|
||||||
<Drawer
|
<Drawer
|
||||||
on:drawerHide
|
on:drawerHide
|
||||||
on:drawerShow
|
on:drawerShow
|
||||||
{fillWidth}
|
|
||||||
bind:this={bindingDrawer}
|
bind:this={bindingDrawer}
|
||||||
{title}
|
title={title ?? placeholder ?? "Bindings"}
|
||||||
left={drawerLeft}
|
left={drawerLeft}
|
||||||
headless
|
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="description">
|
<Button cta slot="buttons" on:click={saveBinding}>Save</Button>
|
||||||
Add the objects on the left to enrich your text.
|
|
||||||
</svelte:fragment>
|
|
||||||
<Button cta slot="buttons" disabled={!valid} on:click={saveBinding}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={panel}
|
this={panel}
|
||||||
slot="body"
|
slot="body"
|
||||||
bind:valid
|
|
||||||
value={readableValue}
|
value={readableValue}
|
||||||
on:change={event => (tempValue = event.detail)}
|
on:change={event => (tempValue = event.detail)}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
<script>
|
||||||
|
import formatHighlight from "json-format-highlight"
|
||||||
|
import { Icon, ProgressCircle, notifications } from "@budibase/bbui"
|
||||||
|
import { copyToClipboard } from "@budibase/bbui/helpers"
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
|
||||||
|
export let expressionResult
|
||||||
|
export let evaluating = false
|
||||||
|
export let expression = null
|
||||||
|
|
||||||
|
$: error = expressionResult === "Error while executing JS"
|
||||||
|
$: empty = expression == null || expression?.trim() === ""
|
||||||
|
$: success = !error && !empty
|
||||||
|
$: highlightedResult = highlight(expressionResult)
|
||||||
|
|
||||||
|
const highlight = json => {
|
||||||
|
if (json == null) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Attempt to parse and then stringify, in case this is valid JSON
|
||||||
|
try {
|
||||||
|
json = JSON.stringify(JSON.parse(json), null, 2)
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
return formatHighlight(json, {
|
||||||
|
keyColor: "#e06c75",
|
||||||
|
numberColor: "#e5c07b",
|
||||||
|
stringColor: "#98c379",
|
||||||
|
trueColor: "#d19a66",
|
||||||
|
falseColor: "#d19a66",
|
||||||
|
nullColor: "#c678dd",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
let clipboardVal = expressionResult
|
||||||
|
if (typeof clipboardVal === "object") {
|
||||||
|
clipboardVal = JSON.stringify(clipboardVal, null, 2)
|
||||||
|
}
|
||||||
|
copyToClipboard(clipboardVal)
|
||||||
|
notifications.success("Value copied to clipboard")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="evaluation-side-panel">
|
||||||
|
<div class="header" class:success class:error>
|
||||||
|
<div class="header-content">
|
||||||
|
{#if error}
|
||||||
|
<Icon name="Alert" color="var(--spectrum-global-color-red-600)" />
|
||||||
|
<div>Error</div>
|
||||||
|
{#if evaluating}
|
||||||
|
<div transition:fade|local={{ duration: 130 }}>
|
||||||
|
<ProgressCircle size="S" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<span />
|
||||||
|
<Icon name="Copy" hoverable on:click={copy} />
|
||||||
|
{:else}
|
||||||
|
<div>Preview</div>
|
||||||
|
{#if evaluating}
|
||||||
|
<div transition:fade|local={{ duration: 130 }}>
|
||||||
|
<ProgressCircle size="S" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<span />
|
||||||
|
{#if !empty}
|
||||||
|
<Icon name="Copy" hoverable on:click={copy} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
{#if empty}
|
||||||
|
Your expression will be evaluated here
|
||||||
|
{:else}
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||||
|
{@html highlightedResult}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.evaluation-side-panel {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
border-left: var(--border-light);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
padding: var(--spacing-m) var(--spacing-l);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
position: relative;
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
}
|
||||||
|
.header-content {
|
||||||
|
height: var(--spectrum-alias-item-height-m);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.header-content span {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.header.error::before {
|
||||||
|
content: "";
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 10%;
|
||||||
|
}
|
||||||
|
.header.error::before {
|
||||||
|
background: var(--spectrum-global-color-red-400);
|
||||||
|
}
|
||||||
|
.body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding: var(--spacing-m) var(--spacing-l);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,115 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Input, Modal, Body, ModalContent } from "@budibase/bbui"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import {
|
|
||||||
readableToRuntimeBinding,
|
|
||||||
runtimeToReadableBinding,
|
|
||||||
} from "dataBinding"
|
|
||||||
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
import { isJSBinding } from "@budibase/string-templates"
|
|
||||||
|
|
||||||
export let panel = ServerBindingPanel
|
|
||||||
export let value = ""
|
|
||||||
export let bindings = []
|
|
||||||
export let title = "Bindings"
|
|
||||||
export let placeholder
|
|
||||||
export let label
|
|
||||||
export let allowJS = false
|
|
||||||
export let updateOnChange = true
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
let bindingModal
|
|
||||||
let valid = true
|
|
||||||
|
|
||||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
|
||||||
$: tempValue = readableValue
|
|
||||||
$: isJS = isJSBinding(value)
|
|
||||||
|
|
||||||
const saveBinding = () => {
|
|
||||||
onChange(tempValue)
|
|
||||||
bindingModal.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onChange = input => {
|
|
||||||
dispatch("change", readableToRuntimeBinding(bindings, input))
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<DrawerBindableInput
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
{...$$props}
|
||||||
<div class="control">
|
forceModal
|
||||||
<Input
|
on:change
|
||||||
{label}
|
on:blur
|
||||||
readonly={isJS}
|
on:drawerHide
|
||||||
value={isJS ? "(JavaScript function)" : readableValue}
|
on:drawerShow
|
||||||
on:change={event => onChange(event.detail)}
|
/>
|
||||||
{placeholder}
|
|
||||||
{updateOnChange}
|
|
||||||
/>
|
|
||||||
<div class="icon" on:click={bindingModal.show}>
|
|
||||||
<Icon size="S" name="FlashOn" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Modal bind:this={bindingModal}>
|
|
||||||
<ModalContent {title} onConfirm={saveBinding} disabled={!valid} size="XL">
|
|
||||||
<Body extraSmall grey>
|
|
||||||
Add the objects on the left to enrich your text.
|
|
||||||
</Body>
|
|
||||||
<div class="panel-wrapper">
|
|
||||||
<svelte:component
|
|
||||||
this={panel}
|
|
||||||
serverSide
|
|
||||||
value={readableValue}
|
|
||||||
bind:valid
|
|
||||||
on:change={e => (tempValue = e.detail)}
|
|
||||||
{bindings}
|
|
||||||
{allowJS}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.control {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
right: 1px;
|
|
||||||
bottom: 1px;
|
|
||||||
position: absolute;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-left: 1px solid var(--spectrum-alias-border-color);
|
|
||||||
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
|
|
||||||
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
|
|
||||||
width: 31px;
|
|
||||||
color: var(--spectrum-alias-text-color);
|
|
||||||
background-color: var(--spectrum-global-color-gray-75);
|
|
||||||
transition: background-color
|
|
||||||
var(--spectrum-global-animation-duration-100, 130ms),
|
|
||||||
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
|
|
||||||
border-color var(--spectrum-global-animation-duration-100, 130ms);
|
|
||||||
height: calc(var(--spectrum-alias-item-height-m) - 2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--spectrum-alias-text-color-hover);
|
|
||||||
background-color: var(--spectrum-global-color-gray-50);
|
|
||||||
border-color: var(--spectrum-alias-border-color-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-wrapper {
|
|
||||||
border: var(--border-light);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control :global(.spectrum-Textfield-input) {
|
|
||||||
padding-right: 40px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
import BindingPanel from "./BindingPanel.svelte"
|
import BindingPanel from "./BindingPanel.svelte"
|
||||||
|
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let valid
|
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let allowJS = false
|
export let allowJS = false
|
||||||
|
export let context = null
|
||||||
|
|
||||||
$: enrichedBindings = enrichBindings(bindings)
|
$: enrichedBindings = enrichBindings(bindings)
|
||||||
|
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BindingPanel
|
<BindingPanel
|
||||||
bind:valid
|
|
||||||
bindings={enrichedBindings}
|
bindings={enrichedBindings}
|
||||||
{value}
|
{value}
|
||||||
{allowJS}
|
{allowJS}
|
||||||
|
{context}
|
||||||
on:change
|
on:change
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
export let bindings
|
export let bindings
|
||||||
export let nested
|
export let nested
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
|
export let title = "Actions"
|
||||||
|
|
||||||
let drawer
|
let drawer
|
||||||
let tmpValue
|
let tmpValue
|
||||||
|
@ -37,7 +38,7 @@
|
||||||
<ActionButton on:click={openDrawer}>{actionText}</ActionButton>
|
<ActionButton on:click={openDrawer}>{actionText}</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Drawer bind:this={drawer} title={"Actions"} on:drawerHide on:drawerShow>
|
<Drawer bind:this={drawer} {title} on:drawerHide on:drawerShow>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Define what actions to run.
|
Define what actions to run.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<Label small>Row IDs</Label>
|
<Label small>Row IDs</Label>
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
{bindings}
|
{bindings}
|
||||||
title="Rows to delete"
|
title="Row IDs to delete"
|
||||||
value={parameters.rowId}
|
value={parameters.rowId}
|
||||||
on:change={value => (parameters.rowId = value.detail)}
|
on:change={value => (parameters.rowId = value.detail)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
<Label small>Row ID</Label>
|
<Label small>Row ID</Label>
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
{bindings}
|
{bindings}
|
||||||
title="Row ID to Fetch"
|
title="Row ID"
|
||||||
value={parameters.rowId}
|
value={parameters.rowId}
|
||||||
on:change={value => (parameters.rowId = value.detail)}
|
on:change={value => (parameters.rowId = value.detail)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
<Label small>{valueLabel}</Label>
|
<Label small>{valueLabel}</Label>
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
title={`Value for "${field[0]}"`}
|
title={field[0]}
|
||||||
value={field[1]}
|
value={field[1]}
|
||||||
{bindings}
|
{bindings}
|
||||||
on:change={event => updateFieldValue(idx, event.detail)}
|
on:change={event => updateFieldValue(idx, event.detail)}
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
<Select bind:value={parameters.type} options={types} placeholder={null} />
|
<Select bind:value={parameters.type} options={types} placeholder={null} />
|
||||||
<Label>Message</Label>
|
<Label>Message</Label>
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
|
title="Message"
|
||||||
{bindings}
|
{bindings}
|
||||||
value={parameters.message}
|
value={parameters.message}
|
||||||
on:change={e => (parameters.message = e.detail)}
|
on:change={e => (parameters.message = e.detail)}
|
||||||
|
|
|
@ -72,6 +72,7 @@
|
||||||
{#if parameters.type === "set"}
|
{#if parameters.type === "set"}
|
||||||
<Label small>Value</Label>
|
<Label small>Value</Label>
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
|
title="Field value"
|
||||||
{bindings}
|
{bindings}
|
||||||
value={parameters.value}
|
value={parameters.value}
|
||||||
on:change={e => (parameters.value = e.detail)}
|
on:change={e => (parameters.value = e.detail)}
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
{#if parameters.type === "set"}
|
{#if parameters.type === "set"}
|
||||||
<Label small>Value</Label>
|
<Label small>Value</Label>
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
|
title="State value"
|
||||||
{bindings}
|
{bindings}
|
||||||
value={parameters.value}
|
value={parameters.value}
|
||||||
on:change={e => (parameters.value = e.detail)}
|
on:change={e => (parameters.value = e.detail)}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
export let key
|
export let key
|
||||||
export let nested
|
export let nested
|
||||||
export let max
|
export let max
|
||||||
|
export let context
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
placeholder="Default"
|
placeholder="Default"
|
||||||
/>
|
/>
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
|
title="Value"
|
||||||
label="Value"
|
label="Value"
|
||||||
value={column.template}
|
value={column.template}
|
||||||
on:change={e => (column.template = e.detail)}
|
on:change={e => (column.template = e.detail)}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Icon name="Settings" hoverable size="S" on:click={open} />
|
<Icon name="Settings" hoverable size="S" on:click={open} />
|
||||||
<Drawer bind:this={drawer} title="Table Columns">
|
<Drawer bind:this={drawer} title={column.name}>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
"{column.name}" column settings
|
"{column.name}" column settings
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
|
@ -48,7 +48,6 @@
|
||||||
let drawer
|
let drawer
|
||||||
let tmpQueryParams
|
let tmpQueryParams
|
||||||
let tmpCustomData
|
let tmpCustomData
|
||||||
let customDataValid = true
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
$: text = value?.label ?? "Choose an option"
|
$: text = value?.label ?? "Choose an option"
|
||||||
|
@ -267,14 +266,11 @@
|
||||||
<Drawer title="Custom data" bind:this={drawer}>
|
<Drawer title="Custom data" bind:this={drawer}>
|
||||||
<div slot="buttons" style="display:contents">
|
<div slot="buttons" style="display:contents">
|
||||||
<Button primary on:click={promptForCSV}>Load CSV</Button>
|
<Button primary on:click={promptForCSV}>Load CSV</Button>
|
||||||
<Button cta on:click={saveCustomData} disabled={!customDataValid}>
|
<Button cta on:click={saveCustomData}>Save</Button>
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div slot="description">Provide a JSON array to use as data</div>
|
<div slot="description">Provide a JSON array to use as data</div>
|
||||||
<ClientBindingPanel
|
<ClientBindingPanel
|
||||||
slot="body"
|
slot="body"
|
||||||
bind:valid={customDataValid}
|
|
||||||
value={tmpCustomData}
|
value={tmpCustomData}
|
||||||
on:change={event => (tmpCustomData = event.detail)}
|
on:change={event => (tmpCustomData = event.detail)}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let panel = ClientBindingPanel
|
export let panel = ClientBindingPanel
|
||||||
export let allowBindings = true
|
export let allowBindings = true
|
||||||
export let fillWidth = false
|
|
||||||
export let datasource
|
export let datasource
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -260,13 +259,12 @@
|
||||||
{#if filter.field && filter.valueType === "Binding"}
|
{#if filter.field && filter.valueType === "Binding"}
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
title={`Value for "${filter.field}"`}
|
title={filter.field}
|
||||||
value={filter.value}
|
value={filter.value}
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
{panel}
|
{panel}
|
||||||
{bindings}
|
{bindings}
|
||||||
on:change={event => (filter.value = event.detail)}
|
on:change={event => (filter.value = event.detail)}
|
||||||
{fillWidth}
|
|
||||||
/>
|
/>
|
||||||
{:else if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)}
|
{:else if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)}
|
||||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||||
|
|
|
@ -105,6 +105,7 @@
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
bindings={allBindings}
|
bindings={allBindings}
|
||||||
name={key}
|
name={key}
|
||||||
|
title={label}
|
||||||
{nested}
|
{nested}
|
||||||
{key}
|
{key}
|
||||||
{type}
|
{type}
|
||||||
|
|
|
@ -143,7 +143,6 @@
|
||||||
value={field.value}
|
value={field.value}
|
||||||
allowJS={false}
|
allowJS={false}
|
||||||
{allowHelpers}
|
{allowHelpers}
|
||||||
fillWidth={true}
|
|
||||||
drawerLeft={bindingDrawerLeft}
|
drawerLeft={bindingDrawerLeft}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -71,6 +71,10 @@
|
||||||
await auth.getSelf()
|
await auth.getSelf()
|
||||||
await admin.init()
|
await admin.init()
|
||||||
|
|
||||||
|
if ($admin.maintenance.length > 0) {
|
||||||
|
$redirect("./maintenance")
|
||||||
|
}
|
||||||
|
|
||||||
if ($auth.user) {
|
if ($auth.user) {
|
||||||
await licensing.init()
|
await licensing.init()
|
||||||
}
|
}
|
||||||
|
|
|
@ -188,7 +188,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeyDown} />
|
<svelte:window on:keydown={handleKeyDown} />
|
||||||
<Modal bind:this={commandPaletteModal}>
|
<Modal bind:this={commandPaletteModal} zIndex={999999}>
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="app-panel">
|
<div class="app-panel">
|
||||||
|
<div class="drawer-container" />
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<UndoRedoControl store={screenStore.history} />
|
<UndoRedoControl store={screenStore.history} />
|
||||||
|
@ -32,7 +33,17 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
padding: 9px var(--spacing-m);
|
padding: 9px 10px 12px 10px;
|
||||||
|
position: relative;
|
||||||
|
transition: width 360ms ease-out;
|
||||||
|
}
|
||||||
|
.drawer-container {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -196,6 +196,16 @@
|
||||||
} else if (type === "add-parent-component") {
|
} else if (type === "add-parent-component") {
|
||||||
const { componentId, parentType } = data
|
const { componentId, parentType } = data
|
||||||
await componentStore.addParent(componentId, parentType)
|
await componentStore.addParent(componentId, parentType)
|
||||||
|
} else if (type === "provide-context") {
|
||||||
|
let context = data?.context
|
||||||
|
if (context) {
|
||||||
|
try {
|
||||||
|
context = JSON.parse(context)
|
||||||
|
} catch (error) {
|
||||||
|
context = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previewStore.setSelectedComponentContext(context)
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Client sent unknown event type: ${type}`)
|
console.warn(`Client sent unknown event type: ${type}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
<script>
|
||||||
|
import { MaintenanceType } from "@budibase/types"
|
||||||
|
import { Heading, Body, Button, Layout } from "@budibase/bbui"
|
||||||
|
import { admin } from "stores/portal"
|
||||||
|
import BudibaseLogo from "../portal/_components/BudibaseLogo.svelte"
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if ($admin.maintenance.length === 0) {
|
||||||
|
window.location = "/builder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<div class="content">
|
||||||
|
<div class="hero">
|
||||||
|
<BudibaseLogo />
|
||||||
|
</div>
|
||||||
|
<div class="inner-content">
|
||||||
|
{#each $admin.maintenance as maintenance}
|
||||||
|
{#if maintenance.type === MaintenanceType.SQS_MISSING}
|
||||||
|
<Layout>
|
||||||
|
<Heading>Please upgrade your Budibase installation</Heading>
|
||||||
|
<Body>
|
||||||
|
We've detected that the version of Budibase you're using depends
|
||||||
|
on a more recent version of the CouchDB database than what you
|
||||||
|
have installed.
|
||||||
|
</Body>
|
||||||
|
<Body>
|
||||||
|
To resolve this, you can either rollback to a previous version of
|
||||||
|
Budibase, or follow the migration guide to update to a later
|
||||||
|
version of CouchDB.
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
<Button
|
||||||
|
on:click={() => (window.location = "https://docs.budibase.com")}
|
||||||
|
>Migration guide</Button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.main {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: auto;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.hero {
|
||||||
|
margin: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.content {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script>
|
||||||
|
import Logo from "assets/bb-emblem.svg"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions-->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<img src={Logo} alt="Budibase Logo" on:click={() => $goto("./apps")} />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,6 +4,7 @@ const INITIAL_PREVIEW_STATE = {
|
||||||
previewDevice: "desktop",
|
previewDevice: "desktop",
|
||||||
previewEventHandler: null,
|
previewEventHandler: null,
|
||||||
showPreview: false,
|
showPreview: false,
|
||||||
|
selectedComponentContext: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createPreviewStore = () => {
|
export const createPreviewStore = () => {
|
||||||
|
@ -52,6 +53,17 @@ export const createPreviewStore = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setSelectedComponentContext = context => {
|
||||||
|
store.update(state => {
|
||||||
|
state.selectedComponentContext = context
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestComponentContext = () => {
|
||||||
|
sendEvent("request-context")
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: store.subscribe,
|
subscribe: store.subscribe,
|
||||||
setDevice,
|
setDevice,
|
||||||
|
@ -60,6 +72,8 @@ export const createPreviewStore = () => {
|
||||||
startDrag,
|
startDrag,
|
||||||
stopDrag,
|
stopDrag,
|
||||||
showPreview,
|
showPreview,
|
||||||
|
setSelectedComponentContext,
|
||||||
|
requestComponentContext,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const DEFAULT_CONFIG = {
|
||||||
adminUser: { checked: false },
|
adminUser: { checked: false },
|
||||||
sso: { checked: false },
|
sso: { checked: false },
|
||||||
},
|
},
|
||||||
|
maintenance: [],
|
||||||
offlineMode: false,
|
offlineMode: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +49,7 @@ export function createAdminStore() {
|
||||||
store.isDev = environment.isDev
|
store.isDev = environment.isDev
|
||||||
store.baseUrl = environment.baseUrl
|
store.baseUrl = environment.baseUrl
|
||||||
store.offlineMode = environment.offlineMode
|
store.offlineMode = environment.offlineMode
|
||||||
|
store.maintenance = environment.maintenance
|
||||||
return store
|
return store
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
appStore,
|
appStore,
|
||||||
devToolsStore,
|
devToolsStore,
|
||||||
devToolsEnabled,
|
devToolsEnabled,
|
||||||
|
environmentStore,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte"
|
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte"
|
||||||
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte"
|
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte"
|
||||||
|
@ -36,6 +37,7 @@
|
||||||
import DevToolsHeader from "components/devtools/DevToolsHeader.svelte"
|
import DevToolsHeader from "components/devtools/DevToolsHeader.svelte"
|
||||||
import DevTools from "components/devtools/DevTools.svelte"
|
import DevTools from "components/devtools/DevTools.svelte"
|
||||||
import FreeFooter from "components/FreeFooter.svelte"
|
import FreeFooter from "components/FreeFooter.svelte"
|
||||||
|
import MaintenanceScreen from "components/MaintenanceScreen.svelte"
|
||||||
import licensing from "../licensing"
|
import licensing from "../licensing"
|
||||||
|
|
||||||
// Provide contexts
|
// Provide contexts
|
||||||
|
@ -111,122 +113,128 @@
|
||||||
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
|
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
|
||||||
class:builder={$builderStore.inBuilder}
|
class:builder={$builderStore.inBuilder}
|
||||||
>
|
>
|
||||||
<DeviceBindingsProvider>
|
{#if $environmentStore.maintenance.length > 0}
|
||||||
<UserBindingsProvider>
|
<MaintenanceScreen maintenanceList={$environmentStore.maintenance} />
|
||||||
<StateBindingsProvider>
|
{:else}
|
||||||
<RowSelectionProvider>
|
<DeviceBindingsProvider>
|
||||||
<QueryParamsProvider>
|
<UserBindingsProvider>
|
||||||
<!-- Settings bar can be rendered outside of device preview -->
|
<StateBindingsProvider>
|
||||||
<!-- Key block needs to be outside the if statement or it breaks -->
|
<RowSelectionProvider>
|
||||||
{#key $builderStore.selectedComponentId}
|
<QueryParamsProvider>
|
||||||
{#if $builderStore.inBuilder}
|
<!-- Settings bar can be rendered outside of device preview -->
|
||||||
<SettingsBar />
|
<!-- Key block needs to be outside the if statement or it breaks -->
|
||||||
{/if}
|
{#key $builderStore.selectedComponentId}
|
||||||
{/key}
|
{#if $builderStore.inBuilder}
|
||||||
|
<SettingsBar />
|
||||||
<!-- Clip boundary for selection indicators -->
|
|
||||||
<div
|
|
||||||
id="clip-root"
|
|
||||||
class:preview={$builderStore.inBuilder}
|
|
||||||
class:tablet-preview={$builderStore.previewDevice === "tablet"}
|
|
||||||
class:mobile-preview={$builderStore.previewDevice === "mobile"}
|
|
||||||
>
|
|
||||||
<!-- Actual app -->
|
|
||||||
<div id="app-root">
|
|
||||||
{#if showDevTools}
|
|
||||||
<DevToolsHeader />
|
|
||||||
{/if}
|
{/if}
|
||||||
|
{/key}
|
||||||
|
|
||||||
<div id="app-body">
|
<!-- Clip boundary for selection indicators -->
|
||||||
{#if permissionError}
|
<div
|
||||||
<div class="error">
|
id="clip-root"
|
||||||
<Layout justifyItems="center" gap="S">
|
class:preview={$builderStore.inBuilder}
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
class:tablet-preview={$builderStore.previewDevice ===
|
||||||
{@html ErrorSVG}
|
"tablet"}
|
||||||
<Heading size="L">
|
class:mobile-preview={$builderStore.previewDevice ===
|
||||||
You don't have permission to use this app
|
"mobile"}
|
||||||
</Heading>
|
>
|
||||||
<Body size="S">
|
<!-- Actual app -->
|
||||||
Ask your administrator to grant you access
|
<div id="app-root">
|
||||||
</Body>
|
{#if showDevTools}
|
||||||
</Layout>
|
<DevToolsHeader />
|
||||||
</div>
|
{/if}
|
||||||
{:else if !$screenStore.activeLayout}
|
|
||||||
<div class="error">
|
|
||||||
<Layout justifyItems="center" gap="S">
|
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
||||||
{@html ErrorSVG}
|
|
||||||
<Heading size="L">
|
|
||||||
Something went wrong rendering your app
|
|
||||||
</Heading>
|
|
||||||
<Body size="S">
|
|
||||||
Get in touch with support if this issue persists
|
|
||||||
</Body>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
{:else if embedNoScreens}
|
|
||||||
<div class="error">
|
|
||||||
<Layout justifyItems="center" gap="S">
|
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
||||||
{@html ErrorSVG}
|
|
||||||
<Heading size="L">
|
|
||||||
This Budibase app is not publicly accessible
|
|
||||||
</Heading>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<CustomThemeWrapper>
|
|
||||||
{#key $screenStore.activeLayout._id}
|
|
||||||
<Component
|
|
||||||
isLayout
|
|
||||||
instance={$screenStore.activeLayout.props}
|
|
||||||
/>
|
|
||||||
{/key}
|
|
||||||
|
|
||||||
<!--
|
<div id="app-body">
|
||||||
|
{#if permissionError}
|
||||||
|
<div class="error">
|
||||||
|
<Layout justifyItems="center" gap="S">
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
{@html ErrorSVG}
|
||||||
|
<Heading size="L">
|
||||||
|
You don't have permission to use this app
|
||||||
|
</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
Ask your administrator to grant you access
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{:else if !$screenStore.activeLayout}
|
||||||
|
<div class="error">
|
||||||
|
<Layout justifyItems="center" gap="S">
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
{@html ErrorSVG}
|
||||||
|
<Heading size="L">
|
||||||
|
Something went wrong rendering your app
|
||||||
|
</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
Get in touch with support if this issue persists
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{:else if embedNoScreens}
|
||||||
|
<div class="error">
|
||||||
|
<Layout justifyItems="center" gap="S">
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
{@html ErrorSVG}
|
||||||
|
<Heading size="L">
|
||||||
|
This Budibase app is not publicly accessible
|
||||||
|
</Heading>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<CustomThemeWrapper>
|
||||||
|
{#key $screenStore.activeLayout._id}
|
||||||
|
<Component
|
||||||
|
isLayout
|
||||||
|
instance={$screenStore.activeLayout.props}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
<!--
|
||||||
Flatpickr needs to be inside the theme wrapper.
|
Flatpickr needs to be inside the theme wrapper.
|
||||||
It also needs its own container because otherwise it hijacks
|
It also needs its own container because otherwise it hijacks
|
||||||
key events on the whole page. It is painful to work with.
|
key events on the whole page. It is painful to work with.
|
||||||
-->
|
-->
|
||||||
<div id="flatpickr-root" />
|
<div id="flatpickr-root" />
|
||||||
|
|
||||||
<!-- Modal container to ensure they sit on top -->
|
<!-- Modal container to ensure they sit on top -->
|
||||||
<div class="modal-container" />
|
<div class="modal-container" />
|
||||||
|
|
||||||
<!-- Layers on top of app -->
|
<!-- Layers on top of app -->
|
||||||
<NotificationDisplay />
|
<NotificationDisplay />
|
||||||
<ConfirmationDisplay />
|
<ConfirmationDisplay />
|
||||||
<PeekScreenDisplay />
|
<PeekScreenDisplay />
|
||||||
</CustomThemeWrapper>
|
</CustomThemeWrapper>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showDevTools}
|
{#if showDevTools}
|
||||||
<DevTools />
|
<DevTools />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !$builderStore.inBuilder && licensing.logoEnabled()}
|
||||||
|
<FreeFooter />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !$builderStore.inBuilder && licensing.logoEnabled()}
|
<!-- Preview and dev tools utilities -->
|
||||||
<FreeFooter />
|
{#if $appStore.isDevApp}
|
||||||
|
<SelectionIndicator />
|
||||||
|
{/if}
|
||||||
|
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
|
||||||
|
<HoverIndicator />
|
||||||
|
{/if}
|
||||||
|
{#if $builderStore.inBuilder}
|
||||||
|
<DNDHandler />
|
||||||
|
<GridDNDHandler />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</QueryParamsProvider>
|
||||||
<!-- Preview and dev tools utilities -->
|
</RowSelectionProvider>
|
||||||
{#if $appStore.isDevApp}
|
</StateBindingsProvider>
|
||||||
<SelectionIndicator />
|
</UserBindingsProvider>
|
||||||
{/if}
|
</DeviceBindingsProvider>
|
||||||
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
|
{/if}
|
||||||
<HoverIndicator />
|
|
||||||
{/if}
|
|
||||||
{#if $builderStore.inBuilder}
|
|
||||||
<DNDHandler />
|
|
||||||
<GridDNDHandler />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</QueryParamsProvider>
|
|
||||||
</RowSelectionProvider>
|
|
||||||
</StateBindingsProvider>
|
|
||||||
</UserBindingsProvider>
|
|
||||||
</DeviceBindingsProvider>
|
|
||||||
</div>
|
</div>
|
||||||
<KeyboardManager />
|
<KeyboardManager />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -575,6 +575,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDataContext = () => {
|
||||||
|
const normalContext = get(context)
|
||||||
|
const additionalContext = ref?.getAdditionalDataContext?.()
|
||||||
|
return {
|
||||||
|
...normalContext,
|
||||||
|
...additionalContext,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Register this component instance for external access
|
// Register this component instance for external access
|
||||||
if ($appStore.isDevApp) {
|
if ($appStore.isDevApp) {
|
||||||
|
@ -583,7 +592,7 @@
|
||||||
component: instance._component,
|
component: instance._component,
|
||||||
getSettings: () => cachedSettings,
|
getSettings: () => cachedSettings,
|
||||||
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
|
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
|
||||||
getDataContext: () => get(context),
|
getDataContext,
|
||||||
reload: () => initialise(instance, true),
|
reload: () => initialise(instance, true),
|
||||||
setEphemeralStyles: styles => (ephemeralStyles = styles),
|
setEphemeralStyles: styles => (ephemeralStyles = styles),
|
||||||
state: store,
|
state: store,
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
<!--
|
||||||
|
This is the public facing maintenance screen. It is displayed when there is
|
||||||
|
required maintenance to be done on the Budibase installation. We only use this
|
||||||
|
if we detect that the Budibase installation is in a state where the vast
|
||||||
|
majority of apps would not function correctly.
|
||||||
|
|
||||||
|
The builder-facing maintenance screen is in
|
||||||
|
packages/builder/src/pages/builder/maintenance/index.svelte, and tends to
|
||||||
|
contain more detailed information and actions for the installation owner to
|
||||||
|
take.
|
||||||
|
-->
|
||||||
|
<script>
|
||||||
|
import { MaintenanceType } from "@budibase/types"
|
||||||
|
import { Heading, Body, Layout } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let maintenanceList
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
{#each maintenanceList as maintenance}
|
||||||
|
{#if maintenance.type === MaintenanceType.SQS_MISSING}
|
||||||
|
<Layout>
|
||||||
|
<Heading>Budibase installation requires maintenance</Heading>
|
||||||
|
<Body>
|
||||||
|
The administrator of this Budibase installation needs to take actions
|
||||||
|
to update components that are out of date. Please contact them and
|
||||||
|
show them this warning. More information will be available when they
|
||||||
|
log into their account.
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.content {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: auto;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.content {
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -30,6 +30,7 @@
|
||||||
ActionTypes,
|
ActionTypes,
|
||||||
createContextStore,
|
createContextStore,
|
||||||
Provider,
|
Provider,
|
||||||
|
generateGoldenSample,
|
||||||
} = getContext("sdk")
|
} = getContext("sdk")
|
||||||
|
|
||||||
let grid
|
let grid
|
||||||
|
@ -48,6 +49,19 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Provide additional data context for live binding eval
|
||||||
|
export const getAdditionalDataContext = () => {
|
||||||
|
const rows = get(grid?.getContext()?.rows)
|
||||||
|
const goldenRow = generateGoldenSample(rows)
|
||||||
|
const id = get(component).id
|
||||||
|
return {
|
||||||
|
[id]: goldenRow,
|
||||||
|
eventContext: {
|
||||||
|
row: goldenRow,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Parses columns to fix older formats
|
// Parses columns to fix older formats
|
||||||
const getParsedColumns = columns => {
|
const getParsedColumns = columns => {
|
||||||
// If the first element has an active key all elements should be in the new format
|
// If the first element has an active key all elements should be in the new format
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "components/BlockComponent.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let dataSource
|
export let dataSource
|
||||||
|
@ -31,7 +32,9 @@
|
||||||
export let linkColumn
|
export let linkColumn
|
||||||
export let noRowsMessage
|
export let noRowsMessage
|
||||||
|
|
||||||
const { fetchDatasourceSchema } = getContext("sdk")
|
const context = getContext("context")
|
||||||
|
const { fetchDatasourceSchema, generateGoldenSample } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
let formId
|
let formId
|
||||||
let dataProviderId
|
let dataProviderId
|
||||||
|
@ -62,6 +65,16 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Provide additional data context for live binding eval
|
||||||
|
export const getAdditionalDataContext = () => {
|
||||||
|
const rows = get(context)[dataProviderId]?.rows || []
|
||||||
|
const goldenRow = generateGoldenSample(rows)
|
||||||
|
const id = get(component).id
|
||||||
|
return {
|
||||||
|
[`${id}-repeater`]: goldenRow,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Builds a full details page URL for the card title
|
// Builds a full details page URL for the card title
|
||||||
const buildFullCardUrl = (link, url, repeaterId, linkColumn) => {
|
const buildFullCardUrl = (link, url, repeaterId, linkColumn) => {
|
||||||
if (!link || !url || !repeaterId) {
|
if (!link || !url || !repeaterId) {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "stores"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
import FormBlockWrapper from "./form/FormBlockWrapper.svelte"
|
import FormBlockWrapper from "./form/FormBlockWrapper.svelte"
|
||||||
import { writable } from "svelte/store"
|
import { get, writable } from "svelte/store"
|
||||||
|
|
||||||
export let actionType
|
export let actionType
|
||||||
export let rowId
|
export let rowId
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
export let buttonPosition = "bottom"
|
export let buttonPosition = "bottom"
|
||||||
export let size
|
export let size
|
||||||
|
|
||||||
const { fetchDatasourceSchema } = getContext("sdk")
|
const { fetchDatasourceSchema, generateGoldenSample } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
|
|
||||||
|
@ -45,6 +45,16 @@
|
||||||
$: enrichedSteps = enrichSteps(steps, schema, $component.id, $currentStep)
|
$: enrichedSteps = enrichSteps(steps, schema, $component.id, $currentStep)
|
||||||
$: updateCurrentStep(enrichedSteps, $builderStore, $component)
|
$: updateCurrentStep(enrichedSteps, $builderStore, $component)
|
||||||
|
|
||||||
|
// Provide additional data context for live binding eval
|
||||||
|
export const getAdditionalDataContext = () => {
|
||||||
|
const id = get(component).id
|
||||||
|
const rows = get(context)[`${id}-provider`]?.rows || []
|
||||||
|
const goldenRow = generateGoldenSample(rows)
|
||||||
|
return {
|
||||||
|
[`${id}-repeater`]: goldenRow,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updateCurrentStep = (steps, builderStore, component) => {
|
const updateCurrentStep = (steps, builderStore, component) => {
|
||||||
const { componentId, step } = builderStore.metadata || {}
|
const { componentId, step } = builderStore.metadata || {}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import Placeholder from "components/app/Placeholder.svelte"
|
import Placeholder from "components/app/Placeholder.svelte"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
export let filter
|
export let filter
|
||||||
|
@ -18,8 +19,20 @@
|
||||||
export let gap
|
export let gap
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
const context = getContext("context")
|
||||||
|
const { generateGoldenSample } = getContext("sdk")
|
||||||
|
|
||||||
let providerId
|
let providerId
|
||||||
|
|
||||||
|
// Provide additional data context for live binding eval
|
||||||
|
export const getAdditionalDataContext = () => {
|
||||||
|
const rows = get(context)[providerId]?.rows || []
|
||||||
|
const goldenRow = generateGoldenSample(rows)
|
||||||
|
const id = get(component).id
|
||||||
|
return {
|
||||||
|
[`${id}-repeater`]: goldenRow,
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Block>
|
<Block>
|
||||||
|
|
|
@ -3,25 +3,35 @@
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "components/BlockComponent.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
export let height
|
export let height
|
||||||
|
|
||||||
export let cardTitle
|
export let cardTitle
|
||||||
export let cardSubtitle
|
export let cardSubtitle
|
||||||
export let cardDescription
|
export let cardDescription
|
||||||
export let cardImageURL
|
export let cardImageURL
|
||||||
export let cardSearchField
|
export let cardSearchField
|
||||||
|
|
||||||
export let detailFields
|
export let detailFields
|
||||||
export let detailTitle
|
export let detailTitle
|
||||||
|
|
||||||
export let noRowsMessage
|
export let noRowsMessage
|
||||||
|
|
||||||
const stateKey = generate()
|
const stateKey = generate()
|
||||||
|
const context = getContext("context")
|
||||||
|
const { generateGoldenSample } = getContext("sdk")
|
||||||
|
|
||||||
let listDataProviderId
|
let listDataProviderId
|
||||||
let listRepeaterId
|
let listRepeaterId
|
||||||
|
|
||||||
|
// Provide additional data context for live binding eval
|
||||||
|
export const getAdditionalDataContext = () => {
|
||||||
|
const rows = get(context)[listDataProviderId]?.rows || []
|
||||||
|
const goldenRow = generateGoldenSample(rows)
|
||||||
|
return {
|
||||||
|
[listRepeaterId]: goldenRow,
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Block>
|
<Block>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import InnerFormBlock from "./InnerFormBlock.svelte"
|
import InnerFormBlock from "./InnerFormBlock.svelte"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
import FormBlockWrapper from "./FormBlockWrapper.svelte"
|
import FormBlockWrapper from "./FormBlockWrapper.svelte"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export let actionType
|
export let actionType
|
||||||
export let dataSource
|
export let dataSource
|
||||||
|
@ -11,7 +12,6 @@
|
||||||
export let fields
|
export let fields
|
||||||
export let buttons
|
export let buttons
|
||||||
export let buttonPosition
|
export let buttonPosition
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let description
|
export let description
|
||||||
export let rowId
|
export let rowId
|
||||||
|
@ -25,8 +25,56 @@
|
||||||
export let saveButtonLabel
|
export let saveButtonLabel
|
||||||
export let deleteButtonLabel
|
export let deleteButtonLabel
|
||||||
|
|
||||||
const { fetchDatasourceSchema } = getContext("sdk")
|
const { fetchDatasourceSchema, generateGoldenSample } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
const context = getContext("context")
|
||||||
|
|
||||||
|
let schema
|
||||||
|
|
||||||
|
$: formattedFields = convertOldFieldFormat(fields)
|
||||||
|
$: fieldsOrDefault = getDefaultFields(formattedFields, schema)
|
||||||
|
$: fetchSchema(dataSource)
|
||||||
|
// We could simply spread $$props into the inner form and append our
|
||||||
|
// additions, but that would create svelte warnings about unused props and
|
||||||
|
// make maintenance in future more confusing as we typically always have a
|
||||||
|
// proper mapping of schema settings to component exports, without having to
|
||||||
|
// search multiple files
|
||||||
|
$: innerProps = {
|
||||||
|
dataSource,
|
||||||
|
actionUrl,
|
||||||
|
actionType,
|
||||||
|
size,
|
||||||
|
disabled,
|
||||||
|
fields: fieldsOrDefault,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
schema,
|
||||||
|
notificationOverride,
|
||||||
|
buttons:
|
||||||
|
buttons ||
|
||||||
|
Utils.buildFormBlockButtonConfig({
|
||||||
|
_id: $component.id,
|
||||||
|
showDeleteButton,
|
||||||
|
showSaveButton,
|
||||||
|
saveButtonLabel,
|
||||||
|
deleteButtonLabel,
|
||||||
|
notificationOverride,
|
||||||
|
actionType,
|
||||||
|
actionUrl,
|
||||||
|
dataSource,
|
||||||
|
}),
|
||||||
|
buttonPosition: buttons ? buttonPosition : "top",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide additional data context for live binding eval
|
||||||
|
export const getAdditionalDataContext = () => {
|
||||||
|
const id = get(component).id
|
||||||
|
const rows = get(context)[`${id}-provider`]?.rows || []
|
||||||
|
const goldenRow = generateGoldenSample(rows)
|
||||||
|
return {
|
||||||
|
[`${id}-repeater`]: goldenRow,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const convertOldFieldFormat = fields => {
|
const convertOldFieldFormat = fields => {
|
||||||
if (!fields) {
|
if (!fields) {
|
||||||
|
@ -68,42 +116,6 @@
|
||||||
return [...fields, ...defaultFields].filter(field => field.active)
|
return [...fields, ...defaultFields].filter(field => field.active)
|
||||||
}
|
}
|
||||||
|
|
||||||
let schema
|
|
||||||
|
|
||||||
$: formattedFields = convertOldFieldFormat(fields)
|
|
||||||
$: fieldsOrDefault = getDefaultFields(formattedFields, schema)
|
|
||||||
$: fetchSchema(dataSource)
|
|
||||||
// We could simply spread $$props into the inner form and append our
|
|
||||||
// additions, but that would create svelte warnings about unused props and
|
|
||||||
// make maintenance in future more confusing as we typically always have a
|
|
||||||
// proper mapping of schema settings to component exports, without having to
|
|
||||||
// search multiple files
|
|
||||||
$: innerProps = {
|
|
||||||
dataSource,
|
|
||||||
actionUrl,
|
|
||||||
actionType,
|
|
||||||
size,
|
|
||||||
disabled,
|
|
||||||
fields: fieldsOrDefault,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
schema,
|
|
||||||
notificationOverride,
|
|
||||||
buttons:
|
|
||||||
buttons ||
|
|
||||||
Utils.buildFormBlockButtonConfig({
|
|
||||||
_id: $component.id,
|
|
||||||
showDeleteButton,
|
|
||||||
showSaveButton,
|
|
||||||
saveButtonLabel,
|
|
||||||
deleteButtonLabel,
|
|
||||||
notificationOverride,
|
|
||||||
actionType,
|
|
||||||
actionUrl,
|
|
||||||
dataSource,
|
|
||||||
}),
|
|
||||||
buttonPosition: buttons ? buttonPosition : "top",
|
|
||||||
}
|
|
||||||
const fetchSchema = async () => {
|
const fetchSchema = async () => {
|
||||||
schema = (await fetchDatasourceSchema(dataSource)) || {}
|
schema = (await fetchDatasourceSchema(dataSource)) || {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,13 +58,13 @@
|
||||||
}
|
}
|
||||||
let clonedSchema = {}
|
let clonedSchema = {}
|
||||||
if (!allowedFields?.length) {
|
if (!allowedFields?.length) {
|
||||||
clonedSchema = schema
|
clonedSchema = schema || {}
|
||||||
} else {
|
} else {
|
||||||
allowedFields?.forEach(field => {
|
allowedFields?.forEach(field => {
|
||||||
if (schema[field.name]) {
|
if (schema?.[field.name]) {
|
||||||
clonedSchema[field.name] = schema[field.name]
|
clonedSchema[field.name] = schema[field.name]
|
||||||
clonedSchema[field.name].displayName = field.displayName
|
clonedSchema[field.name].displayName = field.displayName
|
||||||
} else if (schema[field]) {
|
} else if (schema?.[field]) {
|
||||||
clonedSchema[field] = schema[field]
|
clonedSchema[field] = schema[field]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -84,6 +84,18 @@ const loadBudibase = async () => {
|
||||||
} else {
|
} else {
|
||||||
dndStore.actions.reset()
|
dndStore.actions.reset()
|
||||||
}
|
}
|
||||||
|
} else if (type === "request-context") {
|
||||||
|
const { selectedComponentInstance } = get(componentStore)
|
||||||
|
const context = selectedComponentInstance?.getDataContext()
|
||||||
|
let stringifiedContext = null
|
||||||
|
try {
|
||||||
|
stringifiedContext = JSON.stringify(context)
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore - invalid context
|
||||||
|
}
|
||||||
|
eventStore.actions.dispatchEvent("provide-context", {
|
||||||
|
context: stringifiedContext,
|
||||||
|
})
|
||||||
} else if (type === "hover-component") {
|
} else if (type === "hover-component") {
|
||||||
hoverStore.actions.hoverComponent(data)
|
hoverStore.actions.hoverComponent(data)
|
||||||
} else if (type === "builder-meta") {
|
} else if (type === "builder-meta") {
|
||||||
|
|
|
@ -29,7 +29,12 @@ import { fetchDatasourceSchema } from "./utils/schema.js"
|
||||||
import { getAPIKey } from "./utils/api.js"
|
import { getAPIKey } from "./utils/api.js"
|
||||||
import { enrichButtonActions } from "./utils/buttonActions.js"
|
import { enrichButtonActions } from "./utils/buttonActions.js"
|
||||||
import { processStringSync, makePropSafe } from "@budibase/string-templates"
|
import { processStringSync, makePropSafe } from "@budibase/string-templates"
|
||||||
import { fetchData, LuceneUtils, Constants } from "@budibase/frontend-core"
|
import {
|
||||||
|
fetchData,
|
||||||
|
LuceneUtils,
|
||||||
|
Constants,
|
||||||
|
RowUtils,
|
||||||
|
} from "@budibase/frontend-core"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
API,
|
API,
|
||||||
|
@ -65,6 +70,7 @@ export default {
|
||||||
processStringSync,
|
processStringSync,
|
||||||
makePropSafe,
|
makePropSafe,
|
||||||
createContextStore,
|
createContextStore,
|
||||||
|
generateGoldenSample: RowUtils.generateGoldenSample,
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
Provider,
|
Provider,
|
||||||
|
|
|
@ -3,6 +3,7 @@ export * as JSONUtils from "./json"
|
||||||
export * as CookieUtils from "./cookies"
|
export * as CookieUtils from "./cookies"
|
||||||
export * as RoleUtils from "./roles"
|
export * as RoleUtils from "./roles"
|
||||||
export * as Utils from "./utils"
|
export * as Utils from "./utils"
|
||||||
|
export * as RowUtils from "./rows"
|
||||||
export { memo, derivedMemo } from "./memo"
|
export { memo, derivedMemo } from "./memo"
|
||||||
export { createWebsocket } from "./websocket"
|
export { createWebsocket } from "./websocket"
|
||||||
export * from "./download"
|
export * from "./download"
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/**
|
||||||
|
* Util to check is a given value is "better" than another. "Betterness" is
|
||||||
|
* defined as presence and length.
|
||||||
|
*/
|
||||||
|
const isBetterSample = (newValue, oldValue) => {
|
||||||
|
// Prefer non-null values
|
||||||
|
if (oldValue == null && newValue != null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't change type
|
||||||
|
const oldType = typeof oldValue
|
||||||
|
const newType = typeof newValue
|
||||||
|
if (oldType !== newType) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer longer values
|
||||||
|
if (newType === "string" && newValue.length > oldValue.length) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
newType === "object" &&
|
||||||
|
Object.keys(newValue).length > Object.keys(oldValue).length
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a best-case example object of the provided samples.
|
||||||
|
* The generated sample does not necessarily exist - it simply is a sample that
|
||||||
|
* contains "good" examples for every property of all the samples.
|
||||||
|
* The generate sample will have a value for all keys across all samples.
|
||||||
|
*/
|
||||||
|
export const generateGoldenSample = samples => {
|
||||||
|
let goldenSample = {}
|
||||||
|
samples?.slice(0, 100).forEach(sample => {
|
||||||
|
Object.keys(sample).forEach(key => {
|
||||||
|
if (isBetterSample(sample[key], goldenSample[key])) {
|
||||||
|
goldenSample[key] = sample[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return goldenSample
|
||||||
|
}
|
|
@ -1,38 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`/views query returns data for the created view 1`] = `
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"avg": 2333.3333333333335,
|
|
||||||
"count": 3,
|
|
||||||
"group": null,
|
|
||||||
"max": 4000,
|
|
||||||
"min": 1000,
|
|
||||||
"sum": 7000,
|
|
||||||
"sumsqr": 21000000,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`/views query returns data for the created view using a group by 1`] = `
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"avg": 1500,
|
|
||||||
"count": 2,
|
|
||||||
"group": "One",
|
|
||||||
"max": 2000,
|
|
||||||
"min": 1000,
|
|
||||||
"sum": 3000,
|
|
||||||
"sumsqr": 5000000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"avg": 4000,
|
|
||||||
"count": 1,
|
|
||||||
"group": "Two",
|
|
||||||
"max": 4000,
|
|
||||||
"min": 4000,
|
|
||||||
"sum": 4000,
|
|
||||||
"sumsqr": 16000000,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
`;
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,30 +1,38 @@
|
||||||
const setup = require("./utilities")
|
import { events } from "@budibase/backend-core"
|
||||||
const { events } = require("@budibase/backend-core")
|
import * as setup from "./utilities"
|
||||||
|
import {
|
||||||
|
FieldType,
|
||||||
|
INTERNAL_TABLE_SOURCE_ID,
|
||||||
|
SaveTableRequest,
|
||||||
|
Table,
|
||||||
|
TableSourceType,
|
||||||
|
View,
|
||||||
|
ViewCalculation,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
function priceTable() {
|
const priceTable: SaveTableRequest = {
|
||||||
return {
|
name: "table",
|
||||||
name: "table",
|
type: "table",
|
||||||
type: "table",
|
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
||||||
key: "name",
|
sourceType: TableSourceType.INTERNAL,
|
||||||
schema: {
|
schema: {
|
||||||
Price: {
|
Price: {
|
||||||
type: "number",
|
name: "Price",
|
||||||
constraints: {},
|
type: FieldType.NUMBER,
|
||||||
},
|
},
|
||||||
Category: {
|
Category: {
|
||||||
|
name: "Category",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
constraints: {
|
|
||||||
type: "string",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("/views", () => {
|
describe("/views", () => {
|
||||||
let request = setup.getRequest()
|
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
let table
|
let table: Table
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
|
@ -33,38 +41,34 @@ describe("/views", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
table = await config.createTable(priceTable())
|
table = await config.api.table.save(priceTable)
|
||||||
})
|
})
|
||||||
|
|
||||||
const saveView = async view => {
|
const saveView = async (view?: Partial<View>) => {
|
||||||
const viewToSave = {
|
const viewToSave: View = {
|
||||||
name: "TestView",
|
name: "TestView",
|
||||||
field: "Price",
|
field: "Price",
|
||||||
calculation: "stats",
|
calculation: ViewCalculation.STATISTICS,
|
||||||
tableId: table._id,
|
tableId: table._id!,
|
||||||
|
filters: [],
|
||||||
|
schema: {},
|
||||||
...view,
|
...view,
|
||||||
}
|
}
|
||||||
return request
|
return config.api.legacyView.save(viewToSave)
|
||||||
.post(`/api/views`)
|
|
||||||
.send(viewToSave)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
it("returns a success message when the view is successfully created", async () => {
|
it("returns a success message when the view is successfully created", async () => {
|
||||||
const res = await saveView()
|
const res = await saveView()
|
||||||
expect(res.body.tableId).toBe(table._id)
|
|
||||||
expect(events.view.created).toBeCalledTimes(1)
|
expect(events.view.created).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("creates a view with a calculation", async () => {
|
it("creates a view with a calculation", async () => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
const res = await saveView({ calculation: "count" })
|
const view = await saveView({ calculation: ViewCalculation.COUNT })
|
||||||
|
|
||||||
expect(res.body.tableId).toBe(table._id)
|
expect(view.tableId).toBe(table._id)
|
||||||
expect(events.view.created).toBeCalledTimes(1)
|
expect(events.view.created).toBeCalledTimes(1)
|
||||||
expect(events.view.updated).not.toBeCalled()
|
expect(events.view.updated).not.toBeCalled()
|
||||||
expect(events.view.calculationCreated).toBeCalledTimes(1)
|
expect(events.view.calculationCreated).toBeCalledTimes(1)
|
||||||
|
@ -78,8 +82,8 @@ describe("/views", () => {
|
||||||
it("creates a view with a filter", async () => {
|
it("creates a view with a filter", async () => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
const res = await saveView({
|
const view = await saveView({
|
||||||
calculation: null,
|
calculation: undefined,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
value: "1",
|
value: "1",
|
||||||
|
@ -89,7 +93,7 @@ describe("/views", () => {
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(res.body.tableId).toBe(table._id)
|
expect(view.tableId).toBe(table._id)
|
||||||
expect(events.view.created).toBeCalledTimes(1)
|
expect(events.view.created).toBeCalledTimes(1)
|
||||||
expect(events.view.updated).not.toBeCalled()
|
expect(events.view.updated).not.toBeCalled()
|
||||||
expect(events.view.calculationCreated).not.toBeCalled()
|
expect(events.view.calculationCreated).not.toBeCalled()
|
||||||
|
@ -101,52 +105,41 @@ describe("/views", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("updates the table row with the new view metadata", async () => {
|
it("updates the table row with the new view metadata", async () => {
|
||||||
const res = await request
|
await saveView()
|
||||||
.post(`/api/views`)
|
const updatedTable = await config.api.table.get(table._id!)
|
||||||
.send({
|
expect(updatedTable.views).toEqual(
|
||||||
name: "TestView",
|
expect.objectContaining({
|
||||||
field: "Price",
|
TestView: expect.objectContaining({
|
||||||
calculation: "stats",
|
field: "Price",
|
||||||
tableId: table._id,
|
calculation: "stats",
|
||||||
|
tableId: table._id,
|
||||||
|
filters: [],
|
||||||
|
schema: {
|
||||||
|
sum: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
min: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
sumsqr: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
avg: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.set(config.defaultHeaders())
|
)
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body.tableId).toBe(table._id)
|
|
||||||
|
|
||||||
const updatedTable = await config.getTable(table._id)
|
|
||||||
const expectedObj = expect.objectContaining({
|
|
||||||
TestView: expect.objectContaining({
|
|
||||||
field: "Price",
|
|
||||||
calculation: "stats",
|
|
||||||
tableId: table._id,
|
|
||||||
filters: [],
|
|
||||||
schema: {
|
|
||||||
sum: {
|
|
||||||
type: "number",
|
|
||||||
},
|
|
||||||
min: {
|
|
||||||
type: "number",
|
|
||||||
},
|
|
||||||
max: {
|
|
||||||
type: "number",
|
|
||||||
},
|
|
||||||
count: {
|
|
||||||
type: "number",
|
|
||||||
},
|
|
||||||
sumsqr: {
|
|
||||||
type: "number",
|
|
||||||
},
|
|
||||||
avg: {
|
|
||||||
type: "number",
|
|
||||||
},
|
|
||||||
field: {
|
|
||||||
type: "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
expect(updatedTable.views).toEqual(expectedObj)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -168,10 +161,10 @@ describe("/views", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("updates a view calculation", async () => {
|
it("updates a view calculation", async () => {
|
||||||
await saveView({ calculation: "sum" })
|
await saveView({ calculation: ViewCalculation.SUM })
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
await saveView({ calculation: "count" })
|
await saveView({ calculation: ViewCalculation.COUNT })
|
||||||
|
|
||||||
expect(events.view.created).not.toBeCalled()
|
expect(events.view.created).not.toBeCalled()
|
||||||
expect(events.view.updated).toBeCalledTimes(1)
|
expect(events.view.updated).toBeCalledTimes(1)
|
||||||
|
@ -184,10 +177,10 @@ describe("/views", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("deletes a view calculation", async () => {
|
it("deletes a view calculation", async () => {
|
||||||
await saveView({ calculation: "sum" })
|
await saveView({ calculation: ViewCalculation.SUM })
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
await saveView({ calculation: null })
|
await saveView({ calculation: undefined })
|
||||||
|
|
||||||
expect(events.view.created).not.toBeCalled()
|
expect(events.view.created).not.toBeCalled()
|
||||||
expect(events.view.updated).toBeCalledTimes(1)
|
expect(events.view.updated).toBeCalledTimes(1)
|
||||||
|
@ -258,100 +251,98 @@ describe("/views", () => {
|
||||||
|
|
||||||
describe("fetch", () => {
|
describe("fetch", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
table = await config.createTable(priceTable())
|
table = await config.api.table.save(priceTable)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns only custom views", async () => {
|
it("returns only custom views", async () => {
|
||||||
await config.createLegacyView({
|
await saveView({
|
||||||
name: "TestView",
|
name: "TestView",
|
||||||
field: "Price",
|
field: "Price",
|
||||||
calculation: "stats",
|
calculation: ViewCalculation.STATISTICS,
|
||||||
tableId: table._id,
|
tableId: table._id,
|
||||||
})
|
})
|
||||||
const res = await request
|
const views = await config.api.legacyView.fetch()
|
||||||
.get(`/api/views`)
|
expect(views.length).toBe(1)
|
||||||
.set(config.defaultHeaders())
|
expect(views.find(({ name }) => name === "TestView")).toBeDefined()
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body.length).toBe(1)
|
|
||||||
expect(res.body.find(({ name }) => name === "TestView")).toBeDefined()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("query", () => {
|
describe("query", () => {
|
||||||
it("returns data for the created view", async () => {
|
it("returns data for the created view", async () => {
|
||||||
await config.createLegacyView({
|
await saveView({
|
||||||
name: "TestView",
|
name: "TestView",
|
||||||
field: "Price",
|
field: "Price",
|
||||||
calculation: "stats",
|
calculation: ViewCalculation.STATISTICS,
|
||||||
tableId: table._id,
|
tableId: table._id!,
|
||||||
})
|
})
|
||||||
await config.createRow({
|
await config.api.row.save(table._id!, {
|
||||||
tableId: table._id,
|
|
||||||
Price: 1000,
|
Price: 1000,
|
||||||
})
|
})
|
||||||
await config.createRow({
|
await config.api.row.save(table._id!, {
|
||||||
tableId: table._id,
|
|
||||||
Price: 2000,
|
Price: 2000,
|
||||||
})
|
})
|
||||||
await config.createRow({
|
await config.api.row.save(table._id!, {
|
||||||
tableId: table._id,
|
|
||||||
Price: 4000,
|
Price: 4000,
|
||||||
})
|
})
|
||||||
const res = await request
|
const rows = await config.api.legacyView.get("TestView", {
|
||||||
.get(`/api/views/TestView?calculation=stats`)
|
calculation: ViewCalculation.STATISTICS,
|
||||||
.set(config.defaultHeaders())
|
})
|
||||||
.expect("Content-Type", /json/)
|
expect(rows.length).toBe(1)
|
||||||
.expect(200)
|
expect(rows[0]).toEqual({
|
||||||
expect(res.body.length).toBe(1)
|
avg: 2333.3333333333335,
|
||||||
expect(res.body).toMatchSnapshot()
|
count: 3,
|
||||||
|
group: null,
|
||||||
|
max: 4000,
|
||||||
|
min: 1000,
|
||||||
|
sum: 7000,
|
||||||
|
sumsqr: 21000000,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns data for the created view using a group by", async () => {
|
it("returns data for the created view using a group by", async () => {
|
||||||
await config.createLegacyView({
|
await saveView({
|
||||||
calculation: "stats",
|
calculation: ViewCalculation.STATISTICS,
|
||||||
name: "TestView",
|
name: "TestView",
|
||||||
field: "Price",
|
field: "Price",
|
||||||
groupBy: "Category",
|
groupBy: "Category",
|
||||||
tableId: table._id,
|
tableId: table._id,
|
||||||
})
|
})
|
||||||
await config.createRow({
|
await config.api.row.save(table._id!, {
|
||||||
tableId: table._id,
|
|
||||||
Price: 1000,
|
Price: 1000,
|
||||||
Category: "One",
|
Category: "One",
|
||||||
})
|
})
|
||||||
await config.createRow({
|
await config.api.row.save(table._id!, {
|
||||||
tableId: table._id,
|
|
||||||
Price: 2000,
|
Price: 2000,
|
||||||
Category: "One",
|
Category: "One",
|
||||||
})
|
})
|
||||||
await config.createRow({
|
await config.api.row.save(table._id!, {
|
||||||
tableId: table._id,
|
|
||||||
Price: 4000,
|
Price: 4000,
|
||||||
Category: "Two",
|
Category: "Two",
|
||||||
})
|
})
|
||||||
const res = await request
|
const rows = await config.api.legacyView.get("TestView", {
|
||||||
.get(`/api/views/TestView?calculation=stats&group=Category`)
|
calculation: ViewCalculation.STATISTICS,
|
||||||
.set(config.defaultHeaders())
|
group: "Category",
|
||||||
.expect("Content-Type", /json/)
|
})
|
||||||
.expect(200)
|
expect(rows.length).toBe(2)
|
||||||
|
expect(rows[0]).toEqual({
|
||||||
expect(res.body.length).toBe(2)
|
avg: 1500,
|
||||||
expect(res.body).toMatchSnapshot()
|
count: 2,
|
||||||
|
group: "One",
|
||||||
|
max: 2000,
|
||||||
|
min: 1000,
|
||||||
|
sum: 3000,
|
||||||
|
sumsqr: 5000000,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("destroy", () => {
|
describe("destroy", () => {
|
||||||
it("should be able to delete a view", async () => {
|
it("should be able to delete a view", async () => {
|
||||||
const table = await config.createTable(priceTable())
|
const table = await config.api.table.save(priceTable)
|
||||||
const view = await config.createLegacyView()
|
const view = await saveView({ tableId: table._id })
|
||||||
const res = await request
|
const deletedView = await config.api.legacyView.destroy(view.name!)
|
||||||
.delete(`/api/views/${view.name}`)
|
expect(deletedView.map).toBeDefined()
|
||||||
.set(config.defaultHeaders())
|
expect(deletedView.meta?.tableId).toEqual(table._id)
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body.map).toBeDefined()
|
|
||||||
expect(res.body.meta.tableId).toEqual(table._id)
|
|
||||||
expect(events.view.deleted).toBeCalledTimes(1)
|
expect(events.view.deleted).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -362,33 +353,44 @@ describe("/views", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const setupExport = async () => {
|
const setupExport = async () => {
|
||||||
const table = await config.createTable()
|
const table = await config.api.table.save({
|
||||||
await config.createRow({ name: "test-name", description: "ùúûü" })
|
name: "test-table",
|
||||||
|
type: "table",
|
||||||
|
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
||||||
|
sourceType: TableSourceType.INTERNAL,
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
name: "description",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await config.api.row.save(table._id!, {
|
||||||
|
name: "test-name",
|
||||||
|
description: "ùúûü",
|
||||||
|
})
|
||||||
return table
|
return table
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportView = async (viewName, format) => {
|
const assertJsonExport = (res: string) => {
|
||||||
return request
|
const rows = JSON.parse(res)
|
||||||
.get(`/api/views/export?view=${viewName}&format=${format}`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
const assertJsonExport = res => {
|
|
||||||
const rows = JSON.parse(res.text)
|
|
||||||
expect(rows.length).toBe(1)
|
expect(rows.length).toBe(1)
|
||||||
expect(rows[0].name).toBe("test-name")
|
expect(rows[0].name).toBe("test-name")
|
||||||
expect(rows[0].description).toBe("ùúûü")
|
expect(rows[0].description).toBe("ùúûü")
|
||||||
}
|
}
|
||||||
|
|
||||||
const assertCSVExport = res => {
|
const assertCSVExport = (res: string) => {
|
||||||
expect(res.text).toBe(`"name","description"\n"test-name","ùúûü"`)
|
expect(res).toBe(`"name","description"\n"test-name","ùúûü"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
it("should be able to export a table as JSON", async () => {
|
it("should be able to export a table as JSON", async () => {
|
||||||
const table = await setupExport()
|
const table = await setupExport()
|
||||||
|
|
||||||
const res = await exportView(table._id, "json")
|
const res = await config.api.legacyView.export(table._id!, "json")
|
||||||
|
|
||||||
assertJsonExport(res)
|
assertJsonExport(res)
|
||||||
expect(events.table.exported).toBeCalledTimes(1)
|
expect(events.table.exported).toBeCalledTimes(1)
|
||||||
|
@ -398,7 +400,7 @@ describe("/views", () => {
|
||||||
it("should be able to export a table as CSV", async () => {
|
it("should be able to export a table as CSV", async () => {
|
||||||
const table = await setupExport()
|
const table = await setupExport()
|
||||||
|
|
||||||
const res = await exportView(table._id, "csv")
|
const res = await config.api.legacyView.export(table._id!, "csv")
|
||||||
|
|
||||||
assertCSVExport(res)
|
assertCSVExport(res)
|
||||||
expect(events.table.exported).toBeCalledTimes(1)
|
expect(events.table.exported).toBeCalledTimes(1)
|
||||||
|
@ -407,10 +409,15 @@ describe("/views", () => {
|
||||||
|
|
||||||
it("should be able to export a view as JSON", async () => {
|
it("should be able to export a view as JSON", async () => {
|
||||||
let table = await setupExport()
|
let table = await setupExport()
|
||||||
const view = await config.createLegacyView()
|
const view = await config.api.legacyView.save({
|
||||||
table = await config.getTable(table._id)
|
name: "test-view",
|
||||||
|
tableId: table._id!,
|
||||||
|
filters: [],
|
||||||
|
schema: {},
|
||||||
|
})
|
||||||
|
table = await config.api.table.get(table._id!)
|
||||||
|
|
||||||
let res = await exportView(view.name, "json")
|
let res = await config.api.legacyView.export(view.name!, "json")
|
||||||
|
|
||||||
assertJsonExport(res)
|
assertJsonExport(res)
|
||||||
expect(events.view.exported).toBeCalledTimes(1)
|
expect(events.view.exported).toBeCalledTimes(1)
|
||||||
|
@ -419,10 +426,15 @@ describe("/views", () => {
|
||||||
|
|
||||||
it("should be able to export a view as CSV", async () => {
|
it("should be able to export a view as CSV", async () => {
|
||||||
let table = await setupExport()
|
let table = await setupExport()
|
||||||
const view = await config.createLegacyView()
|
const view = await config.api.legacyView.save({
|
||||||
table = await config.getTable(table._id)
|
name: "test-view",
|
||||||
|
tableId: table._id!,
|
||||||
|
filters: [],
|
||||||
|
schema: {},
|
||||||
|
})
|
||||||
|
table = await config.api.table.get(table._id!)
|
||||||
|
|
||||||
let res = await exportView(view.name, "csv")
|
let res = await config.api.legacyView.export(view.name!, "csv")
|
||||||
|
|
||||||
assertCSVExport(res)
|
assertCSVExport(res)
|
||||||
expect(events.view.exported).toBeCalledTimes(1)
|
expect(events.view.exported).toBeCalledTimes(1)
|
|
@ -1,8 +1,36 @@
|
||||||
import { Expectations, TestAPI } from "./base"
|
import { Expectations, TestAPI } from "./base"
|
||||||
import { Row } from "@budibase/types"
|
import { Row, View, ViewCalculation } from "@budibase/types"
|
||||||
|
|
||||||
export class LegacyViewAPI extends TestAPI {
|
export class LegacyViewAPI extends TestAPI {
|
||||||
get = async (id: string, expectations?: Expectations) => {
|
get = async (
|
||||||
return await this._get<Row[]>(`/api/views/${id}`, { expectations })
|
id: string,
|
||||||
|
query?: { calculation: ViewCalculation; group?: string },
|
||||||
|
expectations?: Expectations
|
||||||
|
) => {
|
||||||
|
return await this._get<Row[]>(`/api/views/${id}`, { query, expectations })
|
||||||
|
}
|
||||||
|
|
||||||
|
save = async (body: View, expectations?: Expectations) => {
|
||||||
|
return await this._post<View>(`/api/views/`, { body, expectations })
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch = async (expectations?: Expectations) => {
|
||||||
|
return await this._get<View[]>(`/api/views`, { expectations })
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy = async (id: string, expectations?: Expectations) => {
|
||||||
|
return await this._delete<View>(`/api/views/${id}`, { expectations })
|
||||||
|
}
|
||||||
|
|
||||||
|
export = async (
|
||||||
|
viewName: string,
|
||||||
|
format: "json" | "csv" | "jsonWithSchema",
|
||||||
|
expectations?: Expectations
|
||||||
|
) => {
|
||||||
|
const response = await this._requestRaw("get", `/api/views/export`, {
|
||||||
|
query: { view: viewName, format },
|
||||||
|
expectations,
|
||||||
|
})
|
||||||
|
return response.text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,3 +2,7 @@ export enum ServiceType {
|
||||||
WORKER = "worker",
|
WORKER = "worker",
|
||||||
APPS = "apps",
|
APPS = "apps",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum MaintenanceType {
|
||||||
|
SQS_MISSING = "sqs_missing",
|
||||||
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ export interface View {
|
||||||
map?: string
|
map?: string
|
||||||
reduce?: any
|
reduce?: any
|
||||||
meta?: ViewTemplateOpts
|
meta?: ViewTemplateOpts
|
||||||
|
groupBy?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewV2 {
|
export interface ViewV2 {
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
import { Ctx } from "@budibase/types"
|
import { Ctx, MaintenanceType } from "@budibase/types"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import { env as coreEnv } from "@budibase/backend-core"
|
import { env as coreEnv } from "@budibase/backend-core"
|
||||||
import nodeFetch from "node-fetch"
|
import nodeFetch from "node-fetch"
|
||||||
|
|
||||||
|
// When we come to move to SQS fully and move away from Clouseau, we will need
|
||||||
|
// to flip this to true (or remove it entirely). This will then be used to
|
||||||
|
// determine if we should show the maintenance page that links to the SQS
|
||||||
|
// migration docs.
|
||||||
|
const sqsRequired = false
|
||||||
|
|
||||||
let sqsAvailable: boolean
|
let sqsAvailable: boolean
|
||||||
async function isSqsAvailable() {
|
async function isSqsAvailable() {
|
||||||
|
// We cache this value for the duration of the Node process because we don't
|
||||||
|
// want every page load to be making this relatively expensive check.
|
||||||
if (sqsAvailable !== undefined) {
|
if (sqsAvailable !== undefined) {
|
||||||
return sqsAvailable
|
return sqsAvailable
|
||||||
}
|
}
|
||||||
|
@ -21,6 +29,10 @@ async function isSqsAvailable() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function isSqsMissing() {
|
||||||
|
return sqsRequired && !(await isSqsAvailable())
|
||||||
|
}
|
||||||
|
|
||||||
export const fetch = async (ctx: Ctx) => {
|
export const fetch = async (ctx: Ctx) => {
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
multiTenancy: !!env.MULTI_TENANCY,
|
multiTenancy: !!env.MULTI_TENANCY,
|
||||||
|
@ -30,11 +42,12 @@ export const fetch = async (ctx: Ctx) => {
|
||||||
disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL,
|
disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL,
|
||||||
baseUrl: env.PLATFORM_URL,
|
baseUrl: env.PLATFORM_URL,
|
||||||
isDev: env.isDev() && !env.isTest(),
|
isDev: env.isDev() && !env.isTest(),
|
||||||
|
maintenance: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.SELF_HOSTED) {
|
if (env.SELF_HOSTED) {
|
||||||
ctx.body.infrastructure = {
|
if (await isSqsMissing()) {
|
||||||
sqs: await isSqsAvailable(),
|
ctx.body.maintenance.push({ type: MaintenanceType.SQS_MISSING })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ describe("/api/system/environment", () => {
|
||||||
multiTenancy: true,
|
multiTenancy: true,
|
||||||
baseUrl: "http://localhost:10000",
|
baseUrl: "http://localhost:10000",
|
||||||
offlineMode: false,
|
offlineMode: false,
|
||||||
|
maintenance: [],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -40,9 +41,7 @@ describe("/api/system/environment", () => {
|
||||||
multiTenancy: true,
|
multiTenancy: true,
|
||||||
baseUrl: "http://localhost:10000",
|
baseUrl: "http://localhost:10000",
|
||||||
offlineMode: false,
|
offlineMode: false,
|
||||||
infrastructure: {
|
maintenance: [],
|
||||||
sqs: false,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -8242,11 +8242,16 @@ codemirror-spell-checker@1.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
typo-js "*"
|
typo-js "*"
|
||||||
|
|
||||||
codemirror@^5.59.0, codemirror@^5.63.1:
|
codemirror@^5.63.1:
|
||||||
version "5.65.12"
|
version "5.65.12"
|
||||||
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.12.tgz#294fdf097d10ac5b56a9e011a91eff252afc73ae"
|
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.12.tgz#294fdf097d10ac5b56a9e011a91eff252afc73ae"
|
||||||
integrity sha512-z2jlHBocElRnPYysN2HAuhXbO3DNB0bcSKmNz3hcWR2Js2Dkhc1bEOxG93Z3DeUrnm+qx56XOY5wQmbP5KY0sw==
|
integrity sha512-z2jlHBocElRnPYysN2HAuhXbO3DNB0bcSKmNz3hcWR2Js2Dkhc1bEOxG93Z3DeUrnm+qx56XOY5wQmbP5KY0sw==
|
||||||
|
|
||||||
|
codemirror@^5.65.16:
|
||||||
|
version "5.65.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.16.tgz#efc0661be6bf4988a6a1c2fe6893294638cdb334"
|
||||||
|
integrity sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg==
|
||||||
|
|
||||||
collect-v8-coverage@^1.0.0:
|
collect-v8-coverage@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
|
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
|
||||||
|
@ -13944,6 +13949,11 @@ json-buffer@3.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
|
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
|
||||||
integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
|
integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
|
||||||
|
|
||||||
|
json-format-highlight@^1.0.4:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/json-format-highlight/-/json-format-highlight-1.0.4.tgz#2e44277edabcec79a3d2c84e984c62e2258037b9"
|
||||||
|
integrity sha512-RqenIjKr1I99XfXPAml9G7YlEZg/GnsH7emWyWJh2yuGXqHW8spN7qx6/ME+MoIBb35/fxrMC9Jauj6nvGe4Mg==
|
||||||
|
|
||||||
json-parse-better-errors@^1.0.1:
|
json-parse-better-errors@^1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
|
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
|
||||||
|
|
Loading…
Reference in New Issue