Merge branch 'feature/binding-interface' into feature/binding-interface-frontend

This commit is contained in:
kevmodrome 2020-08-25 10:16:32 +02:00
commit f615f5be3b
No known key found for this signature in database
GPG Key ID: E8F9CD141E63BF38
19 changed files with 688 additions and 115 deletions

View File

@ -1,4 +1,4 @@
import { values } from "lodash/fp" import { values, cloneDeep } from "lodash/fp"
import getNewComponentName from "../getNewComponentName" import getNewComponentName from "../getNewComponentName"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import * as backendStoreActions from "./backend" import * as backendStoreActions from "./backend"
@ -24,8 +24,8 @@ import {
saveCurrentPreviewItem as _saveCurrentPreviewItem, saveCurrentPreviewItem as _saveCurrentPreviewItem,
saveScreenApi as _saveScreenApi, saveScreenApi as _saveScreenApi,
regenerateCssForCurrentScreen, regenerateCssForCurrentScreen,
generateNewIdsForComponent,
} from "../storeUtils" } from "../storeUtils"
export const getStore = () => { export const getStore = () => {
const initial = { const initial = {
apps: [], apps: [],
@ -70,6 +70,8 @@ export const getStore = () => {
store.addTemplatedComponent = addTemplatedComponent(store) store.addTemplatedComponent = addTemplatedComponent(store)
store.setMetadataProp = setMetadataProp(store) store.setMetadataProp = setMetadataProp(store)
store.editPageOrScreen = editPageOrScreen(store) store.editPageOrScreen = editPageOrScreen(store)
store.pasteComponent = pasteComponent(store)
store.storeComponentForCopy = storeComponentForCopy(store)
return store return store
} }
@ -291,9 +293,14 @@ const addChildComponent = store => (componentToAdd, presetProps = {}) => {
state state
) )
state.currentComponentInfo._children = state.currentComponentInfo._children.concat( const currentComponent =
newComponent.props state.components[state.currentComponentInfo._component]
)
const targetParent = currentComponent.children
? state.currentComponentInfo
: getParent(state.currentPreviewItem.props, state.currentComponentInfo)
targetParent._children = targetParent._children.concat(newComponent.props)
state.currentFrontEndType === "page" state.currentFrontEndType === "page"
? _savePage(state) ? _savePage(state)
@ -454,3 +461,50 @@ const setMetadataProp = store => (name, prop) => {
return s return s
}) })
} }
const storeComponentForCopy = store => (component, cut = false) => {
store.update(s => {
const copiedComponent = cloneDeep(component)
s.componentToPaste = copiedComponent
s.componentToPaste.isCut = cut
if (cut) {
const parent = getParent(s.currentPreviewItem.props, component._id)
parent._children = parent._children.filter(c => c._id !== component._id)
selectComponent(s, parent)
}
return s
})
}
const pasteComponent = store => (targetComponent, mode) => {
store.update(s => {
if (!s.componentToPaste) return s
const componentToPaste = cloneDeep(s.componentToPaste)
// retain the same ids as things may be referencing this component
if (componentToPaste.isCut) {
// in case we paste a second time
s.componentToPaste.isCut = false
} else {
generateNewIdsForComponent(componentToPaste, s)
}
delete componentToPaste.isCut
if (mode === "inside") {
targetComponent._children.push(componentToPaste)
return s
}
const parent = getParent(s.currentPreviewItem.props, targetComponent)
const targetIndex = parent._children.indexOf(targetComponent)
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
regenerateCssForCurrentScreen(s)
_saveCurrentPreviewItem(s)
selectComponent(s, componentToPaste)
return s
})
}

View File

@ -1,6 +1,8 @@
import { makePropsSafe } from "components/userInterface/pagesParsing/createProps" import { makePropsSafe } from "components/userInterface/pagesParsing/createProps"
import api from "./api" import api from "./api"
import { generate_screen_css } from "./generate_css" import { generate_screen_css } from "./generate_css"
import { uuid } from "./uuid"
import getNewComponentName from "./getNewComponentName"
export const selectComponent = (state, component) => { export const selectComponent = (state, component) => {
const componentDef = component._component.startsWith("##") const componentDef = component._component.startsWith("##")
@ -79,3 +81,9 @@ export const regenerateCssForCurrentScreen = state => {
]) ])
return state return state
} }
export const generateNewIdsForComponent = (c, state) =>
walkProps(c, p => {
p._id = uuid()
p._instanceName = getNewComponentName(p._component, state)
})

View File

@ -99,7 +99,7 @@
<EditRowPopover {row} /> <EditRowPopover {row} />
</td> </td>
{#each headers as header} {#each headers as header}
<td class="hoverable"> <td>
{#if schema[header].type === 'link'} {#if schema[header].type === 'link'}
<LinkedRecord field={schema[header]} ids={row[header]} /> <LinkedRecord field={schema[header]} ids={row[header]} />
{:else}{row[header] || ''}{/if} {:else}{row[header] || ''}{/if}
@ -168,6 +168,9 @@
max-width: 200px; max-width: 200px;
text-overflow: ellipsis; text-overflow: ellipsis;
border: 1px solid var(--grey-4); border: 1px solid var(--grey-4);
overflow: hidden;
white-space: pre;
box-sizing: border-box;
} }
tbody tr { tbody tr {
@ -187,6 +190,7 @@
.popovers { .popovers {
display: flex; display: flex;
gap: var(--spacing-m);
} }
.no-data { .no-data {

View File

@ -124,8 +124,14 @@
} }
.field { .field {
margin-top: var(--spacing-xl); display: grid;
margin-bottom: var(--spacing-xl); grid-template-columns: auto 20px 1fr;
align-items: center;
grid-gap: 5px;
font-size: 14px;
font-weight: 500;
margin-bottom: var(--spacing-l);
font-family: var(--font-normal);
} }
.button-margin-3 { .button-margin-3 {

View File

@ -31,6 +31,9 @@
{/each} {/each}
</Select> </Select>
{:else} {:else}
{#if type === 'checkbox'}
<label>{label}</label>
{/if}
<Input <Input
thin thin
placeholder={label} placeholder={label}
@ -41,3 +44,12 @@
on:input={handleInput} on:input={handleInput}
on:change={handleInput} /> on:change={handleInput} />
{/if} {/if}
<style>
label {
font-weight: 500;
font-size: var(--font-size-s);
float: left;
margin-right: 8px;
}
</style>

View File

@ -45,8 +45,7 @@
<style> <style>
h5 { h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl); margin-bottom: var(--spacing-l);
margin: 0;
font-weight: 500; font-weight: 500;
} }

View File

@ -11,21 +11,6 @@
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")
let HEADINGS = [
{
title: "Tables",
key: "TABLES",
},
{
title: "Tables",
key: "NAVIGATE",
},
{
title: "Add",
key: "ADD",
},
]
$: selectedTab = $backendUiStore.tabs.NAVIGATION_PANEL $: selectedTab = $backendUiStore.tabs.NAVIGATION_PANEL
function selectModel(model, fieldId) { function selectModel(model, fieldId) {
@ -44,7 +29,7 @@
{#if $backendUiStore.selectedDatabase && $backendUiStore.selectedDatabase._id} {#if $backendUiStore.selectedDatabase && $backendUiStore.selectedDatabase._id}
<div class="hierarchy"> <div class="hierarchy">
<div class="components-list-container"> <div class="components-list-container">
<h3>Tables</h3> <h4>Tables</h4>
<CreateTablePopover /> <CreateTablePopover />
<div class="hierarchy-items-container"> <div class="hierarchy-items-container">
{#each $backendUiStore.models as model} {#each $backendUiStore.models as model}
@ -63,6 +48,10 @@
</div> </div>
<style> <style>
h4 {
font-weight: 500;
}
.items-root { .items-root {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -2,7 +2,6 @@
import { MoreIcon } from "components/common/Icons" import { MoreIcon } from "components/common/Icons"
import { store } from "builderStore" import { store } from "builderStore"
import { getComponentDefinition } from "builderStore/store" import { getComponentDefinition } from "builderStore/store"
import getNewComponentName from "builderStore/getNewComponentName"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last, cloneDeep } from "lodash/fp" import { last, cloneDeep } from "lodash/fp"
import UIkit from "uikit" import UIkit from "uikit"
@ -29,8 +28,7 @@
}) })
$: dropdown && UIkit.util.on(dropdown, "shown", () => (hidden = false)) $: dropdown && UIkit.util.on(dropdown, "shown", () => (hidden = false))
$: noChildrenAllowed = $: noChildrenAllowed =
!component || !component || !getComponentDefinition($store, component._component).children
getComponentDefinition($store, component._component).children === false
$: noPaste = !$store.componentToPaste $: noPaste = !$store.componentToPaste
const lastPartOfName = c => (c ? last(c._component.split("/")) : "") const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
@ -81,7 +79,9 @@
store.update(s => { store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component) const parent = getParent(s.currentPreviewItem.props, component)
const copiedComponent = cloneDeep(component) const copiedComponent = cloneDeep(component)
generateNewIdsForComponent(copiedComponent, s) walkProps(copiedComponent, p => {
p._id = uuid()
})
parent._children = [...parent._children, copiedComponent] parent._children = [...parent._children, copiedComponent]
saveCurrentPreviewItem(s) saveCurrentPreviewItem(s)
s.currentComponentInfo = copiedComponent s.currentComponentInfo = copiedComponent
@ -104,50 +104,14 @@
}) })
} }
const generateNewIdsForComponent = (c, state) =>
walkProps(c, p => {
p._id = uuid()
p._instanceName = getNewComponentName(p._component, state)
})
const storeComponentForCopy = (cut = false) => { const storeComponentForCopy = (cut = false) => {
store.update(s => { // lives in store - also used by drag drop
const copiedComponent = cloneDeep(component) store.storeComponentForCopy(component, cut)
s.componentToPaste = copiedComponent
if (cut) {
const parent = getParent(s.currentPreviewItem.props, component._id)
parent._children = parent._children.filter(c => c._id !== component._id)
selectComponent(s, parent)
}
return s
})
} }
const pasteComponent = mode => { const pasteComponent = mode => {
store.update(s => { // lives in store - also used by drag drop
if (!s.componentToPaste) return s store.pasteComponent(component, mode)
const componentToPaste = cloneDeep(s.componentToPaste)
generateNewIdsForComponent(componentToPaste, s)
delete componentToPaste._cutId
if (mode === "inside") {
component._children.push(componentToPaste)
return s
}
const parent = getParent(s.currentPreviewItem.props, component)
const targetIndex = parent._children.indexOf(component)
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
regenerateCssForCurrentScreen(s)
saveCurrentPreviewItem(s)
selectComponent(s, componentToPaste)
return s
})
} }
</script> </script>

View File

@ -7,21 +7,27 @@
import { store } from "builderStore" import { store } from "builderStore"
import { ArrowDownIcon, ShapeIcon } from "components/common/Icons/" import { ArrowDownIcon, ShapeIcon } from "components/common/Icons/"
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte" import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
import { writable } from "svelte/store"
export let screens = [] export let screens = []
const dragDropStore = writable({})
let confirmDeleteDialog let confirmDeleteDialog
let componentToDelete = "" let componentToDelete = ""
const joinPath = join("/") const joinPath = join("/")
const normalizedName = name => const normalizedName = name =>
pipe(name, [ pipe(
trimCharsStart("./"), name,
trimCharsStart("~/"), [
trimCharsStart("../"), trimCharsStart("./"),
trimChars(" "), trimCharsStart("~/"),
]) trimCharsStart("../"),
trimChars(" "),
]
)
const changeScreen = screen => { const changeScreen = screen => {
store.setCurrentScreen(screen.props._instanceName) store.setCurrentScreen(screen.props._instanceName)
@ -57,7 +63,8 @@
{#if $store.currentPreviewItem.props._instanceName && $store.currentPreviewItem.props._instanceName === screen.props._instanceName && screen.props._children} {#if $store.currentPreviewItem.props._instanceName && $store.currentPreviewItem.props._instanceName === screen.props._instanceName && screen.props._children}
<ComponentsHierarchyChildren <ComponentsHierarchyChildren
components={screen.props._children} components={screen.props._children}
currentComponent={$store.currentComponentInfo} /> currentComponent={$store.currentComponentInfo}
{dragDropStore} />
{/if} {/if}
{/each} {/each}
@ -94,10 +101,6 @@
margin: 0 0 0 5px; margin: 0 0 0 5px;
} }
:global(svg) {
transition: 0.2s;
}
.rotate :global(svg) { .rotate :global(svg) {
transform: rotate(-90deg); transform: rotate(-90deg);
} }

View File

@ -15,11 +15,19 @@
export let currentComponent export let currentComponent
export let onSelect = () => {} export let onSelect = () => {}
export let level = 0 export let level = 0
export let dragDropStore
let dropUnderComponent
let componentToDrop
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1) const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
const get_name = s => (!s ? "" : last(s.split("/"))) const get_name = s => (!s ? "" : last(s.split("/")))
const get_capitalised_name = name => pipe(name, [get_name, capitalise]) const get_capitalised_name = name =>
pipe(
name,
[get_name, capitalise]
)
const isScreenslot = name => name === "##builtin/screenslot" const isScreenslot = name => name === "##builtin/screenslot"
const selectComponent = component => { const selectComponent = component => {
@ -32,15 +40,92 @@
// Go to correct URL // Go to correct URL
$goto(`./:page/:screen/${path}`) $goto(`./:page/:screen/${path}`)
} }
const dragstart = component => e => {
e.dataTransfer.dropEffect = "move"
dragDropStore.update(s => {
s.componentToDrop = component
return s
})
}
const dragover = (component, index) => e => {
const canHaveChildrenButIsEmpty =
$store.components[component._component].children &&
component._children.length === 0
e.dataTransfer.dropEffect = "copy"
dragDropStore.update(s => {
const isBottomHalf = e.offsetY > e.currentTarget.offsetHeight / 2
s.targetComponent = component
// only allow dropping inside when container type
// is empty. If it has children, the user can drag over
// it's existing children
if (canHaveChildrenButIsEmpty) {
if (index === 0) {
// when its the first component in the screen,
// we divide into 3, so we can paste above, inside or below
const pos = e.offsetY / e.currentTarget.offsetHeight
if (pos < 0.4) {
s.dropPosition = "above"
} else if (pos > 0.8) {
// purposely giving this the least space as it is often covered
// by the component below's "above" space
s.dropPosition = "below"
} else {
s.dropPosition = "inside"
}
} else {
s.dropPosition = isBottomHalf ? "below" : "inside"
}
} else {
s.dropPosition = isBottomHalf ? "below" : "above"
}
return s
})
return false
}
const drop = () => {
if ($dragDropStore.targetComponent !== $dragDropStore.componentToDrop) {
store.storeComponentForCopy($dragDropStore.componentToDrop, true)
store.pasteComponent(
$dragDropStore.targetComponent,
$dragDropStore.dropPosition
)
}
dragDropStore.update(s => {
s.dropPosition = ""
s.targetComponent = null
s.componentToDrop = null
return s
})
}
</script> </script>
<ul> <ul>
{#each components as component, index (component._id)} {#each components as component, index (component._id)}
<li on:click|stopPropagation={() => selectComponent(component)}> <li on:click|stopPropagation={() => selectComponent(component)}>
{#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition === 'above'}
<div
on:drop={drop}
ondragover="return false"
ondragenter="return false"
class="budibase__nav-item item drop-item"
style="margin-left: {level * 20 + 40}px" />
{/if}
<div <div
class="budibase__nav-item item" class="budibase__nav-item item"
class:selected={currentComponent === component} class:selected={currentComponent === component}
style="padding-left: {level * 20 + 40}px"> style="padding-left: {level * 20 + 40}px"
draggable={true}
on:dragstart={dragstart(component)}
on:dragover={dragover(component, index)}
on:drop={drop}
ondragover="return false"
ondragenter="return false">
<div class="nav-item"> <div class="nav-item">
<i class="icon ri-arrow-right-circle-line" /> <i class="icon ri-arrow-right-circle-line" />
{isScreenslot(component._component) ? 'Screenslot' : component._instanceName} {isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
@ -55,8 +140,18 @@
components={component._children} components={component._children}
{currentComponent} {currentComponent}
{onSelect} {onSelect}
{dragDropStore}
level={level + 1} /> level={level + 1} />
{/if} {/if}
{#if $dragDropStore && $dragDropStore.targetComponent === component && ($dragDropStore.dropPosition === 'inside' || $dragDropStore.dropPosition === 'below')}
<div
on:drop={drop}
ondragover="return false"
ondragenter="return false"
class="budibase__nav-item item drop-item"
style="margin-left: {(level + ($dragDropStore.dropPosition === 'inside' ? 2 : 0)) * 20 + 40}px" />
{/if}
</li> </li>
{/each} {/each}
</ul> </ul>
@ -78,6 +173,11 @@
align-items: center; align-items: center;
} }
.drop-item {
background: var(--blue-light);
height: 36px;
}
.actions { .actions {
display: none; display: none;
height: 24px; height: 24px;

View File

@ -16,12 +16,14 @@
import { pipe } from "components/common/core" import { pipe } from "components/common/core"
import { store } from "builderStore" import { store } from "builderStore"
import { ArrowDownIcon, GridIcon } from "components/common/Icons/" import { ArrowDownIcon, GridIcon } from "components/common/Icons/"
import { writable } from "svelte/store"
export let layout export let layout
let confirmDeleteDialog let confirmDeleteDialog
let componentToDelete = "" let componentToDelete = ""
const dragDropStore = writable({})
const joinPath = join("/") const joinPath = join("/")
const lastPartOfName = c => const lastPartOfName = c =>
@ -57,7 +59,8 @@
<ComponentsHierarchyChildren <ComponentsHierarchyChildren
thisComponent={_layout.component.props} thisComponent={_layout.component.props}
components={_layout.component.props._children} components={_layout.component.props._children}
currentComponent={$store.currentComponentInfo} /> currentComponent={$store.currentComponentInfo}
{dragDropStore} />
{/if} {/if}
<style> <style>
@ -81,10 +84,6 @@
color: var(--grey-7); color: var(--grey-7);
} }
:global(svg) {
transition: 0.2s;
}
.rotate :global(svg) { .rotate :global(svg) {
transform: rotate(-90deg); transform: rotate(-90deg);
} }

View File

@ -48,7 +48,7 @@ export const createProps = (componentDefinition, derivedFromProps) => {
assign(props, derivedFromProps) assign(props, derivedFromProps)
} }
if (componentDefinition.children !== false && isUndefined(props._children)) { if (isUndefined(props._children)) {
props._children = [] props._children = []
} }

View File

@ -70,6 +70,20 @@ export const layout = [
{ label: "no wrap", value: "noWrap" }, { label: "no wrap", value: "noWrap" },
], ],
}, },
{
label: "Gap",
key: "gap",
control: OptionSelect,
options: [
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
{ label: "16px", value: "16px" },
{ label: "20px", value: "20px" },
{ label: "32px", value: "32px" },
{ label: "64px", value: "64px" },
],
},
] ]
export const margin = [ export const margin = [

View File

@ -313,6 +313,174 @@ export default {
], ],
}, },
}, },
{
name: "Card",
description: "Card components",
icon: "ri-layout-bottom-line",
commonProps: {},
children: [
{
_component: "@budibase/standard-components/card",
name: "Vertical",
description:
"A basic card component that can contain content and actions.",
icon: "ri-layout-column-line",
children: [],
properties: {
design: { ...all },
settings: [
{
label: "Image",
key: "imageUrl",
control: Input,
placeholder: "Image",
},
{
label: "Heading",
key: "heading",
control: Input,
placeholder: "Heading",
},
{
label: "Description",
key: "description",
control: Input,
placeholder: "Description",
},
{
label: "Link Text",
key: "linkText",
control: Input,
placeholder: "Link Text",
},
{
label: "Link Url",
key: "linkUrl",
control: Input,
placeholder: "Link URL",
},
{
label: "Link Color",
key: "color",
control: Input,
placeholder: "Link Color",
},
{
label: "Hover Color",
key: "linkHoverColor",
control: Input,
placeholder: "Hover Color",
},
{
label: "Image Height",
key: "imageHeight",
control: OptionSelect,
options: ["12rem", "16rem", "20rem", "24rem"],
placeholder: "Image Height",
},
{
label: "Card Width",
key: "cardWidth",
control: OptionSelect,
options: ["16rem", "20rem", "24rem"],
placeholder: "Card Width",
},
],
},
},
{
_component: "@budibase/standard-components/cardhorizontal",
name: "Horizontal",
description:
"A basic card component that can contain content and actions.",
icon: "ri-layout-row-line",
children: [],
properties: {
design: { ...all },
settings: [
{
label: "Image",
key: "imageUrl",
control: Input,
placeholder: "Image",
},
{
label: "Heading",
key: "heading",
control: Input,
placeholder: "Heading",
},
{
label: "Description",
key: "description",
control: Input,
placeholder: "Description",
},
{
label: "Subtext",
key: "subtext",
control: Input,
placeholder: "Subtext",
},
{
label: "Link Text",
key: "linkText",
control: Input,
placeholder: "Link Text",
},
{
label: "Link Url",
key: "linkUrl",
control: Input,
placeholder: "Link URL",
},
{
label: "Link Color",
key: "color",
control: Input,
placeholder: "Link Color",
},
{
label: "Hover Color",
key: "linkHoverColor",
control: Input,
placeholder: "Hover Color",
},
{
label: "Card Width",
key: "cardWidth",
control: OptionSelect,
options: [
"24rem",
"28rem",
"32rem",
"40rem",
"48rem",
"60rem",
"100%",
],
placeholder: "Card Height",
},
{
label: "Image Width",
key: "imageWidth",
control: OptionSelect,
options: ["8rem", "12rem", "16rem"],
placeholder: "Image Width",
},
{
label: "Image Height",
key: "imageHeight",
control: OptionSelect,
options: ["8rem", "12rem", "16rem", "auto"],
placeholder: "Image Height",
},
],
},
},
],
},
{ {
_component: "@budibase/materialdesign-components/BasicCard", _component: "@budibase/materialdesign-components/BasicCard",
name: "Card", name: "Card",

View File

@ -72,16 +72,6 @@ describe("createDefaultProps", () => {
expect(props._children).toEqual([]) expect(props._children).toEqual([])
}) })
it("should create a object without _children array, when children===false ", () => {
const comp = getcomponent()
comp.children = false
const { props, errors } = createProps(comp)
expect(errors).toEqual([])
expect(props._children).not.toBeDefined()
})
it("should create a object with single empty array, when prop definition is 'event' ", () => { it("should create a object with single empty array, when prop definition is 'event' ", () => {
const comp = getcomponent() const comp = getcomponent()
comp.props.onClick = "event" comp.props.onClick = "event"
@ -104,24 +94,22 @@ describe("createDefaultProps", () => {
expect(props._children).toEqual([]) expect(props._children).toEqual([])
}) })
it("should create a _children array when children not defined ", () => { it("should always create _children ", () => {
const comp = getcomponent()
const { props, errors } = createProps(comp)
expect(errors).toEqual([])
expect(props._children).toBeDefined()
expect(props._children).toEqual([])
})
it("should not create _children array when children=false ", () => {
const comp = getcomponent() const comp = getcomponent()
comp.children = false comp.children = false
const { props, errors } = createProps(comp) const createRes1 = createProps(comp)
expect(errors).toEqual([]) expect(createRes1.errors).toEqual([])
expect(props._children).not.toBeDefined() expect(createRes1.props._children).toBeDefined()
const comp2 = getcomponent()
comp2.children = true
const createRes2 = createProps(comp)
expect(createRes2.errors).toEqual([])
expect(createRes2.props._children).toBeDefined()
}) })
it("should create an object with multiple prop names", () => { it("should create an object with multiple prop names", () => {

View File

@ -16,6 +16,7 @@
"Navigation": { "Navigation": {
"name": "Navigation", "name": "Navigation",
"description": "A basic header navigation component", "description": "A basic header navigation component",
"children": true,
"props": { "props": {
"logoUrl": "string", "logoUrl": "string",
"title": "string", "title": "string",
@ -250,7 +251,8 @@
} }
}, },
"list": { "list": {
"description": "A configurable data list that attaches to your backend models.", "description": "configurable data list that attaches to your backend models.",
"children": true,
"context": "model", "context": "model",
"data": true, "data": true,
"props": { "props": {
@ -271,12 +273,88 @@
}, },
"recorddetail": { "recorddetail": {
"description": "Loads a record, using an ID in the url", "description": "Loads a record, using an ID in the url",
"children": true,
"context": "model", "context": "model",
"data": true, "data": true,
"props": { "props": {
"model": "models" "model": "models"
} }
}, },
"card": {
"name": "Card",
"props": {
"imageUrl": "string",
"heading": "string",
"description": "string",
"linkText": "string",
"linkUrl": "string",
"color": "string",
"linkHoverColor": "string",
"imageHeight": {
"type": "options",
"default": "20rem",
"options": [
"12rem",
"16rem",
"20rem",
"24rem"
]
},
"cardWidth": {
"type": "options",
"default": "20rem",
"options": [
"16rem",
"20rem",
"24rem"
]
}
}
},
"cardhorizontal": {
"name": "Horizontal Card",
"props": {
"imageUrl": "string",
"heading": "string",
"description": "string",
"subtext": "string",
"linkText": "string",
"linkUrl": "string",
"color": "string",
"linkHoverColor": "string",
"imageWidth": {
"type": "options",
"default": "8rem",
"options": [
"8rem",
"12rem",
"16rem"
]
},
"cardWidth": {
"type": "options",
"default": "32rem",
"options": [
"24rem",
"28rem",
"32rem",
"40rem",
"48rem",
"60rem",
"100%"
]
},
"imageHeight": {
"type": "options",
"default": "8rem",
"options": [
"8rem",
"12rem",
"16rem"
]
}
}
},
"datamap": { "datamap": {
"description": "shiny chart", "description": "shiny chart",
"data": true, "data": true,
@ -534,7 +612,7 @@
"bullet": { "bullet": {
"description": "Bullet chart", "description": "Bullet chart",
"data": true, "data": true,
"props": { "props": {
"model": "string", "model": "string",
"color": "string", "color": "string",
"customSubtitle": "string", "customSubtitle": "string",
@ -608,6 +686,7 @@
}, },
"container": { "container": {
"name": "Container", "name": "Container",
"children": true,
"description": "An element that contains and lays out other elements. e.g. <div>, <header> etc", "description": "An element that contains and lays out other elements. e.g. <div>, <header> etc",
"props": { "props": {
"className": "string", "className": "string",

View File

@ -0,0 +1,80 @@
<script>
import { cssVars, createClasses } from "./cssVars"
export const className = ""
export let imageUrl = ""
export let heading = ""
export let description = ""
export let linkText = ""
export let linkUrl
export let color
export let linkHoverColor
export let imageHeight
export let cardWidth
$: cssVariables = {
color,
linkHoverColor,
imageHeight,
cardWidth,
}
$: showImage = !!imageUrl
</script>
<div use:cssVars={cssVariables} class="container">
{#if showImage}
<img class="image" src={imageUrl} alt="" />
{/if}
<div class="content">
<h2 class="heading">{heading}</h2>
<h4 class="text">{description}</h4>
<a href={linkUrl}>{linkText}</a>
</div>
</div>
<style>
.container {
width: var(--cardWidth);
overflow: hidden !important;
height: auto;
}
.image {
width: 100% !important;
max-width: 100%;
height: var(--imageHeight);
vertical-align: middle;
}
.content {
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.heading {
font-size: 1.25rem;
font-weight: 700;
margin: 0;
}
.text {
font-size: 1rem;
margin: 0;
font-weight: 400;
line-height: 1.5rem;
}
a {
margin: 0.5rem 0;
text-decoration: none;
color: var(--color);
font-weight: 600;
}
a:hover {
color: var(--linkHoverColor);
}
</style>

View File

@ -0,0 +1,104 @@
<script>
import { cssVars, createClasses } from "./cssVars"
export const className = ""
export let imageUrl = ""
export let heading = ""
export let description = ""
export let subtext = ""
export let linkText = ""
export let linkUrl
export let color
export let linkHoverColor
export let cardWidth
export let imageWidth
export let imageHeight
$: cssVariables = {
color,
linkHoverColor,
imageWidth,
cardWidth,
imageHeight,
}
$: showImage = !!imageUrl
</script>
<div use:cssVars={cssVariables} class="container">
{#if showImage}
<img class="image" src={imageUrl} alt="" />
{/if}
<div class="content">
<main>
<h2 class="heading">{heading}</h2>
<p class="text">{description}</p>
</main>
<footer>
<p class="subtext">{subtext}</p>
<a href={linkUrl}>{linkText}</a>
</footer>
</div>
</div>
<style>
.container {
height: 100%;
max-width: var(--cardWidth);
display: flex !important;
text-align: left;
}
.image {
width: var(--imageWidth);
height: var(--imageHeight);
vertical-align: middle;
}
.content {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
padding: 0.85rem 1.5rem;
}
.heading {
font-size: 1rem;
font-weight: 700;
margin: 0;
}
.text {
font-size: 0.85rem;
margin: 0.5rem 0 0 0;
font-weight: 400;
line-height: 1.25rem;
}
footer {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.subtext {
font-size: 0.85rem;
margin: 0;
font-weight: 400;
color: #757575;
}
a {
margin: 0.5rem 0 0 0;
text-decoration: none;
color: var(--color);
font-weight: 600;
font-size: 0.85rem;
margin: 0;
}
a:hover {
color: var(--linkHoverColor);
}
</style>

View File

@ -23,5 +23,7 @@ export { default as list } from "./List.svelte"
export { default as datasearch } from "./DataSearch.svelte" export { default as datasearch } from "./DataSearch.svelte"
export { default as embed } from "./Embed.svelte" export { default as embed } from "./Embed.svelte"
export { default as stackedlist } from "./StackedList.svelte" export { default as stackedlist } from "./StackedList.svelte"
export { default as card } from "./Card.svelte"
export { default as cardhorizontal } from "./CardHorizontal.svelte"
export { default as recorddetail } from "./RecordDetail.svelte" export { default as recorddetail } from "./RecordDetail.svelte"
export * from "./Chart" export * from "./Chart"