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}> <Provider key="user" data={$authStore} {actions}>
<div id="app-root"> <div id="app-root">
<Component definition={$screenStore.activeLayout.props} /> <Component instance={$screenStore.activeLayout.props} />
</div> </div>
<NotificationDisplay /> <NotificationDisplay />
<!-- Key block needs to be outside the if statement or it breaks --> <!-- 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 { enrichProps, propsAreSame } from "../utils/componentProps"
import { builderStore } from "../store" import { builderStore } from "../store"
import { hashString } from "../utils/hash" 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 // Props that will be passed to the component instance
let componentProps let componentProps
@ -28,26 +29,29 @@
const componentStore = writable({}) const componentStore = writable({})
setContext("component", componentStore) setContext("component", componentStore)
// Extract component definition info // Extract component instance info
$: constructor = getComponentConstructor(definition._component) $: constructor = getComponentConstructor(instance._component)
$: children = definition._children || [] $: definition = getComponentDefinition(instance._component)
$: id = definition._id $: children = instance._children || []
$: name = definition._instanceName $: id = instance._id
$: updateComponentProps(definition, $context) $: name = instance._instanceName
$: styles = definition._styles $: empty =
$: transition = definition._transition !children.length && definition?.hasChildren && $builderStore.inBuilder
$: updateComponentProps(instance, $context)
$: selected = $: selected =
$builderStore.inBuilder && $builderStore.inBuilder &&
$builderStore.selectedComponentId === definition._id $builderStore.selectedComponentId === instance._id
// Update component context // Update component context
$: componentStore.set({ $: componentStore.set({
id, id,
children: children.length, children: children.length,
styles: { ...styles, id }, styles: { ...instance._styles, id, empty },
transition, empty,
transition: instance._transition,
selected, selected,
props: componentProps, props: componentProps,
name,
}) })
// Gets the component constructor for the specified component // Gets the component constructor for the specified component
@ -60,14 +64,20 @@
return ComponentLibrary[name] 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 // 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 // Record the timestamp so we can reference it after enrichment
latestUpdateTime = Date.now() latestUpdateTime = Date.now()
const enrichmentTime = latestUpdateTime const enrichmentTime = latestUpdateTime
// Enrich props with context // Enrich props with context
const enrichedProps = enrichProps(definition, context) const enrichedProps = enrichProps(instance, context)
// Abandon this update if a newer update has started // Abandon this update if a newer update has started
if (enrichmentTime !== latestUpdateTime) { if (enrichmentTime !== latestUpdateTime) {
@ -100,14 +110,21 @@
} }
</script> </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} {#key propsHash}
{#if constructor && componentProps} {#if constructor && componentProps}
<svelte:component this={constructor} {...componentProps}> <svelte:component this={constructor} {...componentProps}>
{#if children.length} {#if children.length}
{#each children as child (child._id)} {#each children as child (child._id)}
<svelte:self definition={child} /> <svelte:self instance={child} />
{/each} {/each}
{:else if empty}
<div class="placeholder">{name}</div>
{/if} {/if}
</svelte:component> </svelte:component>
{/if} {/if}
@ -115,7 +132,11 @@
</div> </div>
<style> <style>
div { .component {
display: contents; display: contents;
} }
.placeholder {
color: #888;
padding: 20px;
}
</style> </style>

View File

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

View File

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

View File

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

View File

@ -35,6 +35,11 @@ export const styleable = (node, styles = {}) => {
// Applies a style string to a DOM node // Applies a style string to a DOM node
const applyStyles = styleString => { 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.style = styleString
node.dataset.componentId = componentId node.dataset.componentId = componentId
} }

View File

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

View File

@ -7,6 +7,8 @@
luceneSort, luceneSort,
luceneLimit, luceneLimit,
} from "./lucene" } from "./lucene"
import Placeholder from "./Placeholder.svelte"
import Container from "./Container.svelte"
export let dataSource export let dataSource
export let filter export let filter
@ -231,7 +233,11 @@
<ProgressCircle /> <ProgressCircle />
</div> </div>
{:else} {:else}
<slot /> {#if !$component.children}
<Placeholder />
{:else}
<slot />
{/if}
{#if paginate && internalTable} {#if paginate && internalTable}
<div class="pagination"> <div class="pagination">
<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> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import Placeholder from "./Placeholder.svelte"
export let dataProvider export let dataProvider
export let noRowsMessage export let noRowsMessage
@ -13,35 +14,15 @@
</script> </script>
<div use:styleable={$component.styles}> <div use:styleable={$component.styles}>
{#if rows.length > 0} {#if $component.empty}
{#if $component.children === 0 && $builderStore.inBuilder} <Placeholder />
<p><i class="ri-image-line" />Add some components to display.</p> {:else if rows.length > 0}
{:else} {#each rows as row}
{#each rows as row} <Provider data={row}>
<Provider data={row}> <slot />
<slot /> </Provider>
</Provider> {/each}
{/each}
{/if}
{:else if loaded && noRowsMessage} {:else if loaded && noRowsMessage}
<p><i class="ri-list-check-2" />{noRowsMessage}</p> <Placeholder text={noRowsMessage} />
{/if} {/if}
</div> </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 imageUrl = ""
export let heading = "" export let heading = ""
export let subheading = "" export let subheading = ""
export let destinationUrl = "" export let destinationUrl = "/"
$: showImage = !!imageUrl $: showImage = !!imageUrl
</script> </script>

View File

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

View File

@ -8,30 +8,6 @@ export const capitalise = string => {
return string.substring(0, 1).toUpperCase() + string.substring(1) 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. * Generates a short random ID.
* This is "nanoid" but rollup was derping attempting to bundle it, so the * 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" import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
loadSpectrumIcons() 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 container } from "./Container.svelte"
export { default as dataprovider } from "./DataProvider.svelte" export { default as dataprovider } from "./DataProvider.svelte"
export { default as screenslot } from "./ScreenSlot.svelte" export { default as screenslot } from "./ScreenSlot.svelte"