Add placeholders and empty states automatically as required to any empty client components

This commit is contained in:
Andrew Kingston 2021-06-11 08:05:49 +01:00
parent c6564858b2
commit 3b085d9ac3
14 changed files with 96 additions and 104 deletions

View File

@ -65,7 +65,7 @@
>
<Provider key="user" data={$authStore} {actions}>
<div id="app-root">
<Component definition={$screenStore.activeLayout.props} />
<Component instance={$screenStore.activeLayout.props} />
</div>
<NotificationDisplay />
<!-- Key block needs to be outside the if statement or it breaks -->

View File

@ -6,8 +6,9 @@
import { enrichProps, propsAreSame } from "../utils/componentProps"
import { builderStore } from "../store"
import { hashString } from "../utils/hash"
import Manifest from "@budibase/standard-components/manifest.json"
export let definition = {}
export let instance = {}
// Props that will be passed to the component instance
let componentProps
@ -28,26 +29,29 @@
const componentStore = writable({})
setContext("component", componentStore)
// Extract component definition info
$: constructor = getComponentConstructor(definition._component)
$: children = definition._children || []
$: id = definition._id
$: name = definition._instanceName
$: updateComponentProps(definition, $context)
$: styles = definition._styles
$: transition = definition._transition
// Extract component instance info
$: constructor = getComponentConstructor(instance._component)
$: definition = getComponentDefinition(instance._component)
$: children = instance._children || []
$: id = instance._id
$: name = instance._instanceName
$: empty =
!children.length && definition?.hasChildren && $builderStore.inBuilder
$: updateComponentProps(instance, $context)
$: selected =
$builderStore.inBuilder &&
$builderStore.selectedComponentId === definition._id
$builderStore.selectedComponentId === instance._id
// Update component context
$: componentStore.set({
id,
children: children.length,
styles: { ...styles, id },
transition,
styles: { ...instance._styles, id, empty },
empty,
transition: instance._transition,
selected,
props: componentProps,
name,
})
// Gets the component constructor for the specified component
@ -60,14 +64,20 @@
return ComponentLibrary[name]
}
const getComponentDefinition = component => {
const prefix = "@budibase/standard-components/"
const type = component?.replace(prefix, "")
return type ? Manifest[type] : null
}
// Enriches any string component props using handlebars
const updateComponentProps = (definition, context) => {
const updateComponentProps = (instance, context) => {
// Record the timestamp so we can reference it after enrichment
latestUpdateTime = Date.now()
const enrichmentTime = latestUpdateTime
// Enrich props with context
const enrichedProps = enrichProps(definition, context)
const enrichedProps = enrichProps(instance, context)
// Abandon this update if a newer update has started
if (enrichmentTime !== latestUpdateTime) {
@ -100,14 +110,21 @@
}
</script>
<div class={id} data-type="component" data-id={id} data-name={name}>
<div
class={`component ${id}`}
data-type="component"
data-id={id}
data-name={name}
>
{#key propsHash}
{#if constructor && componentProps}
<svelte:component this={constructor} {...componentProps}>
{#if children.length}
{#each children as child (child._id)}
<svelte:self definition={child} />
<svelte:self instance={child} />
{/each}
{:else if empty}
<div class="placeholder">{name}</div>
{/if}
</svelte:component>
{/if}
@ -115,7 +132,11 @@
</div>
<style>
div {
.component {
display: contents;
}
.placeholder {
color: #888;
padding: 20px;
}
</style>

View File

@ -22,6 +22,6 @@
<!-- Ensure to fully remount when screen changes -->
{#key screenDefinition?._id}
<Provider key="url" data={params}>
<Component definition={screenDefinition} />
<Component instance={screenDefinition} />
</Provider>
{/key}

View File

@ -20,12 +20,12 @@
onMount(() => {
document.addEventListener("mouseover", onMouseOver)
document.addEventListener("mouseleave", onMouseLeave)
window.addEventListener("mouseleave", onMouseLeave)
})
onDestroy(() => {
document.removeEventListener("mouseover", onMouseOver)
document.removeEventListener("mouseleave", onMouseLeave)
window.removeEventListener("mouseleave", onMouseLeave)
})
</script>

View File

@ -1,5 +1,5 @@
import { writable, derived } from "svelte/store"
import manifest from "@budibase/standard-components/manifest.json"
import Manifest from "@budibase/standard-components/manifest.json"
const dispatchEvent = (type, data) => {
window.dispatchEvent(
@ -46,7 +46,7 @@ const createBuilderStore = () => {
const component = findComponentById(asset?.props, selectedComponentId)
const prefix = "@budibase/standard-components/"
const type = component?._component?.replace(prefix, "")
const definition = type ? manifest[type] : null
const definition = type ? Manifest[type] : null
return {
...$state,
selectedComponent: component,

View File

@ -35,6 +35,11 @@ export const styleable = (node, styles = {}) => {
// Applies a style string to a DOM node
const applyStyles = styleString => {
// Apply empty border if required
if (newStyles.empty) {
styleString += "border: 2px dashed rgba(0, 0, 0, 0.25);"
}
node.style = styleString
node.dataset.componentId = componentId
}

View File

@ -9,8 +9,6 @@
export let vAlign
export let size
let element
$: directionClass = direction ? `valid-container direction-${direction}` : ""
$: hAlignClass = hAlign ? `hAlign-${hAlign}` : ""
$: vAlignClass = vAlign ? `vAlign-${vAlign}` : ""
@ -19,23 +17,13 @@
<div
class={[directionClass, hAlignClass, vAlignClass, sizeClass].join(" ")}
class:empty={!$component.children && $builderStore.inBuilder}
class:selected={$component.selected}
in:transition={{ type: $component.transition }}
use:styleable={$component.styles}
bind:this={element}
>
{#if !$component.children && $builderStore.inBuilder}
<div class="placeholder">Add some content</div>
{:else}
<slot />
{/if}
<slot />
</div>
<style>
.empty {
border: 2px dashed rgba(0, 0, 0, 0.25);
}
.valid-container {
display: flex;
max-width: 100%;
@ -96,12 +84,4 @@
.direction-column.hAlign-stretch {
align-items: stretch;
}
.selected {
position: relative;
}
.placeholder {
padding: 20px;
color: #888;
}
</style>

View File

@ -7,6 +7,8 @@
luceneSort,
luceneLimit,
} from "./lucene"
import Placeholder from "./Placeholder.svelte"
import Container from "./Container.svelte"
export let dataSource
export let filter
@ -231,7 +233,11 @@
<ProgressCircle />
</div>
{:else}
<slot />
{#if !$component.children}
<Placeholder />
{:else}
<slot />
{/if}
{#if paginate && internalTable}
<div class="pagination">
<Pagination

View File

@ -0,0 +1,21 @@
<script>
import { getContext } from "svelte"
const { builderStore } = getContext("sdk")
const component = getContext("component")
export let text = $component.name || "Placeholder"
</script>
{#if $builderStore.inBuilder}
<div>
{text}
</div>
{/if}
<style>
div {
padding: 20px;
color: #888;
}
</style>

View File

@ -1,5 +1,6 @@
<script>
import { getContext } from "svelte"
import Placeholder from "./Placeholder.svelte"
export let dataProvider
export let noRowsMessage
@ -13,35 +14,15 @@
</script>
<div use:styleable={$component.styles}>
{#if rows.length > 0}
{#if $component.children === 0 && $builderStore.inBuilder}
<p><i class="ri-image-line" />Add some components to display.</p>
{:else}
{#each rows as row}
<Provider data={row}>
<slot />
</Provider>
{/each}
{/if}
{#if $component.empty}
<Placeholder />
{:else if rows.length > 0}
{#each rows as row}
<Provider data={row}>
<slot />
</Provider>
{/each}
{:else if loaded && noRowsMessage}
<p><i class="ri-list-check-2" />{noRowsMessage}</p>
<Placeholder text={noRowsMessage} />
{/if}
</div>
<style>
p {
margin: 0 var(--spacing-m);
background-color: var(--grey-2);
color: var(--grey-6);
font-size: var(--font-size-s);
padding: var(--spacing-l);
border-radius: var(--border-radius-s);
display: grid;
place-items: center;
}
p i {
margin-bottom: var(--spacing-m);
font-size: 1.5rem;
color: var(--grey-5);
}
</style>

View File

@ -7,7 +7,7 @@
export let imageUrl = ""
export let heading = ""
export let subheading = ""
export let destinationUrl = ""
export let destinationUrl = "/"
$: showImage = !!imageUrl
</script>

View File

@ -1,6 +1,7 @@
<script>
import { getContext } from "svelte"
import { chart } from "svelte-apexcharts"
import Placeholder from "../Placeholder.svelte"
const { styleable, builderStore } = getContext("sdk")
const component = getContext("component")
@ -11,8 +12,8 @@
{#if options}
<div use:chart={options} use:styleable={$component.styles} />
{:else if $builderStore.inBuilder}
<div class="placeholder" use:styleable={$component.styles}>
Use the settings panel to build your chart.
<div use:styleable={{ ...$component.styles, empty: true }}>
<Placeholder text="Use the settings panel to build your chart" />
</div>
{/if}
@ -24,7 +25,4 @@
div :global(.apexcharts-yaxis-label, .apexcharts-xaxis-label) {
fill: #aaa;
}
div.placeholder {
padding: 10px;
}
</style>

View File

@ -8,30 +8,6 @@ export const capitalise = string => {
return string.substring(0, 1).toUpperCase() + string.substring(1)
}
/**
* Svelte action to set CSS variables on a DOM node.
*
* @param node
* @param props
*/
export const cssVars = (node, props) => {
Object.entries(props).forEach(([key, value]) => {
node.style.setProperty(`--${key}`, value)
})
return {
update(new_props) {
Object.entries(new_props).forEach(([key, value]) => {
node.style.setProperty(`--${key}`, value)
delete props[key]
})
Object.keys(props).forEach(name => node.style.removeProperty(`--${name}`))
props = new_props
},
}
}
/**
* Generates a short random ID.
* This is "nanoid" but rollup was derping attempting to bundle it, so the

View File

@ -10,6 +10,10 @@ import "@spectrum-css/page/dist/index-vars.css"
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
loadSpectrumIcons()
// Non user-facing components
export { default as Placeholder } from "./Placeholder.svelte"
// User facing components
export { default as container } from "./Container.svelte"
export { default as dataprovider } from "./DataProvider.svelte"
export { default as screenslot } from "./ScreenSlot.svelte"