Merge branch 'master' of github.com:Budibase/budibase into grid-ux-improvements
This commit is contained in:
commit
f30142831e
157 changed files with 5126 additions and 1314 deletions
|
@ -17,6 +17,8 @@
|
|||
export let customPopoverHeight
|
||||
export let open = false
|
||||
export let loading
|
||||
export let onOptionMouseenter = () => {}
|
||||
export let onOptionMouseleave = () => {}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -97,4 +99,6 @@
|
|||
{autoWidth}
|
||||
{customPopoverHeight}
|
||||
{loading}
|
||||
{onOptionMouseenter}
|
||||
{onOptionMouseleave}
|
||||
/>
|
||||
|
|
|
@ -41,6 +41,8 @@
|
|||
export let footer = null
|
||||
export let customAnchor = null
|
||||
export let loading
|
||||
export let onOptionMouseenter = () => {}
|
||||
export let onOptionMouseleave = () => {}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -199,6 +201,8 @@
|
|||
aria-selected="true"
|
||||
tabindex="0"
|
||||
on:click={() => onSelectOption(getOptionValue(option, idx))}
|
||||
on:mouseenter={e => onOptionMouseenter(e, option)}
|
||||
on:mouseleave={e => onOptionMouseleave(e, option)}
|
||||
class:is-disabled={!isOptionEnabled(option)}
|
||||
>
|
||||
{#if getOptionIcon(option, idx)}
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
export let tag = null
|
||||
export let searchTerm = null
|
||||
export let loading
|
||||
export let onOptionMouseenter = () => {}
|
||||
export let onOptionMouseleave = () => {}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -95,6 +97,8 @@
|
|||
{autocomplete}
|
||||
{sort}
|
||||
{tag}
|
||||
{onOptionMouseenter}
|
||||
{onOptionMouseleave}
|
||||
isPlaceholder={value == null || value === ""}
|
||||
placeholderOption={placeholder === false ? null : placeholder}
|
||||
isOptionSelected={option => compareOptionAndValue(option, value)}
|
||||
|
|
267
packages/bbui/src/Form/Core/Signature.svelte
Normal file
267
packages/bbui/src/Form/Core/Signature.svelte
Normal file
|
@ -0,0 +1,267 @@
|
|||
<script>
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
import Atrament from "atrament"
|
||||
import Icon from "../../Icon/Icon.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let last
|
||||
|
||||
export let value
|
||||
export let disabled = false
|
||||
export let editable = true
|
||||
export let width = 400
|
||||
export let height = 220
|
||||
export let saveIcon = false
|
||||
export let darkMode
|
||||
|
||||
export function toDataUrl() {
|
||||
// PNG to preserve transparency
|
||||
return canvasRef.toDataURL("image/png")
|
||||
}
|
||||
|
||||
export function toFile() {
|
||||
const data = canvasContext
|
||||
.getImageData(0, 0, width, height)
|
||||
.data.some(channel => channel !== 0)
|
||||
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
let dataURIParts = toDataUrl().split(",")
|
||||
if (!dataURIParts.length) {
|
||||
console.error("Could not retrieve signature data")
|
||||
}
|
||||
|
||||
// Pull out the base64 encoded byte data
|
||||
let binaryVal = atob(dataURIParts[1])
|
||||
let blobArray = new Uint8Array(binaryVal.length)
|
||||
let pos = 0
|
||||
while (pos < binaryVal.length) {
|
||||
blobArray[pos] = binaryVal.charCodeAt(pos)
|
||||
pos++
|
||||
}
|
||||
|
||||
const signatureBlob = new Blob([blobArray], {
|
||||
type: "image/png",
|
||||
})
|
||||
|
||||
return new File([signatureBlob], "signature.png", {
|
||||
type: signatureBlob.type,
|
||||
})
|
||||
}
|
||||
|
||||
export function clearCanvas() {
|
||||
return canvasContext.clearRect(0, 0, canvasWidth, canvasHeight)
|
||||
}
|
||||
|
||||
let canvasRef
|
||||
let canvasContext
|
||||
let canvasWrap
|
||||
let canvasWidth
|
||||
let canvasHeight
|
||||
let signature
|
||||
|
||||
let updated = false
|
||||
let signatureFile
|
||||
let urlFailed
|
||||
|
||||
$: if (value) {
|
||||
signatureFile = value
|
||||
}
|
||||
|
||||
$: if (signatureFile?.url) {
|
||||
updated = false
|
||||
}
|
||||
|
||||
$: if (last) {
|
||||
dispatch("update")
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!editable) {
|
||||
return
|
||||
}
|
||||
|
||||
const getPos = e => {
|
||||
var rect = canvasRef.getBoundingClientRect()
|
||||
const canvasX = e.offsetX || e.targetTouches?.[0].pageX - rect.left
|
||||
const canvasY = e.offsetY || e.targetTouches?.[0].pageY - rect.top
|
||||
|
||||
return { x: canvasX, y: canvasY }
|
||||
}
|
||||
|
||||
const checkUp = e => {
|
||||
last = getPos(e)
|
||||
}
|
||||
|
||||
canvasRef.addEventListener("pointerdown", e => {
|
||||
const current = getPos(e)
|
||||
//If the cursor didn't move at all, block the default pointerdown
|
||||
if (last?.x === current?.x && last?.y === current?.y) {
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener("pointerup", checkUp)
|
||||
|
||||
signature = new Atrament(canvasRef, {
|
||||
width,
|
||||
height,
|
||||
color: "white",
|
||||
})
|
||||
|
||||
signature.weight = 4
|
||||
signature.smoothing = 2
|
||||
|
||||
canvasWrap.style.width = `${width}px`
|
||||
canvasWrap.style.height = `${height}px`
|
||||
|
||||
const { width: wrapWidth, height: wrapHeight } =
|
||||
canvasWrap.getBoundingClientRect()
|
||||
|
||||
canvasHeight = wrapHeight
|
||||
canvasWidth = wrapWidth
|
||||
|
||||
canvasContext = canvasRef.getContext("2d")
|
||||
|
||||
return () => {
|
||||
signature.destroy()
|
||||
document.removeEventListener("pointerup", checkUp)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="signature" class:light={!darkMode} class:image-error={urlFailed}>
|
||||
{#if !disabled}
|
||||
<div class="overlay">
|
||||
{#if updated && saveIcon}
|
||||
<span class="save">
|
||||
<Icon
|
||||
name="Checkmark"
|
||||
hoverable
|
||||
tooltip={"Save"}
|
||||
tooltipPosition={"top"}
|
||||
tooltipType={"info"}
|
||||
on:click={() => {
|
||||
dispatch("change", toDataUrl())
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
{#if signatureFile?.url && !updated}
|
||||
<span class="delete">
|
||||
<Icon
|
||||
name="DeleteOutline"
|
||||
hoverable
|
||||
tooltip={"Delete"}
|
||||
tooltipPosition={"top"}
|
||||
tooltipType={"info"}
|
||||
on:click={() => {
|
||||
if (editable) {
|
||||
clearCanvas()
|
||||
}
|
||||
dispatch("clear")
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !editable && signatureFile?.url}
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
{#if !urlFailed}
|
||||
<img
|
||||
src={signatureFile?.url}
|
||||
on:error={() => {
|
||||
urlFailed = true
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
Could not load signature
|
||||
{/if}
|
||||
{:else}
|
||||
<div bind:this={canvasWrap} class="canvas-wrap">
|
||||
<canvas
|
||||
id="signature-canvas"
|
||||
bind:this={canvasRef}
|
||||
style="--max-sig-width: {width}px; --max-sig-height: {height}px"
|
||||
/>
|
||||
{#if editable}
|
||||
<div class="indicator-overlay">
|
||||
<div class="sign-here">
|
||||
<Icon name="Close" />
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.indicator-overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: end;
|
||||
padding: var(--spectrum-global-dimension-size-150);
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
.sign-here {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spectrum-global-dimension-size-150);
|
||||
}
|
||||
.sign-here hr {
|
||||
border: 0;
|
||||
border-top: 2px solid var(--spectrum-global-color-gray-200);
|
||||
width: 100%;
|
||||
}
|
||||
.canvas-wrap {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
}
|
||||
.signature img {
|
||||
max-width: 100%;
|
||||
}
|
||||
#signature-canvas {
|
||||
max-width: var(--max-sig-width);
|
||||
max-height: var(--max-sig-height);
|
||||
}
|
||||
.signature.light img,
|
||||
.signature.light #signature-canvas {
|
||||
-webkit-filter: invert(100%);
|
||||
filter: invert(100%);
|
||||
}
|
||||
.signature.image-error .overlay {
|
||||
padding-top: 0px;
|
||||
}
|
||||
.signature {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
padding: var(--spectrum-global-dimension-size-150);
|
||||
text-align: right;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.save,
|
||||
.delete {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
|
@ -16,3 +16,4 @@ export { default as CoreStepper } from "./Stepper.svelte"
|
|||
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
||||
export { default as CoreSlider } from "./Slider.svelte"
|
||||
export { default as CoreFile } from "./File.svelte"
|
||||
export { default as CoreSignature } from "./Signature.svelte"
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
export let searchTerm = null
|
||||
export let customPopoverHeight
|
||||
export let helpText = null
|
||||
export let onOptionMouseenter = () => {}
|
||||
export let onOptionMouseleave = () => {}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
|
@ -41,6 +43,8 @@
|
|||
{autoWidth}
|
||||
{autocomplete}
|
||||
{customPopoverHeight}
|
||||
{onOptionMouseenter}
|
||||
{onOptionMouseleave}
|
||||
bind:searchTerm
|
||||
on:change={onChange}
|
||||
on:click
|
||||
|
|
|
@ -29,6 +29,9 @@
|
|||
export let tag = null
|
||||
export let helpText = null
|
||||
export let compare
|
||||
export let onOptionMouseenter = () => {}
|
||||
export let onOptionMouseleave = () => {}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
value = e.detail
|
||||
|
@ -67,6 +70,8 @@
|
|||
{customPopoverHeight}
|
||||
{tag}
|
||||
{compare}
|
||||
{onOptionMouseenter}
|
||||
{onOptionMouseleave}
|
||||
on:change={onChange}
|
||||
on:click
|
||||
/>
|
||||
|
|
|
@ -173,6 +173,7 @@
|
|||
}
|
||||
|
||||
.spectrum-Modal {
|
||||
border: 2px solid var(--spectrum-global-color-gray-200);
|
||||
overflow: visible;
|
||||
max-height: none;
|
||||
margin: 40px 0;
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
export let secondaryButtonText = undefined
|
||||
export let secondaryAction = undefined
|
||||
export let secondaryButtonWarning = false
|
||||
export let custom = false
|
||||
|
||||
const { hide, cancel } = getContext(Context.Modal)
|
||||
let loading = false
|
||||
|
@ -63,12 +64,13 @@
|
|||
class:spectrum-Dialog--medium={size === "M"}
|
||||
class:spectrum-Dialog--large={size === "L"}
|
||||
class:spectrum-Dialog--extraLarge={size === "XL"}
|
||||
class:no-grid={custom}
|
||||
style="position: relative;"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="spectrum-Dialog-grid">
|
||||
<div class="modal-core" class:spectrum-Dialog-grid={!custom}>
|
||||
{#if title || $$slots.header}
|
||||
<h1
|
||||
class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"
|
||||
|
@ -153,6 +155,25 @@
|
|||
.spectrum-Dialog-content {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.no-grid .spectrum-Dialog-content {
|
||||
border-top: 2px solid var(--spectrum-global-color-gray-200);
|
||||
border-bottom: 2px solid var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
|
||||
.no-grid .spectrum-Dialog-heading {
|
||||
margin-top: 12px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.spectrum-Dialog.no-grid {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.spectrum-Dialog.no-grid .spectrum-Dialog-buttonGroup {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.spectrum-Dialog-heading {
|
||||
font-family: var(--font-accent);
|
||||
font-weight: 600;
|
||||
|
|
|
@ -1,78 +1,123 @@
|
|||
<script>
|
||||
import { getContext, onMount, createEventDispatcher } from "svelte"
|
||||
import { getContext, onDestroy, createEventDispatcher } from "svelte"
|
||||
import Portal from "svelte-portal"
|
||||
|
||||
export let title
|
||||
export let icon = ""
|
||||
export let id
|
||||
export let href = "#"
|
||||
export let link = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let selected = getContext("tab")
|
||||
let tab_internal
|
||||
let tabInfo
|
||||
let observer
|
||||
let ref
|
||||
|
||||
const setTabInfo = () => {
|
||||
// If the tabs are being rendered inside a component which uses
|
||||
// a svelte transition to enter, then this initial getBoundingClientRect
|
||||
// will return an incorrect position.
|
||||
// We just need to get this off the main thread to fix this, by using
|
||||
// a 0ms timeout.
|
||||
setTimeout(() => {
|
||||
tabInfo = tab_internal?.getBoundingClientRect()
|
||||
if (tabInfo && $selected.title === title) {
|
||||
$selected.info = tabInfo
|
||||
}
|
||||
}, 0)
|
||||
$: isSelected = $selected.title === title
|
||||
$: {
|
||||
if (isSelected && ref) {
|
||||
observe()
|
||||
} else {
|
||||
stopObserving()
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
setTabInfo()
|
||||
})
|
||||
|
||||
//Ensure that the underline is in the correct location
|
||||
$: {
|
||||
if ($selected.title === title && tab_internal) {
|
||||
if ($selected.info?.left !== tab_internal.getBoundingClientRect().left) {
|
||||
setTabInfo()
|
||||
}
|
||||
const setTabInfo = () => {
|
||||
const tabInfo = ref?.getBoundingClientRect()
|
||||
if (tabInfo) {
|
||||
$selected.info = tabInfo
|
||||
}
|
||||
}
|
||||
|
||||
const onAnchorClick = e => {
|
||||
if (e.metaKey || e.shiftKey || e.altKey || e.ctrlKey) return
|
||||
|
||||
e.preventDefault()
|
||||
$selected = {
|
||||
...$selected,
|
||||
title,
|
||||
info: ref.getBoundingClientRect(),
|
||||
}
|
||||
dispatch("click")
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
$selected = {
|
||||
...$selected,
|
||||
title,
|
||||
info: tab_internal.getBoundingClientRect(),
|
||||
info: ref.getBoundingClientRect(),
|
||||
}
|
||||
dispatch("click")
|
||||
}
|
||||
|
||||
const observe = () => {
|
||||
if (!observer) {
|
||||
observer = new ResizeObserver(setTabInfo)
|
||||
observer.observe(ref)
|
||||
}
|
||||
}
|
||||
|
||||
const stopObserving = () => {
|
||||
if (observer) {
|
||||
observer.unobserve(ref)
|
||||
observer = null
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(stopObserving)
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div
|
||||
{id}
|
||||
bind:this={tab_internal}
|
||||
on:click={onClick}
|
||||
class:is-selected={$selected.title === title}
|
||||
class="spectrum-Tabs-item"
|
||||
class:emphasized={$selected.title === title && $selected.emphasized}
|
||||
tabindex="0"
|
||||
>
|
||||
{#if icon}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--sizeM"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
aria-label="Folder"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||
</svg>
|
||||
{/if}
|
||||
<span class="spectrum-Tabs-itemLabel">{title}</span>
|
||||
</div>
|
||||
{#if $selected.title === title}
|
||||
{#if link}
|
||||
<a
|
||||
{href}
|
||||
{id}
|
||||
bind:this={ref}
|
||||
on:click={onAnchorClick}
|
||||
class="spectrum-Tabs-item link"
|
||||
class:is-selected={isSelected}
|
||||
class:emphasized={isSelected && $selected.emphasized}
|
||||
tabindex="0"
|
||||
>
|
||||
{#if icon}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--sizeM"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
aria-label="Folder"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||
</svg>
|
||||
{/if}
|
||||
<span class="spectrum-Tabs-itemLabel">{title}</span>
|
||||
</a>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div
|
||||
{id}
|
||||
bind:this={ref}
|
||||
on:click={onClick}
|
||||
on:click
|
||||
class="spectrum-Tabs-item"
|
||||
class:is-selected={isSelected}
|
||||
class:emphasized={isSelected && $selected.emphasized}
|
||||
tabindex="0"
|
||||
>
|
||||
{#if icon}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--sizeM"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
aria-label="Folder"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||
</svg>
|
||||
{/if}
|
||||
<span class="spectrum-Tabs-itemLabel">{title}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isSelected}
|
||||
<Portal target=".spectrum-Tabs-content-{$selected.id}">
|
||||
<slot />
|
||||
</Portal>
|
||||
|
@ -89,4 +134,7 @@
|
|||
.spectrum-Tabs-item:hover {
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
.link {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
|
79
packages/bbui/src/Tooltip/Context.svelte
Normal file
79
packages/bbui/src/Tooltip/Context.svelte
Normal file
|
@ -0,0 +1,79 @@
|
|||
<script>
|
||||
import Portal from "svelte-portal"
|
||||
import { getContext } from "svelte"
|
||||
import Context from "../context"
|
||||
|
||||
export let anchor
|
||||
export let visible = false
|
||||
export let offset = 0
|
||||
|
||||
$: target = getContext(Context.PopoverRoot) || "#app"
|
||||
|
||||
let hovering = false
|
||||
let tooltip
|
||||
let x = 0
|
||||
let y = 0
|
||||
|
||||
const updatePosition = (anchor, tooltip) => {
|
||||
if (anchor == null || tooltip == null) {
|
||||
return
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const rect = anchor.getBoundingClientRect()
|
||||
const windowOffset =
|
||||
window.innerHeight - offset - (tooltip.clientHeight + rect.y)
|
||||
const tooltipWidth = tooltip.clientWidth
|
||||
|
||||
x = rect.x - tooltipWidth - offset
|
||||
y = windowOffset < 0 ? rect.y + windowOffset : rect.y
|
||||
})
|
||||
}
|
||||
|
||||
$: updatePosition(anchor, tooltip)
|
||||
|
||||
const handleMouseenter = () => {
|
||||
hovering = true
|
||||
}
|
||||
|
||||
const handleMouseleave = () => {
|
||||
hovering = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<Portal {target}>
|
||||
<div
|
||||
role="tooltip"
|
||||
on:mouseenter={handleMouseenter}
|
||||
on:mouseleave={handleMouseleave}
|
||||
style:left={`${x}px`}
|
||||
style:top={`${y}px`}
|
||||
class="wrapper"
|
||||
class:visible={visible || hovering}
|
||||
>
|
||||
<div bind:this={tooltip} class="tooltip">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
background-color: var(--spectrum-global-color-gray-100);
|
||||
box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.42);
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--grey-4);
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
|
@ -53,6 +53,7 @@ export { default as Link } from "./Link/Link.svelte"
|
|||
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
|
||||
export { default as TempTooltip } from "./Tooltip/TempTooltip.svelte"
|
||||
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
|
||||
export { default as ContextTooltip } from "./Tooltip/Context.svelte"
|
||||
export { default as Menu } from "./Menu/Menu.svelte"
|
||||
export { default as MenuSection } from "./Menu/Section.svelte"
|
||||
export { default as MenuSeparator } from "./Menu/Separator.svelte"
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import TestDataModal from "./TestDataModal.svelte"
|
||||
import { flip } from "svelte/animate"
|
||||
import { fly } from "svelte/transition"
|
||||
import { Icon, notifications, Modal } from "@budibase/bbui"
|
||||
import { Icon, notifications, Modal, Toggle } from "@budibase/bbui"
|
||||
import { ActionStepID } from "constants/backend/automations"
|
||||
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
|
||||
|
||||
|
@ -73,6 +73,16 @@
|
|||
Test details
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-spacing">
|
||||
<Toggle
|
||||
text={automation.disabled ? "Paused" : "Activated"}
|
||||
on:change={automationStore.actions.toggleDisabled(
|
||||
automation._id,
|
||||
automation.disabled
|
||||
)}
|
||||
value={!automation.disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="canvas" on:scroll={handleScroll}>
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
selected={automation._id === selectedAutomationId}
|
||||
on:click={() => selectAutomation(automation._id)}
|
||||
selectedBy={$userSelectedResourceMap[automation._id]}
|
||||
disabled={automation.disabled}
|
||||
>
|
||||
<EditAutomationPopover {automation} />
|
||||
</NavItem>
|
||||
|
|
|
@ -39,6 +39,15 @@
|
|||
>Duplicate</MenuItem
|
||||
>
|
||||
<MenuItem icon="Edit" on:click={updateAutomationDialog.show}>Edit</MenuItem>
|
||||
<MenuItem
|
||||
icon={automation.disabled ? "CheckmarkCircle" : "Cancel"}
|
||||
on:click={automationStore.actions.toggleDisabled(
|
||||
automation._id,
|
||||
automation.disabled
|
||||
)}
|
||||
>
|
||||
{automation.disabled ? "Activate" : "Pause"}
|
||||
</MenuItem>
|
||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
||||
</ActionMenu>
|
||||
|
||||
|
|
|
@ -364,6 +364,7 @@
|
|||
value.customType !== "cron" &&
|
||||
value.customType !== "triggerSchema" &&
|
||||
value.customType !== "automationFields" &&
|
||||
value.type !== "signature_single" &&
|
||||
value.type !== "attachment" &&
|
||||
value.type !== "attachment_single"
|
||||
)
|
||||
|
@ -456,7 +457,7 @@
|
|||
value={inputData[key]}
|
||||
options={Object.keys(table?.schema || {})}
|
||||
/>
|
||||
{:else if value.type === "attachment"}
|
||||
{:else if value.type === "attachment" || value.type === "signature_single"}
|
||||
<div class="attachment-field-wrapper">
|
||||
<div class="label-wrapper">
|
||||
<Label>{label}</Label>
|
||||
|
|
|
@ -24,6 +24,11 @@
|
|||
|
||||
let table
|
||||
let schemaFields
|
||||
let attachmentTypes = [
|
||||
FieldType.ATTACHMENTS,
|
||||
FieldType.ATTACHMENT_SINGLE,
|
||||
FieldType.SIGNATURE_SINGLE,
|
||||
]
|
||||
|
||||
$: {
|
||||
table = $tables.list.find(table => table._id === value?.tableId)
|
||||
|
@ -120,15 +125,9 @@
|
|||
{#if schemaFields.length}
|
||||
{#each schemaFields as [field, schema]}
|
||||
{#if !schema.autocolumn}
|
||||
<div
|
||||
class:schema-fields={schema.type !== FieldType.ATTACHMENTS &&
|
||||
schema.type !== FieldType.ATTACHMENT_SINGLE}
|
||||
>
|
||||
<div class:schema-fields={!attachmentTypes.includes(schema.type)}>
|
||||
<Label>{field}</Label>
|
||||
<div
|
||||
class:field-width={schema.type !== FieldType.ATTACHMENTS &&
|
||||
schema.type !== FieldType.ATTACHMENT_SINGLE}
|
||||
>
|
||||
<div class:field-width={!attachmentTypes.includes(schema.type)}>
|
||||
{#if isTestModal}
|
||||
<RowSelectorTypes
|
||||
{isTestModal}
|
||||
|
|
|
@ -21,6 +21,12 @@
|
|||
return clone
|
||||
})
|
||||
|
||||
let attachmentTypes = [
|
||||
FieldType.ATTACHMENTS,
|
||||
FieldType.ATTACHMENT_SINGLE,
|
||||
FieldType.SIGNATURE_SINGLE,
|
||||
]
|
||||
|
||||
function schemaHasOptions(schema) {
|
||||
return !!schema.constraints?.inclusion?.length
|
||||
}
|
||||
|
@ -29,7 +35,8 @@
|
|||
let params = {}
|
||||
|
||||
if (
|
||||
schema.type === FieldType.ATTACHMENT_SINGLE &&
|
||||
(schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||
schema.type === FieldType.SIGNATURE_SINGLE) &&
|
||||
Object.keys(keyValueObj).length === 0
|
||||
) {
|
||||
return []
|
||||
|
@ -100,16 +107,20 @@
|
|||
on:change={e => onChange(e, field)}
|
||||
useLabel={false}
|
||||
/>
|
||||
{:else if schema.type === FieldType.ATTACHMENTS || schema.type === FieldType.ATTACHMENT_SINGLE}
|
||||
{:else if attachmentTypes.includes(schema.type)}
|
||||
<div class="attachment-field-spacinng">
|
||||
<KeyValueBuilder
|
||||
on:change={e =>
|
||||
onChange(
|
||||
{
|
||||
detail:
|
||||
schema.type === FieldType.ATTACHMENT_SINGLE
|
||||
schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||
schema.type === FieldType.SIGNATURE_SINGLE
|
||||
? e.detail.length > 0
|
||||
? { url: e.detail[0].name, filename: e.detail[0].value }
|
||||
? {
|
||||
url: e.detail[0].name,
|
||||
filename: e.detail[0].value,
|
||||
}
|
||||
: {}
|
||||
: e.detail.map(({ name, value }) => ({
|
||||
url: name,
|
||||
|
@ -125,7 +136,8 @@
|
|||
customButtonText={"Add attachment"}
|
||||
keyPlaceholder={"URL"}
|
||||
valuePlaceholder={"Filename"}
|
||||
actionButtonDisabled={schema.type === FieldType.ATTACHMENT_SINGLE &&
|
||||
actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||
schema.type === FieldType.SIGNATURE) &&
|
||||
Object.keys(value[field]).length >= 1}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { API } from "api"
|
||||
import {
|
||||
Input,
|
||||
Select,
|
||||
|
@ -8,11 +9,16 @@
|
|||
Label,
|
||||
RichTextField,
|
||||
TextArea,
|
||||
CoreSignature,
|
||||
ActionButton,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import Dropzone from "components/common/Dropzone.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||
import Editor from "../../integration/QueryEditor.svelte"
|
||||
import { SignatureModal } from "@budibase/frontend-core/src/components"
|
||||
import { themeStore } from "stores/portal"
|
||||
|
||||
export let meta
|
||||
export let value
|
||||
|
@ -38,8 +44,35 @@
|
|||
|
||||
const timeStamp = resolveTimeStamp(value)
|
||||
const isTimeStamp = !!timeStamp || meta?.timeOnly
|
||||
|
||||
$: currentTheme = $themeStore?.theme
|
||||
$: darkMode = !currentTheme.includes("light")
|
||||
|
||||
let signatureModal
|
||||
</script>
|
||||
|
||||
<SignatureModal
|
||||
{darkMode}
|
||||
onConfirm={async sigCanvas => {
|
||||
const signatureFile = sigCanvas.toFile()
|
||||
|
||||
let attachRequest = new FormData()
|
||||
attachRequest.append("file", signatureFile)
|
||||
|
||||
try {
|
||||
const uploadReq = await API.uploadBuilderAttachment(attachRequest)
|
||||
const [signatureAttachment] = uploadReq
|
||||
value = signatureAttachment
|
||||
} catch (error) {
|
||||
$notifications.error(error.message || "Failed to save signature")
|
||||
value = []
|
||||
}
|
||||
}}
|
||||
title={meta.name}
|
||||
{value}
|
||||
bind:this={signatureModal}
|
||||
/>
|
||||
|
||||
{#if type === "options" && meta.constraints.inclusion.length !== 0}
|
||||
<Select
|
||||
{label}
|
||||
|
@ -58,7 +91,51 @@
|
|||
bind:value
|
||||
/>
|
||||
{:else if type === "attachment"}
|
||||
<Dropzone {label} {error} bind:value />
|
||||
<Dropzone
|
||||
compact
|
||||
{label}
|
||||
{error}
|
||||
{value}
|
||||
on:change={e => {
|
||||
value = e.detail
|
||||
}}
|
||||
/>
|
||||
{:else if type === "attachment_single"}
|
||||
<Dropzone
|
||||
compact
|
||||
{label}
|
||||
{error}
|
||||
value={value ? [value] : []}
|
||||
on:change={e => {
|
||||
value = e.detail?.[0]
|
||||
}}
|
||||
maximum={1}
|
||||
/>
|
||||
{:else if type === "signature_single"}
|
||||
<div class="signature">
|
||||
<Label>{label}</Label>
|
||||
<div class="sig-wrap" class:display={value}>
|
||||
{#if value}
|
||||
<CoreSignature
|
||||
{darkMode}
|
||||
{value}
|
||||
editable={false}
|
||||
on:clear={() => {
|
||||
value = null
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<ActionButton
|
||||
fullWidth
|
||||
on:click={() => {
|
||||
signatureModal.show()
|
||||
}}
|
||||
>
|
||||
Add signature
|
||||
</ActionButton>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if type === "boolean"}
|
||||
<Toggle text={label} {error} bind:value />
|
||||
{:else if type === "array" && meta.constraints.inclusion.length !== 0}
|
||||
|
@ -94,3 +171,22 @@
|
|||
{:else}
|
||||
<Input {label} {type} {error} bind:value disabled={readonly} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.signature :global(label.spectrum-FieldLabel) {
|
||||
padding-top: var(--spectrum-fieldlabel-padding-top);
|
||||
padding-bottom: var(--spectrum-fieldlabel-padding-bottom);
|
||||
}
|
||||
.sig-wrap.display {
|
||||
min-height: 50px;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--spectrum-global-color-gray-50);
|
||||
box-sizing: border-box;
|
||||
border: var(--spectrum-alias-border-size-thin)
|
||||
var(--spectrum-alias-border-color) solid;
|
||||
border-radius: var(--spectrum-alias-border-radius-regular);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { datasources, tables, integrations, appStore } from "stores/builder"
|
||||
import { admin } from "stores/portal"
|
||||
import { themeStore, admin } from "stores/portal"
|
||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||
import { TableNames } from "constants"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
|
@ -38,6 +38,9 @@
|
|||
})
|
||||
$: relationshipsEnabled = relationshipSupport(tableDatasource)
|
||||
|
||||
$: currentTheme = $themeStore?.theme
|
||||
$: darkMode = !currentTheme.includes("light")
|
||||
|
||||
const relationshipSupport = datasource => {
|
||||
const integration = $integrations[datasource?.source]
|
||||
return !isInternal && integration?.relationships !== false
|
||||
|
@ -56,6 +59,7 @@
|
|||
<div class="wrapper">
|
||||
<Grid
|
||||
{API}
|
||||
{darkMode}
|
||||
datasource={gridDatasource}
|
||||
canAddRows={!isUsersTable}
|
||||
canDeleteRows={!isUsersTable}
|
||||
|
|
|
@ -9,6 +9,7 @@ const MAX_DEPTH = 1
|
|||
const TYPES_TO_SKIP = [
|
||||
FieldType.FORMULA,
|
||||
FieldType.LONGFORM,
|
||||
FieldType.SIGNATURE_SINGLE,
|
||||
FieldType.ATTACHMENTS,
|
||||
//https://github.com/Budibase/budibase/issues/3030
|
||||
FieldType.INTERNAL,
|
||||
|
@ -55,7 +56,7 @@ export function getBindings({
|
|||
)
|
||||
}
|
||||
const field = Object.values(FIELDS).find(
|
||||
field => field.type === schema.type && field.subtype === schema.subtype
|
||||
field => field.type === schema.type
|
||||
)
|
||||
|
||||
const label = path == null ? column : `${path}.0.${column}`
|
||||
|
|
|
@ -412,6 +412,7 @@
|
|||
FIELDS.FORMULA,
|
||||
FIELDS.JSON,
|
||||
FIELDS.BARCODEQR,
|
||||
FIELDS.SIGNATURE_SINGLE,
|
||||
FIELDS.BIGINT,
|
||||
FIELDS.AUTO,
|
||||
]
|
||||
|
|
|
@ -54,6 +54,10 @@
|
|||
label: "Attachment",
|
||||
value: FieldType.ATTACHMENT_SINGLE,
|
||||
},
|
||||
{
|
||||
label: "Signature",
|
||||
value: FieldType.SIGNATURE_SINGLE,
|
||||
},
|
||||
{
|
||||
label: "Attachment list",
|
||||
value: FieldType.ATTACHMENTS,
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
export let autofocus = false
|
||||
export let jsBindingWrapping = true
|
||||
export let readonly = false
|
||||
export let readonlyLineNumbers = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -240,6 +241,9 @@
|
|||
|
||||
if (readonly) {
|
||||
complete.push(EditorState.readOnly.of(true))
|
||||
if (readonlyLineNumbers) {
|
||||
complete.push(lineNumbers())
|
||||
}
|
||||
} else {
|
||||
complete = [
|
||||
...complete,
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
export let selectedBy = null
|
||||
export let compact = false
|
||||
export let hovering = false
|
||||
export let disabled = false
|
||||
|
||||
const scrollApi = getContext("scroll")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -74,6 +75,7 @@
|
|||
class:scrollable
|
||||
class:highlighted
|
||||
class:selectedBy
|
||||
class:disabled
|
||||
on:dragend
|
||||
on:dragstart
|
||||
on:dragover
|
||||
|
@ -165,6 +167,9 @@
|
|||
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||
color: var(--ink);
|
||||
}
|
||||
.nav-item.disabled span {
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
}
|
||||
.nav-item:hover,
|
||||
.hovering {
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
|
|
|
@ -28,6 +28,12 @@
|
|||
let bindingDrawer
|
||||
let currentVal = value
|
||||
|
||||
let attachmentTypes = [
|
||||
FieldType.ATTACHMENT_SINGLE,
|
||||
FieldType.ATTACHMENTS,
|
||||
FieldType.SIGNATURE_SINGLE,
|
||||
]
|
||||
|
||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||
$: tempValue = readableValue
|
||||
$: isJS = isJSBinding(value)
|
||||
|
@ -105,6 +111,7 @@
|
|||
boolean: isValidBoolean,
|
||||
attachment: false,
|
||||
attachment_single: false,
|
||||
signature_single: false,
|
||||
}
|
||||
|
||||
const isValid = value => {
|
||||
|
@ -126,6 +133,7 @@
|
|||
"bigint",
|
||||
"barcodeqr",
|
||||
"attachment",
|
||||
"signature_single",
|
||||
"attachment_single",
|
||||
].includes(type)
|
||||
) {
|
||||
|
@ -169,7 +177,7 @@
|
|||
{updateOnChange}
|
||||
/>
|
||||
{/if}
|
||||
{#if !disabled && type !== "formula" && !disabled && type !== FieldType.ATTACHMENTS && !disabled && type !== FieldType.ATTACHMENT_SINGLE}
|
||||
{#if !disabled && type !== "formula" && !disabled && !attachmentTypes.includes(type)}
|
||||
<div
|
||||
class={`icon ${getIconClass(value, type)}`}
|
||||
on:click={() => {
|
||||
|
|
|
@ -76,6 +76,7 @@ const componentMap = {
|
|||
"field/array": FormFieldSelect,
|
||||
"field/json": FormFieldSelect,
|
||||
"field/barcodeqr": FormFieldSelect,
|
||||
"field/signature_single": FormFieldSelect,
|
||||
"field/bb_reference": FormFieldSelect,
|
||||
// Some validation types are the same as others, so not all types are
|
||||
// explicitly listed here. e.g. options uses string validation
|
||||
|
@ -85,6 +86,8 @@ const componentMap = {
|
|||
"validation/boolean": ValidationEditor,
|
||||
"validation/datetime": ValidationEditor,
|
||||
"validation/attachment": ValidationEditor,
|
||||
"validation/attachment_single": ValidationEditor,
|
||||
"validation/signature_single": ValidationEditor,
|
||||
"validation/link": ValidationEditor,
|
||||
"validation/bb_reference": ValidationEditor,
|
||||
}
|
||||
|
|
|
@ -24,7 +24,9 @@
|
|||
parameters
|
||||
}
|
||||
$: automations = $automationStore.automations
|
||||
.filter(a => a.definition.trigger?.stepId === TriggerStepID.APP)
|
||||
.filter(
|
||||
a => a.definition.trigger?.stepId === TriggerStepID.APP && !a.disabled
|
||||
)
|
||||
.map(automation => {
|
||||
const schema = Object.entries(
|
||||
automation.definition.trigger.inputs.fields || {}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
||||
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { runtimeToReadableBinding } from "dataBinding"
|
||||
import { isJSBinding } from "@budibase/string-templates"
|
||||
|
|
|
@ -100,9 +100,6 @@
|
|||
on:click={() => {
|
||||
get(store).actions.select(draggableItem.id)
|
||||
}}
|
||||
on:mousedown={() => {
|
||||
get(store).actions.select()
|
||||
}}
|
||||
bind:this={anchors[draggableItem.id]}
|
||||
class:highlighted={draggableItem.id === $store.selected}
|
||||
>
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import { componentStore } from "stores/builder"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { createEventDispatcher, getContext } from "svelte"
|
||||
import { customPositionHandler } from "."
|
||||
import ComponentSettingsSection from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
|
||||
|
||||
export let anchor
|
||||
|
@ -18,76 +17,74 @@
|
|||
|
||||
let popover
|
||||
let drawers = []
|
||||
let open = false
|
||||
let isOpen = false
|
||||
|
||||
// Auto hide the component when another item is selected
|
||||
$: if (open && $draggable.selected !== componentInstance._id) {
|
||||
popover.hide()
|
||||
close()
|
||||
}
|
||||
|
||||
// Open automatically if the component is marked as selected
|
||||
$: if (!open && $draggable.selected === componentInstance._id && popover) {
|
||||
popover.show()
|
||||
open = true
|
||||
open()
|
||||
}
|
||||
|
||||
$: componentDef = componentStore.getDefinition(componentInstance._component)
|
||||
$: parsedComponentDef = processComponentDefinitionSettings(componentDef)
|
||||
|
||||
const open = () => {
|
||||
isOpen = true
|
||||
drawers = []
|
||||
$draggable.actions.select(componentInstance._id)
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
// Slight delay allows us to be able to properly toggle open/close state by
|
||||
// clicking again on the settings icon
|
||||
setTimeout(() => {
|
||||
isOpen = false
|
||||
if ($draggable.selected === componentInstance._id) {
|
||||
$draggable.actions.select()
|
||||
}
|
||||
}, 10)
|
||||
}
|
||||
|
||||
const toggleOpen = () => {
|
||||
if (isOpen) {
|
||||
close()
|
||||
} else {
|
||||
open()
|
||||
}
|
||||
}
|
||||
|
||||
const processComponentDefinitionSettings = componentDef => {
|
||||
if (!componentDef) {
|
||||
return {}
|
||||
}
|
||||
const clone = cloneDeep(componentDef)
|
||||
|
||||
if (typeof parseSettings === "function") {
|
||||
clone.settings = parseSettings(clone.settings)
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
const updateSetting = async (setting, value) => {
|
||||
const nestedComponentInstance = cloneDeep(componentInstance)
|
||||
|
||||
const patchFn = componentStore.updateComponentSetting(setting.key, value)
|
||||
patchFn(nestedComponentInstance)
|
||||
|
||||
dispatch("change", nestedComponentInstance)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Icon
|
||||
name="Settings"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => {
|
||||
if (!open) {
|
||||
popover.show()
|
||||
open = true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Icon name="Settings" hoverable size="S" on:click={toggleOpen} />
|
||||
|
||||
<Popover
|
||||
bind:this={popover}
|
||||
on:open={() => {
|
||||
drawers = []
|
||||
$draggable.actions.select(componentInstance._id)
|
||||
}}
|
||||
on:close={() => {
|
||||
open = false
|
||||
if ($draggable.selected === componentInstance._id) {
|
||||
$draggable.actions.select()
|
||||
}
|
||||
}}
|
||||
open={isOpen}
|
||||
on:close={close}
|
||||
{anchor}
|
||||
align="left-outside"
|
||||
showPopover={drawers.length === 0}
|
||||
clickOutsideOverride={drawers.length > 0}
|
||||
maxHeight={600}
|
||||
offset={18}
|
||||
handlePostionUpdate={customPositionHandler}
|
||||
>
|
||||
<span class="popover-wrap">
|
||||
<Layout noPadding noGap>
|
|
@ -1,18 +0,0 @@
|
|||
export const customPositionHandler = (anchorBounds, eleBounds, cfg) => {
|
||||
let { left, top, offset } = cfg
|
||||
let percentageOffset = 30
|
||||
// left-outside
|
||||
left = anchorBounds.left - eleBounds.width - (offset || 5)
|
||||
|
||||
// shift up from the anchor, if space allows
|
||||
let offsetPos = Math.floor(eleBounds.height / 100) * percentageOffset
|
||||
let defaultTop = anchorBounds.top - offsetPos
|
||||
|
||||
if (window.innerHeight - defaultTop < eleBounds.height) {
|
||||
top = window.innerHeight - eleBounds.height - 5
|
||||
} else {
|
||||
top = anchorBounds.top - offsetPos
|
||||
}
|
||||
|
||||
return { ...cfg, left, top }
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<script>
|
||||
import { ContextTooltip } from "@budibase/bbui"
|
||||
import {
|
||||
StringsAsDates,
|
||||
NumbersAsDates,
|
||||
ScalarJsonOnly,
|
||||
Column,
|
||||
Support,
|
||||
NotRequired,
|
||||
StringsAsNumbers,
|
||||
DatesAsNumbers,
|
||||
} from "./subjects"
|
||||
import subjects from "../subjects"
|
||||
|
||||
export let anchor
|
||||
export let schema
|
||||
export let columnName
|
||||
export let subject = subjects.none
|
||||
</script>
|
||||
|
||||
<ContextTooltip visible={subject !== subjects.none} {anchor} offset={20}>
|
||||
<div class="explanationModalContent">
|
||||
{#if subject === subjects.column}
|
||||
<Column {columnName} {schema} />
|
||||
{:else if subject === subjects.support}
|
||||
<Support />
|
||||
{:else if subject === subjects.stringsAsNumbers}
|
||||
<StringsAsNumbers />
|
||||
{:else if subject === subjects.notRequired}
|
||||
<NotRequired />
|
||||
{:else if subject === subjects.datesAsNumbers}
|
||||
<DatesAsNumbers />
|
||||
{:else if subject === subjects.scalarJsonOnly}
|
||||
<ScalarJsonOnly {columnName} {schema} />
|
||||
{:else if subject === subjects.numbersAsDates}
|
||||
<NumbersAsDates {columnName} />
|
||||
{:else if subject === subjects.stringsAsDates}
|
||||
<StringsAsDates {columnName} />
|
||||
{/if}
|
||||
</div>
|
||||
</ContextTooltip>
|
||||
|
||||
<style>
|
||||
.explanationModalContent {
|
||||
max-width: 300px;
|
||||
padding: 16px 12px 2px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,147 @@
|
|||
<script>
|
||||
import { tables } from "stores/builder"
|
||||
import {
|
||||
BindingValue,
|
||||
Block,
|
||||
Subject,
|
||||
JSONValue,
|
||||
Property,
|
||||
Section,
|
||||
} from "./components"
|
||||
|
||||
export let schema
|
||||
export let columnName
|
||||
|
||||
const parseDate = isoString => {
|
||||
if ([null, undefined, ""].includes(isoString)) {
|
||||
return "None"
|
||||
}
|
||||
|
||||
const unixTime = Date.parse(isoString)
|
||||
const date = new Date(unixTime)
|
||||
|
||||
return date.toLocaleString()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Subject>
|
||||
<div class="heading" slot="heading">
|
||||
Column Overview for <Block>{columnName}</Block>
|
||||
</div>
|
||||
<Section>
|
||||
{#if schema.type === "string"}
|
||||
<Property
|
||||
name="Max Length"
|
||||
value={schema?.constraints?.length?.maximum ?? "None"}
|
||||
/>
|
||||
{:else if schema.type === "datetime"}
|
||||
<Property
|
||||
name="Earliest"
|
||||
value={parseDate(schema?.constraints?.datetime?.earliest)}
|
||||
/>
|
||||
<Property
|
||||
name="Latest"
|
||||
value={parseDate(schema?.constraints?.datetime?.latest)}
|
||||
/>
|
||||
<Property
|
||||
name="Ignore time zones"
|
||||
value={schema?.ignoreTimeZones === true ? "Yes" : "No"}
|
||||
/>
|
||||
<Property
|
||||
name="Date only"
|
||||
value={schema?.dateOnly === true ? "Yes" : "No"}
|
||||
/>
|
||||
{:else if schema.type === "number"}
|
||||
<Property
|
||||
name="Min Value"
|
||||
value={[null, undefined, ""].includes(
|
||||
schema?.constraints?.numericality?.greaterThanOrEqualTo
|
||||
)
|
||||
? "None"
|
||||
: schema?.constraints?.numericality?.greaterThanOrEqualTo}
|
||||
/>
|
||||
<Property
|
||||
name="Max Value"
|
||||
value={[null, undefined, ""].includes(
|
||||
schema?.constraints?.numericality?.lessThanOrEqualTo
|
||||
)
|
||||
? "None"
|
||||
: schema?.constraints?.numericality?.lessThanOrEqualTo}
|
||||
/>
|
||||
{:else if schema.type === "array"}
|
||||
{#each schema?.constraints?.inclusion ?? [] as option, index}
|
||||
<Property name={`Option ${index + 1}`} truncate>
|
||||
<span
|
||||
style:background-color={schema?.optionColors?.[option]}
|
||||
class="optionCircle"
|
||||
/>{option}
|
||||
</Property>
|
||||
{/each}
|
||||
{:else if schema.type === "options"}
|
||||
{#each schema?.constraints?.inclusion ?? [] as option, index}
|
||||
<Property name={`Option ${index + 1}`} truncate>
|
||||
<span
|
||||
style:background-color={schema?.optionColors?.[option]}
|
||||
class="optionCircle"
|
||||
/>{option}
|
||||
</Property>
|
||||
{/each}
|
||||
{:else if schema.type === "json"}
|
||||
<Property name="Schema">
|
||||
<JSONValue value={JSON.stringify(schema?.schema ?? {}, null, 2)} />
|
||||
</Property>
|
||||
{:else if schema.type === "formula"}
|
||||
<Property name="Formula">
|
||||
<BindingValue value={schema?.formula} />
|
||||
</Property>
|
||||
<Property
|
||||
name="Formula type"
|
||||
value={schema?.formulaType === "dynamic" ? "Dynamic" : "Static"}
|
||||
/>
|
||||
{:else if schema.type === "link"}
|
||||
<Property name="Type" value={schema?.relationshipType} />
|
||||
<Property
|
||||
name="Related Table"
|
||||
value={$tables?.list?.find(table => table._id === schema?.tableId)
|
||||
?.name}
|
||||
/>
|
||||
<Property name="Column in Related Table" value={schema?.fieldName} />
|
||||
{:else if schema.type === "bb_reference"}
|
||||
<Property
|
||||
name="Allow multiple users"
|
||||
value={schema?.relationshipType === "many-to-many" ? "Yes" : "No"}
|
||||
/>
|
||||
{/if}
|
||||
<Property
|
||||
name="Required"
|
||||
value={schema?.constraints?.presence?.allowEmpty === false ? "Yes" : "No"}
|
||||
/>
|
||||
</Section>
|
||||
</Subject>
|
||||
|
||||
<style>
|
||||
.heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.heading > :global(.block) {
|
||||
margin-left: 4px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.optionCircle {
|
||||
display: inline-block;
|
||||
background-color: hsla(0, 1%, 50%, 0.3);
|
||||
border-radius: 100%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,63 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import {
|
||||
ExampleSection,
|
||||
ExampleLine,
|
||||
Block,
|
||||
Subject,
|
||||
Section,
|
||||
} from "./components"
|
||||
|
||||
let timestamp = Date.now()
|
||||
|
||||
onMount(() => {
|
||||
let run = true
|
||||
|
||||
const updateTimeStamp = () => {
|
||||
timestamp = Date.now()
|
||||
if (run) {
|
||||
setTimeout(updateTimeStamp, 200)
|
||||
}
|
||||
}
|
||||
|
||||
updateTimeStamp()
|
||||
|
||||
return () => {
|
||||
run = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Subject heading="Dates as Numbers">
|
||||
<Section>
|
||||
A datetime value can be used in place of a numeric value, but it will be
|
||||
converted to a <Block>UNIX time</Block> timestamp, which is the number of milliseconds
|
||||
since Jan 1st 1970. A more recent moment in time will be a higher number.
|
||||
</Section>
|
||||
|
||||
<ExampleSection heading="Examples:">
|
||||
<ExampleLine>
|
||||
<Block>
|
||||
{new Date(946684800000).toLocaleString()}
|
||||
</Block>
|
||||
<span class="separator">{"->"} </span><Block>946684800000</Block>
|
||||
</ExampleLine>
|
||||
<ExampleLine>
|
||||
<Block>
|
||||
{new Date(1577836800000).toLocaleString()}
|
||||
</Block>
|
||||
<span class="separator">{"->"} </span><Block>1577836800000</Block>
|
||||
</ExampleLine>
|
||||
<ExampleLine>
|
||||
<Block>Now</Block><span class="separator">{"->"} </span><Block
|
||||
>{timestamp}</Block
|
||||
>
|
||||
</ExampleLine>
|
||||
</ExampleSection>
|
||||
</Subject>
|
||||
|
||||
<style>
|
||||
.separator {
|
||||
margin: 0 5px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,11 @@
|
|||
<script>
|
||||
import { Block, Subject, Section } from "./components"
|
||||
</script>
|
||||
|
||||
<Subject heading="Required Constraint">
|
||||
<Section>
|
||||
A <Block>required</Block> constraint can be applied to columns to ensure a value
|
||||
is always present. If a column doesn't have this constraint, then its value for
|
||||
a particular row could he missing.
|
||||
</Section>
|
||||
</Subject>
|
|
@ -0,0 +1,65 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import {
|
||||
ExampleSection,
|
||||
ExampleLine,
|
||||
Block,
|
||||
Subject,
|
||||
Section,
|
||||
} from "./components"
|
||||
|
||||
let timestamp = Date.now()
|
||||
|
||||
onMount(() => {
|
||||
let run = true
|
||||
|
||||
const updateTimeStamp = () => {
|
||||
timestamp = Date.now()
|
||||
if (run) {
|
||||
setTimeout(updateTimeStamp, 200)
|
||||
}
|
||||
}
|
||||
|
||||
updateTimeStamp()
|
||||
|
||||
return () => {
|
||||
run = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Subject heading="Numbers as Dates">
|
||||
<Section>
|
||||
A number value can be used in place of a datetime value, but it will be
|
||||
parsed as a <Block>UNIX time</Block> timestamp, which is the number of milliseconds
|
||||
since Jan 1st 1970. A more recent moment in time will be a higher number.
|
||||
</Section>
|
||||
|
||||
<ExampleSection heading="Examples:">
|
||||
<ExampleLine>
|
||||
<Block>946684800000</Block>
|
||||
<span class="separator">{"->"}</span>
|
||||
<Block>
|
||||
{new Date(946684800000).toLocaleString()}
|
||||
</Block>
|
||||
</ExampleLine>
|
||||
<ExampleLine>
|
||||
<Block>1577836800000</Block>
|
||||
<span class="separator">{"->"}</span>
|
||||
<Block>
|
||||
{new Date(1577836800000).toLocaleString()}
|
||||
</Block>
|
||||
</ExampleLine>
|
||||
<ExampleLine>
|
||||
<Block>{timestamp}</Block>
|
||||
<span class="separator">{"->"}</span>
|
||||
<Block>Now</Block>
|
||||
</ExampleLine>
|
||||
</ExampleSection>
|
||||
</Subject>
|
||||
|
||||
<style>
|
||||
.separator {
|
||||
margin: 0 5px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,71 @@
|
|||
<script>
|
||||
import {
|
||||
ExampleSection,
|
||||
ExampleLine,
|
||||
Block,
|
||||
Subject,
|
||||
Section,
|
||||
} from "./components"
|
||||
|
||||
export let schema
|
||||
export let columnName
|
||||
|
||||
const maxScalarDescendantsToFind = 3
|
||||
|
||||
const getScalarDescendants = schema => {
|
||||
const newScalarDescendants = []
|
||||
|
||||
const getScalarDescendantFromSchema = (path, schema) => {
|
||||
if (newScalarDescendants.length >= maxScalarDescendantsToFind) {
|
||||
return
|
||||
}
|
||||
|
||||
if (["string", "number", "boolean"].includes(schema.type)) {
|
||||
newScalarDescendants.push({ name: path.join("."), type: schema.type })
|
||||
} else if (schema.type === "json") {
|
||||
Object.entries(schema.schema ?? {}).forEach(
|
||||
([childName, childSchema]) =>
|
||||
getScalarDescendantFromSchema([...path, childName], childSchema)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Object.entries(schema?.schema ?? {}).forEach(([childName, childSchema]) =>
|
||||
getScalarDescendantFromSchema([columnName, childName], childSchema)
|
||||
)
|
||||
|
||||
return newScalarDescendants
|
||||
}
|
||||
|
||||
$: scalarDescendants = getScalarDescendants(schema)
|
||||
</script>
|
||||
|
||||
<Subject heading="Using Scalar JSON Values">
|
||||
<Section>
|
||||
<Block>JSON objects</Block> can't be used here, but any <Block>number</Block
|
||||
>, <Block>string</Block> or <Block>boolean</Block> values nested within said
|
||||
object can be if they are otherwise compatible with the input. These scalar values
|
||||
can be selected from the same menu as this parent and take the form <Block
|
||||
>parent.child</Block
|
||||
>.
|
||||
</Section>
|
||||
|
||||
{#if scalarDescendants.length > 0}
|
||||
<ExampleSection heading="Examples scalar descendants of this object:">
|
||||
{#each scalarDescendants as descendant}
|
||||
<ExampleLine>
|
||||
<Block truncate>{descendant.name}</Block><span class="separator"
|
||||
>-</span
|
||||
><Block truncate noShrink>{descendant.type}</Block>
|
||||
</ExampleLine>
|
||||
{/each}
|
||||
</ExampleSection>
|
||||
{/if}
|
||||
</Subject>
|
||||
|
||||
<style>
|
||||
.separator {
|
||||
margin: 0 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,107 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import {
|
||||
ExampleSection,
|
||||
ExampleLine,
|
||||
Block,
|
||||
Subject,
|
||||
Section,
|
||||
} from "./components"
|
||||
|
||||
let timestamp = Date.now()
|
||||
$: iso = new Date(timestamp).toISOString()
|
||||
|
||||
onMount(() => {
|
||||
let run = true
|
||||
|
||||
const updateTimeStamp = () => {
|
||||
timestamp = Date.now()
|
||||
if (run) {
|
||||
setTimeout(updateTimeStamp, 200)
|
||||
}
|
||||
}
|
||||
|
||||
updateTimeStamp()
|
||||
|
||||
return () => {
|
||||
run = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Subject heading="Strings as Dates">
|
||||
<Section>
|
||||
A string value can be used in place of a datetime value, but it will be
|
||||
parsed as:
|
||||
</Section>
|
||||
<Section>
|
||||
A <Block>UNIX time</Block> timestamp, which is the number of milliseconds since
|
||||
Jan 1st 1970. A more recent moment in time will be a higher number.
|
||||
</Section>
|
||||
|
||||
<ExampleSection heading="Examples:">
|
||||
<ExampleLine>
|
||||
<Block>946684800000</Block>
|
||||
<span class="separator">{"->"}</span>
|
||||
<Block>
|
||||
{new Date(946684800000).toLocaleString()}
|
||||
</Block>
|
||||
</ExampleLine>
|
||||
<ExampleLine>
|
||||
<Block>1577836800000</Block>
|
||||
<span class="separator">{"->"}</span>
|
||||
<Block>
|
||||
{new Date(1577836800000).toLocaleString()}
|
||||
</Block>
|
||||
</ExampleLine>
|
||||
<ExampleLine>
|
||||
<Block>{timestamp}</Block>
|
||||
<span class="separator">{"->"}</span>
|
||||
<Block>Now</Block>
|
||||
</ExampleLine>
|
||||
</ExampleSection>
|
||||
<Section>
|
||||
An <Block>ISO 8601</Block> datetime string, which represents an exact moment
|
||||
in time as well as the potentional to store the timezone it occured in.
|
||||
</Section>
|
||||
<div class="isoExamples">
|
||||
<ExampleSection heading="Examples:">
|
||||
<ExampleLine>
|
||||
<Block>2000-01-01T00:00:00.000Z</Block>
|
||||
<span class="separator">↓</span>
|
||||
<Block>
|
||||
{new Date(946684800000).toLocaleString()}
|
||||
</Block>
|
||||
</ExampleLine>
|
||||
<ExampleLine>
|
||||
<Block>2000-01-01T00:00:00.000Z</Block>
|
||||
<span class="separator">↓</span>
|
||||
<Block>
|
||||
{new Date(1577836800000).toLocaleString()}
|
||||
</Block>
|
||||
</ExampleLine>
|
||||
<ExampleLine>
|
||||
<Block>{iso}</Block>
|
||||
<span class="separator">↓</span>
|
||||
<Block>Now</Block>
|
||||
</ExampleLine>
|
||||
</ExampleSection>
|
||||
</div>
|
||||
</Subject>
|
||||
|
||||
<style>
|
||||
.separator {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.isoExamples :global(.block) {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.isoExamples :global(.exampleLine) {
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin-bottom: 16px;
|
||||
width: 162px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,56 @@
|
|||
<script>
|
||||
import {
|
||||
ExampleSection,
|
||||
ExampleLine,
|
||||
Block,
|
||||
Subject,
|
||||
Section,
|
||||
} from "./components"
|
||||
</script>
|
||||
|
||||
<Subject heading="Text as Numbers">
|
||||
<Section>
|
||||
Text can be used in place of numbers in certain scenarios, but care needs to
|
||||
be taken; if the value isn't purely numerical it may be converted in an
|
||||
unexpected way.
|
||||
</Section>
|
||||
|
||||
<ExampleSection heading="Examples:">
|
||||
<ExampleLine>
|
||||
<Block>"100"</Block><span class="separator">{"->"}</span><Block>100</Block
|
||||
>
|
||||
</ExampleLine>
|
||||
<ExampleLine>
|
||||
<Block>"100k"</Block><span class="separator">{"->"}</span><Block
|
||||
>100</Block
|
||||
>
|
||||
</ExampleLine>
|
||||
<ExampleLine>
|
||||
<Block>"100,000"</Block><span class="separator">{"->"}</span><Block
|
||||
>100</Block
|
||||
>
|
||||
</ExampleLine>
|
||||
<ExampleLine>
|
||||
<Block>"100 million"</Block><span class="separator">{"->"}</span><Block
|
||||
>100</Block
|
||||
>
|
||||
</ExampleLine>
|
||||
<ExampleLine>
|
||||
<Block>"100.9"</Block><span class="separator">{"->"}</span><Block
|
||||
>100.9</Block
|
||||
>
|
||||
</ExampleLine>
|
||||
<ExampleLine>
|
||||
<Block>"One hundred"</Block><span class="separator">{"->"}</span><Block
|
||||
>Error</Block
|
||||
>
|
||||
</ExampleLine>
|
||||
</ExampleSection>
|
||||
</Subject>
|
||||
|
||||
<style>
|
||||
.separator {
|
||||
margin: 0 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,35 @@
|
|||
<script>
|
||||
import { InfoWord } from "../../typography"
|
||||
import { Subject, Section } from "./components"
|
||||
</script>
|
||||
|
||||
<Subject heading="Data/Component Compatibility">
|
||||
<Section>
|
||||
<InfoWord icon="CheckmarkCircle" color="var(--green)" text="Compatible" />
|
||||
<span class="body"
|
||||
>Fully compatible with the input as long as the data is present.</span
|
||||
>
|
||||
</Section>
|
||||
<Section>
|
||||
<InfoWord
|
||||
icon="AlertCheck"
|
||||
color="var(--yellow)"
|
||||
text="Partially compatible"
|
||||
/>
|
||||
<span class="body"
|
||||
>Partially compatible with the input, but beware of other caveats
|
||||
mentioned.</span
|
||||
>
|
||||
</Section>
|
||||
<Section>
|
||||
<InfoWord icon="Alert" color="var(--red)" text="Not compatible" />
|
||||
<span class="body">Incompatible with the component.</span>
|
||||
</Section>
|
||||
</Subject>
|
||||
|
||||
<style>
|
||||
.body {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,39 @@
|
|||
<script>
|
||||
import { decodeJSBinding } from "@budibase/string-templates"
|
||||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||
import { EditorModes } from "components/common/CodeEditor"
|
||||
import {
|
||||
runtimeToReadableBinding,
|
||||
getDatasourceForProvider,
|
||||
} from "dataBinding"
|
||||
import { tables, selectedScreen, selectedComponent } from "stores/builder"
|
||||
import { getBindings } from "components/backend/DataTable/formula"
|
||||
|
||||
export let value
|
||||
$: datasource = getDatasourceForProvider($selectedScreen, $selectedComponent)
|
||||
$: tableId = datasource.tableId
|
||||
$: table = $tables?.list?.find(table => table._id === tableId)
|
||||
$: bindings = getBindings({ table })
|
||||
|
||||
$: readableBinding = runtimeToReadableBinding(bindings, value)
|
||||
|
||||
$: isJs = value?.startsWith?.("{{ js ")
|
||||
</script>
|
||||
|
||||
<div class="editor">
|
||||
<CodeEditor
|
||||
readonly
|
||||
readonlyLineNumbers
|
||||
value={isJs ? decodeJSBinding(readableBinding) : readableBinding}
|
||||
jsBindingWrapping={isJs}
|
||||
mode={isJs ? EditorModes.JS : EditorModes.Handlebars}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.editor {
|
||||
border: 1px solid var(--grey-2);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,30 @@
|
|||
<script>
|
||||
export let truncate = false
|
||||
export let noShrink = false
|
||||
</script>
|
||||
|
||||
<span class:truncate class:noShrink class="block">
|
||||
<slot />
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.block {
|
||||
font-style: italic;
|
||||
border-radius: 1px;
|
||||
padding: 0px 5px 0px 3px;
|
||||
border-radius: 1px;
|
||||
background-color: var(--grey-3);
|
||||
color: var(--ink);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.truncate {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.noShrink {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,12 @@
|
|||
<li>
|
||||
<div class="exampleLine">
|
||||
<slot />
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<style>
|
||||
.exampleLine {
|
||||
display: flex;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,32 @@
|
|||
<script>
|
||||
import Section from "./Section.svelte"
|
||||
|
||||
export let heading
|
||||
</script>
|
||||
|
||||
<Section>
|
||||
<span class="exampleSectionHeading">
|
||||
<slot name="heading">
|
||||
{heading}
|
||||
</slot>
|
||||
</span>
|
||||
<ul>
|
||||
<slot />
|
||||
</ul>
|
||||
</Section>
|
||||
|
||||
<style>
|
||||
.exampleSectionHeading {
|
||||
display: inline-block;
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0 0 0 23px;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
export let value
|
||||
</script>
|
||||
|
||||
<pre class="pre">
|
||||
{value}
|
||||
</pre>
|
||||
|
||||
<style>
|
||||
.pre {
|
||||
border: 1px solid var(--grey-2);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
margin-top: 3px;
|
||||
padding: 4px;
|
||||
border-radius: 3px;
|
||||
width: 250px;
|
||||
box-sizing: border-box;
|
||||
background-color: black;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,49 @@
|
|||
<script>
|
||||
export let name
|
||||
export let value
|
||||
export let truncate = false
|
||||
</script>
|
||||
|
||||
<div class:truncate class="property">
|
||||
<span class="propertyName">
|
||||
<slot name="name">
|
||||
{name}
|
||||
</slot>
|
||||
</span>
|
||||
<span class="propertyDivider">-</span>
|
||||
<span class="propertyValue">
|
||||
<slot>
|
||||
{value}
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.property {
|
||||
max-width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.truncate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.propertyName {
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.propertyDivider {
|
||||
padding: 0 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.propertyValue {
|
||||
word-break: break-word;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,11 @@
|
|||
<div class="section">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.section {
|
||||
line-height: 20px;
|
||||
margin-bottom: 13px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,51 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
|
||||
export let heading = ""
|
||||
let body
|
||||
|
||||
const handleScroll = e => {
|
||||
if (!body) return
|
||||
|
||||
body.scrollTo({ top: body.scrollTop + e.deltaY, behavior: "smooth" })
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("wheel", handleScroll)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("wheel", handleScroll)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="heading">
|
||||
<span class="heading">
|
||||
<slot name="heading">
|
||||
{heading}
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
<div class="divider" />
|
||||
<div bind:this={body} class="body">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.heading {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-bottom: 1px solid var(--grey-4);
|
||||
margin: 12px 0 12px;
|
||||
}
|
||||
|
||||
.body {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,8 @@
|
|||
export { default as Subject } from "./Subject.svelte"
|
||||
export { default as Property } from "./Property.svelte"
|
||||
export { default as JSONValue } from "./JSONValue.svelte"
|
||||
export { default as BindingValue } from "./BindingValue.svelte"
|
||||
export { default as Section } from "./Section.svelte"
|
||||
export { default as Block } from "./Block.svelte"
|
||||
export { default as ExampleSection } from "./ExampleSection.svelte"
|
||||
export { default as ExampleLine } from "./ExampleLine.svelte"
|
|
@ -0,0 +1,8 @@
|
|||
export { default as Column } from "./Column.svelte"
|
||||
export { default as NotRequired } from "./NotRequired.svelte"
|
||||
export { default as StringsAsNumbers } from "./StringsAsNumbers.svelte"
|
||||
export { default as Support } from "./Support.svelte"
|
||||
export { default as DatesAsNumbers } from "./DatesAsNumbers.svelte"
|
||||
export { default as ScalarJsonOnly } from "./ScalarJsonOnly.svelte"
|
||||
export { default as StringsAsDates } from "./StringsAsDates.svelte"
|
||||
export { default as NumbersAsDates } from "./NumbersAsDates.svelte"
|
|
@ -0,0 +1,102 @@
|
|||
<script>
|
||||
import DetailsModal from "./DetailsModal/index.svelte"
|
||||
import {
|
||||
messages as messageConstants,
|
||||
getExplanationMessagesAndSupport,
|
||||
getExplanationWithPresets,
|
||||
} from "./explanation"
|
||||
import {
|
||||
StringAsDate,
|
||||
NumberAsDate,
|
||||
Column,
|
||||
Support,
|
||||
NotRequired,
|
||||
StringAsNumber,
|
||||
JSONPrimitivesOnly,
|
||||
DateAsNumber,
|
||||
} from "./lines"
|
||||
import subjects from "./subjects"
|
||||
import { appStore } from "stores/builder"
|
||||
|
||||
export let explanation
|
||||
export let columnIcon
|
||||
export let columnType
|
||||
export let columnName
|
||||
|
||||
export let tableHref = () => {}
|
||||
|
||||
export let schema
|
||||
|
||||
$: explanationWithPresets = getExplanationWithPresets(
|
||||
explanation,
|
||||
$appStore.typeSupportPresets
|
||||
)
|
||||
let support
|
||||
let messages = []
|
||||
|
||||
$: {
|
||||
const explanationMessagesAndSupport = getExplanationMessagesAndSupport(
|
||||
schema,
|
||||
explanationWithPresets
|
||||
)
|
||||
support = explanationMessagesAndSupport.support
|
||||
messages = explanationMessagesAndSupport.messages
|
||||
}
|
||||
|
||||
let root = null
|
||||
|
||||
let detailsModalSubject = subjects.none
|
||||
|
||||
const setExplanationSubject = option => {
|
||||
detailsModalSubject = option
|
||||
root = root
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={root} class="tooltipContents">
|
||||
<Column
|
||||
{columnName}
|
||||
{columnIcon}
|
||||
{columnType}
|
||||
{tableHref}
|
||||
{setExplanationSubject}
|
||||
/>
|
||||
<Support {support} {setExplanationSubject} />
|
||||
{#if messages.includes(messageConstants.stringAsNumber)}
|
||||
<StringAsNumber {setExplanationSubject} />
|
||||
{/if}
|
||||
{#if messages.includes(messageConstants.notRequired)}
|
||||
<NotRequired {setExplanationSubject} />
|
||||
{/if}
|
||||
{#if messages.includes(messageConstants.jsonPrimitivesOnly)}
|
||||
<JSONPrimitivesOnly {setExplanationSubject} />
|
||||
{/if}
|
||||
{#if messages.includes(messageConstants.dateAsNumber)}
|
||||
<DateAsNumber {setExplanationSubject} />
|
||||
{/if}
|
||||
{#if messages.includes(messageConstants.numberAsDate)}
|
||||
<NumberAsDate {setExplanationSubject} />
|
||||
{/if}
|
||||
{#if messages.includes(messageConstants.stringAsDate)}
|
||||
<StringAsDate {setExplanationSubject} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if detailsModalSubject !== subjects.none}
|
||||
<DetailsModal
|
||||
{columnName}
|
||||
anchor={root}
|
||||
{schema}
|
||||
subject={detailsModalSubject}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.tooltipContents {
|
||||
max-width: 450px;
|
||||
display: block;
|
||||
padding: 20px 16px 10px;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,100 @@
|
|||
export const messages = {
|
||||
jsonPrimitivesOnly: Symbol("explanation-json-primitives-only"),
|
||||
stringAsNumber: Symbol("explanation-string-as-number"),
|
||||
dateAsNumber: Symbol("explanation-date-as-number"),
|
||||
numberAsDate: Symbol("explanation-number-as-date"),
|
||||
stringAsDate: Symbol("explanation-string-as-date"),
|
||||
notRequired: Symbol("explanation-not-required"),
|
||||
contextError: Symbol("explanation-context-error"),
|
||||
}
|
||||
|
||||
export const support = {
|
||||
unsupported: Symbol("explanation-unsupported"),
|
||||
partialSupport: Symbol("explanation-partialSupport"),
|
||||
supported: Symbol("explanation-supported"),
|
||||
}
|
||||
|
||||
const getSupport = (type, explanation) => {
|
||||
if (!explanation?.typeSupport) {
|
||||
return support.supported
|
||||
}
|
||||
|
||||
if (
|
||||
explanation?.typeSupport?.supported?.find(
|
||||
mapping => mapping === type || mapping?.type === type
|
||||
)
|
||||
) {
|
||||
return support.supported
|
||||
}
|
||||
|
||||
if (
|
||||
explanation?.typeSupport?.partialSupport?.find(
|
||||
mapping => mapping === type || mapping?.type === type
|
||||
)
|
||||
) {
|
||||
return support.partialSupport
|
||||
}
|
||||
|
||||
return support.unsupported
|
||||
}
|
||||
|
||||
const getSupportMessage = (type, explanation) => {
|
||||
if (!explanation?.typeSupport) {
|
||||
return null
|
||||
}
|
||||
|
||||
const supported = explanation?.typeSupport?.supported?.find(
|
||||
mapping => mapping?.type === type
|
||||
)
|
||||
if (supported) {
|
||||
return messages[supported?.message]
|
||||
}
|
||||
|
||||
const partialSupport = explanation?.typeSupport?.partialSupport?.find(
|
||||
mapping => mapping?.type === type
|
||||
)
|
||||
if (partialSupport) {
|
||||
return messages[partialSupport?.message]
|
||||
}
|
||||
|
||||
const unsupported = explanation?.typeSupport?.unsupported?.find(
|
||||
mapping => mapping?.type === type
|
||||
)
|
||||
if (unsupported) {
|
||||
return messages[unsupported?.message]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const getExplanationMessagesAndSupport = (fieldSchema, explanation) => {
|
||||
try {
|
||||
const explanationMessagesAndSupport = {
|
||||
support: getSupport(fieldSchema.type, explanation),
|
||||
messages: [getSupportMessage(fieldSchema.type, explanation)],
|
||||
}
|
||||
|
||||
const isRequired = fieldSchema?.constraints?.presence?.allowEmpty === false
|
||||
if (!isRequired) {
|
||||
explanationMessagesAndSupport.messages.push(messages.notRequired)
|
||||
}
|
||||
|
||||
return explanationMessagesAndSupport
|
||||
} catch (e) {
|
||||
return {
|
||||
support: support.partialSupport,
|
||||
messages: [messages.contextError],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getExplanationWithPresets = (explanation, presets) => {
|
||||
if (explanation?.typeSupport?.preset) {
|
||||
return {
|
||||
...explanation,
|
||||
typeSupport: presets[explanation?.typeSupport?.preset],
|
||||
}
|
||||
}
|
||||
|
||||
return explanation
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { default as Explanation } from "./Explanation.svelte"
|
|
@ -0,0 +1,84 @@
|
|||
<script>
|
||||
import {
|
||||
Line,
|
||||
InfoWord,
|
||||
DocumentationLink,
|
||||
Text,
|
||||
Period,
|
||||
} from "../typography"
|
||||
import subjects from "../subjects"
|
||||
|
||||
export let columnName
|
||||
export let columnIcon
|
||||
export let columnType
|
||||
export let tableHref
|
||||
export let setExplanationSubject
|
||||
|
||||
const getDocLink = columnType => {
|
||||
if (columnType === "Number") {
|
||||
return "https://docs.budibase.com/docs/number"
|
||||
}
|
||||
if (columnType === "Text") {
|
||||
return "https://docs.budibase.com/docs/text"
|
||||
}
|
||||
if (columnType === "Attachment") {
|
||||
return "https://docs.budibase.com/docs/attachments"
|
||||
}
|
||||
if (columnType === "Multi-select") {
|
||||
return "https://docs.budibase.com/docs/multi-select"
|
||||
}
|
||||
if (columnType === "JSON") {
|
||||
return "https://docs.budibase.com/docs/json"
|
||||
}
|
||||
if (columnType === "Date/Time") {
|
||||
return "https://docs.budibase.com/docs/datetime"
|
||||
}
|
||||
if (columnType === "User") {
|
||||
return "https://docs.budibase.com/docs/user"
|
||||
}
|
||||
if (columnType === "QR") {
|
||||
return "https://docs.budibase.com/docs/barcodeqr"
|
||||
}
|
||||
if (columnType === "Relationship") {
|
||||
return "https://docs.budibase.com/docs/relationships"
|
||||
}
|
||||
if (columnType === "Formula") {
|
||||
return "https://docs.budibase.com/docs/formula"
|
||||
}
|
||||
if (columnType === "Options") {
|
||||
return "https://docs.budibase.com/docs/options"
|
||||
}
|
||||
if (columnType === "BigInt") {
|
||||
// No BigInt docs
|
||||
return null
|
||||
}
|
||||
if (columnType === "Boolean") {
|
||||
return "https://docs.budibase.com/docs/boolean-truefalse"
|
||||
}
|
||||
if (columnType === "Signature") {
|
||||
// No Signature docs
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
$: docLink = getDocLink(columnType)
|
||||
</script>
|
||||
|
||||
<Line noWrap>
|
||||
<InfoWord
|
||||
on:mouseenter={() => setExplanationSubject(subjects.column)}
|
||||
on:mouseleave={() => setExplanationSubject(subjects.none)}
|
||||
href={tableHref}
|
||||
text={columnName}
|
||||
/>
|
||||
<Text value=" is a " />
|
||||
<DocumentationLink
|
||||
disabled={docLink === null}
|
||||
href={docLink}
|
||||
icon={columnIcon}
|
||||
text={`${columnType} column`}
|
||||
/>
|
||||
<Period />
|
||||
</Line>
|
|
@ -0,0 +1,16 @@
|
|||
<script>
|
||||
import { Line, InfoWord, Text, Period } from "../typography"
|
||||
import subjects from "../subjects"
|
||||
|
||||
export let setExplanationSubject
|
||||
</script>
|
||||
|
||||
<Line>
|
||||
<Text value="Will be converted to a " />
|
||||
<InfoWord
|
||||
on:mouseenter={() => setExplanationSubject(subjects.datesAsNumbers)}
|
||||
on:mouseleave={() => setExplanationSubject(subjects.none)}
|
||||
text="UNIX time value"
|
||||
/>
|
||||
<Period />
|
||||
</Line>
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
import { Line, InfoWord, Text, Period } from "../typography"
|
||||
import subjects from "../subjects"
|
||||
|
||||
export let setExplanationSubject
|
||||
</script>
|
||||
|
||||
<Line>
|
||||
<InfoWord
|
||||
on:mouseenter={() => setExplanationSubject(subjects.scalarJsonOnly)}
|
||||
on:mouseleave={() => setExplanationSubject(subjects.none)}
|
||||
>Scalar JSON values</InfoWord
|
||||
>
|
||||
<Text
|
||||
value=" can be used with this input if their individual types are supported"
|
||||
/>
|
||||
<Period />
|
||||
</Line>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -0,0 +1,25 @@
|
|||
<script>
|
||||
import { Line, InfoWord, DocumentationLink, Space, Text } from "../typography"
|
||||
import subjects from "../subjects"
|
||||
|
||||
export let setExplanationSubject
|
||||
</script>
|
||||
|
||||
<Line>
|
||||
<Text value="No " />
|
||||
<InfoWord
|
||||
on:mouseenter={() => setExplanationSubject(subjects.notRequired)}
|
||||
on:mouseleave={() => setExplanationSubject(subjects.none)}
|
||||
text="required"
|
||||
/>
|
||||
<Space />
|
||||
<DocumentationLink
|
||||
icon="DataUnavailable"
|
||||
href="https://docs.budibase.com/docs/budibasedb#constraints"
|
||||
text="Constraint"
|
||||
/>
|
||||
<Text value=", so values may be missing." />
|
||||
</Line>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -0,0 +1,16 @@
|
|||
<script>
|
||||
import { Line, InfoWord, Text, Period } from "../typography"
|
||||
import subjects from "../subjects"
|
||||
|
||||
export let setExplanationSubject
|
||||
</script>
|
||||
|
||||
<Line>
|
||||
<Text value="Will be treated as a " />
|
||||
<InfoWord
|
||||
on:mouseenter={() => setExplanationSubject(subjects.numbersAsDates)}
|
||||
on:mouseleave={() => setExplanationSubject(subjects.none)}
|
||||
text="UNIX time value"
|
||||
/>
|
||||
<Period />
|
||||
</Line>
|
|
@ -0,0 +1,16 @@
|
|||
<script>
|
||||
import { Line, InfoWord, Text, Period } from "../typography"
|
||||
import subjects from "../subjects"
|
||||
|
||||
export let setExplanationSubject
|
||||
</script>
|
||||
|
||||
<Line>
|
||||
<Text value="Will be treated as a " />
|
||||
<InfoWord
|
||||
on:mouseenter={() => setExplanationSubject(subjects.stringsAsDates)}
|
||||
on:mouseleave={() => setExplanationSubject(subjects.none)}
|
||||
text="UNIX time or ISO 8601 value"
|
||||
/>
|
||||
<Period />
|
||||
</Line>
|
|
@ -0,0 +1,16 @@
|
|||
<script>
|
||||
import { Line, InfoWord, Text, Period } from "../typography"
|
||||
import subjects from "../subjects"
|
||||
|
||||
export let setExplanationSubject
|
||||
</script>
|
||||
|
||||
<Line>
|
||||
<Text value="Will be converted to a number, ignoring any " />
|
||||
<InfoWord
|
||||
on:mouseenter={() => setExplanationSubject(subjects.stringsAsNumbers)}
|
||||
on:mouseleave={() => setExplanationSubject(subjects.none)}
|
||||
text="non-numerical values"
|
||||
/>
|
||||
<Period />
|
||||
</Line>
|
|
@ -0,0 +1,59 @@
|
|||
<script>
|
||||
import { Line, InfoWord, DocumentationLink, Text } from "../typography"
|
||||
import subjects from "../subjects"
|
||||
import * as explanation from "../explanation"
|
||||
|
||||
export let setExplanationSubject
|
||||
export let support
|
||||
|
||||
const getIcon = support => {
|
||||
if (support === explanation.support.unsupported) {
|
||||
return "Alert"
|
||||
} else if (support === explanation.support.supported) {
|
||||
return "CheckmarkCircle"
|
||||
}
|
||||
|
||||
return "AlertCheck"
|
||||
}
|
||||
|
||||
const getColor = support => {
|
||||
if (support === explanation.support.unsupported) {
|
||||
return "var(--red)"
|
||||
} else if (support === explanation.support.supported) {
|
||||
return "var(--green)"
|
||||
}
|
||||
|
||||
return "var(--yellow)"
|
||||
}
|
||||
|
||||
const getText = support => {
|
||||
if (support === explanation.support.unsupported) {
|
||||
return "Not compatible"
|
||||
} else if (support === explanation.support.supported) {
|
||||
return "Compatible"
|
||||
}
|
||||
|
||||
return "Partially compatible"
|
||||
}
|
||||
|
||||
$: icon = getIcon(support)
|
||||
$: color = getColor(support)
|
||||
$: text = getText(support)
|
||||
</script>
|
||||
|
||||
<Line>
|
||||
<InfoWord
|
||||
on:mouseenter={() => setExplanationSubject(subjects.support)}
|
||||
on:mouseleave={() => setExplanationSubject(subjects.none)}
|
||||
{icon}
|
||||
{color}
|
||||
{text}
|
||||
/>
|
||||
<Text value=" with this " />
|
||||
<DocumentationLink
|
||||
href="https://docs.budibase.com/docs/chart"
|
||||
icon="GraphPie"
|
||||
text="Chart component"
|
||||
/>
|
||||
<Text value=" input." />
|
||||
</Line>
|
|
@ -0,0 +1,8 @@
|
|||
export { default as Column } from "./Column.svelte"
|
||||
export { default as NotRequired } from "./NotRequired.svelte"
|
||||
export { default as StringAsNumber } from "./StringAsNumber.svelte"
|
||||
export { default as Support } from "./Support.svelte"
|
||||
export { default as JSONPrimitivesOnly } from "./JSONPrimitivesOnly.svelte"
|
||||
export { default as DateAsNumber } from "./DateAsNumber.svelte"
|
||||
export { default as NumberAsDate } from "./NumberAsDate.svelte"
|
||||
export { default as StringAsDate } from "./StringAsDate.svelte"
|
|
@ -0,0 +1,13 @@
|
|||
const subjects = {
|
||||
column: Symbol("details-modal-column"),
|
||||
support: Symbol("details-modal-support"),
|
||||
stringsAsNumbers: Symbol("details-modal-strings-as-numbers"),
|
||||
datesAsNumbers: Symbol("details-modal-dates-as-numbers"),
|
||||
numbersAsDates: Symbol("explanation-numbers-as-dates"),
|
||||
stringsAsDates: Symbol("explanation-strings-as-dates"),
|
||||
notRequired: Symbol("details-modal-not-required"),
|
||||
scalarJsonOnly: Symbol("explanation-scalar-json-only"),
|
||||
none: Symbol("details-modal-none"),
|
||||
}
|
||||
|
||||
export default subjects
|
|
@ -0,0 +1,14 @@
|
|||
<span class="comma">,</span>
|
||||
|
||||
<style>
|
||||
.comma {
|
||||
color: var(--grey-6);
|
||||
font-size: 17px;
|
||||
display: inline block;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
bottom: 2px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,66 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
|
||||
export let icon
|
||||
export let text
|
||||
export let href
|
||||
export let disabled = false
|
||||
</script>
|
||||
|
||||
<a
|
||||
class:disabled
|
||||
tabindex="0"
|
||||
{href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>
|
||||
<Icon size="XS" name={icon} />
|
||||
<span class="text">
|
||||
<slot>
|
||||
{text}
|
||||
</slot>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.text {
|
||||
color: var(--ink);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
padding: 1px 0 2px;
|
||||
filter: brightness(100%);
|
||||
align-items: center;
|
||||
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid var(--blue);
|
||||
transition: filter 300ms;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
filter: brightness(120%);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
filter: brightness(100%);
|
||||
border-bottom: 1px solid var(--grey-6);
|
||||
}
|
||||
|
||||
.link :global(svg) {
|
||||
margin-right: 3px;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.disabled :global(svg) {
|
||||
color: var(--grey-6);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,78 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
|
||||
export let icon = null
|
||||
export let color = null
|
||||
export let text
|
||||
export let href = null
|
||||
</script>
|
||||
|
||||
{#if href !== null}
|
||||
<a
|
||||
tabindex="0"
|
||||
{href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
class="infoWord"
|
||||
style:color
|
||||
style:border-color={color}
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
>
|
||||
{#if icon}
|
||||
<Icon size="XS" name={icon} />
|
||||
{/if}
|
||||
<span class="text">
|
||||
<slot>
|
||||
{text}
|
||||
</slot>
|
||||
</span>
|
||||
</a>
|
||||
{:else}
|
||||
<div
|
||||
role="tooltip"
|
||||
class="infoWord"
|
||||
style:color
|
||||
style:border-color={color}
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
>
|
||||
{#if icon}
|
||||
<Icon size="XS" name={icon} />
|
||||
{/if}
|
||||
<span class="text">
|
||||
<slot>
|
||||
{text}
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.infoWord {
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
padding: 1px 0 2px;
|
||||
filter: brightness(100%);
|
||||
overflow: hidden;
|
||||
transition: filter 300ms;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--grey-6);
|
||||
}
|
||||
|
||||
.infoWord:hover {
|
||||
filter: brightness(120%);
|
||||
}
|
||||
|
||||
.infoWord :global(svg) {
|
||||
color: inherit;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--ink);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,41 @@
|
|||
<script>
|
||||
export let noWrap = false
|
||||
</script>
|
||||
|
||||
<div class="line">
|
||||
<span class="bullet">•</span>
|
||||
<div class="content" class:noWrap>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.line {
|
||||
color: var(--ink);
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bullet {
|
||||
color: var(--grey-6);
|
||||
font-size: 17px;
|
||||
display: inline block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
line-height: 17px;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 3px;
|
||||
}
|
||||
|
||||
.noWrap {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,13 @@
|
|||
<span class="period">.</span>
|
||||
|
||||
<style>
|
||||
.period {
|
||||
color: var(--grey-6);
|
||||
font-size: 20px;
|
||||
display: inline block;
|
||||
margin-left: 2px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
bottom: 2px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,9 @@
|
|||
<span class="space">{" "}</span>
|
||||
|
||||
<style>
|
||||
.space {
|
||||
white-space: pre;
|
||||
width: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,64 @@
|
|||
<script>
|
||||
import Comma from "./Comma.svelte"
|
||||
import Period from "./Period.svelte"
|
||||
import Space from "./Space.svelte"
|
||||
|
||||
export let value = null
|
||||
|
||||
const punctuation = [" ", ",", "."]
|
||||
|
||||
// TODO regex might work here now
|
||||
const getWords = value => {
|
||||
if (typeof value !== "string") {
|
||||
return []
|
||||
}
|
||||
|
||||
const newWords = []
|
||||
let lastIndex = 0
|
||||
|
||||
const makeWord = i => {
|
||||
// No word to make, multiple spaces, spaces at start of text etc
|
||||
if (i - lastIndex > 0) {
|
||||
newWords.push(value.substring(lastIndex, i))
|
||||
}
|
||||
|
||||
lastIndex = i + 1
|
||||
}
|
||||
|
||||
value.split("").forEach((character, i) => {
|
||||
if (punctuation.includes(character)) {
|
||||
makeWord(i)
|
||||
newWords.push(character)
|
||||
}
|
||||
})
|
||||
|
||||
makeWord(value.length)
|
||||
|
||||
return newWords
|
||||
}
|
||||
|
||||
$: words = getWords(value)
|
||||
</script>
|
||||
|
||||
{#each words as word}
|
||||
{#if word === " "}
|
||||
<Space />
|
||||
{:else if word === ","}
|
||||
<Comma />
|
||||
{:else if word === "."}
|
||||
<Period />
|
||||
{:else}
|
||||
<span class="text">
|
||||
{word}
|
||||
</span>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.text {
|
||||
/* invisible properties to match other inline text elements that do have borders. If we don't match here we run into subpixel issues */
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid transparent;
|
||||
padding: 1px 0 2px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,7 @@
|
|||
export { default as Space } from "./Space.svelte"
|
||||
export { default as Comma } from "./Comma.svelte"
|
||||
export { default as Period } from "./Period.svelte"
|
||||
export { default as Text } from "./Text.svelte"
|
||||
export { default as InfoWord } from "./InfoWord.svelte"
|
||||
export { default as DocumentationLink } from "./DocumentationLink.svelte"
|
||||
export { default as Line } from "./Line.svelte"
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
||||
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||
import { Toggle, Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
|
|
@ -41,6 +41,7 @@ export const FieldTypeToComponentMap = {
|
|||
[FieldType.BOOLEAN]: "booleanfield",
|
||||
[FieldType.LONGFORM]: "longformfield",
|
||||
[FieldType.DATETIME]: "datetimefield",
|
||||
[FieldType.SIGNATURE_SINGLE]: "signaturesinglefield",
|
||||
[FieldType.ATTACHMENTS]: "attachmentfield",
|
||||
[FieldType.ATTACHMENT_SINGLE]: "attachmentsinglefield",
|
||||
[FieldType.LINK]: "relationshipfield",
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { Select, ContextTooltip } from "@budibase/bbui"
|
||||
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
||||
import { selectedScreen } from "stores/builder"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { Explanation } from "./Explanation"
|
||||
import { debounce } from "lodash"
|
||||
import { params } from "@roxi/routify"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { FIELDS } from "constants/backend"
|
||||
|
||||
export let componentInstance = {}
|
||||
export let value = ""
|
||||
export let placeholder
|
||||
export let explanation
|
||||
|
||||
let contextTooltipAnchor = null
|
||||
let currentOption = null
|
||||
let contextTooltipVisible = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
$: datasource = getDatasourceForProvider($selectedScreen, componentInstance)
|
||||
|
@ -32,6 +42,77 @@
|
|||
boundValue = getValidValue(value.detail, options)
|
||||
dispatch("change", boundValue)
|
||||
}
|
||||
|
||||
const updateTooltip = debounce((e, option) => {
|
||||
if (option == null) {
|
||||
contextTooltipVisible = false
|
||||
} else {
|
||||
contextTooltipAnchor = e?.target
|
||||
currentOption = option
|
||||
contextTooltipVisible = true
|
||||
}
|
||||
}, 200)
|
||||
|
||||
const onOptionMouseenter = (e, option) => {
|
||||
updateTooltip(e, option)
|
||||
}
|
||||
|
||||
const onOptionMouseleave = e => {
|
||||
updateTooltip(e, null)
|
||||
}
|
||||
const getOptionIcon = optionKey => {
|
||||
const option = schema[optionKey]
|
||||
if (!option) return ""
|
||||
|
||||
if (option.autocolumn) {
|
||||
return "MagicWand"
|
||||
}
|
||||
const { type, subtype } = option
|
||||
|
||||
const result =
|
||||
typeof Constants.TypeIconMap[type] === "object" && subtype
|
||||
? Constants.TypeIconMap[type][subtype]
|
||||
: Constants.TypeIconMap[type]
|
||||
|
||||
return result || "Text"
|
||||
}
|
||||
|
||||
const getOptionIconTooltip = optionKey => {
|
||||
const option = schema[optionKey]
|
||||
|
||||
const type = option?.type
|
||||
const field = Object.values(FIELDS).find(f => f.type === type)
|
||||
|
||||
if (field) {
|
||||
return field.name
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
</script>
|
||||
|
||||
<Select {placeholder} value={boundValue} on:change={onChange} {options} />
|
||||
<Select
|
||||
{placeholder}
|
||||
value={boundValue}
|
||||
on:change={onChange}
|
||||
{options}
|
||||
{onOptionMouseenter}
|
||||
{onOptionMouseleave}
|
||||
/>
|
||||
|
||||
{#if explanation}
|
||||
<ContextTooltip
|
||||
visible={contextTooltipVisible}
|
||||
anchor={contextTooltipAnchor}
|
||||
offset={20}
|
||||
>
|
||||
<Explanation
|
||||
tableHref={`/builder/app/${$params.application}/data/table/${datasource?.tableId}`}
|
||||
schema={schema[currentOption]}
|
||||
columnIcon={getOptionIcon(currentOption)}
|
||||
columnName={currentOption}
|
||||
columnType={getOptionIconTooltip(currentOption)}
|
||||
{explanation}
|
||||
/>
|
||||
</ContextTooltip>
|
||||
{/if}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
||||
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||
import { Toggle, Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
||||
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { setContext } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
|
|
|
@ -67,6 +67,7 @@ const toGridFormat = draggableListColumns => {
|
|||
label: entry.label,
|
||||
field: entry.field,
|
||||
active: entry.active,
|
||||
width: entry.width,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -81,6 +82,7 @@ const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => {
|
|||
field: column.field,
|
||||
label: column.label,
|
||||
columnType: schema[column.field].type,
|
||||
width: column.width,
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
<script>
|
||||
import { Multiselect } from "@budibase/bbui"
|
||||
import { Multiselect, ContextTooltip } from "@budibase/bbui"
|
||||
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
||||
import { selectedScreen } from "stores/builder"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { Explanation } from "./Explanation"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import { params } from "@roxi/routify"
|
||||
import { debounce } from "lodash"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
export let componentInstance = {}
|
||||
export let value = ""
|
||||
export let placeholder
|
||||
export let explanation
|
||||
|
||||
let contextTooltipAnchor = null
|
||||
let currentOption = null
|
||||
let contextTooltipVisible = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
$: datasource = getDatasourceForProvider($selectedScreen, componentInstance)
|
||||
|
@ -26,6 +36,84 @@
|
|||
boundValue = getValidOptions(value.detail, options)
|
||||
dispatch("change", boundValue)
|
||||
}
|
||||
|
||||
const getOptionIcon = optionKey => {
|
||||
const option = schema[optionKey]
|
||||
if (!option) return ""
|
||||
|
||||
if (option.autocolumn) {
|
||||
return "MagicWand"
|
||||
}
|
||||
const { type, subtype } = option
|
||||
|
||||
const result =
|
||||
typeof Constants.TypeIconMap[type] === "object" && subtype
|
||||
? Constants.TypeIconMap[type][subtype]
|
||||
: Constants.TypeIconMap[type]
|
||||
|
||||
return result || "Text"
|
||||
}
|
||||
|
||||
const getOptionIconTooltip = optionKey => {
|
||||
const option = schema[optionKey]
|
||||
|
||||
const type = option?.type
|
||||
const field = Object.values(FIELDS).find(f => f.type === type)
|
||||
|
||||
if (field) {
|
||||
return field.name
|
||||
} else if (type === "jsonarray") {
|
||||
// `jsonarray` isn't present in the above FIELDS constant
|
||||
|
||||
return "JSON Array"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
const updateTooltip = debounce((e, option) => {
|
||||
if (option == null) {
|
||||
contextTooltipVisible = false
|
||||
} else {
|
||||
contextTooltipAnchor = e?.target
|
||||
currentOption = option
|
||||
contextTooltipVisible = true
|
||||
}
|
||||
}, 200)
|
||||
|
||||
const onOptionMouseenter = (e, option) => {
|
||||
updateTooltip(e, option)
|
||||
}
|
||||
|
||||
const onOptionMouseleave = e => {
|
||||
updateTooltip(e, null)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Multiselect {placeholder} value={boundValue} on:change={setValue} {options} />
|
||||
<Multiselect
|
||||
iconPosition="right"
|
||||
{placeholder}
|
||||
value={boundValue}
|
||||
on:change={setValue}
|
||||
{options}
|
||||
align="right"
|
||||
{onOptionMouseenter}
|
||||
{onOptionMouseleave}
|
||||
/>
|
||||
|
||||
{#if explanation}
|
||||
<ContextTooltip
|
||||
visible={contextTooltipVisible}
|
||||
anchor={contextTooltipAnchor}
|
||||
offset={20}
|
||||
>
|
||||
<Explanation
|
||||
tableHref={`/builder/app/${$params.application}/data/table/${datasource?.tableId}`}
|
||||
schema={schema[currentOption]}
|
||||
columnIcon={getOptionIcon(currentOption)}
|
||||
columnName={currentOption}
|
||||
columnType={getOptionIconTooltip(currentOption)}
|
||||
{explanation}
|
||||
/>
|
||||
</ContextTooltip>
|
||||
{/if}
|
||||
|
|
|
@ -108,6 +108,8 @@
|
|||
Constraints.MaxFileSize,
|
||||
Constraints.MaxUploadSize,
|
||||
],
|
||||
["attachment_single"]: [Constraints.Required, Constraints.MaxUploadSize],
|
||||
["signature_single"]: [Constraints.Required],
|
||||
["link"]: [
|
||||
Constraints.Required,
|
||||
Constraints.Contains,
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
NewFormSteps,
|
||||
} from "./steps"
|
||||
import { API } from "api"
|
||||
import { customPositionHandler } from "components/design/settings/controls/EditComponentPopover"
|
||||
|
||||
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
||||
|
||||
|
@ -187,7 +186,6 @@ const getTours = () => {
|
|||
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_CREATE_STEPS)
|
||||
builderStore.highlightSetting("steps", "info")
|
||||
},
|
||||
positionHandler: customPositionHandler,
|
||||
align: "left-outside",
|
||||
},
|
||||
],
|
||||
|
@ -203,7 +201,6 @@ const getTours = () => {
|
|||
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_ROW_ID)
|
||||
builderStore.highlightSetting("rowId", "info")
|
||||
},
|
||||
positionHandler: customPositionHandler,
|
||||
align: "left-outside",
|
||||
},
|
||||
{
|
||||
|
@ -219,7 +216,6 @@ const getTours = () => {
|
|||
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_VIEW_UPDATE_STEPS)
|
||||
builderStore.highlightSetting("steps", "info")
|
||||
},
|
||||
positionHandler: customPositionHandler,
|
||||
align: "left-outside",
|
||||
scrollIntoView: true,
|
||||
},
|
||||
|
|
|
@ -127,6 +127,14 @@ export const FIELDS = {
|
|||
presence: false,
|
||||
},
|
||||
},
|
||||
SIGNATURE_SINGLE: {
|
||||
name: "Signature",
|
||||
type: FieldType.SIGNATURE_SINGLE,
|
||||
icon: "AnnotatePen",
|
||||
constraints: {
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
LINK: {
|
||||
name: "Relationship",
|
||||
type: FieldType.LINK,
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
} from "@budibase/bbui"
|
||||
import AppActions from "components/deploy/AppActions.svelte"
|
||||
import { API } from "api"
|
||||
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
||||
import { isActive, url, goto, layout, redirect } from "@roxi/routify"
|
||||
import { capitalise } from "helpers"
|
||||
import { onMount, onDestroy } from "svelte"
|
||||
import VerificationPromptBanner from "components/common/VerificationPromptBanner.svelte"
|
||||
|
@ -69,7 +69,7 @@
|
|||
// e.g. if one of your screens is selected on front end, then
|
||||
// you browse to backend, when you click frontend, you will be
|
||||
// brought back to the same screen.
|
||||
const topItemNavigate = path => () => {
|
||||
const topItemNavigate = path => {
|
||||
const activeTopNav = $layout.children.find(c => $isActive(c.path))
|
||||
if (activeTopNav) {
|
||||
builderStore.setPreviousTopNavPath(
|
||||
|
@ -105,10 +105,6 @@
|
|||
}
|
||||
|
||||
onMount(async () => {
|
||||
document.fonts.onloadingdone = e => {
|
||||
builderStore.loadFonts(e.fontfaces)
|
||||
}
|
||||
|
||||
if (!hasSynced && application) {
|
||||
try {
|
||||
await API.syncApp(application)
|
||||
|
@ -140,28 +136,23 @@
|
|||
<div class="top-nav">
|
||||
{#if $appStore.initialised}
|
||||
<div class="topleftnav">
|
||||
<span class="back-to-apps">
|
||||
<Icon
|
||||
size="S"
|
||||
hoverable
|
||||
name="BackAndroid"
|
||||
on:click={() => $goto("../../portal/apps")}
|
||||
/>
|
||||
</span>
|
||||
<a href={$url("../../portal/apps")} class="linkWrapper back-to-apps">
|
||||
<Icon size="S" hoverable name="BackAndroid" />
|
||||
</a>
|
||||
<Tabs {selected} size="M">
|
||||
{#key $builderStore?.fonts}
|
||||
{#each $layout.children as { path, title }}
|
||||
<TourWrap stepKeys={[`builder-${title}-section`]}>
|
||||
<Tab
|
||||
quiet
|
||||
selected={$isActive(path)}
|
||||
on:click={topItemNavigate(path)}
|
||||
title={capitalise(title)}
|
||||
id={`builder-${title}-tab`}
|
||||
/>
|
||||
</TourWrap>
|
||||
{/each}
|
||||
{/key}
|
||||
{#each $layout.children as { path, title }}
|
||||
<TourWrap stepKeys={[`builder-${title}-section`]}>
|
||||
<Tab
|
||||
link
|
||||
href={$url(path)}
|
||||
quiet
|
||||
selected={$isActive(path)}
|
||||
on:click={() => topItemNavigate(path)}
|
||||
title={capitalise(title)}
|
||||
id={`builder-${title}-tab`}
|
||||
/>
|
||||
</TourWrap>
|
||||
{/each}
|
||||
</Tabs>
|
||||
</div>
|
||||
<div class="topcenternav">
|
||||
|
@ -207,6 +198,11 @@
|
|||
<EnterpriseBasicTrialModal />
|
||||
|
||||
<style>
|
||||
.linkWrapper {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.back-to-apps {
|
||||
display: contents;
|
||||
}
|
||||
|
|
|
@ -191,6 +191,9 @@
|
|||
// Number fields
|
||||
min: setting.min ?? null,
|
||||
max: setting.max ?? null,
|
||||
|
||||
// Field select settings
|
||||
explanation: setting.explanation,
|
||||
}}
|
||||
{bindings}
|
||||
{componentBindings}
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
"multifieldselect",
|
||||
"s3upload",
|
||||
"codescanner",
|
||||
"signaturesinglefield",
|
||||
"bbreferencesinglefield",
|
||||
"bbreferencefield"
|
||||
]
|
||||
|
|
|
@ -19,6 +19,7 @@ export const INITIAL_APP_META_STATE = {
|
|||
showNotificationAction: false,
|
||||
sidePanel: false,
|
||||
},
|
||||
typeSupportPresets: {},
|
||||
features: {
|
||||
componentValidation: false,
|
||||
disableUserMetadata: false,
|
||||
|
@ -79,6 +80,13 @@ export class AppMetaStore extends BudiStore {
|
|||
}))
|
||||
}
|
||||
|
||||
syncClientTypeSupportPresets(typeSupportPresets) {
|
||||
this.update(state => ({
|
||||
...state,
|
||||
typeSupportPresets,
|
||||
}))
|
||||
}
|
||||
|
||||
async syncAppRoutes() {
|
||||
const resp = await API.fetchAppRoutes()
|
||||
this.update(state => ({
|
||||
|
|
|
@ -82,6 +82,7 @@ const automationActions = store => ({
|
|||
steps: [],
|
||||
trigger,
|
||||
},
|
||||
disabled: false,
|
||||
}
|
||||
const response = await store.actions.save(automation)
|
||||
await store.actions.fetch()
|
||||
|
@ -134,6 +135,28 @@ const automationActions = store => ({
|
|||
})
|
||||
await store.actions.fetch()
|
||||
},
|
||||
toggleDisabled: async automationId => {
|
||||
let automation
|
||||
try {
|
||||
automation = store.actions.getDefinition(automationId)
|
||||
if (!automation) {
|
||||
return
|
||||
}
|
||||
automation.disabled = !automation.disabled
|
||||
await store.actions.save(automation)
|
||||
notifications.success(
|
||||
`Automation ${
|
||||
automation.disabled ? "enabled" : "disabled"
|
||||
} successfully`
|
||||
)
|
||||
} catch (error) {
|
||||
notifications.error(
|
||||
`Error ${
|
||||
automation && automation.disabled ? "enabling" : "disabling"
|
||||
} automation`
|
||||
)
|
||||
}
|
||||
},
|
||||
updateBlockInputs: async (block, data) => {
|
||||
// Create new modified block
|
||||
let newBlock = {
|
||||
|
|
|
@ -14,7 +14,6 @@ export const INITIAL_BUILDER_STATE = {
|
|||
tourKey: null,
|
||||
tourStepKey: null,
|
||||
hoveredComponentId: null,
|
||||
fonts: null,
|
||||
}
|
||||
|
||||
export class BuilderStore extends BudiStore {
|
||||
|
@ -37,16 +36,6 @@ export class BuilderStore extends BudiStore {
|
|||
this.websocket
|
||||
}
|
||||
|
||||
loadFonts(fontFaces) {
|
||||
const ff = fontFaces.map(
|
||||
fontFace => `${fontFace.family}-${fontFace.weight}`
|
||||
)
|
||||
this.update(state => ({
|
||||
...state,
|
||||
fonts: [...(state.fonts || []), ...ff],
|
||||
}))
|
||||
}
|
||||
|
||||
init(app) {
|
||||
if (!app?.appId) {
|
||||
console.error("BuilderStore: No appId supplied for websocket")
|
||||
|
|
|
@ -108,6 +108,7 @@ export class ComponentStore extends BudiStore {
|
|||
|
||||
// Sync client features to app store
|
||||
appStore.syncClientFeatures(components.features)
|
||||
appStore.syncClientTypeSupportPresets(components?.typeSupportPresets ?? {})
|
||||
|
||||
return components
|
||||
}
|
||||
|
|
|
@ -91,6 +91,14 @@ describe("Application Meta Store", () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("Sync type support information to state", async ctx => {
|
||||
ctx.test.appStore.syncClientTypeSupportPresets({ preset: "information" })
|
||||
|
||||
expect(ctx.test.store.typeSupportPresets).toStrictEqual({
|
||||
preset: "information",
|
||||
})
|
||||
})
|
||||
|
||||
it("Sync component feature flags to state", async ctx => {
|
||||
ctx.test.appStore.syncClientFeatures(clientFeaturesResp)
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ vi.mock("stores/builder", async () => {
|
|||
update: mockAppStore.update,
|
||||
set: mockAppStore.set,
|
||||
syncClientFeatures: vi.fn(),
|
||||
syncClientTypeSupportPresets: vi.fn(),
|
||||
}
|
||||
const mockTableStore = writable()
|
||||
const tables = {
|
||||
|
|
|
@ -13,6 +13,42 @@
|
|||
"sidePanel": true,
|
||||
"skeletonLoader": true
|
||||
},
|
||||
"typeSupportPresets": {
|
||||
"numberLike": {
|
||||
"supported": ["number", "boolean"],
|
||||
"partialSupport": [
|
||||
{ "type": "longform", "message": "stringAsNumber" },
|
||||
{ "type": "string", "message": "stringAsNumber" },
|
||||
{ "type": "bigint", "message": "stringAsNumber" },
|
||||
{ "type": "options", "message": "stringAsNumber" },
|
||||
{ "type": "formula", "message": "stringAsNumber" },
|
||||
{ "type": "datetime", "message": "dateAsNumber"}
|
||||
],
|
||||
"unsupported": [
|
||||
{ "type": "json", "message": "jsonPrimitivesOnly" }
|
||||
]
|
||||
},
|
||||
"stringLike": {
|
||||
"supported": ["string", "number", "bigint", "options", "longform", "boolean", "datetime"],
|
||||
"unsupported": [
|
||||
{ "type": "json", "message": "jsonPrimitivesOnly" }
|
||||
]
|
||||
},
|
||||
"datetimeLike": {
|
||||
"supported": ["datetime"],
|
||||
"partialSupport": [
|
||||
{ "type": "longform", "message": "stringAsDate" },
|
||||
{ "type": "string", "message": "stringAsDate" },
|
||||
{ "type": "options", "message": "stringAsDate" },
|
||||
{ "type": "formula", "message": "stringAsDate" },
|
||||
{ "type": "bigint", "message": "stringAsDate" },
|
||||
{ "type": "number", "message": "numberAsDate"}
|
||||
],
|
||||
"unsupported": [
|
||||
{ "type": "json", "message": "jsonPrimitivesOnly" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"name": "Layout",
|
||||
"description": "This component is specific only to layouts",
|
||||
|
@ -1602,6 +1638,7 @@
|
|||
]
|
||||
},
|
||||
"bar": {
|
||||
"documentationLink": "https://docs.budibase.com/docs/bar-chart",
|
||||
"name": "Bar Chart",
|
||||
"description": "Bar chart",
|
||||
"icon": "GraphBarVertical",
|
||||
|
@ -1626,6 +1663,11 @@
|
|||
"label": "Label column",
|
||||
"key": "labelColumn",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "stringLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -1633,6 +1675,11 @@
|
|||
"label": "Data columns",
|
||||
"key": "valueColumns",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -1760,6 +1807,7 @@
|
|||
]
|
||||
},
|
||||
"line": {
|
||||
"documentationLink": "https://docs.budibase.com/docs/line-chart",
|
||||
"name": "Line Chart",
|
||||
"description": "Line chart",
|
||||
"icon": "GraphTrend",
|
||||
|
@ -1784,6 +1832,11 @@
|
|||
"label": "Label column",
|
||||
"key": "labelColumn",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "stringLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -1791,6 +1844,11 @@
|
|||
"label": "Data columns",
|
||||
"key": "valueColumns",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -1913,6 +1971,7 @@
|
|||
]
|
||||
},
|
||||
"area": {
|
||||
"documentationLink": "https://docs.budibase.com/docs/area-chart",
|
||||
"name": "Area Chart",
|
||||
"description": "Line chart",
|
||||
"icon": "GraphAreaStacked",
|
||||
|
@ -1937,6 +1996,11 @@
|
|||
"label": "Label column",
|
||||
"key": "labelColumn",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "stringLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -1944,6 +2008,11 @@
|
|||
"label": "Data columns",
|
||||
"key": "valueColumns",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -2078,6 +2147,7 @@
|
|||
]
|
||||
},
|
||||
"pie": {
|
||||
"documentationLink": "https://docs.budibase.com/docs/pie-donut-chart",
|
||||
"name": "Pie Chart",
|
||||
"description": "Pie chart",
|
||||
"icon": "GraphPie",
|
||||
|
@ -2102,13 +2172,23 @@
|
|||
"label": "Label column",
|
||||
"key": "labelColumn",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "stringLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Data columns",
|
||||
"label": "Data column",
|
||||
"key": "valueColumn",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -2207,6 +2287,7 @@
|
|||
]
|
||||
},
|
||||
"donut": {
|
||||
"documentationLink": "https://docs.budibase.com/docs/pie-donut-chart",
|
||||
"name": "Donut Chart",
|
||||
"description": "Donut chart",
|
||||
"icon": "GraphDonut",
|
||||
|
@ -2231,6 +2312,11 @@
|
|||
"label": "Label column",
|
||||
"key": "labelColumn",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "stringLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -2238,6 +2324,11 @@
|
|||
"label": "Data columns",
|
||||
"key": "valueColumn",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -2336,6 +2427,7 @@
|
|||
]
|
||||
},
|
||||
"candlestick": {
|
||||
"documentationLink": "https://docs.budibase.com/docs/candlestick-chart",
|
||||
"name": "Candlestick Chart",
|
||||
"description": "Candlestick chart",
|
||||
"icon": "GraphBarVerticalStacked",
|
||||
|
@ -2360,6 +2452,11 @@
|
|||
"label": "Date column",
|
||||
"key": "dateColumn",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "datetimeLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -2367,6 +2464,11 @@
|
|||
"label": "Open column",
|
||||
"key": "openColumn",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -2374,6 +2476,11 @@
|
|||
"label": "Close column",
|
||||
"key": "closeColumn",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -2381,6 +2488,11 @@
|
|||
"label": "High column",
|
||||
"key": "highColumn",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -2388,6 +2500,11 @@
|
|||
"label": "Low column",
|
||||
"key": "lowColumn",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -2427,6 +2544,7 @@
|
|||
]
|
||||
},
|
||||
"histogram": {
|
||||
"documentationLink": "https://docs.budibase.com/docs/histogram-chart",
|
||||
"name": "Histogram Chart",
|
||||
"description": "Histogram chart",
|
||||
"icon": "Histogram",
|
||||
|
@ -2434,7 +2552,6 @@
|
|||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -2452,6 +2569,11 @@
|
|||
"label": "Data column",
|
||||
"key": "valueColumn",
|
||||
"dependsOn": "dataProvider",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -2746,6 +2868,14 @@
|
|||
"type": "plainText",
|
||||
"label": "Label",
|
||||
"key": "label"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label": "Initial width",
|
||||
"key": "width",
|
||||
"placeholder": "Auto",
|
||||
"min": 80,
|
||||
"max": 9999
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -4107,6 +4237,55 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"signaturesinglefield": {
|
||||
"name": "Signature",
|
||||
"icon": "AnnotatePen",
|
||||
"styles": ["size"],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 50
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/signature_single",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Label",
|
||||
"key": "label"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Help text",
|
||||
"key": "helpText"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
"label": "On change",
|
||||
"key": "onChange",
|
||||
"context": [
|
||||
{
|
||||
"label": "Field Value",
|
||||
"key": "value"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "validation/signature_single",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"embeddedmap": {
|
||||
"name": "Embedded Map",
|
||||
"icon": "Location",
|
||||
|
@ -4372,7 +4551,7 @@
|
|||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "validation/attachment",
|
||||
"type": "validation/attachment_single",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
},
|
||||
|
@ -5256,6 +5435,11 @@
|
|||
"label": "Label column",
|
||||
"key": "labelColumn",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "stringLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -5263,6 +5447,11 @@
|
|||
"label": "Data column",
|
||||
"key": "valueColumn",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
|
@ -5281,6 +5470,11 @@
|
|||
"label": "Label column",
|
||||
"key": "labelColumn",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "stringLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -5288,6 +5482,11 @@
|
|||
"label": "Data column",
|
||||
"key": "valueColumn",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
|
@ -5306,6 +5505,11 @@
|
|||
"label": "Label column",
|
||||
"key": "labelColumn",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "stringLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -5313,6 +5517,11 @@
|
|||
"label": "Data columns",
|
||||
"key": "valueColumns",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -5360,6 +5569,11 @@
|
|||
"label": "Value column",
|
||||
"key": "valueColumn",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -5401,6 +5615,11 @@
|
|||
"label": "Label column",
|
||||
"key": "labelColumn",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "stringLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -5408,6 +5627,11 @@
|
|||
"label": "Data columns",
|
||||
"key": "valueColumns",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -5450,6 +5674,11 @@
|
|||
"label": "Label columns",
|
||||
"key": "labelColumn",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "stringLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -5457,6 +5686,11 @@
|
|||
"label": "Data columns",
|
||||
"key": "valueColumns",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -5511,6 +5745,11 @@
|
|||
"label": "Date column",
|
||||
"key": "dateColumn",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "datetimeLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -5518,6 +5757,11 @@
|
|||
"label": "Open column",
|
||||
"key": "openColumn",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -5525,6 +5769,11 @@
|
|||
"label": "Close column",
|
||||
"key": "closeColumn",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -5532,6 +5781,11 @@
|
|||
"label": "High column",
|
||||
"key": "highColumn",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -5539,6 +5793,11 @@
|
|||
"label": "Low column",
|
||||
"key": "lowColumn",
|
||||
"dependsOn": "dataSource",
|
||||
"explanation": {
|
||||
"typeSupport": {
|
||||
"preset": "numberLike"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"@budibase/string-templates": "0.0.0",
|
||||
"@budibase/types": "0.0.0",
|
||||
"@spectrum-css/card": "3.0.3",
|
||||
"apexcharts": "^3.22.1",
|
||||
"apexcharts": "^3.48.0",
|
||||
"dayjs": "^1.10.8",
|
||||
"downloadjs": "1.4.7",
|
||||
"html5-qrcode": "^2.2.1",
|
||||
|
@ -33,8 +33,8 @@
|
|||
"sanitize-html": "^2.7.0",
|
||||
"screenfull": "^6.0.1",
|
||||
"shortid": "^2.2.15",
|
||||
"svelte-apexcharts": "^1.0.2",
|
||||
"svelte-spa-router": "^4.0.1"
|
||||
"svelte-spa-router": "^4.0.1",
|
||||
"atrament": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
|
|
|
@ -193,6 +193,9 @@
|
|||
$: pad = pad || (interactive && hasChildren && inDndPath)
|
||||
$: $dndIsDragging, (pad = false)
|
||||
|
||||
$: currentTheme = $context?.device?.theme
|
||||
$: darkMode = !currentTheme?.includes("light")
|
||||
|
||||
// Update component context
|
||||
$: store.set({
|
||||
id,
|
||||
|
@ -222,6 +225,7 @@
|
|||
parent: id,
|
||||
ancestors: [...($component?.ancestors ?? []), instance._component],
|
||||
path: [...($component?.path ?? []), id],
|
||||
darkMode,
|
||||
})
|
||||
|
||||
const initialise = (instance, force = false) => {
|
||||
|
@ -283,10 +287,23 @@
|
|||
const dependsOnKey = setting.dependsOn.setting || setting.dependsOn
|
||||
const dependsOnValue = setting.dependsOn.value
|
||||
const realDependentValue = instance[dependsOnKey]
|
||||
|
||||
const sectionDependsOnKey =
|
||||
setting.sectionDependsOn?.setting || setting.sectionDependsOn
|
||||
const sectionDependsOnValue = setting.sectionDependsOn?.value
|
||||
const sectionRealDependentValue = instance[sectionDependsOnKey]
|
||||
|
||||
if (dependsOnValue == null && realDependentValue == null) {
|
||||
return false
|
||||
}
|
||||
if (dependsOnValue !== realDependentValue) {
|
||||
if (dependsOnValue != null && dependsOnValue !== realDependentValue) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
sectionDependsOnValue != null &&
|
||||
sectionDependsOnValue !== sectionRealDependentValue
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,13 +37,16 @@
|
|||
|
||||
let grid
|
||||
let gridContext
|
||||
let minHeight
|
||||
let minHeight = 0
|
||||
|
||||
$: currentTheme = $context?.device?.theme
|
||||
$: darkMode = !currentTheme?.includes("light")
|
||||
$: parsedColumns = getParsedColumns(columns)
|
||||
$: columnWhitelist = parsedColumns.filter(x => x.active).map(x => x.field)
|
||||
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
||||
$: enrichedButtons = enrichButtons(buttons)
|
||||
$: selectedRows = deriveSelectedRows(gridContext)
|
||||
$: styles = patchStyles($component.styles, minHeight)
|
||||
$: data = { selectedRows: $selectedRows }
|
||||
$: actions = [
|
||||
{
|
||||
|
@ -84,9 +87,11 @@
|
|||
|
||||
const getSchemaOverrides = columns => {
|
||||
let overrides = {}
|
||||
columns.forEach(column => {
|
||||
columns.forEach((column, idx) => {
|
||||
overrides[column.field] = {
|
||||
displayName: column.label,
|
||||
width: column.width,
|
||||
order: idx,
|
||||
}
|
||||
})
|
||||
return overrides
|
||||
|
@ -128,52 +133,55 @@
|
|||
)
|
||||
}
|
||||
|
||||
const patchStyles = (styles, minHeight) => {
|
||||
return {
|
||||
...styles,
|
||||
normal: {
|
||||
...styles?.normal,
|
||||
"min-height": `${minHeight}px`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
gridContext = grid.getContext()
|
||||
gridContext.minHeight.subscribe($height => (minHeight = $height))
|
||||
})
|
||||
</script>
|
||||
|
||||
<span style="--min-height:{minHeight}px">
|
||||
<div
|
||||
use:styleable={$component.styles}
|
||||
class:in-builder={$builderStore.inBuilder}
|
||||
>
|
||||
<Grid
|
||||
bind:this={grid}
|
||||
datasource={table}
|
||||
{API}
|
||||
{stripeRows}
|
||||
{quiet}
|
||||
{initialFilter}
|
||||
{initialSortColumn}
|
||||
{initialSortOrder}
|
||||
{fixedRowHeight}
|
||||
{columnWhitelist}
|
||||
{schemaOverrides}
|
||||
canAddRows={allowAddRows}
|
||||
canEditRows={allowEditRows}
|
||||
canDeleteRows={allowDeleteRows}
|
||||
canEditColumns={false}
|
||||
canExpandRows={false}
|
||||
canSaveSchema={false}
|
||||
canSelectRows={true}
|
||||
showControls={false}
|
||||
notifySuccess={notificationStore.actions.success}
|
||||
notifyError={notificationStore.actions.error}
|
||||
buttons={enrichedButtons}
|
||||
isCloud={$environmentStore.cloud}
|
||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
<div use:styleable={styles} class:in-builder={$builderStore.inBuilder}>
|
||||
<Grid
|
||||
bind:this={grid}
|
||||
datasource={table}
|
||||
{API}
|
||||
{stripeRows}
|
||||
{quiet}
|
||||
{darkMode}
|
||||
{initialFilter}
|
||||
{initialSortColumn}
|
||||
{initialSortOrder}
|
||||
{fixedRowHeight}
|
||||
{columnWhitelist}
|
||||
{schemaOverrides}
|
||||
canAddRows={allowAddRows}
|
||||
canEditRows={allowEditRows}
|
||||
canDeleteRows={allowDeleteRows}
|
||||
canEditColumns={false}
|
||||
canExpandRows={false}
|
||||
canSaveSchema={false}
|
||||
canSelectRows={true}
|
||||
showControls={false}
|
||||
notifySuccess={notificationStore.actions.success}
|
||||
notifyError={notificationStore.actions.error}
|
||||
buttons={enrichedButtons}
|
||||
isCloud={$environmentStore.cloud}
|
||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Provider {data} {actions} />
|
||||
|
||||
<style>
|
||||
span {
|
||||
display: contents;
|
||||
}
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -182,7 +190,6 @@
|
|||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
height: 410px;
|
||||
min-height: var(--min-height);
|
||||
}
|
||||
div.in-builder :global(*) {
|
||||
pointer-events: none;
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
// Bar/Line/Area
|
||||
export let valueColumns
|
||||
export let yAxisUnits
|
||||
export let valueUnits
|
||||
export let yAxisLabel
|
||||
export let xAxisLabel
|
||||
export let curve
|
||||
|
@ -51,8 +51,6 @@
|
|||
export let bucketCount
|
||||
|
||||
let dataProviderId
|
||||
|
||||
$: colors = c1 && c2 && c3 && c4 && c5 ? [c1, c2, c3, c4, c5] : null
|
||||
</script>
|
||||
|
||||
<Block>
|
||||
|
@ -84,8 +82,7 @@
|
|||
dataLabels,
|
||||
legend,
|
||||
animate,
|
||||
...colors,
|
||||
yAxisUnits,
|
||||
valueUnits,
|
||||
yAxisLabel,
|
||||
xAxisLabel,
|
||||
stacked,
|
||||
|
@ -98,6 +95,11 @@
|
|||
lowColumn,
|
||||
dateColumn,
|
||||
bucketCount,
|
||||
c1,
|
||||
c2,
|
||||
c3,
|
||||
c4,
|
||||
c5,
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
[FieldType.BOOLEAN]: "booleanfield",
|
||||
[FieldType.LONGFORM]: "longformfield",
|
||||
[FieldType.DATETIME]: "datetimefield",
|
||||
[FieldType.SIGNATURE_SINGLE]: "signaturesinglefield",
|
||||
[FieldType.ATTACHMENTS]: "attachmentfield",
|
||||
[FieldType.ATTACHMENT_SINGLE]: "attachmentsinglefield",
|
||||
[FieldType.LINK]: "relationshipfield",
|
||||
|
|
|
@ -1,25 +1,85 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { chart } from "svelte-apexcharts"
|
||||
import Placeholder from "../Placeholder.svelte"
|
||||
import ApexCharts from "apexcharts"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { cloneDeep } from "./utils"
|
||||
|
||||
const { styleable, builderStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let options
|
||||
|
||||
// Apex charts directly modifies the options object with default properties and internal variables. These being present could unintentionally cause issues to the provider of this prop as the changes are reflected in that component as well. To prevent any issues we clone options here to provide a buffer.
|
||||
$: optionsCopy = cloneDeep(options)
|
||||
|
||||
let chartElement
|
||||
let chart
|
||||
let currentType = null
|
||||
|
||||
const updateChart = async newOptions => {
|
||||
// Line charts have issues transitioning between "datetime" and "category" types, and will ignore the provided formatters
|
||||
// in certain scenarios. Rerendering the chart when the user changes label type fixes this, but unfortunately it does
|
||||
// cause a little bit of jankiness with animations.
|
||||
if (newOptions?.xaxis?.type && newOptions.xaxis.type !== currentType) {
|
||||
await renderChart(chartElement)
|
||||
} else {
|
||||
await chart?.updateOptions(newOptions)
|
||||
}
|
||||
}
|
||||
|
||||
const renderChart = async newChartElement => {
|
||||
try {
|
||||
await chart?.destroy()
|
||||
chart = new ApexCharts(newChartElement, optionsCopy)
|
||||
currentType = optionsCopy?.xaxis?.type
|
||||
await chart.render()
|
||||
} catch (e) {
|
||||
// Apex for some reason throws this error when creating a new chart.
|
||||
// It doesn't actually cause any issues with the function of the chart, so
|
||||
// just suppress it so the console doesn't get spammed
|
||||
if (
|
||||
e.message !==
|
||||
"Cannot read properties of undefined (reading 'parentNode')"
|
||||
) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: noData = optionsCopy == null || optionsCopy?.series?.length === 0
|
||||
|
||||
// Call render chart upon changes to noData, as apex charts has issues with rendering upon changes automatically
|
||||
// if the chart is hidden.
|
||||
$: renderChart(chartElement, noData)
|
||||
$: updateChart(optionsCopy)
|
||||
</script>
|
||||
|
||||
{#if options}
|
||||
{#key options.customColor}
|
||||
<div use:chart={options} use:styleable={$component.styles} />
|
||||
{/key}
|
||||
{:else if $builderStore.inBuilder}
|
||||
<div use:styleable={$component.styles}>
|
||||
<Placeholder />
|
||||
</div>
|
||||
{/if}
|
||||
{#key optionsCopy?.customColor}
|
||||
<div
|
||||
class:hide={noData}
|
||||
use:styleable={$component.styles}
|
||||
bind:this={chartElement}
|
||||
/>
|
||||
{#if $builderStore.inBuilder && noData}
|
||||
<div
|
||||
class="component-placeholder"
|
||||
use:styleable={{
|
||||
...$component.styles,
|
||||
normal: {},
|
||||
custom: null,
|
||||
empty: true,
|
||||
}}
|
||||
>
|
||||
<Icon name="Alert" color="var(--spectrum-global-color-static-red-600)" />
|
||||
Add rows to your data source to start using your component
|
||||
</div>
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
<style>
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
div :global(.apexcharts-legend-series) {
|
||||
display: flex !important;
|
||||
text-transform: capitalize;
|
||||
|
@ -59,4 +119,25 @@
|
|||
) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.component-placeholder {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
font-size: var(--font-size-s);
|
||||
padding: var(--spacing-xs);
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
/* Common styles for all error states to use */
|
||||
.component-placeholder :global(mark) {
|
||||
background-color: var(--spectrum-global-color-gray-400);
|
||||
padding: 0 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.component-placeholder :global(.spectrum-Link) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,195 +0,0 @@
|
|||
export class ApexOptionsBuilder {
|
||||
constructor() {
|
||||
this.formatters = {
|
||||
["Default"]: val => (isNaN(val) ? val : Math.round(val * 100) / 100),
|
||||
["Thousands"]: val => `${Math.round(val / 1000)}K`,
|
||||
["Millions"]: val => `${Math.round(val / 1000000)}M`,
|
||||
}
|
||||
this.options = {
|
||||
series: [],
|
||||
legend: {
|
||||
show: false,
|
||||
position: "top",
|
||||
horizontalAlign: "right",
|
||||
showForSingleSeries: true,
|
||||
showForNullSeries: true,
|
||||
showForZeroSeries: true,
|
||||
},
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
zoom: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
labels: {
|
||||
formatter: this.formatters.Default,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: this.formatters.Default,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
setOption(path, value) {
|
||||
if (value == null || value === "") {
|
||||
return this
|
||||
}
|
||||
let tmp = this.options
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const step = path[i]
|
||||
if (!tmp[step]) {
|
||||
tmp[step] = {}
|
||||
}
|
||||
tmp = tmp[step]
|
||||
}
|
||||
tmp[path[path.length - 1]] = value
|
||||
return this
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this.options
|
||||
}
|
||||
|
||||
type(type) {
|
||||
return this.setOption(["chart", "type"], type)
|
||||
}
|
||||
|
||||
title(title) {
|
||||
return this.setOption(["title", "text"], title)
|
||||
}
|
||||
|
||||
colors(colors) {
|
||||
if (!colors) {
|
||||
delete this.options.colors
|
||||
this.options["customColor"] = false
|
||||
return this
|
||||
}
|
||||
this.options["customColor"] = true
|
||||
return this.setOption(["colors"], colors)
|
||||
}
|
||||
|
||||
width(width) {
|
||||
return this.setOption(["chart", "width"], width || undefined)
|
||||
}
|
||||
|
||||
height(height) {
|
||||
return this.setOption(["chart", "height"], height || undefined)
|
||||
}
|
||||
|
||||
xLabel(label) {
|
||||
return this.setOption(["xaxis", "title", "text"], label)
|
||||
}
|
||||
|
||||
yLabel(label) {
|
||||
return this.setOption(["yaxis", "title", "text"], label)
|
||||
}
|
||||
|
||||
xCategories(categories) {
|
||||
return this.setOption(["xaxis", "categories"], categories)
|
||||
}
|
||||
|
||||
yCategories(categories) {
|
||||
return this.setOption(["yaxis", "categories"], categories)
|
||||
}
|
||||
|
||||
series(series) {
|
||||
return this.setOption(["series"], series)
|
||||
}
|
||||
|
||||
horizontal(horizontal) {
|
||||
return this.setOption(["plotOptions", "bar", "horizontal"], horizontal)
|
||||
}
|
||||
|
||||
dataLabels(dataLabels) {
|
||||
return this.setOption(["dataLabels", "enabled"], dataLabels)
|
||||
}
|
||||
|
||||
animate(animate) {
|
||||
return this.setOption(["chart", "animations", "enabled"], animate)
|
||||
}
|
||||
|
||||
curve(curve) {
|
||||
return this.setOption(["stroke", "curve"], curve)
|
||||
}
|
||||
|
||||
gradient(gradient) {
|
||||
const fill = {
|
||||
type: "gradient",
|
||||
gradient: {
|
||||
shadeIntensity: 1,
|
||||
opacityFrom: 0.7,
|
||||
opacityTo: 0.9,
|
||||
stops: [0, 90, 100],
|
||||
},
|
||||
}
|
||||
return this.setOption(["fill"], gradient ? fill : undefined)
|
||||
}
|
||||
|
||||
legend(legend) {
|
||||
return this.setOption(["legend", "show"], legend)
|
||||
}
|
||||
|
||||
legendPosition(position) {
|
||||
return this.setOption(["legend", "position"], position)
|
||||
}
|
||||
|
||||
stacked(stacked) {
|
||||
return this.setOption(["chart", "stacked"], stacked)
|
||||
}
|
||||
|
||||
labels(labels) {
|
||||
return this.setOption(["labels"], labels)
|
||||
}
|
||||
|
||||
xUnits(units) {
|
||||
return this.setOption(
|
||||
["xaxis", "labels", "formatter"],
|
||||
this.formatters[units || "Default"]
|
||||
)
|
||||
}
|
||||
|
||||
yUnits(units) {
|
||||
return this.setOption(
|
||||
["yaxis", "labels", "formatter"],
|
||||
this.formatters[units || "Default"]
|
||||
)
|
||||
}
|
||||
|
||||
clearXFormatter() {
|
||||
delete this.options.xaxis.labels
|
||||
return this
|
||||
}
|
||||
|
||||
clearYFormatter() {
|
||||
delete this.options.yaxis.labels
|
||||
return this
|
||||
}
|
||||
|
||||
xType(type) {
|
||||
return this.setOption(["xaxis", "type"], type)
|
||||
}
|
||||
|
||||
yType(type) {
|
||||
return this.setOption(["yaxis", "type"], type)
|
||||
}
|
||||
|
||||
yTooltip(yTooltip) {
|
||||
return this.setOption(["yaxis", "tooltip", "enabled"], yTooltip)
|
||||
}
|
||||
|
||||
palette(palette) {
|
||||
if (!palette) {
|
||||
return this
|
||||
}
|
||||
return this.setOption(
|
||||
["theme", "palette"],
|
||||
palette.toLowerCase().replace(/[\W]/g, "")
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,159 @@
|
|||
<script>
|
||||
import LineChart from "./LineChart.svelte"
|
||||
import ApexChart from "./ApexChart.svelte"
|
||||
import { formatters, parsePalette } from "./utils"
|
||||
|
||||
export let title
|
||||
export let dataProvider
|
||||
export let labelColumn
|
||||
export let valueColumns
|
||||
export let xAxisLabel
|
||||
export let yAxisLabel
|
||||
export let height
|
||||
export let width
|
||||
export let animate
|
||||
export let dataLabels
|
||||
export let curve
|
||||
export let legend
|
||||
export let yAxisUnits
|
||||
export let palette
|
||||
export let c1, c2, c3, c4, c5
|
||||
|
||||
// Area specific props
|
||||
export let area
|
||||
export let stacked
|
||||
export let gradient
|
||||
|
||||
$: series = getSeries(dataProvider, valueColumns)
|
||||
$: categories = getCategories(dataProvider, labelColumn)
|
||||
|
||||
$: labelType =
|
||||
dataProvider?.schema?.[labelColumn]?.type === "datetime"
|
||||
? "datetime"
|
||||
: "category"
|
||||
$: xAxisFormatter = getFormatter(labelType, yAxisUnits, "x")
|
||||
$: yAxisFormatter = getFormatter(labelType, yAxisUnits, "y")
|
||||
$: fill = getFill(gradient)
|
||||
|
||||
$: options = {
|
||||
series,
|
||||
stroke: {
|
||||
curve: curve.toLowerCase(),
|
||||
},
|
||||
colors: palette === "Custom" ? [c1, c2, c3, c4, c5] : [],
|
||||
theme: {
|
||||
palette: parsePalette(palette),
|
||||
},
|
||||
fill,
|
||||
legend: {
|
||||
show: legend,
|
||||
position: "top",
|
||||
horizontalAlign: "right",
|
||||
showForSingleSeries: true,
|
||||
showForNullSeries: true,
|
||||
showForZeroSeries: true,
|
||||
},
|
||||
title: {
|
||||
text: title,
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: dataLabels,
|
||||
},
|
||||
chart: {
|
||||
height: height == null || height === "" ? "auto" : height,
|
||||
width: width == null || width === "" ? "100%" : width,
|
||||
type: "area",
|
||||
stacked,
|
||||
animations: {
|
||||
enabled: animate,
|
||||
},
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
zoom: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: labelType,
|
||||
categories,
|
||||
labels: {
|
||||
formatter: xAxisFormatter,
|
||||
},
|
||||
title: {
|
||||
text: xAxisLabel,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: yAxisFormatter,
|
||||
},
|
||||
title: {
|
||||
text: yAxisLabel,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const getSeries = (dataProvider, valueColumns = []) => {
|
||||
const rows = dataProvider.rows ?? []
|
||||
|
||||
return valueColumns.map(column => ({
|
||||
name: column,
|
||||
data: rows.map(row => {
|
||||
const value = row?.[column]
|
||||
|
||||
if (dataProvider?.schema?.[column]?.type === "datetime" && value) {
|
||||
return Date.parse(value)
|
||||
}
|
||||
|
||||
return value
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
const getCategories = (dataProvider, labelColumn) => {
|
||||
const rows = dataProvider.rows ?? []
|
||||
|
||||
return rows.map(row => {
|
||||
const value = row?.[labelColumn]
|
||||
|
||||
// If a nullish or non-scalar type, replace it with an empty string
|
||||
if (!["string", "number", "boolean"].includes(typeof value)) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return value
|
||||
})
|
||||
}
|
||||
|
||||
const getFormatter = (labelType, yAxisUnits, axis) => {
|
||||
const isLabelAxis = axis === "x"
|
||||
|
||||
if (labelType === "datetime" && isLabelAxis) {
|
||||
return formatters["Datetime"]
|
||||
}
|
||||
|
||||
if (isLabelAxis) {
|
||||
return formatters["Default"]
|
||||
}
|
||||
|
||||
return formatters[yAxisUnits]
|
||||
}
|
||||
|
||||
const getFill = gradient => {
|
||||
if (gradient) {
|
||||
return {
|
||||
type: "gradient",
|
||||
gradient: {
|
||||
shadeIntensity: 1,
|
||||
opacityFrom: 0.7,
|
||||
opacityTo: 0.9,
|
||||
stops: [0, 90, 100],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return { type: "solid" }
|
||||
}
|
||||
</script>
|
||||
|
||||
<LineChart {...$$props} area />
|
||||
<ApexChart {options} />
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue